From 8caf31296f208fa71bbdc51573ff7d0489c11979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 14:49:25 +0200 Subject: [PATCH 001/160] [BREAKING] Xcode 16+, Swift 6+ --- GRDB.swift.podspec | 2 +- GRDB/Core/DatabasePool.swift | 6 ++--- GRDB/Core/DatabaseSnapshotPool.swift | 3 +-- GRDB/Core/WALSnapshot.swift | 7 +++-- GRDB/Core/WALSnapshotTransaction.swift | 3 +-- .../Observers/ValueConcurrentObserver.swift | 3 +-- Package.swift | 2 +- README.md | 2 +- .../DatabaseReaderReadPublisherTests.swift | 12 ++++----- .../DatabaseAbortedTransactionTests.swift | 4 +-- .../DatabaseConfigurationTests.swift | 4 +-- Tests/GRDBTests/DatabasePoolTests.swift | 8 +++--- Tests/GRDBTests/DatabaseReaderTests.swift | 26 +++++++++---------- .../GRDBTests/DatabaseSnapshotPoolTests.swift | 2 +- .../ValueObservationPrintTests.swift | 8 +++--- Tests/GRDBTests/ValueObservationTests.swift | 10 +++---- Tests/SPM/PlainPackage/Package.swift | 4 +-- 17 files changed, 50 insertions(+), 56 deletions(-) diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index 403fd74f9c..d01661f93c 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/groue/GRDB.swift.git', :tag => "v#{s.version}" } s.module_name = 'GRDB' - s.swift_versions = ['5.7'] + s.swift_versions = ['5.10'] s.ios.deployment_target = '11.0' s.osx.deployment_target = '10.13' s.watchos.deployment_target = '4.0' diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index 7dd0525fc2..34a9e7fdeb 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -644,8 +644,7 @@ extension DatabasePool: DatabaseReader { // MARK: - WAL Snapshot Transactions - // swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) /// Returns a long-lived WAL snapshot transaction on a reader connection. func walSnapshotTransaction() throws -> WALSnapshotTransaction { guard let readerPool else { @@ -870,8 +869,7 @@ extension DatabasePool { purpose: "snapshot.\($databaseSnapshotCount.increment())") } - // swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) /// Creates a database snapshot that allows concurrent accesses to an /// unchanging database content, as it exists at the moment the snapshot /// is created. diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index c32b0b1a26..35fec5a2d4 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -1,5 +1,4 @@ -// swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) /// A database connection that allows concurrent accesses to an unchanging /// database content, as it existed at the moment the snapshot was created. /// diff --git a/GRDB/Core/WALSnapshot.swift b/GRDB/Core/WALSnapshot.swift index 4d50c67e08..b6122426e9 100644 --- a/GRDB/Core/WALSnapshot.swift +++ b/GRDB/Core/WALSnapshot.swift @@ -1,5 +1,4 @@ -// swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) /// An instance of WALSnapshot records the state of a WAL mode database for some /// specific point in history. /// @@ -15,8 +14,8 @@ /// With custom SQLite builds, it only works if `SQLITE_ENABLE_SNAPSHOT` /// is defined. /// -/// With system SQLite, it can only work when the SDK exposes the C apis and -/// their availability, which means XCode 14 (identified with Swift 5.7). +/// With system SQLite, it works because the SDK exposes the C apis and +/// since XCode 14. /// /// Yes, this is an awfully complex logic. /// diff --git a/GRDB/Core/WALSnapshotTransaction.swift b/GRDB/Core/WALSnapshotTransaction.swift index 5f56f1d2fc..4340b67e4e 100644 --- a/GRDB/Core/WALSnapshotTransaction.swift +++ b/GRDB/Core/WALSnapshotTransaction.swift @@ -1,5 +1,4 @@ -// swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) /// A long-live read-only WAL transaction. /// /// `WALSnapshotTransaction` **takes ownership** of its reader diff --git a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift index a287a37675..a72c4aba8b 100644 --- a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift @@ -273,8 +273,7 @@ extension ValueConcurrentObserver { } } -// swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) extension ValueConcurrentObserver { /// Synchronously starts the observation, and returns the initial value. /// diff --git a/Package.swift b/Package.swift index a6f51175e1..b31da24a76 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import Foundation diff --git a/README.md b/README.md index a5a40d8fc0..0f54ad77c2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ **Latest release**: August 24, 2024 • [version 6.29.2](https://github.com/groue/GRDB.swift/tree/v6.29.2) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md) -**Requirements**: iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ • SQLite 3.19.3+ • Swift 5.7+ / Xcode 14+ +**Requirements**: iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ **Contact**: diff --git a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift index 2b1cf9394f..f2b42977cc 100644 --- a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift @@ -44,7 +44,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() } #endif } @@ -149,7 +149,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshotPool() } #endif } @@ -189,7 +189,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() } #endif } @@ -229,7 +229,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() } #endif } @@ -270,7 +270,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() } #endif } @@ -298,7 +298,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshotPool() } #endif } diff --git a/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift b/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift index d6229e88d8..1ac85a08fe 100644 --- a/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift +++ b/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift @@ -40,7 +40,7 @@ class DatabaseAbortedTransactionTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -85,7 +85,7 @@ class DatabaseAbortedTransactionTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } diff --git a/Tests/GRDBTests/DatabaseConfigurationTests.swift b/Tests/GRDBTests/DatabaseConfigurationTests.swift index 88fdb622b2..079286e7ea 100644 --- a/Tests/GRDBTests/DatabaseConfigurationTests.swift +++ b/Tests/GRDBTests/DatabaseConfigurationTests.swift @@ -27,7 +27,7 @@ class DatabaseConfigurationTests: GRDBTestCase { try pool.makeSnapshot().read { _ in } XCTAssertEqual(connectionCount, 5) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try pool.makeSnapshotPool().read { _ in } XCTAssertEqual(connectionCount, 6) #endif @@ -74,7 +74,7 @@ class DatabaseConfigurationTests: GRDBTestCase { XCTFail("Expected TestError") } catch is TestError { } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) do { error = TestError() _ = try pool.makeSnapshotPool() diff --git a/Tests/GRDBTests/DatabasePoolTests.swift b/Tests/GRDBTests/DatabasePoolTests.swift index 7580b1a347..440af6b6b4 100644 --- a/Tests/GRDBTests/DatabasePoolTests.swift +++ b/Tests/GRDBTests/DatabasePoolTests.swift @@ -41,7 +41,7 @@ class DatabasePoolTests: GRDBTestCase { XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal")) XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm")) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // A non-empty wal file makes sure ValueObservation can use wal snapshots. // See let walURL = URL(fileURLWithPath: dbPool.path + "-wal") @@ -65,7 +65,7 @@ class DatabasePoolTests: GRDBTestCase { XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal")) XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm")) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // A non-empty wal file makes sure ValueObservation can use wal snapshots. // See let walURL = URL(fileURLWithPath: dbPool.path + "-wal") @@ -91,7 +91,7 @@ class DatabasePoolTests: GRDBTestCase { XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal")) XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm")) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // A non-empty wal file makes sure ValueObservation can use wal snapshots. // See let walURL = URL(fileURLWithPath: dbPool.path + "-wal") @@ -114,7 +114,7 @@ class DatabasePoolTests: GRDBTestCase { XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal")) XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm")) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // A non-empty wal file makes sure ValueObservation can use wal snapshots. // See let walURL = URL(fileURLWithPath: dbPool.path + "-wal") diff --git a/Tests/GRDBTests/DatabaseReaderTests.swift b/Tests/GRDBTests/DatabaseReaderTests.swift index bf2d51c3f8..d8702a60a3 100644 --- a/Tests/GRDBTests/DatabaseReaderTests.swift +++ b/Tests/GRDBTests/DatabaseReaderTests.swift @@ -44,7 +44,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(setup(makeDatabaseQueue())) try test(setup(makeDatabasePool())) try test(setup(makeDatabasePool()).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(setup(makeDatabasePool()).makeSnapshotPool()) #endif } @@ -67,7 +67,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(setup(makeDatabaseQueue())) try await test(setup(makeDatabasePool())) try await test(setup(makeDatabasePool()).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(setup(makeDatabasePool()).makeSnapshotPool()) #endif } @@ -86,7 +86,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -106,7 +106,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) try await test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -130,7 +130,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(setup(makeDatabaseQueue())) try test(setup(makeDatabasePool())) try test(setup(makeDatabasePool()).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(setup(makeDatabasePool()).makeSnapshotPool()) #endif } @@ -153,7 +153,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(setup(makeDatabaseQueue())) try await test(setup(makeDatabasePool())) try await test(setup(makeDatabasePool()).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(setup(makeDatabasePool()).makeSnapshotPool()) #endif } @@ -177,7 +177,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(setup(makeDatabaseQueue())) try test(setup(makeDatabasePool())) try test(setup(makeDatabasePool()).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(setup(makeDatabasePool()).makeSnapshotPool()) #endif } @@ -197,7 +197,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -244,7 +244,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -273,7 +273,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -293,7 +293,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -313,7 +313,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -340,7 +340,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(setup(makeDatabaseQueue(configuration: Configuration()))) try test(setup(makeDatabasePool(configuration: Configuration()))) try test(setup(makeDatabasePool(configuration: Configuration())).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(setup(makeDatabasePool(configuration: Configuration())).makeSnapshotPool()) #endif } diff --git a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift index 6176865e8e..99ae96abdd 100644 --- a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift +++ b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift @@ -1,4 +1,4 @@ -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) import XCTest import GRDB diff --git a/Tests/GRDBTests/ValueObservationPrintTests.swift b/Tests/GRDBTests/ValueObservationPrintTests.swift index 51f57f5b6f..74e2450be1 100644 --- a/Tests/GRDBTests/ValueObservationPrintTests.swift +++ b/Tests/GRDBTests/ValueObservationPrintTests.swift @@ -54,7 +54,7 @@ class ValueObservationPrintTests: GRDBTestCase { try test(makeDatabaseQueue(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshotPool()) #endif } @@ -91,7 +91,7 @@ class ValueObservationPrintTests: GRDBTestCase { try test(makeDatabaseQueue(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshotPool()) #endif } @@ -129,7 +129,7 @@ class ValueObservationPrintTests: GRDBTestCase { try test(makeDatabaseQueue(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshotPool()) #endif } @@ -167,7 +167,7 @@ class ValueObservationPrintTests: GRDBTestCase { try test(makeDatabaseQueue(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshotPool()) #endif } diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index 7beeb4d6f5..fe2ccee22d 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -448,7 +448,7 @@ class ValueObservationTests: GRDBTestCase { } let expectedCounts: [Int] -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // Optimization available expectedCounts = [0, 1] #else @@ -498,7 +498,7 @@ class ValueObservationTests: GRDBTestCase { } let expectedCounts: [Int] -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // Optimization available expectedCounts = [0, 1] #else @@ -525,7 +525,7 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Snapshot Observation -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) func testDatabaseSnapshotPoolObservation() throws { let dbPool = try makeDatabasePool() try dbPool.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } @@ -1096,7 +1096,7 @@ class ValueObservationTests: GRDBTestCase { } let initialValueExpectation = self.expectation(description: "") -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) initialValueExpectation.assertForOverFulfill = true #else // ValueObservation on DatabasePool will notify the first value twice @@ -1154,7 +1154,7 @@ class ValueObservationTests: GRDBTestCase { } let initialValueExpectation = self.expectation(description: "") -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) initialValueExpectation.assertForOverFulfill = true #else // ValueObservation on DatabasePool will notify the first value twice diff --git a/Tests/SPM/PlainPackage/Package.swift b/Tests/SPM/PlainPackage/Package.swift index 3673c7afcb..578a011bc3 100644 --- a/Tests/SPM/PlainPackage/Package.swift +++ b/Tests/SPM/PlainPackage/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -9,6 +9,6 @@ let package = Package( .package(name: "GRDB", path: "../../.."), ], targets: [ - .target(name: "SPM", dependencies: ["GRDB"]), + .executableTarget(name: "SPM", dependencies: ["GRDB"]), ] ) From 29eb04c39a801000cb681a9e338a985e1511fe14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 15:04:22 +0200 Subject: [PATCH 002/160] [BREAKING] iOS 12+ --- GRDB.swift.podspec | 2 +- GRDB/Core/Database+Statements.swift | 2 +- GRDB/Core/Statement.swift | 2 +- GRDB/Documentation.docc/Extension/TransactionObserver.md | 3 --- GRDB/FTS/FTS5.swift | 2 +- README.md | 2 +- SQLiteCustom/GRDBDeploymentTarget.xcconfig | 2 +- Support/GRDBDeploymentTarget.xcconfig | 2 +- Tests/CocoaPods/GRDBiOS-framework/Podfile | 2 +- .../GRDBiOS-framework/iOS.xcodeproj/project.pbxproj | 8 ++++---- Tests/CocoaPods/GRDBiOS-static/Podfile | 2 +- .../GRDBiOS-static/iOS.xcodeproj/project.pbxproj | 4 ++-- 12 files changed, 15 insertions(+), 18 deletions(-) diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index d01661f93c..724bdc838b 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |s| s.module_name = 'GRDB' s.swift_versions = ['5.10'] - s.ios.deployment_target = '11.0' + s.ios.deployment_target = '12.0' s.osx.deployment_target = '10.13' s.watchos.deployment_target = '4.0' s.tvos.deployment_target = '11.0' diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index 061c5b3646..07ed9b5a81 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -538,7 +538,7 @@ struct StatementCache { let statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT)) #else let statement: Statement - if #available(iOS 12, macOS 10.14, watchOS 5, *) { // SQLite 3.20+ + if #available(macOS 10.14, watchOS 5, *) { // SQLite 3.20+ statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT)) } else { statement = try db.makeStatement(sql: sql) diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index e1a90a1082..6c99b4b92d 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -137,7 +137,7 @@ public final class Statement { database.sqliteConnection, statementStart, -1, prepFlags, &sqliteStatement, statementEnd) #else - if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) { // SQLite 3.20+ + if #available(macOS 10.14, tvOS 12, watchOS 5, *) { // SQLite 3.20+ code = sqlite3_prepare_v3( database.sqliteConnection, statementStart, -1, prepFlags, &sqliteStatement, statementEnd) diff --git a/GRDB/Documentation.docc/Extension/TransactionObserver.md b/GRDB/Documentation.docc/Extension/TransactionObserver.md index 9815e08520..72acd5e512 100644 --- a/GRDB/Documentation.docc/Extension/TransactionObserver.md +++ b/GRDB/Documentation.docc/Extension/TransactionObserver.md @@ -205,11 +205,8 @@ This extra API can be activated in two ways: 1. Use the GRDB.swift CocoaPod with a custom compilation option, as below. - It uses the system SQLite, which is compiled with `SQLITE_ENABLE_PREUPDATE_HOOK` support, but only on iOS 11.0+ (we don't know the minimum version of macOS, tvOS, watchOS): - ```ruby pod 'GRDB.swift' - platform :ios, '11.0' # or above post_install do |installer| installer.pods_project.targets.select { |target| target.name == "GRDB.swift" }.each do |target| diff --git a/GRDB/FTS/FTS5.swift b/GRDB/FTS/FTS5.swift index a18da5a3f0..bca5c4e5ae 100644 --- a/GRDB/FTS/FTS5.swift +++ b/GRDB/FTS/FTS5.swift @@ -121,7 +121,7 @@ public struct FTS5 { return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer) #else // GRDB is linked against the system SQLite. - if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) { // SQLite 3.20+ + if #available(macOS 10.14, tvOS 12, watchOS 5, *) { // SQLite 3.20+ return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer) } else { return api_v1(db) diff --git a/README.md b/README.md index 0f54ad77c2..1a8f790722 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ **Latest release**: August 24, 2024 • [version 6.29.2](https://github.com/groue/GRDB.swift/tree/v6.29.2) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md) -**Requirements**: iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ +**Requirements**: iOS 12.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ **Contact**: diff --git a/SQLiteCustom/GRDBDeploymentTarget.xcconfig b/SQLiteCustom/GRDBDeploymentTarget.xcconfig index 9042041251..a9ba8e8897 100644 --- a/SQLiteCustom/GRDBDeploymentTarget.xcconfig +++ b/SQLiteCustom/GRDBDeploymentTarget.xcconfig @@ -1,4 +1,4 @@ -IPHONEOS_DEPLOYMENT_TARGET = 11.0 +IPHONEOS_DEPLOYMENT_TARGET = 12.0 MACOSX_DEPLOYMENT_TARGET = 10.13 TVOS_DEPLOYMENT_TARGET = 11.0 WATCHOS_DEPLOYMENT_TARGET = 4.0 diff --git a/Support/GRDBDeploymentTarget.xcconfig b/Support/GRDBDeploymentTarget.xcconfig index 753467b40d..4e708e1de0 100644 --- a/Support/GRDBDeploymentTarget.xcconfig +++ b/Support/GRDBDeploymentTarget.xcconfig @@ -1,4 +1,4 @@ -IPHONEOS_DEPLOYMENT_TARGET = 11.0 +IPHONEOS_DEPLOYMENT_TARGET = 12.0 MACOSX_DEPLOYMENT_TARGET = 10.13 TVOS_DEPLOYMENT_TARGET = 11.0 WATCHOS_DEPLOYMENT_TARGET = 4.0 diff --git a/Tests/CocoaPods/GRDBiOS-framework/Podfile b/Tests/CocoaPods/GRDBiOS-framework/Podfile index 8a4c9e93a9..2c4299502a 100644 --- a/Tests/CocoaPods/GRDBiOS-framework/Podfile +++ b/Tests/CocoaPods/GRDBiOS-framework/Podfile @@ -1,6 +1,6 @@ use_frameworks! target 'iOS' -platform :ios, '11.0' +platform :ios, '12.0' pod 'GRDB.swift', :path => '../../..' post_install do |installer| diff --git a/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj b/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj index 4de5afc205..7a961bd71c 100644 --- a/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj @@ -251,7 +251,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -301,7 +301,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; @@ -318,7 +318,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; INFOPLIST_FILE = Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBTest; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -335,7 +335,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; INFOPLIST_FILE = Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBTest; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Tests/CocoaPods/GRDBiOS-static/Podfile b/Tests/CocoaPods/GRDBiOS-static/Podfile index 94614c25b1..3746f208fc 100644 --- a/Tests/CocoaPods/GRDBiOS-static/Podfile +++ b/Tests/CocoaPods/GRDBiOS-static/Podfile @@ -1,5 +1,5 @@ target 'iOS' -platform :ios, '11.0' +platform :ios, '12.0' pod 'GRDB.swift', :path => '../../..' post_install do |installer| diff --git a/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj b/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj index 6255545123..3ee741cb0d 100644 --- a/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj @@ -232,7 +232,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -282,7 +282,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; From 509cbb9881a875bdbd5983c7b4fb85e0a8c83894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 15:06:57 +0200 Subject: [PATCH 003/160] [BREAKING] tvOS 12+ --- GRDB.swift.podspec | 2 +- README.md | 2 +- SQLiteCustom/GRDBDeploymentTarget.xcconfig | 2 +- Support/GRDBDeploymentTarget.xcconfig | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index 724bdc838b..285b23b354 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.ios.deployment_target = '12.0' s.osx.deployment_target = '10.13' s.watchos.deployment_target = '4.0' - s.tvos.deployment_target = '11.0' + s.tvos.deployment_target = '12.0' s.default_subspec = 'standard' s.subspec 'standard' do |ss| diff --git a/README.md b/README.md index 1a8f790722..38f792764c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ **Latest release**: August 24, 2024 • [version 6.29.2](https://github.com/groue/GRDB.swift/tree/v6.29.2) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md) -**Requirements**: iOS 12.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ +**Requirements**: iOS 12.0+ / macOS 10.13+ / tvOS 12.0+ / watchOS 4.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ **Contact**: diff --git a/SQLiteCustom/GRDBDeploymentTarget.xcconfig b/SQLiteCustom/GRDBDeploymentTarget.xcconfig index a9ba8e8897..db069babcd 100644 --- a/SQLiteCustom/GRDBDeploymentTarget.xcconfig +++ b/SQLiteCustom/GRDBDeploymentTarget.xcconfig @@ -1,4 +1,4 @@ IPHONEOS_DEPLOYMENT_TARGET = 12.0 MACOSX_DEPLOYMENT_TARGET = 10.13 -TVOS_DEPLOYMENT_TARGET = 11.0 +TVOS_DEPLOYMENT_TARGET = 12.0 WATCHOS_DEPLOYMENT_TARGET = 4.0 diff --git a/Support/GRDBDeploymentTarget.xcconfig b/Support/GRDBDeploymentTarget.xcconfig index 4e708e1de0..16b2393c48 100644 --- a/Support/GRDBDeploymentTarget.xcconfig +++ b/Support/GRDBDeploymentTarget.xcconfig @@ -1,6 +1,6 @@ IPHONEOS_DEPLOYMENT_TARGET = 12.0 MACOSX_DEPLOYMENT_TARGET = 10.13 -TVOS_DEPLOYMENT_TARGET = 11.0 +TVOS_DEPLOYMENT_TARGET = 12.0 WATCHOS_DEPLOYMENT_TARGET = 4.0 OTHER_SWIFT_FLAGS = $(inherited) -D SQLITE_ENABLE_FTS5 From 79cb862478915bc2a683f9a545b9a07d6833bef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 15:14:54 +0200 Subject: [PATCH 004/160] [BREAKING] watchOS 7+ Rationale: https://github.com/groue/GRDB.swift/issues/1472#issuecomment-1960696836 --- GRDB.swift.podspec | 2 +- GRDB/Core/Database+Statements.swift | 2 +- GRDB/Core/DatabasePublishers.swift | 2 +- GRDB/Core/DatabaseReader.swift | 10 +++---- GRDB/Core/DatabaseRegionObservation.swift | 6 ++--- GRDB/Core/DatabaseWriter.swift | 24 ++++++++--------- GRDB/Core/Statement.swift | 2 +- GRDB/Dump/DumpFormats/JSONDumpFormat.swift | 2 +- GRDB/FTS/FTS3.swift | 2 +- GRDB/FTS/FTS5.swift | 4 +-- GRDB/Fixits.swift | 6 ++--- GRDB/Migration/DatabaseMigrator.swift | 4 +-- .../Request/QueryInterfaceRequest.swift | 2 +- .../Request/RequestProtocols.swift | 2 +- GRDB/QueryInterface/SQL/SQLExpression.swift | 6 ++--- GRDB/QueryInterface/SQL/SQLFunctions.swift | 10 +++---- GRDB/QueryInterface/SQL/Table.swift | 6 ++--- .../Schema/TableAlteration.swift | 2 +- .../TableRecord+QueryInterfaceRequest.swift | 2 +- GRDB/Record/FetchableRecord+TableRecord.swift | 4 +-- GRDB/Record/TableRecord.swift | 6 ++--- GRDB/Utils/OnDemandFuture.swift | 4 +-- GRDB/Utils/ReceiveValuesOn.swift | 6 ++--- .../SharedValueObservation.swift | 4 +-- GRDB/ValueObservation/ValueObservation.swift | 8 +++--- README.md | 2 +- SQLiteCustom/GRDBDeploymentTarget.xcconfig | 2 +- Support/GRDBDeploymentTarget.xcconfig | 2 +- .../AvailableElements.swift | 2 +- .../PublisherExpectations/Finished.swift | 2 +- .../PublisherExpectations/Inverted.swift | 2 +- .../PublisherExpectations/Map.swift | 4 +-- .../PublisherExpectations/Next.swift | 2 +- .../PublisherExpectations/NextOne.swift | 2 +- .../PublisherExpectations/Prefix.swift | 2 +- .../PublisherExpectations/Recording.swift | 2 +- Tests/CombineExpectations/Recorder.swift | 8 +++--- .../DatabaseReaderReadPublisherTests.swift | 12 ++++----- ...abaseRegionObservationPublisherTests.swift | 4 +-- .../DatabaseWriterWritePublisherTests.swift | 26 +++++++++---------- Tests/GRDBCombineTests/Support.swift | 6 ++--- .../ValueObservationPublisherTests.swift | 24 ++++++++--------- .../DatabaseDataEncodingStrategyTests.swift | 8 +++--- .../DatabaseDateEncodingStrategyTests.swift | 8 +++--- Tests/GRDBTests/DatabaseDumpTests.swift | 2 +- Tests/GRDBTests/DatabaseMigratorTests.swift | 12 ++++----- Tests/GRDBTests/DatabaseReaderTests.swift | 6 ++--- .../DatabaseRegionObservationTests.swift | 2 +- .../GRDBTests/DatabaseSnapshotPoolTests.swift | 2 +- .../DatabaseUUIDEncodingStrategyTests.swift | 8 +++--- Tests/GRDBTests/DatabaseWriterTests.swift | 16 ++++++------ Tests/GRDBTests/FTS3TableBuilderTests.swift | 2 +- Tests/GRDBTests/FTS5TableBuilderTests.swift | 2 +- Tests/GRDBTests/JoinSupportTests.swift | 6 ++--- .../QueryInterfaceExpressionsTests.swift | 10 +++---- .../QueryInterfaceRequestTests.swift | 8 +++--- ...imalNonOptionalPrimaryKeySingleTests.swift | 20 +++++++------- .../RecordMinimalPrimaryKeyRowIDTests.swift | 20 +++++++------- .../RecordMinimalPrimaryKeySingleTests.swift | 20 +++++++------- .../RecordPrimaryKeyHiddenRowIDTests.swift | 20 +++++++------- .../SharedValueObservationTests.swift | 10 +++---- Tests/GRDBTests/TableDefinitionTests.swift | 4 +-- ...bleRecord+QueryInterfaceRequestTests.swift | 4 +-- Tests/GRDBTests/TableRecordDeleteTests.swift | 18 ++++++------- Tests/GRDBTests/TableRecordUpdateTests.swift | 4 +-- Tests/GRDBTests/TableTests.swift | 12 ++++----- Tests/GRDBTests/ValueObservationTests.swift | 12 ++++----- 67 files changed, 234 insertions(+), 234 deletions(-) diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index 285b23b354..932a8867ee 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -12,7 +12,7 @@ Pod::Spec.new do |s| s.swift_versions = ['5.10'] s.ios.deployment_target = '12.0' s.osx.deployment_target = '10.13' - s.watchos.deployment_target = '4.0' + s.watchos.deployment_target = '7.0' s.tvos.deployment_target = '12.0' s.default_subspec = 'standard' diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index 07ed9b5a81..9159140f2d 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -538,7 +538,7 @@ struct StatementCache { let statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT)) #else let statement: Statement - if #available(macOS 10.14, watchOS 5, *) { // SQLite 3.20+ + if #available(macOS 10.14, *) { // SQLite 3.20+ statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT)) } else { statement = try db.makeStatement(sql: sql) diff --git a/GRDB/Core/DatabasePublishers.swift b/GRDB/Core/DatabasePublishers.swift index 8f8054953b..88c0c48067 100644 --- a/GRDB/Core/DatabasePublishers.swift +++ b/GRDB/Core/DatabasePublishers.swift @@ -1,5 +1,5 @@ #if canImport(Combine) /// A namespace for database Combine publishers. -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) public enum DatabasePublishers { } #endif diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 5fac0df44e..ca694be0d7 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -458,7 +458,7 @@ extension DatabaseReader { /// - parameter value: A closure which accesses the database. /// - throws: The error thrown by `value`, or any ``DatabaseError`` that /// would happen while establishing the database access. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func read(_ value: @Sendable @escaping (Database) throws -> T) async throws -> T { try await withUnsafeThrowingContinuation { continuation in asyncRead { result in @@ -504,7 +504,7 @@ extension DatabaseReader { /// - parameter value: A closure which accesses the database. /// - throws: The error thrown by `value`, or any ``DatabaseError`` that /// would happen while establishing the database access. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func unsafeRead(_ value: @Sendable @escaping (Database) throws -> T) async throws -> T { try await withUnsafeThrowingContinuation { continuation in asyncUnsafeRead { result in @@ -549,7 +549,7 @@ extension DatabaseReader { /// /// - parameter scheduler: A Combine Scheduler. /// - parameter value: A closure which accesses the database. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func readPublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, value: @escaping (Database) throws -> Output) @@ -567,7 +567,7 @@ extension DatabaseReader { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension DatabasePublishers { /// A publisher that reads from the database. /// @@ -586,7 +586,7 @@ extension DatabasePublishers { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Publisher where Failure == Error { fileprivate func eraseToReadPublisher() -> DatabasePublishers.Read { .init(upstream: eraseToAnyPublisher()) diff --git a/GRDB/Core/DatabaseRegionObservation.swift b/GRDB/Core/DatabaseRegionObservation.swift index 4f1e1473c0..242214bc44 100644 --- a/GRDB/Core/DatabaseRegionObservation.swift +++ b/GRDB/Core/DatabaseRegionObservation.swift @@ -128,7 +128,7 @@ extension DatabaseRegionObservation { } #if canImport(Combine) -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension DatabaseRegionObservation { // MARK: - Publishing Impactful Transactions @@ -140,7 +140,7 @@ extension DatabaseRegionObservation { /// /// Do not reschedule the publisher with `receive(on:options:)` or any /// `Publisher` method that schedules publisher elements. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func publisher(in writer: some DatabaseWriter) -> DatabasePublishers.DatabaseRegion { DatabasePublishers.DatabaseRegion(self, in: writer) } @@ -186,7 +186,7 @@ private class DatabaseRegionObserver: TransactionObserver { } #if canImport(Combine) -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension DatabasePublishers { /// A publisher that tracks transactions that modify a database region. /// diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index b4de8f6fd8..87937214ad 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -541,7 +541,7 @@ extension DatabaseWriter { /// /// - Parameter filePath: file path for new database @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) + @available(iOS 14, macOS 10.16, tvOS 14, *) public func vacuum(into filePath: String) throws { try writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -607,7 +607,7 @@ extension DatabaseWriter { /// - throws: The error thrown by `updates`, or any ``DatabaseError`` that /// would happen while establishing the database access or committing /// the transaction. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func write(_ updates: @Sendable @escaping (Database) throws -> T) async throws -> T { try await withUnsafeThrowingContinuation { continuation in asyncWrite(updates, completion: { _, result in @@ -645,7 +645,7 @@ extension DatabaseWriter { /// /// - parameter updates: A closure which accesses the database. /// - throws: The error thrown by `updates`. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func writeWithoutTransaction(_ updates: @Sendable @escaping (Database) throws -> T) async throws -> T { try await withUnsafeThrowingContinuation { continuation in asyncWriteWithoutTransaction { db in @@ -697,7 +697,7 @@ extension DatabaseWriter { /// /// - parameter updates: A closure which accesses the database. /// - throws: The error thrown by `updates`. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func barrierWriteWithoutTransaction( _ updates: @Sendable @escaping (Database) throws -> T) async throws -> T @@ -712,7 +712,7 @@ extension DatabaseWriter { /// Erase the database: delete all content, drop all tables, etc. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func erase() async throws { try await writeWithoutTransaction { try $0.erase() } } @@ -723,7 +723,7 @@ extension DatabaseWriter { /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// Related SQLite documentation: - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func vacuum() async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM") } } @@ -740,7 +740,7 @@ extension DatabaseWriter { /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -755,7 +755,7 @@ extension DatabaseWriter { /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -800,7 +800,7 @@ extension DatabaseWriter { /// /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which accesses the database. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, updates: @escaping (Database) throws -> Output) @@ -865,7 +865,7 @@ extension DatabaseWriter { /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which writes in the database. /// - parameter value: A closure which reads from the database. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func writePublisher( receiveOn scheduler: S = DispatchQueue.main, updates: @escaping (Database) throws -> T, @@ -897,7 +897,7 @@ extension DatabaseWriter { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension DatabasePublishers { /// A publisher that writes into the database. /// @@ -916,7 +916,7 @@ extension DatabasePublishers { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Publisher where Failure == Error { fileprivate func eraseToWritePublisher() -> DatabasePublishers.Write { .init(upstream: self.eraseToAnyPublisher()) diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index 6c99b4b92d..b649d395c9 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -137,7 +137,7 @@ public final class Statement { database.sqliteConnection, statementStart, -1, prepFlags, &sqliteStatement, statementEnd) #else - if #available(macOS 10.14, tvOS 12, watchOS 5, *) { // SQLite 3.20+ + if #available(macOS 10.14, tvOS 12, *) { // SQLite 3.20+ code = sqlite3_prepare_v3( database.sqliteConnection, statementStart, -1, prepFlags, &sqliteStatement, statementEnd) diff --git a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift index 7e3bc61d5b..661e2d071d 100644 --- a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift @@ -43,7 +43,7 @@ public struct JSONDumpFormat: Sendable { public static var defaultEncoder: JSONEncoder { // This encoder MUST NOT CHANGE, because some people rely on this format. let encoder = JSONEncoder() - if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) { + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, *) { encoder.outputFormatting = .withoutEscapingSlashes } encoder.nonConformingFloatEncodingStrategy = .convertToString( diff --git a/GRDB/FTS/FTS3.swift b/GRDB/FTS/FTS3.swift index 80e677ae18..26f0643d11 100644 --- a/GRDB/FTS/FTS3.swift +++ b/GRDB/FTS/FTS3.swift @@ -47,7 +47,7 @@ public struct FTS3 { #elseif !GRDBCIPHER /// Remove diacritics from Latin script characters. This option matches /// the `remove_diacritics=2` tokenizer argument. - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.27+ + @available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.27+ case remove #endif } diff --git a/GRDB/FTS/FTS5.swift b/GRDB/FTS/FTS5.swift index bca5c4e5ae..b8b82d10c6 100644 --- a/GRDB/FTS/FTS5.swift +++ b/GRDB/FTS/FTS5.swift @@ -60,7 +60,7 @@ public struct FTS5 { /// Remove diacritics from Latin script characters. This /// option matches the raw "remove_diacritics=2" tokenizer argument, /// available from SQLite 3.27.0 - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.27+ + @available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.27+ case remove #endif } @@ -121,7 +121,7 @@ public struct FTS5 { return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer) #else // GRDB is linked against the system SQLite. - if #available(macOS 10.14, tvOS 12, watchOS 5, *) { // SQLite 3.20+ + if #available(macOS 10.14, tvOS 12, *) { // SQLite 3.20+ return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer) } else { return api_v1(db) diff --git a/GRDB/Fixits.swift b/GRDB/Fixits.swift index ed82d514de..516c650bb4 100644 --- a/GRDB/Fixits.swift +++ b/GRDB/Fixits.swift @@ -119,7 +119,7 @@ extension PersistableRecord { public func performSave(_ db: Database) throws { preconditionFailure() } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension QueryInterfaceRequest where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public func selectID() -> QueryInterfaceRequest { preconditionFailure() } @@ -144,13 +144,13 @@ extension SelectionRequest { @available(*, unavailable, renamed: "SQLExpression.AssociativeBinaryOperator") public typealias SQLAssociativeBinaryOperator = SQLExpression.AssociativeBinaryOperator -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public func selectID() -> QueryInterfaceRequest { preconditionFailure() } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public static func selectID() -> QueryInterfaceRequest { preconditionFailure() } diff --git a/GRDB/Migration/DatabaseMigrator.swift b/GRDB/Migration/DatabaseMigrator.swift index f4de96c69e..2bb9d8517f 100644 --- a/GRDB/Migration/DatabaseMigrator.swift +++ b/GRDB/Migration/DatabaseMigrator.swift @@ -496,7 +496,7 @@ extension DatabaseMigrator { /// - parameter writer: A DatabaseWriter. /// where migrations should apply. /// - parameter scheduler: A Combine Scheduler. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func migratePublisher( _ writer: some DatabaseWriter, receiveOn scheduler: some Scheduler = DispatchQueue.main) @@ -514,7 +514,7 @@ extension DatabaseMigrator { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension DatabasePublishers { /// A publisher that migrates a database. /// diff --git a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift index 1d0d7cdadd..54826e1488 100644 --- a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift @@ -646,7 +646,7 @@ extension QueryInterfaceRequest { /// - parameter db: A database connection. /// - returns: A set of deleted ids. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) // Identifiable + @available(iOS 13, macOS 10.15, tvOS 13, *) // Identifiable public func deleteAndFetchIds(_ db: Database) throws -> Set where RowDecoder: TableRecord & Identifiable, diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index def5c2f52c..71413dda7f 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -583,7 +583,7 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension TableRequest where Self: FilteredRequest, Self: TypedRequest, diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift index eb63e59c40..eb4d58ceb4 100644 --- a/GRDB/QueryInterface/SQL/SQLExpression.swift +++ b/GRDB/QueryInterface/SQL/SQLExpression.swift @@ -1989,7 +1989,7 @@ struct SQLAggregateFunctionInvocation { var arguments: [SQLExpression] var isDistinct = false var ordering: SQLOrdering? = nil // SQLite 3.44.0+ - var filter: SQLExpression? = nil // @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) SQLite 3.30+ + var filter: SQLExpression? = nil // @available(iOS 14, macOS 10.16, tvOS 14, *) SQLite 3.30+ /// A boolean value indicating if a function is known to return a /// JSON value. @@ -2329,13 +2329,13 @@ extension SQLSpecificExpressible { } #elseif !GRDBCIPHER /// An ordering term for ascending order (nulls last). - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ + @available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public var ascNullsLast: SQLOrdering { .ascNullsLast(sqlExpression) } /// An ordering term for descending order (nulls first). - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ + @available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public var descNullsFirst: SQLOrdering { .descNullsFirst(sqlExpression) } diff --git a/GRDB/QueryInterface/SQL/SQLFunctions.swift b/GRDB/QueryInterface/SQL/SQLFunctions.swift index 08a1a39ea8..fab42b8e42 100644 --- a/GRDB/QueryInterface/SQL/SQLFunctions.swift +++ b/GRDB/QueryInterface/SQL/SQLFunctions.swift @@ -34,7 +34,7 @@ public func average( /// // AVG(length) FILTER (WHERE length > 0) /// average(Column("length"), filter: Column("length") > 0) /// ``` -@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +@available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public func average( _ value: some SQLSpecificExpressible, filter: some SQLSpecificExpressible) @@ -145,7 +145,7 @@ public func max( /// // MAX(score) FILTER (WHERE score < 0) /// max(Column("score"), filter: Column("score") < 0) /// ``` -@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +@available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public func max( _ value: some SQLSpecificExpressible, filter: some SQLSpecificExpressible) @@ -190,7 +190,7 @@ public func min( /// // MIN(score) FILTER (WHERE score > 0) /// min(Column("score"), filter: Column("score") > 0) /// ``` -@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +@available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public func min( _ value: some SQLSpecificExpressible, filter: some SQLSpecificExpressible) @@ -248,7 +248,7 @@ public func sum( /// See also ``total(_:)``. /// /// Related SQLite documentation: . -@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +@available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public func sum( _ value: some SQLSpecificExpressible, filter: some SQLSpecificExpressible) @@ -312,7 +312,7 @@ public func total( /// See also ``total(_:)``. /// /// Related SQLite documentation: . -@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +@available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public func total( _ value: some SQLSpecificExpressible, filter: some SQLSpecificExpressible) diff --git a/GRDB/QueryInterface/SQL/Table.swift b/GRDB/QueryInterface/SQL/Table.swift index c6de851c74..86948aa7bb 100644 --- a/GRDB/QueryInterface/SQL/Table.swift +++ b/GRDB/QueryInterface/SQL/Table.swift @@ -723,7 +723,7 @@ extension Table { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { /// Returns a request filtered by primary key. /// @@ -1546,7 +1546,7 @@ extension Table { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible @@ -1688,7 +1688,7 @@ extension Table { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible diff --git a/GRDB/QueryInterface/Schema/TableAlteration.swift b/GRDB/QueryInterface/Schema/TableAlteration.swift index c731e7ea54..a61e6ba8d9 100644 --- a/GRDB/QueryInterface/Schema/TableAlteration.swift +++ b/GRDB/QueryInterface/Schema/TableAlteration.swift @@ -129,7 +129,7 @@ public final class TableAlteration { /// /// - parameter name: the old name of the column. /// - parameter newName: the new name of the column. - @available(iOS 13, tvOS 13, watchOS 6, *) // SQLite 3.25+ + @available(iOS 13, tvOS 13, *) // SQLite 3.25+ public func rename(column name: String, to newName: String) { _rename(column: name, to: newName) } diff --git a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift index 8044373f30..74b802feb1 100644 --- a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift @@ -605,7 +605,7 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns a request filtered by primary key. /// diff --git a/GRDB/Record/FetchableRecord+TableRecord.swift b/GRDB/Record/FetchableRecord+TableRecord.swift index 97178adebe..be0b839265 100644 --- a/GRDB/Record/FetchableRecord+TableRecord.swift +++ b/GRDB/Record/FetchableRecord+TableRecord.swift @@ -217,7 +217,7 @@ extension FetchableRecord where Self: TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseValueConvertible { // MARK: Fetching by Single-Column Primary Key @@ -359,7 +359,7 @@ extension FetchableRecord where Self: TableRecord & Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension FetchableRecord where Self: TableRecord & Hashable & Identifiable, ID: DatabaseValueConvertible { /// Returns a set of records identified by their primary keys. /// diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index 4e8d8b1b4c..bf6b1a58e9 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -318,7 +318,7 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns whether a record exists for this primary key. /// @@ -454,7 +454,7 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Deletes records identified by their primary keys, and returns the number /// of deleted records. @@ -740,7 +740,7 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns an error for a record that does not exist in the database. /// diff --git a/GRDB/Utils/OnDemandFuture.swift b/GRDB/Utils/OnDemandFuture.swift index 6826014a47..e1d8dff6ca 100644 --- a/GRDB/Utils/OnDemandFuture.swift +++ b/GRDB/Utils/OnDemandFuture.swift @@ -14,7 +14,7 @@ import Foundation /// Both two extra scheduling guarantees are used by GRDB in order to be /// able to spawn concurrent database reads right from the database writer /// queue, and fulfill GRDB preconditions. -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) struct OnDemandFuture: Publisher { typealias Promise = (Result) -> Void typealias Output = Output @@ -33,7 +33,7 @@ struct OnDemandFuture: Publisher { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) private class OnDemandFutureSubscription: Subscription { typealias Promise = (Result) -> Void diff --git a/GRDB/Utils/ReceiveValuesOn.swift b/GRDB/Utils/ReceiveValuesOn.swift index abc688f78a..7cbbde7c12 100644 --- a/GRDB/Utils/ReceiveValuesOn.swift +++ b/GRDB/Utils/ReceiveValuesOn.swift @@ -11,7 +11,7 @@ import Foundation /// This scheduling guarantee is used by GRDB in order to be able /// to make promises on the scheduling of database values without surprising /// the users as in . -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) struct ReceiveValuesOn: Publisher { typealias Output = Upstream.Output typealias Failure = Upstream.Failure @@ -30,7 +30,7 @@ struct ReceiveValuesOn: Publisher { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) private class ReceiveValuesOnSubscription: Subscription, Subscriber where Upstream: Publisher, @@ -211,7 +211,7 @@ where } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Publisher { /// Specifies the scheduler on which to receive values from the publisher /// diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index 0a0e742ae3..e58d01885c 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -291,7 +291,7 @@ public final class SharedValueObservation { /// print("fresh players: \(players)") /// } /// ``` - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func publisher() -> DatabasePublishers.Value { DatabasePublishers.Value { onError, onChange in self.start(onError: onError, onChange: onChange) @@ -368,7 +368,7 @@ extension SharedValueObservation { /// print("Fresh players: \(players)") /// } /// ``` - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func values(bufferingPolicy: AsyncValueObservation.BufferingPolicy = .unbounded) -> AsyncValueObservation { diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index 37c313b20a..26f6de0755 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -296,7 +296,7 @@ extension ValueObservation { /// - parameter reader: A DatabaseReader. /// - parameter scheduler: A ValueObservationScheduler. By default, fresh /// values are dispatched asynchronously on the main dispatch queue. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func values( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main), @@ -332,7 +332,7 @@ extension ValueObservation { /// /// You build an `AsyncValueObservation` from ``ValueObservation`` or /// ``SharedValueObservation``. -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) public struct AsyncValueObservation: AsyncSequence { public typealias BufferingPolicy = AsyncThrowingStream.Continuation.BufferingPolicy public typealias AsyncIterator = Iterator @@ -434,7 +434,7 @@ extension ValueObservation { /// - parameter scheduler: A ValueObservationScheduler. By default, fresh /// values are dispatched asynchronously on the main dispatch queue. /// - returns: A Combine publisher - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) public func publisher( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main)) @@ -451,7 +451,7 @@ extension ValueObservation { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension DatabasePublishers { /// A publisher that publishes the values of a ``ValueObservation``. /// diff --git a/README.md b/README.md index 38f792764c..f02c02a46e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ **Latest release**: August 24, 2024 • [version 6.29.2](https://github.com/groue/GRDB.swift/tree/v6.29.2) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md) -**Requirements**: iOS 12.0+ / macOS 10.13+ / tvOS 12.0+ / watchOS 4.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ +**Requirements**: iOS 12.0+ / macOS 10.13+ / tvOS 12.0+ / watchOS 7.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ **Contact**: diff --git a/SQLiteCustom/GRDBDeploymentTarget.xcconfig b/SQLiteCustom/GRDBDeploymentTarget.xcconfig index db069babcd..3006917366 100644 --- a/SQLiteCustom/GRDBDeploymentTarget.xcconfig +++ b/SQLiteCustom/GRDBDeploymentTarget.xcconfig @@ -1,4 +1,4 @@ IPHONEOS_DEPLOYMENT_TARGET = 12.0 MACOSX_DEPLOYMENT_TARGET = 10.13 TVOS_DEPLOYMENT_TARGET = 12.0 -WATCHOS_DEPLOYMENT_TARGET = 4.0 +WATCHOS_DEPLOYMENT_TARGET = 7.0 diff --git a/Support/GRDBDeploymentTarget.xcconfig b/Support/GRDBDeploymentTarget.xcconfig index 16b2393c48..50434f84d7 100644 --- a/Support/GRDBDeploymentTarget.xcconfig +++ b/Support/GRDBDeploymentTarget.xcconfig @@ -1,7 +1,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 12.0 MACOSX_DEPLOYMENT_TARGET = 10.13 TVOS_DEPLOYMENT_TARGET = 12.0 -WATCHOS_DEPLOYMENT_TARGET = 4.0 +WATCHOS_DEPLOYMENT_TARGET = 7.0 OTHER_SWIFT_FLAGS = $(inherited) -D SQLITE_ENABLE_FTS5 //// Compile with all opt-in APIs diff --git a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift index 402b3f5535..b32fdbde49 100644 --- a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift +++ b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the timeout to expire, or /// the recorded publisher to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Finished.swift b/Tests/CombineExpectations/PublisherExpectations/Finished.swift index 0a47d09f62..5f866e7c9e 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Finished.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Finished.swift @@ -17,7 +17,7 @@ import XCTest // try wait(for: recorder.finished.inverted, timeout: 1) // } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Inverted.swift b/Tests/CombineExpectations/PublisherExpectations/Inverted.swift index c7f8d72cc3..6900f834ed 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Inverted.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Inverted.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation that fails if the base expectation is fulfilled. /// diff --git a/Tests/CombineExpectations/PublisherExpectations/Map.swift b/Tests/CombineExpectations/PublisherExpectations/Map.swift index ab4f95c733..0c6a22c4cf 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Map.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Map.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation that transforms the value of a base expectation. /// @@ -20,7 +20,7 @@ extension PublisherExpectations { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension PublisherExpectation { /// Returns a publisher expectation that transforms the value of the /// base expectation. diff --git a/Tests/CombineExpectations/PublisherExpectations/Next.swift b/Tests/CombineExpectations/PublisherExpectations/Next.swift index 76ad0c1055..70e73836fe 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Next.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Next.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `count` elements, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/NextOne.swift b/Tests/CombineExpectations/PublisherExpectations/NextOne.swift index 84ee6233e5..6bc8ff10ed 100644 --- a/Tests/CombineExpectations/PublisherExpectations/NextOne.swift +++ b/Tests/CombineExpectations/PublisherExpectations/NextOne.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// one element, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Prefix.swift b/Tests/CombineExpectations/PublisherExpectations/Prefix.swift index 11aced69da..f3fcd20538 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Prefix.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Prefix.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `maxLength` elements, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Recording.swift b/Tests/CombineExpectations/PublisherExpectations/Recording.swift index 0b95292dc0..2327188f69 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Recording.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Recording.swift @@ -2,7 +2,7 @@ import Combine import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. diff --git a/Tests/CombineExpectations/Recorder.swift b/Tests/CombineExpectations/Recorder.swift index 0d7031aaf1..3d5be6f7b7 100644 --- a/Tests/CombineExpectations/Recorder.swift +++ b/Tests/CombineExpectations/Recorder.swift @@ -13,7 +13,7 @@ import XCTest /// /// let elements = try wait(for: recorder.elements, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) public class Recorder: Subscriber { public typealias Input = Input public typealias Failure = Failure @@ -287,7 +287,7 @@ public class Recorder: Subscriber { // MARK: - Publisher Expectations -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// The type of the publisher expectation returned by `Recorder.completion`. public typealias Completion = Map, Subscribers.Completion> @@ -302,7 +302,7 @@ extension PublisherExpectations { public typealias Single = Map, Input> } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Recorder { /// Returns a publisher expectation which waits for the timeout to expire, /// or the recorded publisher to complete. @@ -584,7 +584,7 @@ extension Recorder { // MARK: - Publisher + Recorder -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Publisher { /// Returns a subscribed Recorder. /// diff --git a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift index f2b42977cc..56cb6d1e15 100644 --- a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift @@ -22,7 +22,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -128,7 +128,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // frame #71: 0x00007fff72311cc9 libdyld.dylib`start + 1 // frame #72: 0x00007fff72311cc9 libdyld.dylib`start + 1 func testReadPublisherError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -157,7 +157,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -197,7 +197,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherDefaultScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -237,7 +237,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherCustomScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -278,7 +278,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherIsReadonly() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift index e126c54b39..8c5e870f1c 100644 --- a/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift @@ -20,7 +20,7 @@ private struct Player: Codable, FetchableRecord, PersistableRecord { class DatabaseRegionObservationPublisherTests : XCTestCase { func testChangesNotifications() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -61,7 +61,7 @@ class DatabaseRegionObservationPublisherTests : XCTestCase { // TODO: do the same, but asynchronously. If this is too hard, update the // public API so that users can easily do it. func testPrependInitialDatabaseSync() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift b/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift index 1ab1420818..cc3903dabf 100644 --- a/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift @@ -22,7 +22,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -49,7 +49,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherValue() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -76,7 +76,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -99,7 +99,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { } func testWritePublisherErrorRollbacksTransaction() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -132,7 +132,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -168,7 +168,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherDefaultScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -206,7 +206,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherCustomScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -247,7 +247,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // TODO: Fix flaky test with both pool and on-disk queue: // - Expectation timeout func testWriteThenReadPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -274,7 +274,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWriteThenReadPublisherIsReadonly() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -299,7 +299,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWriteThenReadPublisherWriteError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -322,7 +322,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { } func testWriteThenReadPublisherWriteErrorRollbacksTransaction() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -359,7 +359,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // TODO: Fix flaky test with both pool and on-disk queue: // - Expectation timeout func testWriteThenReadPublisherReadError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -386,7 +386,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // Regression test against deadlocks created by concurrent completion // and cancellations triggered by .switchToLatest().prefix(1) func testDeadlockPrevention() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBCombineTests/Support.swift b/Tests/GRDBCombineTests/Support.swift index d8f2ec8381..0805d0bc07 100644 --- a/Tests/GRDBCombineTests/Support.swift +++ b/Tests/GRDBCombineTests/Support.swift @@ -51,7 +51,7 @@ final class Test { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) final class AsyncTest { // Raise the repeatCount in order to help spotting flaky tests. private let repeatCount: Int @@ -100,7 +100,7 @@ final class AsyncTest { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) public func assertNoFailure( _ completion: Subscribers.Completion, file: StaticString = #file, @@ -111,7 +111,7 @@ public func assertNoFailure( } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) public func assertFailure( _ completion: Subscribers.Completion, file: StaticString = #file, diff --git a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift index b55fd98c6d..fff31440fa 100644 --- a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift @@ -22,7 +22,7 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Default Scheduler func testDefaultSchedulerChangesNotifications() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -64,7 +64,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDefaultSchedulerFirstValueIsEmittedAsynchronously() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -97,7 +97,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDefaultSchedulerError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -123,7 +123,7 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Immediate Scheduler func testImmediateSchedulerChangesNotifications() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -165,7 +165,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testImmediateSchedulerEmitsFirstValueSynchronously() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -201,7 +201,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testImmediateSchedulerError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -226,7 +226,7 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Demand - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) private class DemandSubscriber: Subscriber { private var subscription: Subscription? let subject = PassthroughSubject() @@ -257,7 +257,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandNoneReceivesNoElement() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -292,7 +292,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandOneReceivesOneElement() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -330,7 +330,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandOneDoesNotReceiveTwoElements() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -372,7 +372,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandTwoReceivesTwoElements() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -418,7 +418,7 @@ class ValueObservationPublisherTests : XCTestCase { /// Regression test for https://github.com/groue/GRDB.swift/issues/1194 func testIssue1194() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift index 6fdf7d8759..b9219589f4 100644 --- a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift @@ -23,7 +23,7 @@ private struct RecordWithData: EncodableRecord, Enco var data: Data } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension RecordWithData: Identifiable { var id: Data { data } } @@ -33,7 +33,7 @@ private struct RecordWithOptionalData: EncodableReco var data: Data? } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension RecordWithOptionalData: Identifiable { var id: Data? { data } } @@ -150,7 +150,7 @@ extension DatabaseDataEncodingStrategyTests { } func testFilterID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } @@ -230,7 +230,7 @@ extension DatabaseDataEncodingStrategyTests { } func testDeleteID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } diff --git a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift index 98b1ab605b..6db6c0b4cc 100644 --- a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift @@ -50,7 +50,7 @@ private struct RecordWithDate: EncodableRecord, Enco var date: Date } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension RecordWithDate: Identifiable { var id: Date { date } } @@ -60,7 +60,7 @@ private struct RecordWithOptionalDate: EncodableReco var date: Date? } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension RecordWithOptionalDate: Identifiable { var id: Date? { date } } @@ -260,7 +260,7 @@ extension DatabaseDateEncodingStrategyTests { } func testFilterID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } @@ -340,7 +340,7 @@ extension DatabaseDateEncodingStrategyTests { } func testDeleteID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } diff --git a/Tests/GRDBTests/DatabaseDumpTests.swift b/Tests/GRDBTests/DatabaseDumpTests.swift index 06d86ccb22..ad64862fe0 100644 --- a/Tests/GRDBTests/DatabaseDumpTests.swift +++ b/Tests/GRDBTests/DatabaseDumpTests.swift @@ -229,7 +229,7 @@ final class DatabaseDumpTests: GRDBTestCase { // MARK: - JSON func test_json_value_formatting() throws { - guard #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) else { + guard #available(iOS 13.0, macOS 10.15, tvOS 13.0, *) else { throw XCTSkip("Skip because this test relies on JSONEncoder.OutputFormatting.withoutEscapingSlashes") } diff --git a/Tests/GRDBTests/DatabaseMigratorTests.swift b/Tests/GRDBTests/DatabaseMigratorTests.swift index 9f98158249..c117b5729f 100644 --- a/Tests/GRDBTests/DatabaseMigratorTests.swift +++ b/Tests/GRDBTests/DatabaseMigratorTests.swift @@ -41,7 +41,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testEmptyMigratorPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -153,7 +153,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testNonEmptyMigratorPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -209,7 +209,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testEmptyMigratorPublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -235,7 +235,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testNonEmptyMigratorPublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -262,7 +262,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testMigratorPublisherDefaultScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -291,7 +291,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testMigratorPublisherCustomScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBTests/DatabaseReaderTests.swift b/Tests/GRDBTests/DatabaseReaderTests.swift index d8702a60a3..6150cd4455 100644 --- a/Tests/GRDBTests/DatabaseReaderTests.swift +++ b/Tests/GRDBTests/DatabaseReaderTests.swift @@ -49,7 +49,7 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_ReadCanRead() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -91,7 +91,7 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_ReadPreventsDatabaseModification() async throws { func test(_ dbReader: some DatabaseReader) async throws { do { @@ -135,7 +135,7 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_UnsafeReadCanRead() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in diff --git a/Tests/GRDBTests/DatabaseRegionObservationTests.swift b/Tests/GRDBTests/DatabaseRegionObservationTests.swift index 0625d91c6b..9d54aafbef 100644 --- a/Tests/GRDBTests/DatabaseRegionObservationTests.swift +++ b/Tests/GRDBTests/DatabaseRegionObservationTests.swift @@ -9,7 +9,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { _ = observation.start(in: writer, onError: { _ in }, onChange: { _ in }) - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { _ = observation.publisher(in: writer) } } diff --git a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift index 99ae96abdd..6a58ff4056 100644 --- a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift +++ b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift @@ -220,7 +220,7 @@ final class DatabaseSnapshotPoolTests: GRDBTestCase { try XCTAssertEqual(dbPool.read(counter.value), 2) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func test_read_async() async throws { let dbPool = try makeDatabasePool() let counter = try Counter(dbPool: dbPool) // 0 diff --git a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift index 2245a8be6a..85c573377a 100644 --- a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift @@ -23,7 +23,7 @@ private struct RecordWithUUID: EncodableRecord, Enco var uuid: UUID } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension RecordWithUUID: Identifiable { var id: UUID { uuid } } @@ -33,7 +33,7 @@ private struct RecordWithOptionalUUID: EncodableReco var uuid: UUID? } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension RecordWithOptionalUUID: Identifiable { var id: UUID? { uuid } } @@ -184,7 +184,7 @@ extension DatabaseUUIDEncodingStrategyTests { } func testFilterID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } @@ -303,7 +303,7 @@ extension DatabaseUUIDEncodingStrategyTests { } func testDeleteID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 9139b1f9eb..4660971fc3 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -195,7 +195,7 @@ class DatabaseWriterTests : GRDBTestCase { } func testVacuumInto() throws { - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("VACUUM INTO is not available") } // Prevent SQLCipher failures @@ -266,7 +266,7 @@ class DatabaseWriterTests : GRDBTestCase { try DatabaseQueue().backup(to: dbQueue) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_write() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -286,7 +286,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_writeWithoutTransaction() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -309,7 +309,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_barrierWriteWithoutTransaction() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -332,7 +332,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_erase() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -350,7 +350,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_vacuum() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -366,7 +366,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // async + vacuum into + @available(iOS 14, macOS 10.16, tvOS 14, *) // async + vacuum into func testAsyncAwait_vacuumInto() async throws { // Prevent SQLCipher failures guard sqlite3_libversion_number() >= 3027000 else { @@ -397,7 +397,7 @@ class DatabaseWriterTests : GRDBTestCase { } /// A test related to - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncWriteThenRead() async throws { /// An async read performed after an async write should see the write. func test(_ dbWriter: some DatabaseWriter) async throws { diff --git a/Tests/GRDBTests/FTS3TableBuilderTests.swift b/Tests/GRDBTests/FTS3TableBuilderTests.swift index b2434da37a..d0f1bbaa61 100644 --- a/Tests/GRDBTests/FTS3TableBuilderTests.swift +++ b/Tests/GRDBTests/FTS3TableBuilderTests.swift @@ -76,7 +76,7 @@ class FTS3TableBuilderTests: GRDBTestCase { } #elseif !GRDBCIPHER func testUnicode61TokenizerDiacriticsRemove() throws { - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip() } let dbQueue = try makeDatabaseQueue() diff --git a/Tests/GRDBTests/FTS5TableBuilderTests.swift b/Tests/GRDBTests/FTS5TableBuilderTests.swift index b44dc6589f..fe63cfc427 100644 --- a/Tests/GRDBTests/FTS5TableBuilderTests.swift +++ b/Tests/GRDBTests/FTS5TableBuilderTests.swift @@ -130,7 +130,7 @@ class FTS5TableBuilderTests: GRDBTestCase { } #elseif !GRDBCIPHER func testUnicode61TokenizerDiacriticsRemove() throws { - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip() } diff --git a/Tests/GRDBTests/JoinSupportTests.swift b/Tests/GRDBTests/JoinSupportTests.swift index b2ae7eeeac..4b703bd71a 100644 --- a/Tests/GRDBTests/JoinSupportTests.swift +++ b/Tests/GRDBTests/JoinSupportTests.swift @@ -92,7 +92,7 @@ private struct FlatModel: FetchableRecord { self.t5count = row.scopes[Scopes.suffix]!["t5count"] } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) static func modernAll() -> some FetchRequest { all() } @@ -138,7 +138,7 @@ private struct CodableFlatModel: FetchableRecord, Codable { var t3: T3? var t5count: Int - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) static func modernAll() -> some FetchRequest { all() } @@ -186,7 +186,7 @@ private struct CodableNestedModel: FetchableRecord, Codable { var t3: T3? var t5count: Int - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) static func modernAll() -> some FetchRequest { all() } diff --git a/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift b/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift index e2e50cb240..a4248160fc 100644 --- a/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift +++ b/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift @@ -1512,7 +1512,7 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { throw XCTSkip("FILTER clause on aggregate functions is not available") } #else - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("FILTER clause on aggregate functions is not available") } #endif @@ -1561,7 +1561,7 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { throw XCTSkip("FILTER clause on aggregate functions is not available") } #else - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("FILTER clause on aggregate functions is not available") } #endif @@ -1594,7 +1594,7 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { throw XCTSkip("FILTER clause on aggregate functions is not available") } #else - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("FILTER clause on aggregate functions is not available") } #endif @@ -1627,7 +1627,7 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { throw XCTSkip("FILTER clause on aggregate functions is not available") } #else - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("FILTER clause on aggregate functions is not available") } #endif @@ -1684,7 +1684,7 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { throw XCTSkip("FILTER clause on aggregate functions is not available") } #else - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("FILTER clause on aggregate functions is not available") } #endif diff --git a/Tests/GRDBTests/QueryInterfaceRequestTests.swift b/Tests/GRDBTests/QueryInterfaceRequestTests.swift index 91f0323549..0e075fb28b 100644 --- a/Tests/GRDBTests/QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/QueryInterfaceRequestTests.swift @@ -779,7 +779,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { sql(dbQueue, tableRequest.order(Col.age.descNullsFirst)), "SELECT * FROM \"readers\" ORDER BY \"age\" DESC NULLS FIRST") #elseif !GRDBCIPHER - if #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) { + if #available(iOS 14, macOS 10.16, tvOS 14, *) { XCTAssertEqual( sql(dbQueue, tableRequest.order(Col.age.ascNullsLast)), "SELECT * FROM \"readers\" ORDER BY \"age\" ASC NULLS LAST") @@ -809,7 +809,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { sql(dbQueue, tableRequest.order(Col.name.collating(.nocase).descNullsFirst)), "SELECT * FROM \"readers\" ORDER BY \"name\" COLLATE NOCASE DESC NULLS FIRST") #elseif !GRDBCIPHER - if #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) { + if #available(iOS 14, macOS 10.16, tvOS 14, *) { XCTAssertEqual( sql(dbQueue, tableRequest.order(Col.name.collating(.nocase).ascNullsLast)), "SELECT * FROM \"readers\" ORDER BY \"name\" COLLATE NOCASE ASC NULLS LAST") @@ -858,7 +858,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { sql(dbQueue, tableRequest.order(Col.age.ascNullsLast).reversed()), "SELECT * FROM \"readers\" ORDER BY \"age\" DESC NULLS FIRST") #elseif !GRDBCIPHER - if #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) { + if #available(iOS 14, macOS 10.16, tvOS 14, *) { XCTAssertEqual( sql(dbQueue, tableRequest.order(Col.age.descNullsFirst).reversed()), "SELECT * FROM \"readers\" ORDER BY \"age\" ASC NULLS LAST") @@ -888,7 +888,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { sql(dbQueue, tableRequest.order(Col.name.collating(.nocase).descNullsFirst).reversed()), "SELECT * FROM \"readers\" ORDER BY \"name\" COLLATE NOCASE ASC NULLS LAST") #elseif !GRDBCIPHER - if #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) { + if #available(iOS 14, macOS 10.16, tvOS 14, *) { XCTAssertEqual( sql(dbQueue, tableRequest.order(Col.name.collating(.nocase).ascNullsLast).reversed()), "SELECT * FROM \"readers\" ORDER BY \"name\" COLLATE NOCASE DESC NULLS FIRST") diff --git a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift index 1ce98c1829..21896ae5b3 100644 --- a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift @@ -41,7 +41,7 @@ private class MinimalNonOptionalPrimaryKeySingle: Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension MinimalNonOptionalPrimaryKeySingle: Identifiable { } class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { @@ -471,7 +471,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) @@ -510,7 +510,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) @@ -548,7 +548,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) @@ -583,7 +583,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.fetchOne(db, id: record.id)! XCTAssertTrue(fetchedRecord.id == record.id) @@ -611,7 +611,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { _ = try MinimalNonOptionalPrimaryKeySingle.find(db, key: "missing") XCTFail("Expected RecordError") @@ -651,7 +651,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) @@ -690,7 +690,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) @@ -728,7 +728,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) @@ -763,7 +763,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.filter(id: record.id).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift index 43e649f467..37a577b216 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift @@ -45,7 +45,7 @@ class MinimalRowID : Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension MinimalRowID: Identifiable { } class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { @@ -505,7 +505,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let cursor = try MinimalRowID.fetchCursor(db, ids: ids) @@ -544,7 +544,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) @@ -582,7 +582,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) @@ -617,7 +617,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalRowID.fetchOne(db, id: record.id!)! XCTAssertTrue(fetchedRecord.id == record.id) @@ -648,7 +648,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { _ = try MinimalRowID.find(db, id: -1) XCTFail("Expected RecordError") @@ -691,7 +691,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) @@ -730,7 +730,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) @@ -768,7 +768,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) @@ -803,7 +803,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalRowID.filter(id: record.id!).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift index 8c5c1f0369..028d6ea6d0 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift @@ -39,7 +39,7 @@ class MinimalSingle: Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension MinimalSingle: Identifiable { /// Test non-optional ID type var id: String { UUID! } @@ -529,7 +529,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) @@ -570,7 +570,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) @@ -610,7 +610,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) @@ -646,7 +646,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalSingle.fetchOne(db, id: record.UUID!)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) @@ -675,7 +675,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { _ = try MinimalSingle.find(db, id: "missing") XCTFail("Expected RecordError") @@ -717,7 +717,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) @@ -758,7 +758,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) @@ -798,7 +798,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) @@ -834,7 +834,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalSingle.filter(id: record.UUID!).fetchOne(db)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) diff --git a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift index 6777722816..d2388e1d94 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift @@ -77,7 +77,7 @@ private class Person : Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Person: Identifiable { } class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { @@ -597,7 +597,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let cursor = try Person.fetchCursor(db, ids: ids) @@ -636,7 +636,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.fetchAll(db, ids: ids) @@ -674,7 +674,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.fetchSet(db, ids: ids) @@ -712,7 +712,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try Person.fetchOne(db, id: record.id!)! XCTAssertTrue(fetchedRecord.id == record.id) @@ -749,7 +749,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { _ = try Person.find(db, id: -1) XCTFail("Expected RecordError") @@ -795,7 +795,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let cursor = try Person.filter(ids: ids).fetchCursor(db) @@ -834,7 +834,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) @@ -872,7 +872,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) @@ -910,7 +910,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try Person.filter(id: record.id!).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) diff --git a/Tests/GRDBTests/SharedValueObservationTests.swift b/Tests/GRDBTests/SharedValueObservationTests.swift index 156960805c..572f362ff3 100644 --- a/Tests/GRDBTests/SharedValueObservationTests.swift +++ b/Tests/GRDBTests/SharedValueObservationTests.swift @@ -120,7 +120,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_immediate_publisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -397,7 +397,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_async_publisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -527,7 +527,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_error_recovery_observationLifetime() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -583,7 +583,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_error_recovery_whileObserved() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -637,7 +637,7 @@ class SharedValueObservationTests: GRDBTestCase { } #endif - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait() async throws { let dbQueue = try makeDatabaseQueue() try await dbQueue.write { db in diff --git a/Tests/GRDBTests/TableDefinitionTests.swift b/Tests/GRDBTests/TableDefinitionTests.swift index 0092b26d79..88cf3030e9 100644 --- a/Tests/GRDBTests/TableDefinitionTests.swift +++ b/Tests/GRDBTests/TableDefinitionTests.swift @@ -797,7 +797,7 @@ class TableDefinitionTests: GRDBTestCase { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } #if !GRDBCUSTOMSQLITE && !GRDBCIPHER - guard #available(iOS 13, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, tvOS 13, *) else { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } #endif @@ -825,7 +825,7 @@ class TableDefinitionTests: GRDBTestCase { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } #if !GRDBCUSTOMSQLITE && !GRDBCIPHER - guard #available(iOS 13, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, tvOS 13, *) else { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } #endif diff --git a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift index a141e18e91..6d01e8f7a0 100644 --- a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift @@ -259,7 +259,7 @@ class TableRecordQueryInterfaceRequestTests: GRDBTestCase { sql(dbQueue, Reader.order(Col.age.descNullsFirst)), "SELECT * FROM \"readers\" ORDER BY \"age\" DESC NULLS FIRST") #elseif !GRDBCIPHER - if #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) { + if #available(iOS 14, macOS 10.16, tvOS 14, *) { XCTAssertEqual( sql(dbQueue, Reader.order(Col.age.ascNullsLast)), "SELECT * FROM \"readers\" ORDER BY \"age\" ASC NULLS LAST") @@ -357,7 +357,7 @@ class TableRecordQueryInterfaceRequestTests: GRDBTestCase { } func testExistsIdentifiable() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable is not available") } diff --git a/Tests/GRDBTests/TableRecordDeleteTests.swift b/Tests/GRDBTests/TableRecordDeleteTests.swift index e45252a555..37a7249c1a 100644 --- a/Tests/GRDBTests/TableRecordDeleteTests.swift +++ b/Tests/GRDBTests/TableRecordDeleteTests.swift @@ -6,7 +6,7 @@ private struct Hacker : TableRecord { var id: Int64? // Optional } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Hacker: Identifiable { } private struct Person : Codable, PersistableRecord, FetchableRecord, Hashable { @@ -16,7 +16,7 @@ private struct Person : Codable, PersistableRecord, FetchableRecord, Hashable { var email: String } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Person: Identifiable { } private struct Citizenship : TableRecord { @@ -46,7 +46,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertTrue(deleted) XCTAssertEqual(try Hacker.fetchCount(db), 0) - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [1, "Arthur"]) try XCTAssertFalse(Hacker.deleteOne(db, id: nil)) deleted = try Hacker.deleteOne(db, id: 1) @@ -62,7 +62,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(deletedCount, 2) XCTAssertEqual(try Hacker.fetchCount(db), 1) - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [2, "Barbara"]) try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [3, "Craig"]) let deletedCount = try Hacker.deleteAll(db, ids: [2, 3, 4]) @@ -85,7 +85,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertTrue(deleted) XCTAssertEqual(try Person.fetchCount(db), 0) - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [1, "Arthur", "arthur@example.com"]) deleted = try Person.deleteOne(db, id: 1) XCTAssertTrue(deleted) @@ -100,7 +100,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(deletedCount, 2) XCTAssertEqual(try Person.fetchCount(db), 1) - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [2, "Barbara", "barbara@example.com"]) try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [3, "Craig", "craig@example.com"]) let deletedCount = try Person.deleteAll(db, ids: [2, 3, 4]) @@ -190,7 +190,7 @@ class TableRecordDeleteTests: GRDBTestCase { try Person.filter(keys: [1, 2]).deleteAll(db) XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2)") - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { try Person.filter(id: 1).deleteAll(db) XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1") @@ -279,7 +279,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2) RETURNING *") #if GRDBCUSTOMSQLITE || GRDBCIPHER - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { _ = try Person.filter(id: 1).deleteAndFetchCursor(db).next() XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1 RETURNING *") @@ -364,7 +364,7 @@ class TableRecordDeleteTests: GRDBTestCase { } } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) // Identifiable + @available(iOS 13, macOS 10.15, tvOS 13, *) // Identifiable func testRequestDeleteAndFetchIds() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { diff --git a/Tests/GRDBTests/TableRecordUpdateTests.swift b/Tests/GRDBTests/TableRecordUpdateTests.swift index b5de3fab7a..dd375bc8d3 100644 --- a/Tests/GRDBTests/TableRecordUpdateTests.swift +++ b/Tests/GRDBTests/TableRecordUpdateTests.swift @@ -17,7 +17,7 @@ private struct Player: Codable, PersistableRecord, FetchableRecord, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +@available(iOS 13, macOS 10.15, tvOS 13, *) extension Player: Identifiable { } private enum Columns: String, ColumnExpression { @@ -56,7 +56,7 @@ class TableRecordUpdateTests: GRDBTestCase { UPDATE "player" SET "score" = 0 WHERE "id" IN (1, 2) """) - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { try Player.filter(id: 1).updateAll(db, assignment) XCTAssertEqual(self.lastSQLQuery, """ UPDATE "player" SET "score" = 0 WHERE "id" = 1 diff --git a/Tests/GRDBTests/TableTests.swift b/Tests/GRDBTests/TableTests.swift index 46c8f5a192..7278fa11f3 100644 --- a/Tests/GRDBTests/TableTests.swift +++ b/Tests/GRDBTests/TableTests.swift @@ -117,7 +117,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { struct Player: Identifiable { var id: Int64 } let t = Table("player") @@ -129,7 +129,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { struct Player: Identifiable { var id: Int64? } let t = Table("player") @@ -806,7 +806,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { // Non-optional ID struct Country: Identifiable { var id: String } @@ -821,7 +821,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { // Optional ID struct Country: Identifiable { var id: String? } @@ -920,7 +920,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { // Non-optional ID struct Country: Identifiable { var id: String } @@ -930,7 +930,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + if #available(iOS 13, macOS 10.15, tvOS 13, *) { // Optional ID struct Country: Identifiable { var id: String? } diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index fe2ccee22d..325fb10c32 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -21,7 +21,7 @@ class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testValuesFromAnyDatabaseWriter(writer: any DatabaseWriter) { func observe( fetch: @Sendable @escaping (Database) throws -> T @@ -873,7 +873,7 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Async Await - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_values_prefix() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -911,7 +911,7 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_values_prefix_immediate_scheduling() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -949,7 +949,7 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_values_break() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -991,7 +991,7 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_values_immediate_break() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -1030,7 +1030,7 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_values_cancelled() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change From 6895164a200e6d16889b0e1493d4366451be902b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 15:25:06 +0200 Subject: [PATCH 005/160] CI: use Xcode 16.1 --- .github/workflows/CI.yml | 89 ++++++++++------------------------------ 1 file changed, 22 insertions(+), 67 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6a9f3ec808..f328ffc2f5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,4 @@ -# https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md +# https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md name: "GRDB CI" @@ -40,42 +40,18 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 destination: "platform=macOS" name: "macOS" - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 destination: "OS=16.4,name=iPhone 14 Pro" name: "iOS" - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 destination: "OS=16.4,name=Apple TV" name: "tvOS" - - xcode: "Xcode_14.2.app" - runsOn: macOS-13 - destination: "platform=macOS" - name: "macOS" - - xcode: "Xcode_14.2.app" - runsOn: macOS-13 - destination: "OS=16.2,name=iPhone 14" - name: "iOS" - - xcode: "Xcode_14.1.app" - runsOn: macOS-13 - destination: "platform=macOS" - name: "macOS" - - xcode: "Xcode_14.1.app" - runsOn: macOS-13 - destination: "OS=16.1,name=iPhone 14" - name: "iOS" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - destination: "platform=macOS" - name: "macOS" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - destination: "OS=16.0,name=iPhone 14" - name: "iOS" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -90,18 +66,9 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - name: "Xcode 14.3.1" - - xcode: "Xcode_14.2.app" - runsOn: macOS-13 - name: "Xcode 14.2" - - xcode: "Xcode_14.1.app" - runsOn: macOS-13 - name: "Xcode 14.1" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - name: "Xcode 14.0.1" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + name: "Xcode 16.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -116,12 +83,9 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - name: "Xcode 14.3.1" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - name: "Xcode 14.0.1" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + name: "Xcode 16.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -136,12 +100,9 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - name: "Xcode 14.3.1" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - name: "Xcode 14.0.1" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + name: "Xcode 16.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -156,12 +117,9 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - name: "Xcode 14.3.1" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - name: "Xcode 14.0.1" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + name: "Xcode 16.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -176,12 +134,9 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - name: "Xcode 14.3.1" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - name: "Xcode 14.0.1" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + name: "Xcode 16.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} From 0240ab552caa2ccf714f36ce29b7b622a7c0e534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 15:32:40 +0200 Subject: [PATCH 006/160] Fix compiler warnings --- .../PublisherExpectations/AvailableElements.swift | 2 +- Tests/GRDBTests/DatabaseQueueConcurrencyTests.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift index b32fdbde49..5b78e39ed4 100644 --- a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift +++ b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift @@ -46,7 +46,7 @@ extension PublisherExpectations { } /// A waiter that waits but never fails - private class Waiter: XCTWaiter, XCTWaiterDelegate { + private class Waiter: XCTWaiter, XCTWaiterDelegate, @unchecked Sendable { init() { super.init(delegate: nil) delegate = self diff --git a/Tests/GRDBTests/DatabaseQueueConcurrencyTests.swift b/Tests/GRDBTests/DatabaseQueueConcurrencyTests.swift index 613d6b5ad8..a051152887 100644 --- a/Tests/GRDBTests/DatabaseQueueConcurrencyTests.swift +++ b/Tests/GRDBTests/DatabaseQueueConcurrencyTests.swift @@ -334,22 +334,22 @@ class ConcurrencyTests: GRDBTestCase { } // Queue 2 - var rows1: [Row]? - var rows2: [Row]? + var count1: Int? + var count2: Int? queue.async(group: group) { try! dbQueue2.writeWithoutTransaction { db in _ = s1.wait(timeout: .distantFuture) - rows1 = try Row.fetchAll(db, sql: "SELECT * FROM stuffs") + count1 = try Row.fetchAll(db, sql: "SELECT * FROM stuffs").count s2.signal() _ = s3.wait(timeout: .distantFuture) - rows2 = try Row.fetchAll(db, sql: "SELECT * FROM stuffs") + count2 = try Row.fetchAll(db, sql: "SELECT * FROM stuffs").count } } _ = group.wait(timeout: .distantFuture) - XCTAssertEqual(rows1!.count, 0) // uncommitted changes are not visible - XCTAssertEqual(rows2!.count, 1) // committed changes are visible + XCTAssertEqual(count1, 0) // uncommitted changes are not visible + XCTAssertEqual(count2, 1) // committed changes are visible } func testReaderInDeferredTransactionDuringDefaultTransaction() throws { From 31be1782c52483b5e90dd0d5bd47ec616e20345f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 16:22:45 +0200 Subject: [PATCH 007/160] Makefile: document TOOLCHAIN --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 819e8dcae7..81352940d5 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,9 @@ XCRUN := $(shell command -v xcrun) XCODEBUILD := set -o pipefail && $(shell command -v xcodebuild) ifdef TOOLCHAIN + # Look for the toolchain identifier in the CFBundleIdentifier key of its Info.plist: + # TOOLCHAIN=org.swift.600202404221a make test + # If TOOLCHAIN is specified, add xcodebuild parameter XCODEBUILD += -toolchain $(TOOLCHAIN) From e2106167ccaf303e58b8b5d691a1b83607340c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 16:22:57 +0200 Subject: [PATCH 008/160] GRDB7 TODO --- TODO.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/TODO.md b/TODO.md index 17266e7548..e3da5e4706 100644 --- a/TODO.md +++ b/TODO.md @@ -87,6 +87,65 @@ - [ ] Database.clearSchemaCache() is fine, but what about dbPool readers? Can we invalidate the cache for a whole pool? - [ ] What can we do with `cross-module-optimization`? See https://github.com/apple/swift-homomorphic-encryption +- [ ] GRDB7/BREAKING: insertAndFetch, saveAndFetch, and updateAndFetch no longer return optionals (32f41472) +- [ ] GRDB7/BREAKING: AsyncValueObservation does not need any scheduler (83c0e643) +- [ ] GRDB7/BREAKING: Stop exporting SQLite (679d6463) +- [ ] GRDB7/BREAKING: Remove Configuration.defaultTransactionKind (2661ff46) +- [ ] GRDB7: Replace LockedBox with Mutex (00ccab06) +- [ ] GRDB7: Sendable: BusyCallback (e0d8e20b) +- [ ] GRDB7: Sendable: BusyMode (e0d8e20b) +- [ ] GRDB7: Sendable: TransactionClock (f7dc72a5) +- [ ] GRDB7: Sendable: Configuration (54ffb21f) +- [ ] GRDB7: Sendable: DatabaseDataEncodingStrategy (264d7fb5) +- [ ] GRDB7: Sendable: DatabaseDateEncodingStrategy (264d7fb5) +- [ ] GRDB7: Sendable: DatabaseColumnEncodingStrategy (264d7fb5) +- [ ] GRDB7: Sendable: DatabaseDataDecodingStrategy (264d7fb5) +- [ ] GRDB7: Sendable: DatabaseDateDecodingStrategy (264d7fb5) +- [ ] GRDB7: Sendable: DatabaseColumnDecodingStrategy (264d7fb5) +- [ ] GRDB7/BREAKING: Remove DatabaseFuture and concurrentRead (05f7d3c8) +- [ ] GRDB7: Sendable: DatabaseFunction (6e691fe7) +- [ ] GRDB7: Sendable: DatabaseMigrator (22114ad4) +- [ ] GRDB7: Not Sendable: FilterCursor (b26e9709) +- [ ] GRDB7: Sendable: RowAdapter (d138af26) +- [ ] GRDB7: Sendable: ValueObservationScheduler (8429eb68) +- [ ] GRDB7: Sendable: DatabaseCollation (4d9d67dd) +- [ ] GRDB7: Sendable: LogErrorFunction (f362518d) +- [ ] GRDB7: Sendable: ReadWriteBox (57a86a0e) +- [ ] GRDB7: Sendable: Pool (f13b2d2e) +- [ ] GRDB7: Sendable: OnDemandFuture fulfill (2aabc4c1) +- [ ] GRDB7: Sendable: WALSnapshotTransaction (7fd34012) +- [ ] GRDB7: sending closures for SerializedDatabase +- [ ] GRDB7: sending closures for ValueObservationScheduler +- [ ] GRDB7: Sendable closures for ValueObservation.handleEvents +- [ ] GRDB7: Not Sendable: Record (make it explicit if subclasses can be made sendable) +- [ ] GRDB7: Not Sendable: databasepublishers/databaseregion, migrate, read, value, write +- [ ] GRDB7: Sendable closures for writePublisher +- [ ] GRDB7: Sendable closures for readPublisher +- [ ] GRDB7: Not Sendable: fts5customtokenizer, fts5tokenizer, fts5wrappertokenizer +- [ ] GRDB7: Sendable: DatabasePromise (05899228, 5a2c15b8) +- [ ] GRDB7: Sendable: TableAlias (f2b0b186) +- [ ] GRDB7: Sendable: SQLRelation (9545bf70) +- [ ] GRDB7: Sendable: SQL (ac33856f) +- [ ] GRDB7: Split Row.swift (2ce8a619) +- [ ] GRDB7: Cleanup ValueReducer (6c73b1c5) +- [ ] GRDB7: DatabaseCursor has a primary associated type (b11c5dd2) +- [ ] GRDB7: Enable Strict Concurrency Checks (6aa43ded) +- [ ] GRDB7: Sendable: OrderedDictionary (e022c35b) +- [ ] GRDB7: Rename ReadWriteBox to ReadWriteLock (7f5205ef) +- [ ] GRDB7: Sendable: DatabaseRegionConvertible (b4677ded) +- [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65, 5465d056) +- [ ] GRDB7: Sendable: ValueWriteOnlyObserver (ff2a7548) +- [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65) +- [ ] GRDB7: Sendable: DatabaseCancellable (2f93f00b, 8f486a5e) +- [ ] GRDB7: ValueObservation closures +- [?] GRDB7: DatabasePublishers.ValueSubscription +- [ ] GRDB7: Sendable: ValueObservation (93f6f982) +- [?] GRDB7: Not Sendable: SharedValueObservation +- [ ] GRDB7: doc (c0838cf9) +- [ ] GRDB7/BREAKING: PersistenceContainer is Sendable (50eefa8c) +- [ ] GRDB7: TableRecord.databaseSelection must be declared as a computed property (24d232aa) + +- [?] GRDB7: Change ValueObservation callback argument so that it could expose snapshots? https://github.com/groue/GRDB.swift/discussions/1523#discussioncomment-9092500 ## Unsure if necessary From 6157a40dadc51e03edc71a92706007861f4588cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 16:34:35 +0200 Subject: [PATCH 009/160] Fix DocC warnings --- GRDB/Core/Cursor.swift | 6 ------ GRDB/Core/DatabaseValueConvertible.swift | 8 ++++---- GRDB/Core/Row.swift | 13 ++++++------- GRDB/Core/Statement.swift | 7 ++++--- GRDB/Core/StatementColumnConvertible.swift | 9 ++++----- GRDB/Dump/Database+Dump.swift | 2 +- GRDB/Dump/DatabaseReader+dump.swift | 2 +- GRDB/JSON/SQLJSONFunctions.swift | 4 ++-- .../Request/CommonTableExpression.swift | 12 ++++++------ GRDB/QueryInterface/SQL/Table.swift | 4 ++-- .../Schema/Database+SchemaDefinition.swift | 4 ++-- GRDB/QueryInterface/Schema/TableDefinition.swift | 3 +++ GRDB/QueryInterface/Schema/VirtualTableModule.swift | 2 +- GRDB/QueryInterface/TableRecord+Association.swift | 4 ++-- GRDB/Record/FetchableRecord.swift | 12 ++++-------- GRDB/Record/MutablePersistableRecord.swift | 3 ++- GRDB/Record/Record.swift | 3 ++- GRDB/ValueObservation/ValueObservation.swift | 2 ++ 18 files changed, 48 insertions(+), 52 deletions(-) diff --git a/GRDB/Core/Cursor.swift b/GRDB/Core/Cursor.swift index af874297c1..5ce6dc7c03 100644 --- a/GRDB/Core/Cursor.swift +++ b/GRDB/Core/Cursor.swift @@ -670,9 +670,6 @@ extension Cursor where Element: Equatable { extension Cursor where Element: Comparable { /// Returns the maximum element in the cursor. /// - /// - Parameter areInIncreasingOrder: A predicate that returns `true` - /// if its first argument should be ordered before its second - /// argument; otherwise, `false`. /// - Returns: The cursor's maximum element, according to /// `areInIncreasingOrder`. If the cursor has no elements, returns /// `nil`. @@ -682,9 +679,6 @@ extension Cursor where Element: Comparable { /// Returns the minimum element in the cursor. /// - /// - Parameter areInIncreasingOrder: A predicate that returns `true` - /// if its first argument should be ordered before its second - /// argument; otherwise, `false`. /// - Returns: The cursor's minimum element, according to /// `areInIncreasingOrder`. If the cursor has no elements, returns /// `nil`. diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index df9b1b6939..7ce4fb4307 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -571,7 +571,7 @@ extension DatabaseValueConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A ``DatabaseValueCursor`` over fetched values. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchCursor(_ db: Database, _ request: some FetchRequest) throws -> DatabaseValueCursor { @@ -605,7 +605,7 @@ extension DatabaseValueConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An array. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Self] { @@ -642,7 +642,7 @@ extension DatabaseValueConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An optional value. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Self? { @@ -678,7 +678,7 @@ extension DatabaseValueConvertible where Self: Hashable { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A set. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set { diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index e11f35d1ff..76ded70da4 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -1424,8 +1424,7 @@ extension Row { /// elements are undefined. /// /// - parameters: - /// - db: A database connection. - /// - sql: An SQL string. + /// - statement: The statement to iterate. /// - arguments: Optional statement arguments. /// - adapter: Optional RowAdapter /// - returns: A ``RowCursor`` over fetched rows. @@ -1713,7 +1712,7 @@ extension Row { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A ``RowCursor`` over fetched rows. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchCursor(_ db: Database, _ request: some FetchRequest) throws -> RowCursor { @@ -1744,7 +1743,7 @@ extension Row { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An array of rows. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Row] { @@ -1776,7 +1775,7 @@ extension Row { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A set of rows. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set { @@ -1812,7 +1811,7 @@ extension Row { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An optional row. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Row? { @@ -2191,7 +2190,7 @@ extension Row { /// /// See ``Row/scopesTree`` for more information. /// - /// - parameter key: An association key. + /// - parameter name: The scope name. public subscript(_ name: String) -> Row? { var fifo = Array(scopes) while !fifo.isEmpty { diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index b649d395c9..a1d8b6544c 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -929,7 +929,8 @@ where C: Collection, C.Element == DatabaseValue /// ## Concatenating Arguments /// /// Several arguments can be concatenated and mixed with the -/// ``append(contentsOf:)`` method and the `+`, `&+`, `+=` operators: +/// ``StatementArguments/append(contentsOf:)`` method and the `+`, `&+`, +/// `+=` operators: /// /// ```swift /// var arguments: StatementArguments = ["Arthur"] @@ -949,8 +950,8 @@ where C: Collection, C.Element == DatabaseValue /// arguments += ["name": "Barbara"] /// ``` /// -/// On the other side, `&+` and ``append(contentsOf:)`` allow overriding -/// named arguments: +/// On the other side, `&+` and ``StatementArguments/append(contentsOf:)`` +/// allow overriding named arguments: /// /// ```swift /// var arguments: StatementArguments = ["name": "Arthur"] diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index cb081ec63d..04a689fc5f 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -108,7 +108,6 @@ public protocol StatementColumnConvertible { /// - parameters: /// - sqliteStatement: A pointer to an SQLite statement. /// - index: The column index. - /// - returns: A decoded value, or, if decoding is impossible, nil. init?(sqliteStatement: SQLiteStatement, index: CInt) } @@ -585,7 +584,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A ``FastDatabaseValueCursor`` over fetched values. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchCursor(_ db: Database, _ request: some FetchRequest) @@ -621,7 +620,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An array of values. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Self] { @@ -658,7 +657,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An optional value. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Self? { @@ -694,7 +693,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible & Hash /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A set of values. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set { diff --git a/GRDB/Dump/Database+Dump.swift b/GRDB/Dump/Database+Dump.swift index d4abfb653a..bb5aed2641 100644 --- a/GRDB/Dump/Database+Dump.swift +++ b/GRDB/Dump/Database+Dump.swift @@ -45,7 +45,7 @@ extension Database { /// ``` /// /// - Parameters: - /// - request : The executed request. + /// - request: The executed request. /// - format: The output format. /// - stream: A stream for text output, which directs output to the /// console by default. diff --git a/GRDB/Dump/DatabaseReader+dump.swift b/GRDB/Dump/DatabaseReader+dump.swift index 55e829c89d..cbe3e922de 100644 --- a/GRDB/Dump/DatabaseReader+dump.swift +++ b/GRDB/Dump/DatabaseReader+dump.swift @@ -38,7 +38,7 @@ extension DatabaseReader { /// ``` /// /// - Parameters: - /// - request : The executed request. + /// - request: The executed request. /// - format: The output format. /// - stream: A stream for text output, which directs output to the /// console by default. diff --git a/GRDB/JSON/SQLJSONFunctions.swift b/GRDB/JSON/SQLJSONFunctions.swift index 85122f2895..e573c12a24 100644 --- a/GRDB/JSON/SQLJSONFunctions.swift +++ b/GRDB/JSON/SQLJSONFunctions.swift @@ -712,7 +712,7 @@ extension Database { /// /// - Parameters: /// - value: A JSON value. - /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonRemove(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { .function("JSON_REMOVE", [value.sqlExpression, path.sqlExpression]) @@ -768,7 +768,7 @@ extension Database { /// /// - Parameters: /// - value: A JSON value. - /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonType(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { .function("JSON_TYPE", [value.sqlExpression, path.sqlExpression]) diff --git a/GRDB/QueryInterface/Request/CommonTableExpression.swift b/GRDB/QueryInterface/Request/CommonTableExpression.swift index 17d192fb2a..8e1e5d2c65 100644 --- a/GRDB/QueryInterface/Request/CommonTableExpression.swift +++ b/GRDB/QueryInterface/Request/CommonTableExpression.swift @@ -380,8 +380,8 @@ extension CommonTableExpression { /// /// - parameter cte: A common table expression. /// - parameter condition: A function that returns the joining clause. - /// - parameter left: A `TableAlias` for the left table. - /// - parameter right: A `TableAlias` for the right table. + /// First argument is a ``TableAlias`` for the left table, second + /// argument an alias for the right table. /// - returns: An association to the common table expression. public func association( to cte: CommonTableExpression, @@ -416,8 +416,8 @@ extension CommonTableExpression { /// - parameter destination: The record type at the other side of /// the association. /// - parameter condition: A function that returns the joining clause. - /// - parameter left: A `TableAlias` for the left table. - /// - parameter right: A `TableAlias` for the right table. + /// First argument is a ``TableAlias`` for the left table, second + /// argument an alias for the right table. /// - returns: An association to the common table expression. public func association( to destination: Destination.Type, @@ -453,8 +453,8 @@ extension CommonTableExpression { /// /// - parameter destination: The table at the other side of the association. /// - parameter condition: A function that returns the joining clause. - /// - parameter left: A `TableAlias` for the left table. - /// - parameter right: A `TableAlias` for the right table. + /// First argument is a ``TableAlias`` for the left table, second + /// argument an alias for the right table. /// - returns: An association to the common table expression. public func association( to destination: Table, diff --git a/GRDB/QueryInterface/SQL/Table.swift b/GRDB/QueryInterface/SQL/Table.swift index 86948aa7bb..6b85f3e51c 100644 --- a/GRDB/QueryInterface/SQL/Table.swift +++ b/GRDB/QueryInterface/SQL/Table.swift @@ -1292,8 +1292,8 @@ extension Table { /// /// - parameter cte: A common table expression. /// - parameter condition: A function that returns the joining clause. - /// - parameter left: A `TableAlias` for the left table. - /// - parameter right: A `TableAlias` for the right table. + /// First argument is a ``TableAlias`` for the left table, second + /// argument an alias for the right table. /// - returns: An association to the common table expression. public func association( to cte: CommonTableExpression, diff --git a/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift b/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift index 10b95ebc3a..fb40efa738 100644 --- a/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift +++ b/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift @@ -356,7 +356,7 @@ extension Database { /// Related SQLite documentation: /// /// - parameters: - /// - view: The view name. + /// - name: The view name. /// - options: View creation options. /// - columns: The columns of the view. If nil, the columns are the /// columns of the request. @@ -410,7 +410,7 @@ extension Database { /// Related SQLite documentation: /// /// - parameters: - /// - view: The view name. + /// - name: The view name. /// - options: View creation options. /// - columns: The columns of the view. If nil, the columns are the /// columns of the request. diff --git a/GRDB/QueryInterface/Schema/TableDefinition.swift b/GRDB/QueryInterface/Schema/TableDefinition.swift index 235c54bf88..2395d62b08 100644 --- a/GRDB/QueryInterface/Schema/TableDefinition.swift +++ b/GRDB/QueryInterface/Schema/TableDefinition.swift @@ -136,6 +136,7 @@ public final class TableDefinition { /// - /// - /// + /// - parameter name: the name of the primary key. /// - parameter conflictResolution: An optional conflict resolution /// (see ). /// - returns: `self` so that you can further refine the column definition. @@ -163,6 +164,8 @@ public final class TableDefinition { /// /// - parameter name: the column name. /// - parameter type: the column type. + /// - parameter conflictResolution: An optional conflict resolution + /// (see ). /// - returns: A ``ColumnDefinition`` that allows you to refine the /// column definition. @discardableResult diff --git a/GRDB/QueryInterface/Schema/VirtualTableModule.swift b/GRDB/QueryInterface/Schema/VirtualTableModule.swift index 07bdf7bdd8..6194abe7ba 100644 --- a/GRDB/QueryInterface/Schema/VirtualTableModule.swift +++ b/GRDB/QueryInterface/Schema/VirtualTableModule.swift @@ -113,7 +113,7 @@ extension Database { /// Related SQLite documentation: /// /// - parameters: - /// - name: The table name. + /// - tableName: The table name. /// - ifNotExists: If false (the default), an error is thrown if the /// table already exists. Otherwise, the table is created unless it /// already exists. diff --git a/GRDB/QueryInterface/TableRecord+Association.swift b/GRDB/QueryInterface/TableRecord+Association.swift index e86a548141..67249d4d17 100644 --- a/GRDB/QueryInterface/TableRecord+Association.swift +++ b/GRDB/QueryInterface/TableRecord+Association.swift @@ -334,8 +334,8 @@ extension TableRecord { /// /// - parameter cte: A common table expression. /// - parameter condition: A function that returns the joining clause. - /// - parameter left: A `TableAlias` for the left table. - /// - parameter right: A `TableAlias` for the right table. + /// First argument is a ``TableAlias`` for the left table, second + /// argument an alias for the right table. /// - returns: An association to the common table expression. public static func association( to cte: CommonTableExpression, diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index 609279560f..90dd6669ca 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -555,7 +555,7 @@ extension FetchableRecord { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. + /// - request: a fetch request. /// - returns: A ``RecordCursor`` over fetched records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchCursor(_ db: Database, _ request: some FetchRequest) throws -> RecordCursor { @@ -586,7 +586,7 @@ extension FetchableRecord { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. + /// - request: a fetch request. /// - returns: An array of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Self] { @@ -621,7 +621,7 @@ extension FetchableRecord { /// ``` /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. + /// - request: a fetch request. /// - returns: An optional record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Self? { @@ -661,7 +661,7 @@ extension FetchableRecord where Self: Hashable { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. + /// - request: a fetch request. /// - returns: A set of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set { @@ -714,7 +714,6 @@ extension FetchRequest where RowDecoder: FetchableRecord { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. /// - returns: A ``RecordCursor`` over fetched records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public func fetchCursor(_ db: Database) throws -> RecordCursor { @@ -743,7 +742,6 @@ extension FetchRequest where RowDecoder: FetchableRecord { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. /// - returns: An array of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public func fetchAll(_ db: Database) throws -> [RowDecoder] { @@ -771,7 +769,6 @@ extension FetchRequest where RowDecoder: FetchableRecord { /// ``` /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. /// - returns: An optional record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public func fetchOne(_ db: Database) throws -> RowDecoder? { @@ -802,7 +799,6 @@ extension FetchRequest where RowDecoder: FetchableRecord & Hashable { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. /// - returns: A set of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public func fetchSet(_ db: Database) throws -> Set { diff --git a/GRDB/Record/MutablePersistableRecord.swift b/GRDB/Record/MutablePersistableRecord.swift index d26cb1cbcc..3741c06f73 100644 --- a/GRDB/Record/MutablePersistableRecord.swift +++ b/GRDB/Record/MutablePersistableRecord.swift @@ -193,6 +193,7 @@ public protocol MutablePersistableRecord: EncodableRecord, TableRecord { /// Default implementation does nothing. /// /// - parameter db: A database connection. + /// - parameter columns: The updated columns. func willUpdate(_ db: Database, columns: Set) throws /// Persistence callback called around the record update. @@ -253,7 +254,7 @@ public protocol MutablePersistableRecord: EncodableRecord, TableRecord { /// ``` /// /// - parameter db: A database connection. - /// - parameter update: A function that updates the record. Its result is + /// - parameter save: A function that saves the record. Its result is /// reserved for GRDB usage. func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws diff --git a/GRDB/Record/Record.swift b/GRDB/Record/Record.swift index 47f37d7421..cb7394fe14 100644 --- a/GRDB/Record/Record.swift +++ b/GRDB/Record/Record.swift @@ -281,6 +281,7 @@ open class Record { /// your implementation. /// /// - parameter db: A database connection. + /// - parameter columns: The updated columns. open func willUpdate(_ db: Database, columns: Set) throws { } /// Called around the record update. @@ -348,7 +349,7 @@ open class Record { /// ``` /// /// - parameter db: A database connection. - /// - parameter update: A function that updates the record. Its result is + /// - parameter save: A function that saves the recordsave: A function that saves the record. Its result is /// reserved for GRDB usage. open func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws { _ = try save() diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index 26f6de0755..079f71680b 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -296,6 +296,8 @@ extension ValueObservation { /// - parameter reader: A DatabaseReader. /// - parameter scheduler: A ValueObservationScheduler. By default, fresh /// values are dispatched asynchronously on the main dispatch queue. + /// - parameter bufferingPolicy: see the documntation + /// of `AsyncThrowingStream`. @available(iOS 13, macOS 10.15, tvOS 13, *) public func values( in reader: some DatabaseReader, From 38e867c4054ac72e0894774333698e024e2c7699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 16:54:56 +0200 Subject: [PATCH 010/160] Bump minimum targets in Package.swift --- Package.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index b31da24a76..5d546af48d 100644 --- a/Package.swift +++ b/Package.swift @@ -34,10 +34,10 @@ let package = Package( name: "GRDB", defaultLocalization: "en", // for tests platforms: [ - .iOS(.v11), + .iOS(.v12), .macOS(.v10_13), - .tvOS(.v11), - .watchOS(.v4), + .tvOS(.v12), + .watchOS(.v7), ], products: [ .library(name: "CSQLite", targets: ["CSQLite"]), From 81cf93f02a7e7cf7a92036bf5add4138e1a67ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 16:55:05 +0200 Subject: [PATCH 011/160] Bump iOS version on GitHub CI --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f328ffc2f5..54a73ddacb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -46,11 +46,11 @@ jobs: name: "macOS" - xcode: "Xcode_16.1.app" runsOn: macOS-14 - destination: "OS=16.4,name=iPhone 14 Pro" + destination: "OS=18.1,name=iPhone 15 Pro" name: "iOS" - xcode: "Xcode_16.1.app" runsOn: macOS-14 - destination: "OS=16.4,name=Apple TV" + destination: "OS=18.0,name=Apple TV" name: "tvOS" steps: - uses: actions/checkout@v4 From cd4e853b74cb32fa80250f50b392fe9718e32738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 10 Feb 2024 19:00:13 +0100 Subject: [PATCH 012/160] [BREAKING] insertAndFetch, saveAndFetch, and updateAndFetch no longer return optionals. Those optionals were just an annoyance. Now, nil results are replaced with `RecordError.recordNotFound`. Such an error is only expected with the IGNORE conflict policy. --- .../MutablePersistableRecord+Insert.swift | 66 ++++++------ .../MutablePersistableRecord+Save.swift | 48 +++++---- .../MutablePersistableRecord+Update.swift | 101 +++++++++++------- GRDB/Record/PersistableRecord+Insert.swift | 44 ++++---- GRDB/Record/PersistableRecord+Save.swift | 30 +++--- GRDB/Record/TableRecord.swift | 34 ++++++ README.md | 18 ++-- .../MutablePersistableRecordTests.swift | 26 +++-- Tests/GRDBTests/PersistableRecordTests.swift | 8 +- 9 files changed, 228 insertions(+), 147 deletions(-) diff --git a/GRDB/Record/MutablePersistableRecord+Insert.swift b/GRDB/Record/MutablePersistableRecord+Insert.swift index 4b2b1e1d04..e089cb23c1 100644 --- a/GRDB/Record/MutablePersistableRecord+Insert.swift +++ b/GRDB/Record/MutablePersistableRecord+Insert.swift @@ -91,7 +91,6 @@ extension MutablePersistableRecord { extension MutablePersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -108,22 +107,22 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The inserted record, if any. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The inserted record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.insertAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -161,11 +160,10 @@ extension MutablePersistableRecord { /// var partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -174,19 +172,24 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { - try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + let record = self + return try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) } } @@ -266,7 +269,6 @@ extension MutablePersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -283,23 +285,23 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The inserted record, if any. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The inserted record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.insertAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -337,11 +339,10 @@ extension MutablePersistableRecord { /// var partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -350,20 +351,25 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { - try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + let record = self + return try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) } } diff --git a/GRDB/Record/MutablePersistableRecord+Save.swift b/GRDB/Record/MutablePersistableRecord+Save.swift index a9776cc820..6a238ac2f9 100644 --- a/GRDB/Record/MutablePersistableRecord+Save.swift +++ b/GRDB/Record/MutablePersistableRecord+Save.swift @@ -87,7 +87,6 @@ extension MutablePersistableRecord { extension MutablePersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -107,22 +106,22 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The saved record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The saved record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.saveAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -135,26 +134,31 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { + let record = self success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) }) return success!.saved } @@ -211,7 +215,6 @@ extension MutablePersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -231,23 +234,23 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The saved record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The saved record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.saveAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -260,27 +263,32 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { + let record = self success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) }) return success!.saved } diff --git a/GRDB/Record/MutablePersistableRecord+Update.swift b/GRDB/Record/MutablePersistableRecord+Update.swift index 691d77d8d4..8875ad5750 100644 --- a/GRDB/Record/MutablePersistableRecord+Update.swift +++ b/GRDB/Record/MutablePersistableRecord+Update.swift @@ -218,7 +218,6 @@ extension MutablePersistableRecord { extension MutablePersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `UPDATE RETURNING` statement on all columns, and returns a /// new record built from the updated row. /// @@ -226,23 +225,23 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The updated record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The updated record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { try updateAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `UPDATE RETURNING` statement on all columns, and returns a /// new record built from the updated row. /// @@ -251,25 +250,28 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try updateAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -280,12 +282,13 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter modify: A closure that modifies the record. - /// - returns: An updated record, or nil if the record has no change, or - /// in case of a failed update due to the `IGNORE` conflict policy. + /// - returns: An updated record, or nil if the record has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func updateChangesAndFetch( _ db: Database, @@ -297,7 +300,6 @@ extension MutablePersistableRecord { try updateChangesAndFetch(db, onConflict: conflictResolution, as: Self.self, modify: modify) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -310,12 +312,13 @@ extension MutablePersistableRecord { /// - parameter returnedType: The type of the returned record. /// - parameter modify: A closure that modifies the record. /// - returns: A record of type `returnedType`, or nil if the record has - /// no change, or in case of a failed update due to the `IGNORE` - /// conflict policy. + /// no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func updateChangesAndFetch( _ db: Database, @@ -324,10 +327,16 @@ extension MutablePersistableRecord { modify: (inout Self) throws -> Void) throws -> T? { - try updateChangesAndFetch( + let record = self + return try updateChangesAndFetch( db, onConflict: conflictResolution, selection: T.databaseSelection, - fetch: { try T.fetchOne($0) }, + fetch: { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) + }, modify: modify) } @@ -482,7 +491,6 @@ extension MutablePersistableRecord { fetch: fetch) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -495,7 +503,8 @@ extension MutablePersistableRecord { /// - parameter selection: The returned columns (must not be empty). /// - parameter fetch: A function that executes it ``Statement`` argument. /// - parameter modify: A closure that modifies the record. - /// - returns: The result of the `fetch` function. + /// - returns: The result of the `fetch` function, or nil if the record + /// has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the @@ -506,7 +515,7 @@ extension MutablePersistableRecord { _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?, + fetch: (Statement) throws -> T, modify: (inout Self) throws -> Void) throws -> T? { @@ -519,7 +528,6 @@ extension MutablePersistableRecord { fetch: fetch) } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `UPDATE RETURNING` statement on all columns, and returns a /// new record built from the updated row. /// @@ -527,18 +535,19 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The updated record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The updated record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { try updateAndFetch(db, onConflict: conflictResolution, as: Self.self) @@ -552,26 +561,29 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try updateAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -582,12 +594,13 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter modify: A closure that modifies the record. - /// - returns: An updated record, or nil if the record has no change, or - /// in case of a failed update due to the `IGNORE` conflict policy. + /// - returns: An updated record, or nil if the record has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func updateChangesAndFetch( @@ -600,7 +613,6 @@ extension MutablePersistableRecord { try updateChangesAndFetch(db, onConflict: conflictResolution, as: Self.self, modify: modify) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -613,12 +625,13 @@ extension MutablePersistableRecord { /// - parameter returnedType: The type of the returned record. /// - parameter modify: A closure that modifies the record. /// - returns: A record of type `returnedType`, or nil if the record has - /// no change, or in case of a failed update due to the `IGNORE` - /// conflict policy. + /// no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func updateChangesAndFetch( @@ -628,10 +641,16 @@ extension MutablePersistableRecord { modify: (inout Self) throws -> Void) throws -> T? { - try updateChangesAndFetch( + let record = self + return try updateChangesAndFetch( db, onConflict: conflictResolution, selection: T.databaseSelection, - fetch: { try T.fetchOne($0) }, + fetch: { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) + }, modify: modify) } @@ -789,7 +808,6 @@ extension MutablePersistableRecord { fetch: fetch) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -802,7 +820,8 @@ extension MutablePersistableRecord { /// - parameter selection: The returned columns (must not be empty). /// - parameter fetch: A function that executes it ``Statement`` argument. /// - parameter modify: A closure that modifies the record. - /// - returns: The result of the `fetch` function. + /// - returns: The result of the `fetch` function, or nil if the record + /// has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the @@ -814,7 +833,7 @@ extension MutablePersistableRecord { _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?, + fetch: (Statement) throws -> T, modify: (inout Self) throws -> Void) throws -> T? { @@ -839,7 +858,7 @@ extension MutablePersistableRecord { onConflict conflictResolution: Database.ConflictResolution?, from container: PersistenceContainer, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?) + fetch: (Statement) throws -> T) throws -> T? { let changes = try PersistenceContainer(db, self).changesIterator(from: container) @@ -861,7 +880,7 @@ extension MutablePersistableRecord { onConflict conflictResolution: Database.ConflictResolution?, from container: PersistenceContainer, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?) + fetch: (Statement) throws -> T) throws -> T? { let changes = try PersistenceContainer(db, self).changesIterator(from: container) diff --git a/GRDB/Record/PersistableRecord+Insert.swift b/GRDB/Record/PersistableRecord+Insert.swift index b44f3f4328..6292e6a0e5 100644 --- a/GRDB/Record/PersistableRecord+Insert.swift +++ b/GRDB/Record/PersistableRecord+Insert.swift @@ -56,7 +56,6 @@ extension PersistableRecord { extension PersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -94,11 +93,10 @@ extension PersistableRecord { /// let partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -107,19 +105,23 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } @@ -199,7 +201,6 @@ extension PersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -237,11 +238,10 @@ extension PersistableRecord { /// let partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -250,20 +250,24 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } diff --git a/GRDB/Record/PersistableRecord+Save.swift b/GRDB/Record/PersistableRecord+Save.swift index 9ba55346b9..80a9f3f815 100644 --- a/GRDB/Record/PersistableRecord+Save.swift +++ b/GRDB/Record/PersistableRecord+Save.swift @@ -39,7 +39,6 @@ extension PersistableRecord { extension PersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -52,26 +51,30 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) }) return success!.saved } @@ -128,7 +131,6 @@ extension PersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -141,27 +143,31 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) }) return success!.saved } diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index bf6b1a58e9..db6edc45e0 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -29,6 +29,7 @@ import Foundation /// - ``exists(_:id:)`` /// - ``exists(_:key:)-60hf2`` /// - ``exists(_:key:)-6ha6`` +/// - ``recordNotFound(_:)`` /// /// ### Throwing Record Not Found Errors /// @@ -697,6 +698,15 @@ extension TableRecord { public enum RecordError: Error { /// A record does not exist in the database. /// + /// This error can be thrown from methods that update, such as + /// ``MutablePersistableRecord/update(_:onConflict:)``. In this case, + /// the error means that the database was not changed. + /// + /// It can also be thrown from methods that inserts or update with a + /// `RETURNING` clause, and the `IGNORE` conflict policy. In this case, + /// the error notifies that a conflict has prevented the change from + /// being applied. + /// /// - parameters: /// - databaseTableName: The table of the missing record. /// - key: The key of the missing record (column and values). @@ -740,6 +750,30 @@ extension TableRecord { } } +extension TableRecord where Self: EncodableRecord { + /// Returns an error that tells that the record does not exist in + /// the database. + /// + /// - returns: ``RecordError/recordNotFound(databaseTableName:key:)``, or + /// any error that prevented the `RecordError` from being constructed. + public func recordNotFound(_ db: Database) -> any Error { + do { + let databaseTableName = type(of: self).databaseTableName + let primaryKey = try db.primaryKey(databaseTableName) + + let container = try PersistenceContainer(db, self) + let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map { + ($0, container[caseInsensitive: $0]?.databaseValue ?? .null) + }) + return RecordError.recordNotFound( + databaseTableName: databaseTableName, + key: key) + } catch { + return error + } + } +} + @available(iOS 13, macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns an error for a record that does not exist in the database. diff --git a/README.md b/README.md index f02c02a46e..761c3d6c18 100644 --- a/README.md +++ b/README.md @@ -2330,11 +2330,10 @@ try dbQueue.write { db in let partialPlayer = PartialPlayer(name: "Alice") // INSERT INTO player (name) VALUES ('Alice') RETURNING * - if let player = try partialPlayer.insertAndFetch(db, as: Player.self) { - print(player.id) // The inserted id - print(player.name) // The inserted name - print(player.score) // The default score - } + let player = try partialPlayer.insertAndFetch(db, as: Player.self) + print(player.id) // The inserted id + print(player.name) // The inserted name + print(player.score) // The default score } ``` @@ -3108,15 +3107,16 @@ struct Player : MutablePersistableRecord { try player.insert(db) ``` -> **Note**: If you specify the `ignore` policy for inserts, the [`didInsert` callback](#persistence-callbacks) will be called with some random id in case of failed insert. You can detect failed insertions with `insertAndFetch`: +> **Note**: If you specify the `ignore` policy for inserts, the [`didInsert` callback](#persistence-callbacks) will be called with some random id in case of failed insert. You can detect failed insertions with `insertAndFetch`: > > ```swift > // How to detect failed `INSERT OR IGNORE`: > // INSERT OR IGNORE INTO player ... RETURNING * -> if let insertedPlayer = try player.insertAndFetch(db) { +> do { +> let insertedPlayer = try player.insertAndFetch(db) { > // Succesful insertion -> } else { -> // Ignored failure +> catch RecordError.recordNotFound { +> // Failed insertion due to IGNORE policy > } > ``` > diff --git a/Tests/GRDBTests/MutablePersistableRecordTests.swift b/Tests/GRDBTests/MutablePersistableRecordTests.swift index 18a15c1eeb..7348cbbbb6 100644 --- a/Tests/GRDBTests/MutablePersistableRecordTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordTests.swift @@ -1261,7 +1261,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let player = FullPlayer(id: nil, name: "Arthur", score: 1000) - let insertedPlayer = try XCTUnwrap(player.insertAndFetch(db)) + let insertedPlayer = try player.insertAndFetch(db) XCTAssertEqual(insertedPlayer.id, 1) XCTAssertEqual(insertedPlayer.name, "Arthur") XCTAssertEqual(insertedPlayer.score, 1000) @@ -1284,7 +1284,7 @@ extension MutablePersistableRecordTests { do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.insertAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (NULL,'Arthur') RETURNING * @@ -1423,7 +1423,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let player = FullPlayer(id: nil, name: "Arthur", score: 1000) - let savedPlayer = try XCTUnwrap(player.saveAndFetch(db)) + let savedPlayer = try player.saveAndFetch(db) XCTAssertEqual(savedPlayer.id, 1) XCTAssertEqual(savedPlayer.name, "Arthur") XCTAssertEqual(savedPlayer.score, 1000) @@ -1446,7 +1446,7 @@ extension MutablePersistableRecordTests { do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("UPDATE") }) XCTAssert(sqlQueries.contains(""" @@ -1483,7 +1483,7 @@ extension MutablePersistableRecordTests { var partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) sqlQueries.removeAll() - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" UPDATE "player" SET "name"='Arthur' WHERE "id"=1 RETURNING * @@ -1521,7 +1521,7 @@ extension MutablePersistableRecordTests { do { sqlQueries.removeAll() var partialPlayer = PartialPlayer(id: 1, name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("INSERT") }) XCTAssert(sqlQueries.contains(""" @@ -1709,7 +1709,7 @@ extension MutablePersistableRecordTests { player.name = "Barbara" do { - let updatedPlayer = try XCTUnwrap(player.updateAndFetch(db)) + let updatedPlayer = try player.updateAndFetch(db) XCTAssertEqual(updatedPlayer.id, 1) XCTAssertEqual(updatedPlayer.name, "Barbara") XCTAssertEqual(updatedPlayer.score, 1000) @@ -1760,7 +1760,7 @@ extension MutablePersistableRecordTests { player.name = "Barbara" do { - let updatedPlayer = try XCTUnwrap(player.updateAndFetch(db, as: PartialPlayer.self)) + let updatedPlayer = try player.updateAndFetch(db, as: PartialPlayer.self) XCTAssertEqual(updatedPlayer.id, 1) XCTAssertEqual(updatedPlayer.name, "Barbara") } @@ -2041,11 +2041,15 @@ extension MutablePersistableRecordTests { try player.insert(db) do { - let updatedRow = try player.updateChangesAndFetch( + // Update with no change + let update = try player.updateChangesAndFetch( db, selection: [AllColumns()], - fetch: { statement in try Row.fetchOne(statement) }, + fetch: { statement in + XCTFail("Should not be called") + return "ignored" + }, modify: { $0.name = "Barbara" }) - XCTAssertNil(updatedRow) + XCTAssertNil(update) } do { diff --git a/Tests/GRDBTests/PersistableRecordTests.swift b/Tests/GRDBTests/PersistableRecordTests.swift index d919eeb989..5c9bc6761b 100644 --- a/Tests/GRDBTests/PersistableRecordTests.swift +++ b/Tests/GRDBTests/PersistableRecordTests.swift @@ -1342,7 +1342,7 @@ extension PersistableRecordTests { do { sqlQueries.removeAll() let partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.insertAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (NULL,'Arthur') RETURNING * @@ -1444,7 +1444,7 @@ extension PersistableRecordTests { do { sqlQueries.removeAll() let partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("UPDATE") }) XCTAssert(sqlQueries.contains(""" @@ -1480,7 +1480,7 @@ extension PersistableRecordTests { let partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) sqlQueries.removeAll() - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" UPDATE "player" SET "name"='Arthur' WHERE "id"=1 RETURNING * @@ -1517,7 +1517,7 @@ extension PersistableRecordTests { do { sqlQueries.removeAll() let partialPlayer = PartialPlayer(id: 1, name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("INSERT") }) XCTAssert(sqlQueries.contains(""" From be708ee6f7b82e73c99e439d8baafaa7508c7d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 17:21:20 +0200 Subject: [PATCH 013/160] TODO --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index e3da5e4706..fe911efe6e 100644 --- a/TODO.md +++ b/TODO.md @@ -87,7 +87,7 @@ - [ ] Database.clearSchemaCache() is fine, but what about dbPool readers? Can we invalidate the cache for a whole pool? - [ ] What can we do with `cross-module-optimization`? See https://github.com/apple/swift-homomorphic-encryption -- [ ] GRDB7/BREAKING: insertAndFetch, saveAndFetch, and updateAndFetch no longer return optionals (32f41472) +- [X] GRDB7/BREAKING: insertAndFetch, saveAndFetch, and updateAndFetch no longer return optionals (32f41472) - [ ] GRDB7/BREAKING: AsyncValueObservation does not need any scheduler (83c0e643) - [ ] GRDB7/BREAKING: Stop exporting SQLite (679d6463) - [ ] GRDB7/BREAKING: Remove Configuration.defaultTransactionKind (2661ff46) From 9cd1d849cccacc92be49c664fd779c279cdada18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Mar 2024 12:53:19 +0100 Subject: [PATCH 014/160] [BREAKING] Stop exporting SQLite --- .../GRDBDemoWatchOS Extension/InterfaceController.swift | 1 + GRDB.xcodeproj/project.pbxproj | 4 ---- GRDB/Core/Configuration.swift | 9 +++++++++ GRDB/Core/Database+Schema.swift | 9 +++++++++ GRDB/Core/Database+Statements.swift | 9 +++++++++ GRDB/Core/Database.swift | 9 +++++++++ GRDB/Core/DatabaseCollation.swift | 9 +++++++++ GRDB/Core/DatabaseError.swift | 9 +++++++++ GRDB/Core/DatabaseFunction.swift | 9 +++++++++ GRDB/Core/DatabaseSnapshotPool.swift | 9 +++++++++ GRDB/Core/DatabaseValue.swift | 9 +++++++++ GRDB/Core/Row.swift | 9 +++++++++ GRDB/Core/RowDecodingError.swift | 9 +++++++++ GRDB/Core/Statement.swift | 9 +++++++++ GRDB/Core/StatementAuthorizer.swift | 9 +++++++++ GRDB/Core/StatementColumnConvertible.swift | 9 +++++++++ GRDB/Core/Support/Foundation/Data.swift | 9 +++++++++ .../Core/Support/Foundation/DatabaseDateComponents.swift | 9 +++++++++ GRDB/Core/Support/Foundation/Date.swift | 9 +++++++++ GRDB/Core/Support/Foundation/Decimal.swift | 9 +++++++++ GRDB/Core/Support/Foundation/UUID.swift | 9 +++++++++ GRDB/Core/Support/StandardLibrary/Optional.swift | 9 +++++++++ GRDB/Core/Support/StandardLibrary/StandardLibrary.swift | 9 +++++++++ GRDB/Core/TransactionObserver.swift | 9 +++++++++ GRDB/Core/WALSnapshot.swift | 9 +++++++++ GRDB/Dump/DumpFormats/DebugDumpFormat.swift | 9 +++++++++ GRDB/Dump/DumpFormats/JSONDumpFormat.swift | 9 +++++++++ GRDB/Dump/DumpFormats/LineDumpFormat.swift | 9 +++++++++ GRDB/Dump/DumpFormats/ListDumpFormat.swift | 9 +++++++++ GRDB/Dump/DumpFormats/QuoteDumpFormat.swift | 9 +++++++++ GRDB/Export.swift | 8 -------- GRDB/FTS/FTS5.swift | 9 +++++++++ GRDB/FTS/FTS5CustomTokenizer.swift | 9 +++++++++ GRDB/FTS/FTS5Tokenizer.swift | 9 +++++++++ GRDB/FTS/FTS5WrapperTokenizer.swift | 9 +++++++++ GRDB/Record/FetchableRecord+Decodable.swift | 9 +++++++++ GRDBCustom.xcodeproj/project.pbxproj | 4 ---- README.md | 5 +++-- TODO.md | 2 +- Tests/CustomSQLite/CustomSQLite/AppDelegate.swift | 2 ++ Tests/GRDBTests/AssociationPrefetchingRowTests.swift | 9 +++++++++ Tests/GRDBTests/DataMemoryTests.swift | 9 +++++++++ Tests/GRDBTests/DatabaseConfigurationTests.swift | 9 +++++++++ Tests/GRDBTests/DatabaseDumpTests.swift | 9 +++++++++ Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift | 9 +++++++++ Tests/GRDBTests/DatabasePoolTests.swift | 9 +++++++++ Tests/GRDBTests/DatabaseRegionTests.swift | 9 +++++++++ Tests/GRDBTests/DatabaseWriterTests.swift | 9 +++++++++ Tests/GRDBTests/FTS5TokenizerTests.swift | 9 +++++++++ Tests/GRDBTests/GRDBTestCase.swift | 9 +++++++++ .../GRDBTests/StatementColumnConvertibleFetchTests.swift | 9 +++++++++ Tests/GRDBTests/TableDefinitionTests.swift | 9 +++++++++ Tests/GRDBTests/UpdateStatementTests.swift | 9 +++++++++ .../GRDBPerformance/FetchPositionalValuesTests.swift | 1 + .../GRDBPerformance/FetchRecordStructTests.swift | 1 + .../GRDBPerformance/InsertPositionalValuesTests.swift | 1 + Tests/SPM/PlainPackage/Sources/SPM/main.swift | 1 + 57 files changed, 425 insertions(+), 19 deletions(-) delete mode 100644 GRDB/Export.swift diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/InterfaceController.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/InterfaceController.swift index 71d59c7948..a116db10fa 100644 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/InterfaceController.swift +++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/InterfaceController.swift @@ -1,6 +1,7 @@ import WatchKit import Foundation import GRDB +import SQLite3 class InterfaceController: WKInterfaceController { diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 9c13f9eb07..213fe83be5 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -267,7 +267,6 @@ 56A2388B1B9C75030082EB20 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238781B9C75030082EB20 /* Statement.swift */; }; 56A238931B9C750B0082EB20 /* DatabaseMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238921B9C750B0082EB20 /* DatabaseMigrator.swift */; }; 56A238A41B9C753B0082EB20 /* Record.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238A11B9C753B0082EB20 /* Record.swift */; }; - 56A2FA3624424D2A00E97D23 /* Export.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2FA3524424D2A00E97D23 /* Export.swift */; }; 56A5EF0F1EF7F20B00F03071 /* ForeignKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A5EF0E1EF7F20B00F03071 /* ForeignKeyInfoTests.swift */; }; 56A8C2301D1914540096E9D4 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A8C22F1D1914540096E9D4 /* UUID.swift */; }; 56AACAA822ACED7100A40F2A /* Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AACAA722ACED7100A40F2A /* Fetch.swift */; }; @@ -756,7 +755,6 @@ 56A238921B9C750B0082EB20 /* DatabaseMigrator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseMigrator.swift; sourceTree = ""; }; 56A238A11B9C753B0082EB20 /* Record.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Record.swift; sourceTree = ""; }; 56A238B51B9CA2590082EB20 /* DatabaseTimestampTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseTimestampTests.swift; sourceTree = ""; }; - 56A2FA3524424D2A00E97D23 /* Export.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Export.swift; sourceTree = ""; }; 56A4CDAF1D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLExpressionLiteralTests.swift; sourceTree = ""; }; 56A5E4081BA2BCF900707640 /* RecordWithColumnNameManglingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordWithColumnNameManglingTests.swift; sourceTree = ""; }; 56A5EF0E1EF7F20B00F03071 /* ForeignKeyInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForeignKeyInfoTests.swift; sourceTree = ""; }; @@ -1740,7 +1738,6 @@ DC37742D19C8CC90004FCF85 /* GRDB */ = { isa = PBXGroup; children = ( - 56A2FA3524424D2A00E97D23 /* Export.swift */, 566DDE0C288D763C0000DCFB /* Fixits.swift */, 648704AD2B7E66390036480B /* PrivacyInfo.xcprivacy */, 56A2386F1B9C75030082EB20 /* Core */, @@ -2226,7 +2223,6 @@ 5617294E223533F40006E219 /* EncodableRecord.swift in Sources */, 56B7EE832863781300C0525F /* WALSnapshot.swift in Sources */, 5698AD181DAAD17A0056AF8C /* FTS5Tokenizer.swift in Sources */, - 56A2FA3624424D2A00E97D23 /* Export.swift in Sources */, 56B964B11DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */, 4E13D2F32769B87F0037588C /* DatabaseBackupProgress.swift in Sources */, 560A37A71C8FF6E500949E71 /* SerializedDatabase.swift in Sources */, diff --git a/GRDB/Core/Configuration.swift b/GRDB/Core/Configuration.swift index 6b261902b6..8314b55cb8 100644 --- a/GRDB/Core/Configuration.swift +++ b/GRDB/Core/Configuration.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Dispatch import Foundation diff --git a/GRDB/Core/Database+Schema.swift b/GRDB/Core/Database+Schema.swift index 878cbe72bf..f3d9257d14 100644 --- a/GRDB/Core/Database+Schema.swift +++ b/GRDB/Core/Database+Schema.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + extension Database { /// A cache for the available database schemas. struct SchemaCache { diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index 9159140f2d..1238e79bbd 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation extension Database { diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 474373db5f..4a20d19be4 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A raw SQLite connection, suitable for the SQLite C API. diff --git a/GRDB/Core/DatabaseCollation.swift b/GRDB/Core/DatabaseCollation.swift index 2f76223c56..18d89e1435 100644 --- a/GRDB/Core/DatabaseCollation.swift +++ b/GRDB/Core/DatabaseCollation.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// `DatabaseCollation` is a custom string comparison function used by SQLite. diff --git a/GRDB/Core/DatabaseError.swift b/GRDB/Core/DatabaseError.swift index 53eb009b37..902fd1a4ac 100644 --- a/GRDB/Core/DatabaseError.swift +++ b/GRDB/Core/DatabaseError.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// An SQLite result code. diff --git a/GRDB/Core/DatabaseFunction.swift b/GRDB/Core/DatabaseFunction.swift index 81fe825748..f1e821b6cd 100644 --- a/GRDB/Core/DatabaseFunction.swift +++ b/GRDB/Core/DatabaseFunction.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A custom SQL function or aggregate. /// /// ## Topics diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index 35fec5a2d4..162b2ab6b0 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A database connection that allows concurrent accesses to an unchanging /// database content, as it existed at the moment the snapshot was created. /// diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index 574ea97916..9348611543 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A value stored in a database table. diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 76ded70da4..ccb2405ec6 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A database row. diff --git a/GRDB/Core/RowDecodingError.swift b/GRDB/Core/RowDecodingError.swift index e2ca849d4d..c550c28c05 100644 --- a/GRDB/Core/RowDecodingError.swift +++ b/GRDB/Core/RowDecodingError.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A key that is used to decode a value in a row @usableFromInline enum RowKey: Hashable, Sendable { diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index a1d8b6544c..7ae4c8962b 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A raw SQLite statement, suitable for the SQLite C API. diff --git a/GRDB/Core/StatementAuthorizer.swift b/GRDB/Core/StatementAuthorizer.swift index 54d9057781..971a461460 100644 --- a/GRDB/Core/StatementAuthorizer.swift +++ b/GRDB/Core/StatementAuthorizer.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + #if canImport(string_h) import string_h #elseif os(Linux) diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index 04a689fc5f..281ebbb1ec 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A type that can decode itself from the low-level C interface to /// SQLite results. /// diff --git a/GRDB/Core/Support/Foundation/Data.swift b/GRDB/Core/Support/Foundation/Data.swift index 6827b71d9c..67367c5faa 100644 --- a/GRDB/Core/Support/Foundation/Data.swift +++ b/GRDB/Core/Support/Foundation/Data.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// Data is convertible to and from DatabaseValue. diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index d4ebba478a..879729d741 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A database value that holds date components. diff --git a/GRDB/Core/Support/Foundation/Date.swift b/GRDB/Core/Support/Foundation/Date.swift index cc056bea45..16b31164fe 100644 --- a/GRDB/Core/Support/Foundation/Date.swift +++ b/GRDB/Core/Support/Foundation/Date.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation #if !os(Linux) diff --git a/GRDB/Core/Support/Foundation/Decimal.swift b/GRDB/Core/Support/Foundation/Decimal.swift index 44fe2eaeb1..ecff4b68dd 100644 --- a/GRDB/Core/Support/Foundation/Decimal.swift +++ b/GRDB/Core/Support/Foundation/Decimal.swift @@ -1,4 +1,13 @@ #if !os(Linux) +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// Decimal adopts DatabaseValueConvertible diff --git a/GRDB/Core/Support/Foundation/UUID.swift b/GRDB/Core/Support/Foundation/UUID.swift index 8aa88d0901..30b1e3dbea 100644 --- a/GRDB/Core/Support/Foundation/UUID.swift +++ b/GRDB/Core/Support/Foundation/UUID.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation #if !os(Linux) diff --git a/GRDB/Core/Support/StandardLibrary/Optional.swift b/GRDB/Core/Support/StandardLibrary/Optional.swift index 4c9e3a3b9b..9692020180 100644 --- a/GRDB/Core/Support/StandardLibrary/Optional.swift +++ b/GRDB/Core/Support/StandardLibrary/Optional.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + extension Optional: StatementBinding where Wrapped: StatementBinding { public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt { switch self { diff --git a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift index 021beb4926..c1d1b8dff9 100644 --- a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift +++ b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + // MARK: - Value Types /// Bool adopts DatabaseValueConvertible and StatementColumnConvertible. diff --git a/GRDB/Core/TransactionObserver.swift b/GRDB/Core/TransactionObserver.swift index d45b3b1206..c18442de66 100644 --- a/GRDB/Core/TransactionObserver.swift +++ b/GRDB/Core/TransactionObserver.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + extension Database { // MARK: - Database Observation diff --git a/GRDB/Core/WALSnapshot.swift b/GRDB/Core/WALSnapshot.swift index b6122426e9..b56956feef 100644 --- a/GRDB/Core/WALSnapshot.swift +++ b/GRDB/Core/WALSnapshot.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// An instance of WALSnapshot records the state of a WAL mode database for some /// specific point in history. /// diff --git a/GRDB/Dump/DumpFormats/DebugDumpFormat.swift b/GRDB/Dump/DumpFormats/DebugDumpFormat.swift index 02f2f11304..bb4354ff43 100644 --- a/GRDB/Dump/DumpFormats/DebugDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/DebugDumpFormat.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A format that prints one line per database row, suitable diff --git a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift index 661e2d071d..62ffce35ab 100644 --- a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A format that prints database rows as a JSON array. diff --git a/GRDB/Dump/DumpFormats/LineDumpFormat.swift b/GRDB/Dump/DumpFormats/LineDumpFormat.swift index a0c9f9a1fd..3b010cb94f 100644 --- a/GRDB/Dump/DumpFormats/LineDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/LineDumpFormat.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A format that prints one line per database value. All blob values diff --git a/GRDB/Dump/DumpFormats/ListDumpFormat.swift b/GRDB/Dump/DumpFormats/ListDumpFormat.swift index c256af4c82..a9415dbfd7 100644 --- a/GRDB/Dump/DumpFormats/ListDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/ListDumpFormat.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A format that prints one line per database row. All blob values diff --git a/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift b/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift index 3c6ae5c17a..813cc237ec 100644 --- a/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A format that prints one line per database row, formatting values /// as SQL literals. /// diff --git a/GRDB/Export.swift b/GRDB/Export.swift deleted file mode 100644 index f5aaed5728..0000000000 --- a/GRDB/Export.swift +++ /dev/null @@ -1,8 +0,0 @@ -// Export the underlying SQLite library -#if SWIFT_PACKAGE -@_exported import CSQLite -#elseif GRDBCIPHER -@_exported import SQLCipher -#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER -@_exported import SQLite3 -#endif diff --git a/GRDB/FTS/FTS5.swift b/GRDB/FTS/FTS5.swift index b8b82d10c6..8fefc26083 100644 --- a/GRDB/FTS/FTS5.swift +++ b/GRDB/FTS/FTS5.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_FTS5 +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// The virtual table module for the FTS5 full-text engine. diff --git a/GRDB/FTS/FTS5CustomTokenizer.swift b/GRDB/FTS/FTS5CustomTokenizer.swift index a294c35b3e..5fdd2e8392 100644 --- a/GRDB/FTS/FTS5CustomTokenizer.swift +++ b/GRDB/FTS/FTS5CustomTokenizer.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_FTS5 +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A type that implements a custom tokenizer for the ``FTS5`` full-text engine. /// /// See [FTS5 Tokenizers](https://github.com/groue/GRDB.swift/blob/master/Documentation/FTS5Tokenizers.md) diff --git a/GRDB/FTS/FTS5Tokenizer.swift b/GRDB/FTS/FTS5Tokenizer.swift index 8609a5ad72..f2d912c262 100644 --- a/GRDB/FTS/FTS5Tokenizer.swift +++ b/GRDB/FTS/FTS5Tokenizer.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_FTS5 +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A low-level SQLite function that lets FTS5Tokenizer notify tokens. diff --git a/GRDB/FTS/FTS5WrapperTokenizer.swift b/GRDB/FTS/FTS5WrapperTokenizer.swift index 16ac1e1f96..5159a2ac8f 100644 --- a/GRDB/FTS/FTS5WrapperTokenizer.swift +++ b/GRDB/FTS/FTS5WrapperTokenizer.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_FTS5 +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// Flags that tell SQLite how to register a token. diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index f89a12a5c2..5f4118e907 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation extension FetchableRecord where Self: Decodable { diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 253e90b00e..7a9c12418e 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -254,7 +254,6 @@ 569BBA43229066CB00478429 /* InflectionsTests.json in Resources */ = {isa = PBXBuildFile; fileRef = 569BBA42229066CB00478429 /* InflectionsTests.json */; }; 569BBA4D229170B300478429 /* Inflections+English.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569BBA4B229170B300478429 /* Inflections+English.swift */; }; 569EF0E6200D37FD00A9FA45 /* DatabaseRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569EF0E5200D37FC00A9FA45 /* DatabaseRegion.swift */; }; - 56A2FA3924424F4200E97D23 /* Export.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2FA3724424F4200E97D23 /* Export.swift */; }; 56A4CDB31D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A4CDAF1D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift */; }; 56A5EF121EF7F20B00F03071 /* ForeignKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A5EF0E1EF7F20B00F03071 /* ForeignKeyInfoTests.swift */; }; 56A6EB2426076F6A00C27594 /* SQL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A6EB2226076F6A00C27594 /* SQL.swift */; }; @@ -770,7 +769,6 @@ 56A238921B9C750B0082EB20 /* DatabaseMigrator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseMigrator.swift; sourceTree = ""; }; 56A238A11B9C753B0082EB20 /* Record.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Record.swift; sourceTree = ""; }; 56A238B51B9CA2590082EB20 /* DatabaseTimestampTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseTimestampTests.swift; sourceTree = ""; }; - 56A2FA3724424F4200E97D23 /* Export.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Export.swift; sourceTree = ""; }; 56A4CDAF1D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLExpressionLiteralTests.swift; sourceTree = ""; }; 56A5E4081BA2BCF900707640 /* RecordWithColumnNameManglingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordWithColumnNameManglingTests.swift; sourceTree = ""; }; 56A5EF0E1EF7F20B00F03071 /* ForeignKeyInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForeignKeyInfoTests.swift; sourceTree = ""; }; @@ -1746,7 +1744,6 @@ DC37742D19C8CC90004FCF85 /* GRDB */ = { isa = PBXGroup; children = ( - 56A2FA3724424F4200E97D23 /* Export.swift */, 566DDE11288D76400000DCFB /* Fixits.swift */, 648704B82B8261070036480B /* PrivacyInfo.xcprivacy */, 56A2386F1B9C75030082EB20 /* Core */, @@ -2055,7 +2052,6 @@ F3BA806F1CFB2E55003DC1BA /* DatabaseValue.swift in Sources */, 56C0539722ACEECD0029D27D /* ValueReducer.swift in Sources */, 4E13D2F82769BC230037588C /* DatabaseBackupProgress.swift in Sources */, - 56A2FA3924424F4200E97D23 /* Export.swift in Sources */, 56FA0C3728B1F2EB00B2DFF7 /* MutablePersistableRecord+Upsert.swift in Sources */, 56B964B31DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */, F3BA80731CFB2E55003DC1BA /* RowAdapter.swift in Sources */, diff --git a/README.md b/README.md index 761c3d6c18..93dde995d3 100644 --- a/README.md +++ b/README.md @@ -1632,10 +1632,11 @@ For more information, see [`tableExists(_:)`](https://swiftpackageindex.com/grou **If not all SQLite APIs are exposed in GRDB, you can still use the [SQLite C Interface](https://www.sqlite.org/c3ref/intro.html) and call [SQLite C functions](https://www.sqlite.org/c3ref/funclist.html).** -Those functions are embedded right into the GRDB module, regardless of the underlying SQLite implementation (system SQLite, [SQLCipher](#encryption), or [custom SQLite build]): +To access the C SQLite functions from SQLCipher or the system SQLite, you need to perform an extra import: ```swift -import GRDB +import SQLite3 // System SQLite +import SQLCipher // SQLCipher let sqliteVersion = String(cString: sqlite3_libversion()) ``` diff --git a/TODO.md b/TODO.md index fe911efe6e..941eae011b 100644 --- a/TODO.md +++ b/TODO.md @@ -89,7 +89,7 @@ - [X] GRDB7/BREAKING: insertAndFetch, saveAndFetch, and updateAndFetch no longer return optionals (32f41472) - [ ] GRDB7/BREAKING: AsyncValueObservation does not need any scheduler (83c0e643) -- [ ] GRDB7/BREAKING: Stop exporting SQLite (679d6463) +- [X] GRDB7/BREAKING: Stop exporting SQLite (679d6463) - [ ] GRDB7/BREAKING: Remove Configuration.defaultTransactionKind (2661ff46) - [ ] GRDB7: Replace LockedBox with Mutex (00ccab06) - [ ] GRDB7: Sendable: BusyCallback (e0d8e20b) diff --git a/Tests/CustomSQLite/CustomSQLite/AppDelegate.swift b/Tests/CustomSQLite/CustomSQLite/AppDelegate.swift index 12798376f0..e8f1bf3367 100644 --- a/Tests/CustomSQLite/CustomSQLite/AppDelegate.swift +++ b/Tests/CustomSQLite/CustomSQLite/AppDelegate.swift @@ -7,5 +7,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { _ = try! DatabaseQueue() _ = FTS5() _ = sqlite3_preupdate_new(nil, 0, nil) + let sqliteVersion = String(cString: sqlite3_libversion()) + print(sqliteVersion) } } diff --git a/Tests/GRDBTests/AssociationPrefetchingRowTests.swift b/Tests/GRDBTests/AssociationPrefetchingRowTests.swift index 5962b2e7b7..2792e32385 100644 --- a/Tests/GRDBTests/AssociationPrefetchingRowTests.swift +++ b/Tests/GRDBTests/AssociationPrefetchingRowTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/DataMemoryTests.swift b/Tests/GRDBTests/DataMemoryTests.swift index a6f3292983..2a996e2f96 100644 --- a/Tests/GRDBTests/DataMemoryTests.swift +++ b/Tests/GRDBTests/DataMemoryTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest @testable import GRDB diff --git a/Tests/GRDBTests/DatabaseConfigurationTests.swift b/Tests/GRDBTests/DatabaseConfigurationTests.swift index 079286e7ea..aa2f8c1888 100644 --- a/Tests/GRDBTests/DatabaseConfigurationTests.swift +++ b/Tests/GRDBTests/DatabaseConfigurationTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/DatabaseDumpTests.swift b/Tests/GRDBTests/DatabaseDumpTests.swift index ad64862fe0..1a50d566d5 100644 --- a/Tests/GRDBTests/DatabaseDumpTests.swift +++ b/Tests/GRDBTests/DatabaseDumpTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift b/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift index 9b6bb306c4..40bd4ec6b4 100644 --- a/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift +++ b/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest @testable import GRDB diff --git a/Tests/GRDBTests/DatabasePoolTests.swift b/Tests/GRDBTests/DatabasePoolTests.swift index 440af6b6b4..391c8da297 100644 --- a/Tests/GRDBTests/DatabasePoolTests.swift +++ b/Tests/GRDBTests/DatabasePoolTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/DatabaseRegionTests.swift b/Tests/GRDBTests/DatabaseRegionTests.swift index 7044aa3a88..eff5d1a735 100644 --- a/Tests/GRDBTests/DatabaseRegionTests.swift +++ b/Tests/GRDBTests/DatabaseRegionTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest @testable import GRDB diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 4660971fc3..2ddfdc4850 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/FTS5TokenizerTests.swift b/Tests/GRDBTests/FTS5TokenizerTests.swift index af88e73515..a6d147796e 100644 --- a/Tests/GRDBTests/FTS5TokenizerTests.swift +++ b/Tests/GRDBTests/FTS5TokenizerTests.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_FTS5 +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index 9028d491f4..e2ad237601 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation import XCTest @testable import GRDB diff --git a/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift b/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift index ed94e9e699..09fb4f7098 100644 --- a/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift +++ b/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/TableDefinitionTests.swift b/Tests/GRDBTests/TableDefinitionTests.swift index 88cf3030e9..8a831882c4 100644 --- a/Tests/GRDBTests/TableDefinitionTests.swift +++ b/Tests/GRDBTests/TableDefinitionTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/UpdateStatementTests.swift b/Tests/GRDBTests/UpdateStatementTests.swift index faf20f6763..859f662a7e 100644 --- a/Tests/GRDBTests/UpdateStatementTests.swift +++ b/Tests/GRDBTests/UpdateStatementTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import CSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/Performance/GRDBPerformance/FetchPositionalValuesTests.swift b/Tests/Performance/GRDBPerformance/FetchPositionalValuesTests.swift index 90f39ded92..72b41d0e04 100644 --- a/Tests/Performance/GRDBPerformance/FetchPositionalValuesTests.swift +++ b/Tests/Performance/GRDBPerformance/FetchPositionalValuesTests.swift @@ -1,5 +1,6 @@ import XCTest import GRDB +import SQLite3 #if GRDB_COMPARE import SQLite #endif diff --git a/Tests/Performance/GRDBPerformance/FetchRecordStructTests.swift b/Tests/Performance/GRDBPerformance/FetchRecordStructTests.swift index a8b734954f..13387d5e1d 100644 --- a/Tests/Performance/GRDBPerformance/FetchRecordStructTests.swift +++ b/Tests/Performance/GRDBPerformance/FetchRecordStructTests.swift @@ -1,5 +1,6 @@ import XCTest import GRDB +import SQLite3 #if GRDB_COMPARE import SQLite #endif diff --git a/Tests/Performance/GRDBPerformance/InsertPositionalValuesTests.swift b/Tests/Performance/GRDBPerformance/InsertPositionalValuesTests.swift index 382c6b516d..ec6edb99b1 100644 --- a/Tests/Performance/GRDBPerformance/InsertPositionalValuesTests.swift +++ b/Tests/Performance/GRDBPerformance/InsertPositionalValuesTests.swift @@ -1,5 +1,6 @@ import XCTest import GRDB +import SQLite3 #if GRDB_COMPARE import SQLite #endif diff --git a/Tests/SPM/PlainPackage/Sources/SPM/main.swift b/Tests/SPM/PlainPackage/Sources/SPM/main.swift index 1e00a3a2b6..01ba311513 100644 --- a/Tests/SPM/PlainPackage/Sources/SPM/main.swift +++ b/Tests/SPM/PlainPackage/Sources/SPM/main.swift @@ -1,4 +1,5 @@ import GRDB +import SQLite3 let cVersion = String(cString: sqlite3_libversion()) print("SQLite version from C API: \(cVersion)") From 9e1ff422498c1398a5c2ae7a0327b062e421ef60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 23 Jun 2024 15:50:58 +0200 Subject: [PATCH 015/160] [BREAKING] Rename CSQLite to GRDBSQLite Fix #1528 --- .../xcschemes/GRDB-Package.xcscheme | 134 ------------------ GRDB/Core/Configuration.swift | 2 +- GRDB/Core/Database+Schema.swift | 2 +- GRDB/Core/Database+Statements.swift | 2 +- GRDB/Core/Database.swift | 2 +- GRDB/Core/DatabaseCollation.swift | 2 +- GRDB/Core/DatabaseError.swift | 2 +- GRDB/Core/DatabaseFunction.swift | 2 +- GRDB/Core/DatabaseSnapshotPool.swift | 2 +- GRDB/Core/DatabaseValue.swift | 2 +- GRDB/Core/Row.swift | 2 +- GRDB/Core/RowDecodingError.swift | 2 +- GRDB/Core/Statement.swift | 2 +- GRDB/Core/StatementAuthorizer.swift | 2 +- GRDB/Core/StatementColumnConvertible.swift | 2 +- GRDB/Core/Support/Foundation/Data.swift | 2 +- .../Foundation/DatabaseDateComponents.swift | 2 +- GRDB/Core/Support/Foundation/Date.swift | 2 +- GRDB/Core/Support/Foundation/Decimal.swift | 2 +- GRDB/Core/Support/Foundation/UUID.swift | 2 +- .../Support/StandardLibrary/Optional.swift | 2 +- .../StandardLibrary/StandardLibrary.swift | 2 +- GRDB/Core/TransactionObserver.swift | 2 +- GRDB/Core/WALSnapshot.swift | 2 +- GRDB/Dump/DumpFormats/DebugDumpFormat.swift | 2 +- GRDB/Dump/DumpFormats/JSONDumpFormat.swift | 2 +- GRDB/Dump/DumpFormats/LineDumpFormat.swift | 2 +- GRDB/Dump/DumpFormats/ListDumpFormat.swift | 2 +- GRDB/Dump/DumpFormats/QuoteDumpFormat.swift | 2 +- GRDB/FTS/FTS5.swift | 2 +- GRDB/FTS/FTS5CustomTokenizer.swift | 2 +- GRDB/FTS/FTS5Tokenizer.swift | 2 +- GRDB/FTS/FTS5WrapperTokenizer.swift | 2 +- GRDB/Record/FetchableRecord+Decodable.swift | 2 +- Package.swift | 6 +- README.md | 2 +- .../{CSQLite => GRDBSQLite}/module.modulemap | 2 +- Sources/{CSQLite => GRDBSQLite}/shim.h | 0 .../AssociationPrefetchingRowTests.swift | 2 +- Tests/GRDBTests/DataMemoryTests.swift | 2 +- .../DatabaseConfigurationTests.swift | 2 +- Tests/GRDBTests/DatabaseDumpTests.swift | 2 +- .../DatabasePoolReleaseMemoryTests.swift | 2 +- Tests/GRDBTests/DatabasePoolTests.swift | 2 +- Tests/GRDBTests/DatabaseRegionTests.swift | 2 +- Tests/GRDBTests/DatabaseWriterTests.swift | 2 +- Tests/GRDBTests/FTS5TokenizerTests.swift | 2 +- Tests/GRDBTests/GRDBTestCase.swift | 2 +- ...StatementColumnConvertibleFetchTests.swift | 2 +- Tests/GRDBTests/TableDefinitionTests.swift | 2 +- Tests/GRDBTests/UpdateStatementTests.swift | 2 +- 51 files changed, 51 insertions(+), 185 deletions(-) delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/GRDB-Package.xcscheme rename Sources/{CSQLite => GRDBSQLite}/module.modulemap (65%) rename Sources/{CSQLite => GRDBSQLite}/shim.h (100%) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/GRDB-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/GRDB-Package.xcscheme deleted file mode 100644 index b60345c202..0000000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/GRDB-Package.xcscheme +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/GRDB/Core/Configuration.swift b/GRDB/Core/Configuration.swift index 8314b55cb8..2c6e62498a 100644 --- a/GRDB/Core/Configuration.swift +++ b/GRDB/Core/Configuration.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Database+Schema.swift b/GRDB/Core/Database+Schema.swift index f3d9257d14..3cdf75c823 100644 --- a/GRDB/Core/Database+Schema.swift +++ b/GRDB/Core/Database+Schema.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index 1238e79bbd..b83006a19d 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 4a20d19be4..7d1e6c2f8a 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/DatabaseCollation.swift b/GRDB/Core/DatabaseCollation.swift index 18d89e1435..38c0dba681 100644 --- a/GRDB/Core/DatabaseCollation.swift +++ b/GRDB/Core/DatabaseCollation.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/DatabaseError.swift b/GRDB/Core/DatabaseError.swift index 902fd1a4ac..524c18d63e 100644 --- a/GRDB/Core/DatabaseError.swift +++ b/GRDB/Core/DatabaseError.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/DatabaseFunction.swift b/GRDB/Core/DatabaseFunction.swift index f1e821b6cd..2fb1301f5f 100644 --- a/GRDB/Core/DatabaseFunction.swift +++ b/GRDB/Core/DatabaseFunction.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index 162b2ab6b0..d17f672404 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -1,7 +1,7 @@ #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index 9348611543..1c8e721f98 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index ccb2405ec6..5b1c130a52 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/RowDecodingError.swift b/GRDB/Core/RowDecodingError.swift index c550c28c05..ed82c1f891 100644 --- a/GRDB/Core/RowDecodingError.swift +++ b/GRDB/Core/RowDecodingError.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index 7ae4c8962b..698855b19d 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/StatementAuthorizer.swift b/GRDB/Core/StatementAuthorizer.swift index 971a461460..d2427ac30f 100644 --- a/GRDB/Core/StatementAuthorizer.swift +++ b/GRDB/Core/StatementAuthorizer.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index 281ebbb1ec..d16c038429 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Support/Foundation/Data.swift b/GRDB/Core/Support/Foundation/Data.swift index 67367c5faa..55d6bdb20f 100644 --- a/GRDB/Core/Support/Foundation/Data.swift +++ b/GRDB/Core/Support/Foundation/Data.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index 879729d741..fb861feb8a 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Support/Foundation/Date.swift b/GRDB/Core/Support/Foundation/Date.swift index 16b31164fe..6676c065ae 100644 --- a/GRDB/Core/Support/Foundation/Date.swift +++ b/GRDB/Core/Support/Foundation/Date.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Support/Foundation/Decimal.swift b/GRDB/Core/Support/Foundation/Decimal.swift index ecff4b68dd..e200c6c596 100644 --- a/GRDB/Core/Support/Foundation/Decimal.swift +++ b/GRDB/Core/Support/Foundation/Decimal.swift @@ -1,7 +1,7 @@ #if !os(Linux) // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Support/Foundation/UUID.swift b/GRDB/Core/Support/Foundation/UUID.swift index 30b1e3dbea..1a8bdff2a5 100644 --- a/GRDB/Core/Support/Foundation/UUID.swift +++ b/GRDB/Core/Support/Foundation/UUID.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Support/StandardLibrary/Optional.swift b/GRDB/Core/Support/StandardLibrary/Optional.swift index 9692020180..0902097d9a 100644 --- a/GRDB/Core/Support/StandardLibrary/Optional.swift +++ b/GRDB/Core/Support/StandardLibrary/Optional.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift index c1d1b8dff9..8a4c5a7d83 100644 --- a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift +++ b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/TransactionObserver.swift b/GRDB/Core/TransactionObserver.swift index c18442de66..99fb4dcfdf 100644 --- a/GRDB/Core/TransactionObserver.swift +++ b/GRDB/Core/TransactionObserver.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Core/WALSnapshot.swift b/GRDB/Core/WALSnapshot.swift index b56956feef..a8c0168a73 100644 --- a/GRDB/Core/WALSnapshot.swift +++ b/GRDB/Core/WALSnapshot.swift @@ -1,7 +1,7 @@ #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Dump/DumpFormats/DebugDumpFormat.swift b/GRDB/Dump/DumpFormats/DebugDumpFormat.swift index bb4354ff43..b3ae312485 100644 --- a/GRDB/Dump/DumpFormats/DebugDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/DebugDumpFormat.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift index 62ffce35ab..027cca3791 100644 --- a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Dump/DumpFormats/LineDumpFormat.swift b/GRDB/Dump/DumpFormats/LineDumpFormat.swift index 3b010cb94f..a71b05ca69 100644 --- a/GRDB/Dump/DumpFormats/LineDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/LineDumpFormat.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Dump/DumpFormats/ListDumpFormat.swift b/GRDB/Dump/DumpFormats/ListDumpFormat.swift index a9415dbfd7..dc7bfdb6c3 100644 --- a/GRDB/Dump/DumpFormats/ListDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/ListDumpFormat.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift b/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift index 813cc237ec..3818c7c17e 100644 --- a/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/FTS/FTS5.swift b/GRDB/FTS/FTS5.swift index 8fefc26083..e6dce2abbe 100644 --- a/GRDB/FTS/FTS5.swift +++ b/GRDB/FTS/FTS5.swift @@ -1,7 +1,7 @@ #if SQLITE_ENABLE_FTS5 // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/FTS/FTS5CustomTokenizer.swift b/GRDB/FTS/FTS5CustomTokenizer.swift index 5fdd2e8392..370f1256c5 100644 --- a/GRDB/FTS/FTS5CustomTokenizer.swift +++ b/GRDB/FTS/FTS5CustomTokenizer.swift @@ -1,7 +1,7 @@ #if SQLITE_ENABLE_FTS5 // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/FTS/FTS5Tokenizer.swift b/GRDB/FTS/FTS5Tokenizer.swift index f2d912c262..e4aecc36f5 100644 --- a/GRDB/FTS/FTS5Tokenizer.swift +++ b/GRDB/FTS/FTS5Tokenizer.swift @@ -1,7 +1,7 @@ #if SQLITE_ENABLE_FTS5 // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/FTS/FTS5WrapperTokenizer.swift b/GRDB/FTS/FTS5WrapperTokenizer.swift index 5159a2ac8f..5851866be7 100644 --- a/GRDB/FTS/FTS5WrapperTokenizer.swift +++ b/GRDB/FTS/FTS5WrapperTokenizer.swift @@ -1,7 +1,7 @@ #if SQLITE_ENABLE_FTS5 // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 5f4118e907..9fbf0cfabf 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Package.swift b/Package.swift index 5d546af48d..52e4c335a3 100644 --- a/Package.swift +++ b/Package.swift @@ -40,18 +40,18 @@ let package = Package( .watchOS(.v7), ], products: [ - .library(name: "CSQLite", targets: ["CSQLite"]), + .library(name: "GRDBSQLite", targets: ["GRDBSQLite"]), .library(name: "GRDB", targets: ["GRDB"]), .library(name: "GRDB-dynamic", type: .dynamic, targets: ["GRDB"]), ], dependencies: dependencies, targets: [ .systemLibrary( - name: "CSQLite", + name: "GRDBSQLite", providers: [.apt(["libsqlite3-dev"])]), .target( name: "GRDB", - dependencies: ["CSQLite"], + dependencies: ["GRDBSQLite"], path: "GRDB", resources: [.copy("PrivacyInfo.xcprivacy")], cSettings: cSettings, diff --git a/README.md b/README.md index 93dde995d3..a27d198d7d 100644 --- a/README.md +++ b/README.md @@ -348,7 +348,7 @@ GRDB offers two libraries, `GRDB` and `GRDB-dynamic`. Pick only one. When in dou > **Note**: Linux is not currently supported. > -> **Warning**: Due to an Xcode bug, you will get "No such module 'CSQLite'" errors when you want to embed the GRDB package in other targets than the main application (watch extensions, for example). UI and Unit testing targets are OK, though. See [#642](https://github.com/groue/GRDB.swift/issues/642#issuecomment-575994093) for more information. +> **Warning**: Due to an Xcode bug, you will get "No such module 'GRDBSQLite'" errors when you want to embed the GRDB package in other targets than the main application (watch extensions, for example). UI and Unit testing targets are OK, though. See [#642](https://github.com/groue/GRDB.swift/issues/642#issuecomment-575994093) and [#1424](https://github.com/groue/GRDB.swift/issues/1424#issuecomment-1774088155) for more information. ## CocoaPods diff --git a/Sources/CSQLite/module.modulemap b/Sources/GRDBSQLite/module.modulemap similarity index 65% rename from Sources/CSQLite/module.modulemap rename to Sources/GRDBSQLite/module.modulemap index 0a291b5e20..95e4e886f9 100644 --- a/Sources/CSQLite/module.modulemap +++ b/Sources/GRDBSQLite/module.modulemap @@ -1,4 +1,4 @@ -module CSQLite [system] { +module GRDBSQLite [system] { header "shim.h" link "sqlite3" export * diff --git a/Sources/CSQLite/shim.h b/Sources/GRDBSQLite/shim.h similarity index 100% rename from Sources/CSQLite/shim.h rename to Sources/GRDBSQLite/shim.h diff --git a/Tests/GRDBTests/AssociationPrefetchingRowTests.swift b/Tests/GRDBTests/AssociationPrefetchingRowTests.swift index 2792e32385..ead7d89b92 100644 --- a/Tests/GRDBTests/AssociationPrefetchingRowTests.swift +++ b/Tests/GRDBTests/AssociationPrefetchingRowTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/DataMemoryTests.swift b/Tests/GRDBTests/DataMemoryTests.swift index 2a996e2f96..3f572fc304 100644 --- a/Tests/GRDBTests/DataMemoryTests.swift +++ b/Tests/GRDBTests/DataMemoryTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/DatabaseConfigurationTests.swift b/Tests/GRDBTests/DatabaseConfigurationTests.swift index aa2f8c1888..fccf86e9da 100644 --- a/Tests/GRDBTests/DatabaseConfigurationTests.swift +++ b/Tests/GRDBTests/DatabaseConfigurationTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/DatabaseDumpTests.swift b/Tests/GRDBTests/DatabaseDumpTests.swift index 1a50d566d5..432177ed9d 100644 --- a/Tests/GRDBTests/DatabaseDumpTests.swift +++ b/Tests/GRDBTests/DatabaseDumpTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift b/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift index 40bd4ec6b4..09dfde4057 100644 --- a/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift +++ b/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/DatabasePoolTests.swift b/Tests/GRDBTests/DatabasePoolTests.swift index 391c8da297..2e42bb5ccd 100644 --- a/Tests/GRDBTests/DatabasePoolTests.swift +++ b/Tests/GRDBTests/DatabasePoolTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/DatabaseRegionTests.swift b/Tests/GRDBTests/DatabaseRegionTests.swift index eff5d1a735..848187ed0f 100644 --- a/Tests/GRDBTests/DatabaseRegionTests.swift +++ b/Tests/GRDBTests/DatabaseRegionTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 2ddfdc4850..581f7d3070 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/FTS5TokenizerTests.swift b/Tests/GRDBTests/FTS5TokenizerTests.swift index a6d147796e..d7efff5c07 100644 --- a/Tests/GRDBTests/FTS5TokenizerTests.swift +++ b/Tests/GRDBTests/FTS5TokenizerTests.swift @@ -1,7 +1,7 @@ #if SQLITE_ENABLE_FTS5 // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index e2ad237601..837cf89b46 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift b/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift index 09fb4f7098..cee0f6e954 100644 --- a/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift +++ b/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/TableDefinitionTests.swift b/Tests/GRDBTests/TableDefinitionTests.swift index 8a831882c4..1079b39d61 100644 --- a/Tests/GRDBTests/TableDefinitionTests.swift +++ b/Tests/GRDBTests/TableDefinitionTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER diff --git a/Tests/GRDBTests/UpdateStatementTests.swift b/Tests/GRDBTests/UpdateStatementTests.swift index 859f662a7e..64b9c23c38 100644 --- a/Tests/GRDBTests/UpdateStatementTests.swift +++ b/Tests/GRDBTests/UpdateStatementTests.swift @@ -1,6 +1,6 @@ // Import C SQLite functions #if SWIFT_PACKAGE -import CSQLite +import GRDBSQLite #elseif GRDBCIPHER import SQLCipher #elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER From ad10d881e8eba7f7da1f9583e6b41e935cfc00ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 24 Aug 2024 19:32:54 +0200 Subject: [PATCH 016/160] Fix deprecation warning in Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 52e4c335a3..9377585aac 100644 --- a/Package.swift +++ b/Package.swift @@ -79,5 +79,5 @@ let package = Package( cSettings: cSettings, swiftSettings: swiftSettings) ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v5] ) From 451a9eb49348ecd5284a0cd074165cc5d9ab0af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Mar 2024 14:45:01 +0100 Subject: [PATCH 017/160] [BREAKING] Remove Configuration.defaultTransactionKind GRDB now defaults read-only transactions to DEFERRED, and write transactions to IMMEDIATE. See https://github.com/groue/GRDB.swift/issues/1483 for some context. --- GRDB/Core/Configuration.swift | 22 -- GRDB/Core/Database.swift | 33 ++- GRDB/Core/DatabasePool.swift | 11 +- GRDB/Core/DatabaseQueue.swift | 7 +- GRDB/Core/DatabaseSnapshot.swift | 4 - GRDB/Core/DatabaseSnapshotPool.swift | 4 - GRDB/Documentation.docc/DatabaseSharing.md | 3 +- .../Extension/Configuration.md | 1 - GRDB/Documentation.docc/Transactions.md | 19 +- TODO.md | 2 +- Tests/GRDBTests/BackupTestCase.swift | 4 +- Tests/GRDBTests/DatabaseQueueTests.swift | 2 - Tests/GRDBTests/DatabaseSavepointTests.swift | 228 +----------------- .../GRDBTests/DatabaseSnapshotPoolTests.swift | 13 +- Tests/GRDBTests/DatabaseSuspensionTests.swift | 2 +- Tests/GRDBTests/DatabaseTests.swift | 47 +++- 16 files changed, 88 insertions(+), 314 deletions(-) diff --git a/GRDB/Core/Configuration.swift b/GRDB/Core/Configuration.swift index 2c6e62498a..17a50eff8e 100644 --- a/GRDB/Core/Configuration.swift +++ b/GRDB/Core/Configuration.swift @@ -238,28 +238,6 @@ public struct Configuration { // MARK: - Transactions - /// The default kind of write transactions. - /// - /// The default is ``Database/TransactionKind/deferred``. - /// - /// You can change the default transaction kind. For example, you can force - /// all write transactions to be `IMMEDIATE`: - /// - /// ```swift - /// var config = Configuration() - /// config.defaultTransactionKind = .immediate - /// let dbQueue = try DatabaseQueue(configuration: config) - /// - /// // BEGIN IMMEDIATE TRANSACTION; ...; COMMIT TRANSACTION; - /// try dbQueue.write { db in ... } - /// ``` - /// - /// This property is ignored for read-only transactions. Those always open - /// `DEFERRED` SQLite transactions. - /// - /// Related SQLite documentation: - public var defaultTransactionKind: Database.TransactionKind = .deferred - /// A boolean value indicating whether it is valid to leave a transaction /// opened at the end of a database access method. /// diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 7d1e6c2f8a..ec881c14e0 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1282,14 +1282,10 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// Use ``inSavepoint(_:)`` instead. /// /// - parameters: - /// - kind: The transaction type (default nil). + /// - kind: The transaction type. /// - /// If nil, and the database connection is read-only, the transaction - /// kind is ``TransactionKind/deferred``. - /// - /// If nil, and the database connection is not read-only, the - /// transaction kind is the ``Configuration/defaultTransactionKind`` - /// of the ``configuration``. + /// If nil, the transaction kind is DEFERRED when the current + /// database access is read-only, and IMMEDIATE otherwise. /// - operations: A function that executes SQL statements and returns /// either ``TransactionCompletion/commit`` or ``TransactionCompletion/rollback``. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or the @@ -1413,8 +1409,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib // By default, top level SQLite savepoints open a // deferred transaction. // - // But GRDB database configuration mandates a default transaction - // kind that we have to honor. + // But GRDB prefers immediate transactions for writes. // // Besides, starting some (?) SQLCipher/SQLite version, SQLite has a // bug. Returning 1 from `sqlite3_commit_hook` does not leave the @@ -1502,18 +1497,22 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// Related SQLite documentation: /// /// - parameters: - /// - kind: The transaction type (default nil). - /// - /// If nil, and the database connection is read-only, the transaction - /// kind is ``TransactionKind/deferred``. + /// - kind: The transaction type. /// - /// If nil, and the database connection is not read-only, the - /// transaction kind is the ``Configuration/defaultTransactionKind`` - /// of the ``configuration``. + /// If nil, the transaction kind is DEFERRED when the current + /// database access is read-only, and IMMEDIATE otherwise. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public func beginTransaction(_ kind: TransactionKind? = nil) throws { // SQLite throws an error for non-deferred transactions when read-only. - let kind = kind ?? (isReadOnly ? .deferred : configuration.defaultTransactionKind) + // We prefer immediate transactions for writes, so that write + // transactions can not overlap. This reduces the opportunity for + // SQLITE_BUSY, which is immediately thrown whenever a transaction + // is upgraded after an initial read and a concurrent processes + // has acquired the write lock beforehand. This SQLITE_BUSY error + // can not be avoided with a busy timeout. + // + // See . + let kind = kind ?? (isReadOnly ? .deferred : .immediate) try execute(sql: "BEGIN \(kind.rawValue) TRANSACTION") assert(sqlite3_get_autocommit(sqliteConnection) == 0) } diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index 34a9e7fdeb..771f37038d 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -118,10 +118,6 @@ public final class DatabasePool { configuration.readonly = true - // Readers use deferred transactions by default. - // Other transaction kinds are forbidden by SQLite in read-only connections. - configuration.defaultTransactionKind = .deferred - // // > But there are some obscure cases where a query against a WAL-mode // > database can return SQLITE_BUSY, so applications should be prepared @@ -787,9 +783,10 @@ extension DatabasePool: DatabaseWriter { /// /// - precondition: This method is not reentrant. /// - parameters: - /// - kind: The transaction type (default nil). If nil, the transaction - /// type is the ``Configuration/defaultTransactionKind`` of the - /// the ``configuration``. + /// - kind: The transaction type. + /// + /// If nil, the transaction kind is DEFERRED when the database + /// connection is read-only, and IMMEDIATE otherwise. /// - updates: A function that updates the database. /// - throws: The error thrown by `updates`, or by the wrapping transaction. public func writeInTransaction( diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index ae763f694c..0e43acc422 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -352,9 +352,10 @@ extension DatabaseQueue: DatabaseWriter { /// ``` /// /// - parameters: - /// - kind: The transaction type (default nil). If nil, the transaction - /// type is the ``Configuration/defaultTransactionKind`` of the - /// the ``configuration``. + /// - kind: The transaction type. + /// + /// If nil, the transaction kind is DEFERRED when the database + /// connection is read-only, and IMMEDIATE otherwise. /// - updates: A function that updates the database. /// - throws: The error thrown by `updates`, or by the wrapping transaction. public func inTransaction( diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 1381c1871c..197771b24a 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -126,10 +126,6 @@ public final class DatabaseSnapshot { // DatabaseSnapshot is read-only. configuration.readonly = true - // DatabaseSnapshot uses deferred transactions by default. - // Other transaction kinds are forbidden by SQLite in read-only connections. - configuration.defaultTransactionKind = .deferred - // DatabaseSnapshot keeps a long-lived transaction. configuration.allowsUnsafeTransactions = true diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index d17f672404..525fa1c745 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -242,10 +242,6 @@ public final class DatabaseSnapshotPool { // DatabaseSnapshotPool is read-only. configuration.readonly = true - // DatabaseSnapshotPool uses deferred transactions by default. - // Other transaction kinds are forbidden by SQLite in read-only connections. - configuration.defaultTransactionKind = .deferred - // DatabaseSnapshotPool keeps a long-lived transaction. configuration.allowsUnsafeTransactions = true diff --git a/GRDB/Documentation.docc/DatabaseSharing.md b/GRDB/Documentation.docc/DatabaseSharing.md index 50b9569f4c..29dd6b9b68 100644 --- a/GRDB/Documentation.docc/DatabaseSharing.md +++ b/GRDB/Documentation.docc/DatabaseSharing.md @@ -152,12 +152,11 @@ If several processes want to write in the database, configure the database pool ```swift var configuration = Configuration() -configuration.defaultTransactionKind = .immediate configuration.busyMode = .timeout(/* a TimeInterval */) let dbPool = try DatabasePool(path: ..., configuration: configuration) ``` -Both the `defaultTransactionKind` and `busyMode` are important for preventing `SQLITE_BUSY`. The `immediate` transaction kind prevents write transactions from overlapping, and the busy timeout has write transactions wait, instead of throwing `SQLITE_BUSY`, whenever another process is writing. +The busy timeout has write transactions wait, instead of throwing `SQLITE_BUSY`, whenever another process is writing. GRDB automatically opens all write transactions with the IMMEDIATE kind, preventing write transactions from overlapping. With such a setup, you will still get `SQLITE_BUSY` errors if the database remains locked by another process for longer than the specified timeout. You can catch those errors: diff --git a/GRDB/Documentation.docc/Extension/Configuration.md b/GRDB/Documentation.docc/Extension/Configuration.md index efb77b8a62..55f0d770d8 100644 --- a/GRDB/Documentation.docc/Extension/Configuration.md +++ b/GRDB/Documentation.docc/Extension/Configuration.md @@ -90,7 +90,6 @@ do { ### Configuring GRDB Connections - ``allowsUnsafeTransactions`` -- ``defaultTransactionKind`` - ``label`` - ``maximumReaderCount`` - ``observesSuspensionNotifications`` diff --git a/GRDB/Documentation.docc/Transactions.md b/GRDB/Documentation.docc/Transactions.md index 56dedce47b..2067a1f096 100644 --- a/GRDB/Documentation.docc/Transactions.md +++ b/GRDB/Documentation.docc/Transactions.md @@ -214,6 +214,8 @@ SQLite savepoints are more than nested transactions, though. For advanced uses, SQLite supports [three kinds of transactions](https://www.sqlite.org/lang_transaction.html): deferred (the default), immediate, and exclusive. +By default, GRDB opens DEFERRED transaction for reads, and IMMEDIATE transactions for writes. + The transaction kind can be chosen for individual transaction: ```swift @@ -222,20 +224,3 @@ let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite") // BEGIN EXCLUSIVE TRANSACTION ... try dbQueue.inTransaction(.exclusive) { db in ... } ``` - -It is also possible to configure the ``Configuration/defaultTransactionKind``: - -```swift -var config = Configuration() -config.defaultTransactionKind = .immediate - -let dbQueue = try DatabaseQueue( - path: "/path/to/database.sqlite", - configuration: config) - -// BEGIN IMMEDIATE TRANSACTION ... -try dbQueue.write { db in ... } - -// BEGIN IMMEDIATE TRANSACTION ... -try dbQueue.inTransaction { db in ... } -``` diff --git a/TODO.md b/TODO.md index 941eae011b..11e7d0d93e 100644 --- a/TODO.md +++ b/TODO.md @@ -90,7 +90,7 @@ - [X] GRDB7/BREAKING: insertAndFetch, saveAndFetch, and updateAndFetch no longer return optionals (32f41472) - [ ] GRDB7/BREAKING: AsyncValueObservation does not need any scheduler (83c0e643) - [X] GRDB7/BREAKING: Stop exporting SQLite (679d6463) -- [ ] GRDB7/BREAKING: Remove Configuration.defaultTransactionKind (2661ff46) +- [X] GRDB7/BREAKING: Remove Configuration.defaultTransactionKind (2661ff46) - [ ] GRDB7: Replace LockedBox with Mutex (00ccab06) - [ ] GRDB7: Sendable: BusyCallback (e0d8e20b) - [ ] GRDB7: Sendable: BusyMode (e0d8e20b) diff --git a/Tests/GRDBTests/BackupTestCase.swift b/Tests/GRDBTests/BackupTestCase.swift index 33b2f1ac6a..4ea5d049fd 100644 --- a/Tests/GRDBTests/BackupTestCase.swift +++ b/Tests/GRDBTests/BackupTestCase.swift @@ -84,7 +84,7 @@ class BackupTestCase: GRDBTestCase { let sourceDbPageCount = try setupBackupSource(source) try setupBackupDestination(destination) - try source.write { sourceDb in + try source.read { sourceDb in try destination.barrierWriteWithoutTransaction { destDb in XCTAssertThrowsError( try sourceDb.backup(to: destDb, pagesPerStep: 1) { progress in @@ -102,7 +102,7 @@ class BackupTestCase: GRDBTestCase { XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT id FROM items")!, 1) } - try source.write { dbSource in + try source.read { dbSource in try destination.barrierWriteWithoutTransaction { dbDest in var progressCount: Int = 1 var isCompleted: Bool = false diff --git a/Tests/GRDBTests/DatabaseQueueTests.swift b/Tests/GRDBTests/DatabaseQueueTests.swift index 7bd5adf2b5..66f4cd5554 100644 --- a/Tests/GRDBTests/DatabaseQueueTests.swift +++ b/Tests/GRDBTests/DatabaseQueueTests.swift @@ -369,8 +369,6 @@ class DatabaseQueueTests: GRDBTestCase { func test_busy_timeout_and_IMMEDIATE_transactions_do_prevent_SQLITE_BUSY() throws { var configuration = dbConfiguration! // Test fails when this line is commented - configuration.defaultTransactionKind = .immediate - // Test fails when this line is commented configuration.busyMode = .timeout(10) let dbQueue = try makeDatabaseQueue(filename: "test") diff --git a/Tests/GRDBTests/DatabaseSavepointTests.swift b/Tests/GRDBTests/DatabaseSavepointTests.swift index 1483d1a682..bc727f28eb 100644 --- a/Tests/GRDBTests/DatabaseSavepointTests.swift +++ b/Tests/GRDBTests/DatabaseSavepointTests.swift @@ -96,226 +96,8 @@ class DatabaseSavepointTests: GRDBTestCase { XCTAssertThrowsError(try db.execute(sql: "COMMIT")) } } - - func testReleaseTopLevelSavepointFromDatabaseWithDefaultDeferredTransactions() throws { - dbConfiguration.defaultTransactionKind = .deferred - let dbQueue = try makeDatabaseQueue() - let observer = Observer() - dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - return .commit - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item3") - } - - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "COMMIT TRANSACTION", - "INSERT INTO items (name) VALUES ('item3')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item3"]) - XCTAssertEqual(observer.allRecordedEvents.count, 3) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 3) - #endif - } - - func testRollbackTopLevelSavepointFromDatabaseWithDefaultDeferredTransactions() throws { - dbConfiguration.defaultTransactionKind = .deferred - let dbQueue = try makeDatabaseQueue() - let observer = Observer() - dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - return .rollback - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item3") - } - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "ROLLBACK TRANSACTION", - "INSERT INTO items (name) VALUES ('item3')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item3"]) - XCTAssertEqual(observer.allRecordedEvents.count, 3) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 3) - #endif - } - - func testNestedSavepointFromDatabaseWithDefaultDeferredTransactions() throws { - dbConfiguration.defaultTransactionKind = .deferred - let dbQueue = try makeDatabaseQueue() - let observer = Observer() - dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item3") - return .commit - } - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item4") - return .commit - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item5") - } - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item3')", - "RELEASE SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item4')", - "COMMIT TRANSACTION", - "INSERT INTO items (name) VALUES ('item5')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item3", "item4", "item5"]) - XCTAssertEqual(observer.allRecordedEvents.count, 5) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 5) - #endif - try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } - observer.reset() - - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item3") - return .commit - } - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item4") - return .rollback - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item5") - } - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item3')", - "RELEASE SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item4')", - "ROLLBACK TRANSACTION", - "INSERT INTO items (name) VALUES ('item5')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item5"]) - XCTAssertEqual(observer.allRecordedEvents.count, 5) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 5) - #endif - try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } - observer.reset() - - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item3") - return .rollback - } - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item4") - return .commit - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item5") - } - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item3')", - "ROLLBACK TRANSACTION TO SAVEPOINT grdb", - "RELEASE SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item4')", - "COMMIT TRANSACTION", - "INSERT INTO items (name) VALUES ('item5')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item4", "item5"]) - XCTAssertEqual(observer.allRecordedEvents.count, 4) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 4) - #endif - try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } - observer.reset() - - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item3") - return .rollback - } - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item4") - return .rollback - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item5") - } - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item3')", - "ROLLBACK TRANSACTION TO SAVEPOINT grdb", - "RELEASE SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item4')", - "ROLLBACK TRANSACTION", - "INSERT INTO items (name) VALUES ('item5')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item5"]) - XCTAssertEqual(observer.allRecordedEvents.count, 4) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 4) - #endif - try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } - observer.reset() - } - - func testReleaseTopLevelSavepointFromDatabaseWithDefaultImmediateTransactions() throws { - dbConfiguration.defaultTransactionKind = .immediate + + func testReleaseTopLevelSavepoint() throws { let dbQueue = try makeDatabaseQueue() let observer = Observer() dbQueue.add(transactionObserver: observer) @@ -344,8 +126,7 @@ class DatabaseSavepointTests: GRDBTestCase { #endif } - func testRollbackTopLevelSavepointFromDatabaseWithDefaultImmediateTransactions() throws { - dbConfiguration.defaultTransactionKind = .immediate + func testRollbackTopLevelSavepoint() throws { let dbQueue = try makeDatabaseQueue() let observer = Observer() dbQueue.add(transactionObserver: observer) @@ -374,8 +155,7 @@ class DatabaseSavepointTests: GRDBTestCase { #endif } - func testNestedSavepointFromDatabaseWithDefaultImmediateTransactions() throws { - dbConfiguration.defaultTransactionKind = .immediate + func testNestedSavepoint() throws { let dbQueue = try makeDatabaseQueue() let observer = Observer() dbQueue.add(transactionObserver: observer) diff --git a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift index 6a58ff4056..0caf342139 100644 --- a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift +++ b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift @@ -42,7 +42,14 @@ final class DatabaseSnapshotPoolTests: GRDBTestCase { let dbPool = try makeDatabasePool() let counter = try Counter(dbPool: dbPool) // 0 try dbPool.write(counter.increment) // 1 - let snapshot = try dbPool.write { db in try DatabaseSnapshotPool(db) } // locked at 1 + // We can't open a DatabaseSnapshotPool from an IMMEDIATE + // transaction (as documented by sqlite3_snapshot_get). So we + // force a DEFERRED transaction: + var snapshot: DatabaseSnapshotPool! + try dbPool.writeInTransaction(.deferred) { db in + snapshot = try DatabaseSnapshotPool(db) // locked at 1 + return .commit + } try dbPool.write(counter.increment) // 2 try XCTAssertEqual(dbPool.read(counter.value), 2) @@ -55,7 +62,9 @@ final class DatabaseSnapshotPoolTests: GRDBTestCase { let dbPool = try makeDatabasePool() let counter = try Counter(dbPool: dbPool) // 0 try dbPool.write(counter.increment) // 1 - let snapshot = try dbPool.writeWithoutTransaction { db in try DatabaseSnapshotPool(db) } // locked at 1 + let snapshot = try dbPool.writeWithoutTransaction { db in + try DatabaseSnapshotPool(db) // locked at 1 + } try dbPool.write(counter.increment) // 2 try XCTAssertEqual(dbPool.read(counter.value), 2) diff --git a/Tests/GRDBTests/DatabaseSuspensionTests.swift b/Tests/GRDBTests/DatabaseSuspensionTests.swift index eea8a31c7e..ce205c6cf5 100644 --- a/Tests/GRDBTests/DatabaseSuspensionTests.swift +++ b/Tests/GRDBTests/DatabaseSuspensionTests.swift @@ -507,7 +507,7 @@ class DatabaseSuspensionTests : GRDBTestCase { try db.execute(sql: "SELECT * FROM sqlite_master") XCTAssertEqual(db.journalModeCache, "wal") } - try dbPool.write { db in + dbPool.writeWithoutTransaction { db in XCTAssertEqual(db.journalModeCache, "wal") } try dbPool.read { db in diff --git a/Tests/GRDBTests/DatabaseTests.swift b/Tests/GRDBTests/DatabaseTests.swift index 145340ab3c..064deb00a3 100644 --- a/Tests/GRDBTests/DatabaseTests.swift +++ b/Tests/GRDBTests/DatabaseTests.swift @@ -505,16 +505,54 @@ class DatabaseTests : GRDBTestCase { try dbQueue.inTransaction { db in .commit } } - func testExplicitTransactionManagement() throws { + func testImplicitTransactionManagement() throws { let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + XCTAssertEqual(lastSQLQuery, "BEGIN DEFERRED TRANSACTION") + } + + try dbQueue.write { db in + XCTAssertEqual(lastSQLQuery, "BEGIN IMMEDIATE TRANSACTION") + } + try dbQueue.writeWithoutTransaction { db in try db.beginTransaction() - XCTAssertEqual(lastSQLQuery, "BEGIN DEFERRED TRANSACTION") + XCTAssertEqual(lastSQLQuery, "BEGIN IMMEDIATE TRANSACTION") try db.rollback() XCTAssertEqual(lastSQLQuery, "ROLLBACK TRANSACTION") - try db.beginTransaction(.immediate) - XCTAssertEqual(lastSQLQuery, "BEGIN IMMEDIATE TRANSACTION") + + try db.inSavepoint { + XCTAssertEqual(lastSQLQuery, "BEGIN IMMEDIATE TRANSACTION") + return .commit + } + XCTAssertEqual(lastSQLQuery, "COMMIT TRANSACTION") + + try db.readOnly { + try db.beginTransaction() + XCTAssertEqual(lastSQLQuery, "BEGIN DEFERRED TRANSACTION") + try db.rollback() + XCTAssertEqual(lastSQLQuery, "ROLLBACK TRANSACTION") + + try db.inSavepoint { + XCTAssertEqual(lastSQLQuery, "BEGIN DEFERRED TRANSACTION") + return .rollback + } + XCTAssertEqual(lastSQLQuery, "ROLLBACK TRANSACTION") + } + } + } + + func testExplicitTransactionManagement() throws { + let dbQueue = try makeDatabaseQueue() + + try dbQueue.writeWithoutTransaction { db in + try db.beginTransaction(.deferred) + XCTAssertEqual(lastSQLQuery, "BEGIN DEFERRED TRANSACTION") + try db.commit() + XCTAssertEqual(lastSQLQuery, "COMMIT TRANSACTION") + try db.beginTransaction(.exclusive) + XCTAssertEqual(lastSQLQuery, "BEGIN EXCLUSIVE TRANSACTION") try db.commit() XCTAssertEqual(lastSQLQuery, "COMMIT TRANSACTION") } @@ -561,7 +599,6 @@ class DatabaseTests : GRDBTestCase { } func testReadOnlyTransaction() throws { - dbConfiguration.defaultTransactionKind = .immediate let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { From d96dc2d328830bdd61e3279fd292c62d542e58de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 25 Aug 2024 08:47:58 +0200 Subject: [PATCH 018/160] Aim at debugging failing CI test --- Tests/GRDBTests/ValueObservationTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index 325fb10c32..2d5b77e70b 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -1095,7 +1095,7 @@ class ValueObservationTests: GRDBTestCase { try Table("t").fetchCount($0) } - let initialValueExpectation = self.expectation(description: "") + let initialValueExpectation = self.expectation(description: "initialValue") #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) initialValueExpectation.assertForOverFulfill = true #else @@ -1104,7 +1104,7 @@ class ValueObservationTests: GRDBTestCase { #endif initialValueExpectation.expectedFulfillmentCount = observationCount - let secondValueExpectation = self.expectation(description: "") + let secondValueExpectation = self.expectation(description: "secondValue") secondValueExpectation.expectedFulfillmentCount = observationCount var cancellables: [AnyDatabaseCancellable] = [] From 1c368ed18d34aeca2e236997688ea8284b302c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 25 Aug 2024 10:39:38 +0200 Subject: [PATCH 019/160] Skip flaky test with SQLCipher 3 --- Tests/GRDBTests/ValueObservationTests.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index 2d5b77e70b..326d536476 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -1083,6 +1083,13 @@ class ValueObservationTests: GRDBTestCase { // An attempt at finding a regression test for func testManyObservations() throws { + // TODO: Fix flaky test with SQLCipher 3 + #if GRDBCIPHER + if sqlite3_libversion_number() <= 3020001 { + throw XCTSkip("Skip flaky test with SQLCipher 3") + } + #endif + // We'll start many observations let observationCount = 100 dbConfiguration.maximumReaderCount = 5 From 1de32c267be1489c93eba4db5e254ffabc06d329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Mar 2024 17:29:26 +0100 Subject: [PATCH 020/160] [BREAKING] Remove DatabaseFuture and concurrentRead That's one less DispatchSemaphore. We don't need this method anymore. --- GRDB/Core/DatabasePool.swift | 18 ---- GRDB/Core/DatabaseQueue.swift | 13 --- GRDB/Core/DatabaseWriter.swift | 92 ------------------- GRDB/Documentation.docc/Concurrency.md | 29 +----- TODO.md | 2 +- .../DatabasePoolConcurrencyTests.swift | 70 +------------- 6 files changed, 4 insertions(+), 220 deletions(-) diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index 771f37038d..9b15f063ae 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -442,24 +442,6 @@ extension DatabasePool: DatabaseReader { } } - public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { - // The semaphore that blocks until futureResult is defined: - let futureSemaphore = DispatchSemaphore(value: 0) - var futureResult: Result? = nil - - asyncConcurrentRead { dbResult in - // Fetch and release the future - futureResult = dbResult.flatMap { db in Result { try value(db) } } - futureSemaphore.signal() - } - - return DatabaseFuture { - // Block the future until results are fetched - _ = futureSemaphore.wait(timeout: .distantFuture) - return try futureResult!.get() - } - } - public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { asyncConcurrentRead(value) } diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index 0e43acc422..b6cf6a5b27 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -266,19 +266,6 @@ extension DatabaseQueue: DatabaseReader { try writer.reentrantSync(value) } - public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { - // DatabaseQueue can't perform parallel reads. - // Perform a blocking read instead. - return DatabaseFuture(Result { - // Check that we're on the writer queue, as documented - try writer.execute { db in - try db.isolated(readOnly: true) { - try value(db) - } - } - }) - } - public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { // Check that we're on the writer queue... writer.execute { db in diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index 87937214ad..e92334047b 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -38,9 +38,7 @@ import Dispatch /// /// ### Reading from the Latest Committed Database State /// -/// - ``concurrentRead(_:)`` /// - ``spawnConcurrentRead(_:)`` -/// - ``DatabaseFuture`` /// /// ### Unsafe Methods /// @@ -251,49 +249,6 @@ public protocol DatabaseWriter: DatabaseReader { // MARK: - Reading from Database - /// Schedules read-only database operations for execution, and returns a - /// future value. - /// - /// This method must be called from the writer dispatch queue, outside of - /// any transaction. You'll get a fatal error otherwise. - /// - /// Database operations performed by the `value` closure are isolated in a - /// transaction: they do not see changes performed by eventual concurrent - /// writes (even writes performed by other processes). - /// - /// They see the database in the state left by the last updates performed - /// by the database writer. - /// - /// To access the fetched results, you call the ``DatabaseFuture/wait()`` - /// method of the returned future, on any dispatch queue. - /// - /// In the example below, the number of players is fetched concurrently with - /// the player insertion. Yet the future is guaranteed to return zero: - /// - /// ```swift - /// try writer.writeWithoutTransaction { db in - /// // Delete all players - /// try Player.deleteAll() - /// - /// // Count players concurrently - /// let future = writer.concurrentRead { db in - /// return try Player.fetchCount() - /// } - /// - /// // Insert a player - /// try Player(...).insert(db) - /// - /// // Guaranteed to be zero - /// let count = try future.wait() - /// } - /// ``` - /// - /// - note: Usage of this method is discouraged, because waiting on the - /// returned ``DatabaseFuture`` blocks a thread. You may prefer - /// ``spawnConcurrentRead(_:)`` instead. - /// - parameter value: A closure which accesses the database. - func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture - // Exposed for RxGRDB and GRBCombine. Naming is not stabilized. /// Schedules read-only database operations for execution. /// @@ -924,49 +879,6 @@ extension Publisher where Failure == Error { } #endif -/// A future database value. -/// -/// You get instances of `DatabaseFuture` from the `DatabaseWriter` -/// ``DatabaseWriter/concurrentRead(_:)`` method. For example: -/// -/// ```swift -/// let futureCount: Future = try writer.writeWithoutTransaction { db in -/// try Player(...).insert() -/// -/// // Count players concurrently -/// return writer.concurrentRead { db in -/// return try Player.fetchCount() -/// } -/// } -/// -/// let count: Int = try futureCount.wait() -/// ``` -public class DatabaseFuture { - private var consumed = false - private let _wait: () throws -> Value - - init(_ wait: @escaping () throws -> Value) { - _wait = wait - } - - init(_ result: Result) { - _wait = result.get - } - - /// Blocks the current thread until the value is available, and returns it. - /// - /// It is a programmer error to call this method several times. - /// - /// - throws: Any error that prevented the value from becoming available. - public func wait() throws -> Value { - // Not thread-safe and quick and dirty. - // Goal is that users learn not to call this method twice. - GRDBPrecondition(consumed == false, "DatabaseFuture.wait() must be called only once") - consumed = true - return try _wait() - } -} - /// A type-erased database writer. /// /// An instance of `AnyDatabaseWriter` forwards its operations to an underlying @@ -1056,10 +968,6 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.unsafeReentrantWrite(updates) } - public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { - base.concurrentRead(value) - } - public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { base.spawnConcurrentRead(value) } diff --git a/GRDB/Documentation.docc/Concurrency.md b/GRDB/Documentation.docc/Concurrency.md index 4c5f7e908a..1b8050419b 100644 --- a/GRDB/Documentation.docc/Concurrency.md +++ b/GRDB/Documentation.docc/Concurrency.md @@ -277,31 +277,7 @@ let newPlayerCount = try dbPool.write { db in } ``` -➡️ The synchronous solution is the ``DatabaseWriter/concurrentRead(_:)`` method. It must be called from within a write access, outside of any transaction. It returns a ``DatabaseFuture`` which you consume any time later, with the ``DatabaseFuture/wait()`` method: - -```swift -let future: DatabaseFuture = try dbPool.writeWithoutTransaction { db in - // Increment the number of players - try db.inTransaction { - try Player(...).insert(db) - return .commit - } - - // <- Not in a transaction here - return dbPool.concurrentRead { db - try Player.fetchCount(db) - } -} - -do { - // Handle the new player count - guaranteed greater than zero - let newPlayerCount = try future.wait() -} catch { - // Handle error -} -``` - -🔀 The asynchronous version of `concurrentRead` is ``DatabasePool/asyncConcurrentRead(_:)``: +🔀 The solution is ``DatabasePool/asyncConcurrentRead(_:)``. It must be called from within a write access, outside of any transaction: ```swift try dbPool.writeWithoutTransaction { db in @@ -324,11 +300,10 @@ try dbPool.writeWithoutTransaction { db in } ``` -Both ``DatabaseWriter/concurrentRead(_:)`` and ``DatabasePool/asyncConcurrentRead(_:)`` block until they can guarantee their closure argument an isolated access to the database, in the exact state left by the last transaction. It then asynchronously executes this closure. +The ``DatabasePool/asyncConcurrentRead(_:)`` method blocks until it can guarantee its closure argument an isolated access to the database, in the exact state left by the last transaction. It then asynchronously executes the closure. In the illustration below, the striped band shows the delay needed for the reading thread to acquire isolation. Until then, no other thread can write: - ![DatabasePool Concurrent Read](DatabasePoolConcurrentRead.png) Types that conform to ``TransactionObserver`` can also use those methods in their ``TransactionObserver/databaseDidCommit(_:)`` method, in order to process database changes without blocking other threads that want to write into the database. diff --git a/TODO.md b/TODO.md index 11e7d0d93e..2c3b92375d 100644 --- a/TODO.md +++ b/TODO.md @@ -102,7 +102,7 @@ - [ ] GRDB7: Sendable: DatabaseDataDecodingStrategy (264d7fb5) - [ ] GRDB7: Sendable: DatabaseDateDecodingStrategy (264d7fb5) - [ ] GRDB7: Sendable: DatabaseColumnDecodingStrategy (264d7fb5) -- [ ] GRDB7/BREAKING: Remove DatabaseFuture and concurrentRead (05f7d3c8) +- [X] GRDB7/BREAKING: Remove DatabaseFuture and concurrentRead (05f7d3c8) - [ ] GRDB7: Sendable: DatabaseFunction (6e691fe7) - [ ] GRDB7: Sendable: DatabaseMigrator (22114ad4) - [ ] GRDB7: Not Sendable: FilterCursor (b26e9709) diff --git a/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift b/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift index 7a329c38cf..93eaaae3e9 100644 --- a/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift +++ b/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift @@ -1074,74 +1074,6 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { try test(qos: .userInitiated) } - // MARK: - ConcurrentRead - - func testConcurrentReadOpensATransaction() throws { - let dbPool = try makeDatabasePool() - let future = dbPool.writeWithoutTransaction { db in - dbPool.concurrentRead { db in - XCTAssertTrue(db.isInsideTransaction) - do { - try db.execute(sql: "BEGIN DEFERRED TRANSACTION") - XCTFail("Expected error") - } catch { - } - } - } - try future.wait() - } - - func testConcurrentReadOutsideOfTransaction() throws { - let dbPool = try makeDatabasePool() - try dbPool.write { db in - try db.create(table: "persons") { t in - t.primaryKey("id", .integer) - } - } - - // Writer Reader - // dbPool.writeWithoutTransaction { - // > - // dbPool.concurrentRead { - // < - // INSERT INTO items (id) VALUES (NULL) - // > - let s1 = DispatchSemaphore(value: 0) - // } SELECT COUNT(*) FROM persons -> 0 - // < - // } - - let future: DatabaseFuture = try dbPool.writeWithoutTransaction { db in - let future: DatabaseFuture = dbPool.concurrentRead { db in - _ = s1.wait(timeout: .distantFuture) - return try! Int.fetchOne(db, sql: "SELECT COUNT(*) FROM persons")! - } - try db.execute(sql: "INSERT INTO persons DEFAULT VALUES") - s1.signal() - return future - } - XCTAssertEqual(try future.wait(), 0) - } - - func testConcurrentReadError() throws { - // Necessary for this test to run as quickly as possible - dbConfiguration.readonlyBusyMode = .immediateError - let dbPool = try makeDatabasePool() - try dbPool.writeWithoutTransaction { db in - try db.execute(sql: "PRAGMA locking_mode=EXCLUSIVE") - try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") - let future = dbPool.concurrentRead { db in - fatalError("Should not run") - } - do { - try future.wait() - } catch let error as DatabaseError { - XCTAssertEqual(error.resultCode, .SQLITE_BUSY) - XCTAssertEqual(error.message!, "database is locked") - } - } - } - // MARK: - AsyncConcurrentRead func testAsyncConcurrentReadOpensATransaction() throws { @@ -1179,7 +1111,7 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { // Writer Reader // dbPool.writeWithoutTransaction { // > - // dbPool.concurrentRead { + // dbPool.asyncConcurrentRead { // < // INSERT INTO items (id) VALUES (NULL) // > From e72a37d7d52419c9c51e4bb4ee63000ae774ce81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 11 Feb 2024 15:29:14 +0100 Subject: [PATCH 021/160] Rename LockedBox to Mutex --- GRDB.xcodeproj/project.pbxproj | 12 ++- GRDB/Core/Database.swift | 42 +++++---- GRDB/Core/DatabasePool.swift | 6 +- GRDB/Core/DatabaseRegionObservation.swift | 10 +- GRDB/Utils/LockedBox.swift | 72 --------------- GRDB/Utils/Mutex.swift | 54 +++++++++++ .../Observers/ValueConcurrentObserver.swift | 6 +- GRDB/ValueObservation/ValueObservation.swift | 37 ++++---- GRDBCustom.xcodeproj/project.pbxproj | 12 ++- TODO.md | 2 +- .../GRDBTests.xcodeproj/project.pbxproj | 6 ++ .../GRDBTests.xcodeproj/project.pbxproj | 6 ++ .../AssociationPrefetchingSQLTests.swift | 92 +++++++++---------- .../DatabaseDataEncodingStrategyTests.swift | 4 +- .../DatabaseDateEncodingStrategyTests.swift | 4 +- Tests/GRDBTests/DatabaseErrorTests.swift | 4 +- Tests/GRDBTests/DatabaseSavepointTests.swift | 12 +-- Tests/GRDBTests/DatabaseTests.swift | 4 +- .../DatabaseUUIDEncodingStrategyTests.swift | 6 +- Tests/GRDBTests/DerivableRequestTests.swift | 44 ++++----- Tests/GRDBTests/FTS3RecordTests.swift | 4 +- Tests/GRDBTests/FTS4RecordTests.swift | 4 +- Tests/GRDBTests/FTS5RecordTests.swift | 4 +- .../GRDBTests/ForeignKeyDefinitionTests.swift | 44 ++++----- Tests/GRDBTests/GRDBTestCase.swift | 20 ++-- ...MutablePersistableRecordChangesTests.swift | 32 +++---- .../MutablePersistableRecordTests.swift | 24 ++--- Tests/GRDBTests/Mutex.swift | 54 +++++++++++ Tests/GRDBTests/PersistableRecordTests.swift | 22 ++--- Tests/GRDBTests/TableDefinitionTests.swift | 16 ++-- ...bleRecord+QueryInterfaceRequestTests.swift | 2 +- Tests/GRDBTests/TableRecordUpdateTests.swift | 2 +- Tests/GRDBTests/TableTests.swift | 4 +- .../ValueObservationPrintTests.swift | 5 +- Tests/GRDBTests/ValueObservationTests.swift | 5 +- 35 files changed, 375 insertions(+), 302 deletions(-) delete mode 100644 GRDB/Utils/LockedBox.swift create mode 100644 GRDB/Utils/Mutex.swift create mode 100644 Tests/GRDBTests/Mutex.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 213fe83be5..4185082bb9 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -105,6 +105,7 @@ 563C67B324628BEA00E94EDC /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563C67B224628BEA00E94EDC /* DatabasePoolTests.swift */; }; 563CBBE12A595131008905CE /* SQLIndexGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563CBBE02A595131008905CE /* SQLIndexGenerator.swift */; }; 563DE4F3231A91E2005081B7 /* DatabaseConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563DE4EC231A91E2005081B7 /* DatabaseConfigurationTests.swift */; }; + 563EA3E12C7B3A22001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E02C7B3A22001BE0D4 /* Mutex.swift */; }; 563EF415215F87EB007DAACD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF414215F87EB007DAACD /* OrderedDictionary.swift */; }; 563EF42D2161180D007DAACD /* AssociationAggregate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF42C2161180D007DAACD /* AssociationAggregate.swift */; }; 563EF43F216131D1007DAACD /* AssociationAggregateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF43E216131D1007DAACD /* AssociationAggregateTests.swift */; }; @@ -186,7 +187,7 @@ 566B912B1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */; }; 566B91331FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; 566B9C2025C6CC24004542CF /* RowDecodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B9C1F25C6CC24004542CF /* RowDecodingError.swift */; }; - 566BE71E2342542F00A8254B /* LockedBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BE7172342542F00A8254B /* LockedBox.swift */; }; + 566BE71E2342542F00A8254B /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BE7172342542F00A8254B /* Mutex.swift */; }; 566DDE0D288D763C0000DCFB /* Fixits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566DDE0C288D763C0000DCFB /* Fixits.swift */; }; 56703297212B5450007D270F /* DatabaseUUIDEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56703290212B544F007D270F /* DatabaseUUIDEncodingStrategyTests.swift */; }; 56713FDD2691F409006153C3 /* JSONRequiredEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56713FDC2691F409006153C3 /* JSONRequiredEncoder.swift */; }; @@ -534,6 +535,7 @@ 563C67B224628BEA00E94EDC /* DatabasePoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolTests.swift; sourceTree = ""; }; 563CBBE02A595131008905CE /* SQLIndexGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLIndexGenerator.swift; sourceTree = ""; }; 563DE4EC231A91E2005081B7 /* DatabaseConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseConfigurationTests.swift; sourceTree = ""; }; + 563EA3E02C7B3A22001BE0D4 /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 563EF414215F87EB007DAACD /* OrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; 563EF42C2161180D007DAACD /* AssociationAggregate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociationAggregate.swift; sourceTree = ""; }; 563EF43E216131D1007DAACD /* AssociationAggregateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociationAggregateTests.swift; sourceTree = ""; }; @@ -628,7 +630,7 @@ 566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatementAuthorizer.swift; sourceTree = ""; }; 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionObserver.swift; sourceTree = ""; }; 566B9C1F25C6CC24004542CF /* RowDecodingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowDecodingError.swift; sourceTree = ""; }; - 566BE7172342542F00A8254B /* LockedBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockedBox.swift; sourceTree = ""; }; + 566BE7172342542F00A8254B /* Mutex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 566DDE0C288D763C0000DCFB /* Fixits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fixits.swift; sourceTree = ""; }; 56703290212B544F007D270F /* DatabaseUUIDEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseUUIDEncodingStrategyTests.swift; sourceTree = ""; }; 56713FDC2691F409006153C3 /* JSONRequiredEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequiredEncoder.swift; sourceTree = ""; }; @@ -1010,6 +1012,7 @@ children = ( 56677C14241D14450050755D /* FailureTestCase.swift */, 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, + 563EA3E02C7B3A22001BE0D4 /* Mutex.swift */, 562EA81E1F17B26F00FA528C /* Compilation */, 56A238111B9C74A90082EB20 /* Core */, 567B5BDA2AD3281B00629622 /* Dump */, @@ -1318,7 +1321,7 @@ 56717270261C68E900423B6F /* CaseInsensitiveIdentifier.swift */, 563EF4492161F179007DAACD /* Inflections.swift */, 569BBA482291707D00478429 /* Inflections+English.swift */, - 566BE7172342542F00A8254B /* LockedBox.swift */, + 566BE7172342542F00A8254B /* Mutex.swift */, 563B8FC424A1D3B9007A48C9 /* OnDemandFuture.swift */, 563EF414215F87EB007DAACD /* OrderedDictionary.swift */, 5659F4971EA8D989004A4992 /* Pool.swift */, @@ -2058,6 +2061,7 @@ 5615B275222B107900061C1C /* AssociationHasOneThroughFetchableRecordTests.swift in Sources */, 56D4966F1D81309E008276D7 /* RecordPrimaryKeyNoneTests.swift in Sources */, 56D496621D81304E008276D7 /* StatementArguments+FoundationTests.swift in Sources */, + 563EA3E12C7B3A22001BE0D4 /* Mutex.swift in Sources */, 56176C5D1EACCCC7000F3F2B /* FTS5TokenizerTests.swift in Sources */, 56D496B21D8133CE008276D7 /* DatabaseQueueInMemoryTests.swift in Sources */, 562393601DEE06D300A6B01F /* CursorTests.swift in Sources */, @@ -2265,7 +2269,7 @@ 5690C3401D23E82A00E59934 /* Data.swift in Sources */, 5659F4881EA8D94E004A4992 /* Utils.swift in Sources */, 567B5BE82AD3284100629622 /* DumpFormat.swift in Sources */, - 566BE71E2342542F00A8254B /* LockedBox.swift in Sources */, + 566BE71E2342542F00A8254B /* Mutex.swift in Sources */, 56A238931B9C750B0082EB20 /* DatabaseMigrator.swift in Sources */, 5603CEBB2AC862EC00CF097D /* SQLJSONExpressible.swift in Sources */, 56F89DF72A57EAA9002FE2AA /* ColumnDefinition.swift in Sources */, diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index ec881c14e0..68956eaa1f 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -292,7 +292,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib var isInsideTransactionBlock = false /// Support for `checkForSuspensionViolation(from:)` - @LockedBox var isSuspended = false + let isSuspendedMutex = Mutex(false) /// Support for `checkForSuspensionViolation(from:)` /// This cache is never cleared: we assume journal mode never changes. @@ -1125,22 +1125,25 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// /// Suspension ends with `resume()`. func suspend() { - $isSuspended.update { isSuspended in + let needsInterrupt = isSuspendedMutex.withLock { isSuspended in if isSuspended { - return + return false } - // Prevent future lock acquisition isSuspended = true - - // Interrupt the database because this may trigger an - // SQLITE_INTERRUPT error which may itself abort a transaction and - // release a lock. See + return true + } + + if needsInterrupt { + // Interrupting the database can trigger an SQLITE_INTERRUPT + // error which may itself abort a transaction and + // release a database lock, which is our goal. + // See + // + // Maybe interrupt will not release any lock. To address this, + // we'll issue a rollback on next database access which requires + // a lock. See `checkForSuspensionViolation(from:).` interrupt() - - // Now what about the eventual remaining lock? We'll issue a - // rollback on next database access which requires a lock, in - // checkForSuspensionViolation(from:). } } @@ -1152,7 +1155,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// /// See suspend(). func resume() { - isSuspended = false + isSuspendedMutex.store(false) } /// Support for `checkForSuspensionViolation(from:)` @@ -1182,9 +1185,9 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// /// See `suspend()` and ``Configuration/observesSuspensionNotifications``. func checkForSuspensionViolation(from statement: Statement) throws { - try $isSuspended.read { isSuspended in + let needsAbort = try isSuspendedMutex.withLock { isSuspended in guard isSuspended else { - return + return false } if try journalMode() == "wal" && statement.isReadonly { @@ -1195,7 +1198,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib // Those are not read-only: // - INSERT ... // - BEGIN IMMEDIATE TRANSACTION - return + return false } if statement.releasesDatabaseLock { @@ -1204,9 +1207,14 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib // - ROLLBACK // - ROLLBACK TRANSACTION TO SAVEPOINT // - RELEASE SAVEPOINT - return + return false } + // Assume statement can acquire a write lock: abort. + return true + } + + if needsAbort { // Attempt at releasing an eventual lock with ROLLBACk, // as explained in Database.suspend(). // diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index 9b15f063ae..e7e4ff6098 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -11,7 +11,7 @@ public final class DatabasePool { /// It is constant, until close() sets it to nil. private var readerPool: Pool? - @LockedBox var databaseSnapshotCount = 0 + let databaseSnapshotCountMutex = Mutex(0) /// If Database Suspension is enabled, this array contains the necessary `NotificationCenter` observers. private var suspensionObservers: [NSObjectProtocol] = [] @@ -144,7 +144,7 @@ public final class DatabasePool { } } -// @unchecked because of databaseSnapshotCount, readerPool and suspensionObservers +// @unchecked because of readerPool and suspensionObservers extension DatabasePool: @unchecked Sendable { } extension DatabasePool { @@ -845,7 +845,7 @@ extension DatabasePool { path: path, configuration: DatabasePool.readerConfiguration(writer.configuration), defaultLabel: "GRDB.DatabasePool", - purpose: "snapshot.\($databaseSnapshotCount.increment())") + purpose: "snapshot.\(databaseSnapshotCountMutex.increment())") } #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) diff --git a/GRDB/Core/DatabaseRegionObservation.swift b/GRDB/Core/DatabaseRegionObservation.swift index 242214bc44..3579586f81 100644 --- a/GRDB/Core/DatabaseRegionObservation.swift +++ b/GRDB/Core/DatabaseRegionObservation.swift @@ -89,16 +89,16 @@ extension DatabaseRegionObservation { onChange: @escaping (Database) -> Void) -> AnyDatabaseCancellable { - @LockedBox var state = ObservationState.pending + let stateMutex = Mutex(ObservationState.pending) // Use unsafeReentrantWrite so that observation can start from any // dispatch queue. writer.unsafeReentrantWrite { db in do { let region = try observedRegion(db).observableRegion(db) - $state.update { + stateMutex.withLock { state in let observer = DatabaseRegionObserver(region: region, onChange: { - if case .cancelled = state { + if case .cancelled = stateMutex.load() { return } onChange($0) @@ -111,7 +111,7 @@ extension DatabaseRegionObservation { // the observer. db.add(transactionObserver: observer, extent: .observerLifetime) - $0 = .started(observer) + state = .started(observer) } } catch { onError(error) @@ -122,7 +122,7 @@ extension DatabaseRegionObservation { // Deallocates the transaction observer. This makes sure that the // `onChange` callback will never be called again, because the // observation was started with the `.observerLifetime` extent. - state = .cancelled + stateMutex.store(.cancelled) } } } diff --git a/GRDB/Utils/LockedBox.swift b/GRDB/Utils/LockedBox.swift deleted file mode 100644 index aef512604d..0000000000 --- a/GRDB/Utils/LockedBox.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation - -/// A LockedBox protects a value with an NSLock. -@propertyWrapper -final class LockedBox { - private var _wrappedValue: T - private var lock = NSLock() - - var wrappedValue: T { - get { read { $0 } } - set { update { $0 = newValue } } - } - - var projectedValue: LockedBox { self } - - init(wrappedValue: T) { - _wrappedValue = wrappedValue - } - - /// Runs the provided closure while holding a lock on the value. - /// - /// For example: - /// - /// // Prints "0" - /// @LockedBox var count = 0 - /// $count.read { print($0) } - /// - /// - parameter block: A closure that accepts the value. - @inline(__always) - @usableFromInline - func read(_ block: (T) throws -> U) rethrows -> U { - lock.lock() - defer { lock.unlock() } - return try block(_wrappedValue) - } - - /// Runs the provided closure while holding a lock on the value. - /// - /// For example: - /// - /// // Prints "1" - /// @LockedBox var count = 0 - /// $count.update { $0 += 1 } - /// print(count) - /// - /// - parameter block: A closure that can modify the value. - func update(_ block: (inout T) throws -> U) rethrows -> U { - lock.lock() - defer { lock.unlock() } - return try block(&_wrappedValue) - } -} - -extension LockedBox where T: Numeric { - @discardableResult - func increment() -> T { - update { n in - n += 1 - return n - } - } - - @discardableResult - func decrement() -> T { - update { n in - n -= 1 - return n - } - } -} - -extension LockedBox: @unchecked Sendable where T: Sendable { } diff --git a/GRDB/Utils/Mutex.swift b/GRDB/Utils/Mutex.swift new file mode 100644 index 0000000000..1fb69c2e22 --- /dev/null +++ b/GRDB/Utils/Mutex.swift @@ -0,0 +1,54 @@ +import Foundation + +/// A Mutex protects a value with an NSLock. +/// +/// We'll replace it with the SE-0433 Mutex when it is available. +/// +final class Mutex { + private var _value: T + private var lock = NSLock() + + init(_ value: T) { + _value = value + } + + /// Runs the provided closure while holding a lock on the value. + /// + /// - parameter body: A closure that can modify the value. + func withLock(_ body: (inout T) throws -> U) rethrows -> U { + lock.lock() + defer { lock.unlock() } + return try body(&_value) + } +} + +// Inspired by +extension Mutex { + func load() -> T { + withLock { $0 } + } + + func store(_ value: T) { + withLock { $0 = value } + } +} + +extension Mutex where T: Numeric { + @discardableResult + func increment() -> T { + withLock { n in + n += 1 + return n + } + } + + @discardableResult + func decrement() -> T { + withLock { n in + n -= 1 + return n + } + } +} + +extension Mutex: @unchecked Sendable where T: Sendable { } diff --git a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift index a72c4aba8b..e57aef0983 100644 --- a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift @@ -146,7 +146,7 @@ final class ValueConcurrentObserver ValueObservation> { - let lock = NSLock() + let streamMutex = Mutex(stream ?? PrintOutputStream()) let prefix = prefix.isEmpty ? "" : "\(prefix): " - var stream = stream ?? PrintOutputStream() return handleEvents( willStart: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)start") }, + streamMutex.withLock { $0.write("\(prefix)start") } + }, willFetch: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)fetch") }, - willTrackRegion: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)tracked region: \($0)") }, + streamMutex.withLock { $0.write("\(prefix)fetch") } + }, + willTrackRegion: { region in + streamMutex.withLock { $0.write("\(prefix)tracked region: \(region)") } + }, databaseDidChange: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)database did change") }, - didReceiveValue: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)value: \($0)") }, - didFail: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)failure: \($0)") }, + streamMutex.withLock { $0.write("\(prefix)database did change") } + }, + didReceiveValue: { value in + streamMutex.withLock { $0.write("\(prefix)value: \(value)") } + }, + didFail: { error in + streamMutex.withLock { $0.write("\(prefix)failure: \(error)") } + }, didCancel: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)cancel") }) + streamMutex.withLock { $0.write("\(prefix)cancel") } + }) } // MARK: - Fetching Values diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 7a9c12418e..04d29555ff 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ 563C67B824628C0C00E94EDC /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563C67B624628C0C00E94EDC /* DatabasePoolTests.swift */; }; 563CBBE42A595141008905CE /* SQLIndexGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563CBBE22A595141008905CE /* SQLIndexGenerator.swift */; }; 563DE4F8231A91F6005081B7 /* DatabaseConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563DE4F6231A91F6005081B7 /* DatabaseConfigurationTests.swift */; }; + 563EA3E32C7B3A3A001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E22C7B3A3A001BE0D4 /* Mutex.swift */; }; 563EF420215F8A76007DAACD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF41E215F8A76007DAACD /* OrderedDictionary.swift */; }; 563EF442216131F5007DAACD /* AssociationAggregateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF441216131F5007DAACD /* AssociationAggregateTests.swift */; }; 563EF44D2161F196007DAACD /* Inflections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF44C2161F196007DAACD /* Inflections.swift */; }; @@ -185,7 +186,7 @@ 566B91351FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; 566BD7332927AFD600595649 /* ValueConcurrentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BD7312927AFD600595649 /* ValueConcurrentObserver.swift */; }; 566BD7342927AFD600595649 /* ValueWriteOnlyObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BD7322927AFD600595649 /* ValueWriteOnlyObserver.swift */; }; - 566BE7152342541F00A8254B /* LockedBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BE7132342541F00A8254B /* LockedBox.swift */; }; + 566BE7152342541F00A8254B /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BE7132342541F00A8254B /* Mutex.swift */; }; 566DDE12288D76400000DCFB /* Fixits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566DDE11288D76400000DCFB /* Fixits.swift */; }; 5670329B212B5462007D270F /* DatabaseUUIDEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56703299212B5461007D270F /* DatabaseUUIDEncodingStrategyTests.swift */; }; 567071F4208A00BE006AD95A /* SQLiteDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567071F2208A00BE006AD95A /* SQLiteDateParser.swift */; }; @@ -545,6 +546,7 @@ 563C67B624628C0C00E94EDC /* DatabasePoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolTests.swift; sourceTree = ""; }; 563CBBE22A595141008905CE /* SQLIndexGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLIndexGenerator.swift; sourceTree = ""; }; 563DE4F6231A91F6005081B7 /* DatabaseConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseConfigurationTests.swift; sourceTree = ""; }; + 563EA3E22C7B3A3A001BE0D4 /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 563EF41E215F8A76007DAACD /* OrderedDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; 563EF441216131F5007DAACD /* AssociationAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationAggregateTests.swift; sourceTree = ""; }; 563EF44C2161F196007DAACD /* Inflections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Inflections.swift; sourceTree = ""; }; @@ -655,7 +657,7 @@ 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionObserver.swift; sourceTree = ""; }; 566BD7312927AFD600595649 /* ValueConcurrentObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueConcurrentObserver.swift; sourceTree = ""; }; 566BD7322927AFD600595649 /* ValueWriteOnlyObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueWriteOnlyObserver.swift; sourceTree = ""; }; - 566BE7132342541F00A8254B /* LockedBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockedBox.swift; sourceTree = ""; }; + 566BE7132342541F00A8254B /* Mutex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 566DDE11288D76400000DCFB /* Fixits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fixits.swift; sourceTree = ""; }; 56703299212B5461007D270F /* DatabaseUUIDEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseUUIDEncodingStrategyTests.swift; sourceTree = ""; }; 567071F2208A00BE006AD95A /* SQLiteDateParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteDateParser.swift; sourceTree = ""; }; @@ -1032,6 +1034,7 @@ children = ( 567E4207242AB3CB00CAAD2C /* FailureTestCase.swift */, 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, + 563EA3E22C7B3A3A001BE0D4 /* Mutex.swift */, 562EA81E1F17B26F00FA528C /* Compilation */, 56A238111B9C74A90082EB20 /* Core */, 567B5BFC2AD3285C00629622 /* Dump */, @@ -1339,7 +1342,7 @@ 564D4F91261E1D3300F55856 /* CaseInsensitiveIdentifier.swift */, 563EF44C2161F196007DAACD /* Inflections.swift */, 569BBA4B229170B300478429 /* Inflections+English.swift */, - 566BE7132342541F00A8254B /* LockedBox.swift */, + 566BE7132342541F00A8254B /* Mutex.swift */, 563B8FBC24A1D388007A48C9 /* OnDemandFuture.swift */, 563EF41E215F8A76007DAACD /* OrderedDictionary.swift */, 5659F4971EA8D989004A4992 /* Pool.swift */, @@ -2097,7 +2100,7 @@ 5656A8A72295BF44001FF3FF /* DatabasePromise.swift in Sources */, 5656A86D2295BD56001FF3FF /* HasManyThroughAssociation.swift in Sources */, 56894FE3260658A400268F4D /* Decimal.swift in Sources */, - 566BE7152342541F00A8254B /* LockedBox.swift in Sources */, + 566BE7152342541F00A8254B /* Mutex.swift in Sources */, F3BA806D1CFB2E55003DC1BA /* DatabaseSchemaCache.swift in Sources */, F3BA80841CFB2E67003DC1BA /* StandardLibrary.swift in Sources */, 5656A87F2295BD56001FF3FF /* SQLForeignKeyRequest.swift in Sources */, @@ -2301,6 +2304,7 @@ F3BA81321CFB3064003DC1BA /* RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift in Sources */, 5653EB7420961FB200F46237 /* AssociationRowScopeSearchTests.swift in Sources */, 5691578E231BF2BE00E1D237 /* PoolTests.swift in Sources */, + 563EA3E32C7B3A3A001BE0D4 /* Mutex.swift in Sources */, 5623935A1DEE013C00A6B01F /* FilterCursorTests.swift in Sources */, 56419C7E24A51D6E004967E1 /* DatabaseWriterWritePublisherTests.swift in Sources */, 5665FA3D2129EED8004D8612 /* DatabaseDateEncodingStrategyTests.swift in Sources */, diff --git a/TODO.md b/TODO.md index 2c3b92375d..e2e85fa2b1 100644 --- a/TODO.md +++ b/TODO.md @@ -91,7 +91,7 @@ - [ ] GRDB7/BREAKING: AsyncValueObservation does not need any scheduler (83c0e643) - [X] GRDB7/BREAKING: Stop exporting SQLite (679d6463) - [X] GRDB7/BREAKING: Remove Configuration.defaultTransactionKind (2661ff46) -- [ ] GRDB7: Replace LockedBox with Mutex (00ccab06) +- [X] GRDB7: Replace LockedBox with Mutex (00ccab06) - [ ] GRDB7: Sendable: BusyCallback (e0d8e20b) - [ ] GRDB7: Sendable: BusyMode (e0d8e20b) - [ ] GRDB7: Sendable: TransactionClock (f7dc72a5) diff --git a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj index 2869b204eb..2058f693fb 100644 --- a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ 5623B61E2AED39F700436239 /* DatabaseQueueTemporaryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B61B2AED39F700436239 /* DatabaseQueueTemporaryCopyTests.swift */; }; 5623B61F2AED39F700436239 /* DatabaseQueueInMemoryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B61C2AED39F700436239 /* DatabaseQueueInMemoryCopyTests.swift */; }; 5623B6202AED39F700436239 /* DatabaseQueueInMemoryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B61C2AED39F700436239 /* DatabaseQueueInMemoryCopyTests.swift */; }; + 563EA3E52C7B3A4F001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E42C7B3A4F001BE0D4 /* Mutex.swift */; }; + 563EA3E62C7B3A4F001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E42C7B3A4F001BE0D4 /* Mutex.swift */; }; 56419D6724A54062004967E1 /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419C9C24A54053004967E1 /* DatabasePoolTests.swift */; }; 56419D6824A54062004967E1 /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419C9C24A54053004967E1 /* DatabasePoolTests.swift */; }; 56419D6924A54062004967E1 /* ResultCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419C9D24A54053004967E1 /* ResultCodeTests.swift */; }; @@ -510,6 +512,7 @@ 561F38F82AC9CE6D0051EEE9 /* DatabaseDataEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataEncodingStrategyTests.swift; sourceTree = ""; }; 5623B61B2AED39F700436239 /* DatabaseQueueTemporaryCopyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTemporaryCopyTests.swift; sourceTree = ""; }; 5623B61C2AED39F700436239 /* DatabaseQueueInMemoryCopyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueInMemoryCopyTests.swift; sourceTree = ""; }; + 563EA3E42C7B3A4F001BE0D4 /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 56419C9C24A54053004967E1 /* DatabasePoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolTests.swift; sourceTree = ""; }; 56419C9D24A54053004967E1 /* ResultCodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultCodeTests.swift; sourceTree = ""; }; 56419C9E24A54053004967E1 /* DatabaseQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTests.swift; sourceTree = ""; }; @@ -996,6 +999,7 @@ 56419D2F24A5405E004967E1 /* MutablePersistableRecordEncodableTests.swift */, 56419D4424A5405F004967E1 /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */, 56419D3B24A5405F004967E1 /* MutablePersistableRecordTests.swift */, + 563EA3E42C7B3A4F001BE0D4 /* Mutex.swift */, 56419CFD24A5405A004967E1 /* NumericOverflowTests.swift */, 56419D4F24A54060004967E1 /* OrderedDictionaryTests.swift */, 56419D4524A5405F004967E1 /* PersistableRecordTests.swift */, @@ -1435,6 +1439,7 @@ 56419E0924A54062004967E1 /* DatabaseValueConversionTests.swift in Sources */, 56419DB124A54062004967E1 /* AssociationBelongsToDecodableRecordTests.swift in Sources */, 56419D9324A54062004967E1 /* PrimaryKeyInfoTests.swift in Sources */, + 563EA3E62C7B3A4F001BE0D4 /* Mutex.swift in Sources */, 567B5C392AD32A2D00629622 /* FoundationDecimalTests.swift in Sources */, 5641A1BA24A540D6004967E1 /* Inverted.swift in Sources */, 5641A1B624A540D6004967E1 /* Prefix.swift in Sources */, @@ -1683,6 +1688,7 @@ 56419E0A24A54062004967E1 /* DatabaseValueConversionTests.swift in Sources */, 56419DB224A54062004967E1 /* AssociationBelongsToDecodableRecordTests.swift in Sources */, 56419D9424A54062004967E1 /* PrimaryKeyInfoTests.swift in Sources */, + 563EA3E52C7B3A4F001BE0D4 /* Mutex.swift in Sources */, 567B5C3A2AD32A2D00629622 /* FoundationDecimalTests.swift in Sources */, 5641A1BB24A540D6004967E1 /* Inverted.swift in Sources */, 5641A1B724A540D6004967E1 /* Prefix.swift in Sources */, diff --git a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj index f8c3e463fe..d499535f3c 100644 --- a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ 5623B6242AED3A2200436239 /* DatabaseQueueInMemoryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B6212AED3A2200436239 /* DatabaseQueueInMemoryCopyTests.swift */; }; 5623B6252AED3A2200436239 /* DatabaseQueueTemporaryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B6222AED3A2200436239 /* DatabaseQueueTemporaryCopyTests.swift */; }; 5623B6262AED3A2200436239 /* DatabaseQueueTemporaryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B6222AED3A2200436239 /* DatabaseQueueTemporaryCopyTests.swift */; }; + 563EA3E82C7B3A78001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E72C7B3A78001BE0D4 /* Mutex.swift */; }; + 563EA3E92C7B3A78001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E72C7B3A78001BE0D4 /* Mutex.swift */; }; 56419FC824A540A1004967E1 /* FetchRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419EFD24A54093004967E1 /* FetchRequestTests.swift */; }; 56419FC924A540A1004967E1 /* FetchRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419EFD24A54093004967E1 /* FetchRequestTests.swift */; }; 56419FCA24A540A1004967E1 /* DatabasePoolBackupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419EFE24A54093004967E1 /* DatabasePoolBackupTests.swift */; }; @@ -512,6 +514,7 @@ 561F38FE2AC9CE870051EEE9 /* DatabaseDataDecodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataDecodingStrategyTests.swift; sourceTree = ""; }; 5623B6212AED3A2200436239 /* DatabaseQueueInMemoryCopyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueInMemoryCopyTests.swift; sourceTree = ""; }; 5623B6222AED3A2200436239 /* DatabaseQueueTemporaryCopyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTemporaryCopyTests.swift; sourceTree = ""; }; + 563EA3E72C7B3A78001BE0D4 /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 56419EFD24A54093004967E1 /* FetchRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequestTests.swift; sourceTree = ""; }; 56419EFE24A54093004967E1 /* DatabasePoolBackupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolBackupTests.swift; sourceTree = ""; }; 56419EFF24A54093004967E1 /* TableRecordDeleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRecordDeleteTests.swift; sourceTree = ""; }; @@ -1000,6 +1003,7 @@ 56419F0224A54093004967E1 /* MutablePersistableRecordEncodableTests.swift */, 56419F3124A54096004967E1 /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */, 56419F0124A54093004967E1 /* MutablePersistableRecordTests.swift */, + 563EA3E72C7B3A78001BE0D4 /* Mutex.swift */, 56419F0824A54093004967E1 /* NumericOverflowTests.swift */, 56419F0324A54093004967E1 /* OrderedDictionaryTests.swift */, 56419F1A24A54094004967E1 /* PersistableRecordTests.swift */, @@ -1441,6 +1445,7 @@ 5641A08E24A540A1004967E1 /* DatabaseLogErrorTests.swift in Sources */, 5641A01624A540A1004967E1 /* UtilsTests.swift in Sources */, 5641A12A24A540A1004967E1 /* FlattenCursorTests.swift in Sources */, + 563EA3E82C7B3A78001BE0D4 /* Mutex.swift in Sources */, 5641A10424A540A1004967E1 /* DatabaseValueTests.swift in Sources */, 5641A06824A540A1004967E1 /* RowFetchTests.swift in Sources */, 5641A0E024A540A1004967E1 /* QueryInterfaceExtensibilityTests.swift in Sources */, @@ -1689,6 +1694,7 @@ 5641A08F24A540A1004967E1 /* DatabaseLogErrorTests.swift in Sources */, 5641A01724A540A1004967E1 /* UtilsTests.swift in Sources */, 5641A12B24A540A1004967E1 /* FlattenCursorTests.swift in Sources */, + 563EA3E92C7B3A78001BE0D4 /* Mutex.swift in Sources */, 5641A10524A540A1004967E1 /* DatabaseValueTests.swift in Sources */, 5641A06924A540A1004967E1 /* RowFetchTests.swift in Sources */, 5641A0E124A540A1004967E1 /* QueryInterfaceExtensibilityTests.swift in Sources */, diff --git a/Tests/GRDBTests/AssociationPrefetchingSQLTests.swift b/Tests/GRDBTests/AssociationPrefetchingSQLTests.swift index 5a96828c6f..3813344ffe 100644 --- a/Tests/GRDBTests/AssociationPrefetchingSQLTests.swift +++ b/Tests/GRDBTests/AssociationPrefetchingSQLTests.swift @@ -80,7 +80,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -105,7 +105,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -131,7 +131,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("bs2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -170,7 +170,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .order(Column("colb2"))) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -221,7 +221,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .hasMany(Child.self)) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -244,7 +244,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .hasMany(Child.self)) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -263,7 +263,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .filter(Column("parentA") == "foo") .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -286,7 +286,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey() .limit(1) - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -318,7 +318,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -352,7 +352,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -404,7 +404,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("cs2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -502,7 +502,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: Child.hasMany(GrandChild.self))) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -534,7 +534,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: Child.hasMany(GrandChild.self))) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -550,7 +550,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: Child.hasMany(GrandChild.self))) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -576,7 +576,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .filter(Column("name") == "foo") .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -617,7 +617,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -646,7 +646,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -699,7 +699,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("cs2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -741,7 +741,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -777,7 +777,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("ds3")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -821,7 +821,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -856,7 +856,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -894,7 +894,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -936,7 +936,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -970,7 +970,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1010,7 +1010,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: A.hasMany(C.self)) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1052,7 +1052,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: A.hasMany(C.self)) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1098,7 +1098,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: A.hasMany(C.self)) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1139,7 +1139,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { ) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1191,7 +1191,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("a2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1249,7 +1249,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { ) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1301,7 +1301,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("c2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1361,7 +1361,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey() .filter(Column("cold2") != 8) - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1399,7 +1399,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey() .filter(Column("cold2") != 8) - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1437,7 +1437,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { ) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1492,7 +1492,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("a2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1553,7 +1553,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { ) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1608,7 +1608,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("c2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1671,7 +1671,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey() .filter(Column("cold2") != 8) - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1712,7 +1712,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey() .filter(Column("cold2") != 8) - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1755,7 +1755,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .filter(sql: "1 + 1") .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1802,7 +1802,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { do { // Group an association - sqlQueries.removeAll() + clearSQLQueries() let association = Team.players.select(max(Column("score"))).group(Column("category")) let request = Team.including(all: association) _ = try Row.fetchAll(db, request) @@ -1821,7 +1821,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { do { // Filter an association with an association aggregate - sqlQueries.removeAll() + clearSQLQueries() let association = Team.players.having(Player.awards.isEmpty) let request = Team.including(all: association) _ = try Row.fetchAll(db, request) @@ -1842,7 +1842,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { do { // Annotate an association with an association aggregate - sqlQueries.removeAll() + clearSQLQueries() let association = Team.players.annotated(with: Player.awards.count) let request = Team.including(all: association) _ = try Row.fetchAll(db, request) @@ -1880,7 +1880,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { try db.execute(sql: "INSERT INTO team DEFAULT VALUES") do { - sqlQueries.removeAll() + clearSQLQueries() let association = Team.players.distinct() let request = Team.including(all: association) _ = try Row.fetchAll(db, request) @@ -1916,7 +1916,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { try db.execute(sql: "INSERT INTO team DEFAULT VALUES") do { - sqlQueries.removeAll() + clearSQLQueries() let cte = CommonTableExpression(named: "cte", sql: "SELECT 42") let association = Team.players.with(cte).filter(Column("playerId") == cte.all()) let request = Team.including(all: association) diff --git a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift index b9219589f4..f6e4758821 100644 --- a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift @@ -266,7 +266,7 @@ extension DatabaseDataEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } @@ -286,7 +286,7 @@ extension DatabaseDataEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } diff --git a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift index 6db6c0b4cc..4aec3e6107 100644 --- a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift @@ -376,7 +376,7 @@ extension DatabaseDateEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } @@ -396,7 +396,7 @@ extension DatabaseDateEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } diff --git a/Tests/GRDBTests/DatabaseErrorTests.swift b/Tests/GRDBTests/DatabaseErrorTests.swift index 8bba886ff5..e968b44074 100644 --- a/Tests/GRDBTests/DatabaseErrorTests.swift +++ b/Tests/GRDBTests/DatabaseErrorTests.swift @@ -17,7 +17,7 @@ class DatabaseErrorTests: GRDBTestCase { try dbQueue.inTransaction { db in try db.execute(sql: "CREATE TABLE persons (id INTEGER PRIMARY KEY)") try db.execute(sql: "CREATE TABLE pets (masterId INTEGER NOT NULL REFERENCES persons(id), name TEXT)") - sqlQueries.removeAll() + clearSQLQueries() try db.execute(sql: "INSERT INTO pets (masterId, name) VALUES (?, ?)", arguments: [1, "Bobby"]) XCTFail() return .commit @@ -44,7 +44,7 @@ class DatabaseErrorTests: GRDBTestCase { XCTAssertTrue(db.isInsideTransaction) try db.execute(sql: "CREATE TABLE persons (id INTEGER PRIMARY KEY)") try db.execute(sql: "CREATE TABLE pets (masterId INTEGER NOT NULL REFERENCES persons(id), name TEXT)") - sqlQueries.removeAll() + clearSQLQueries() try db.execute(sql: "INSERT INTO pets (masterId, name) VALUES (?, ?)", arguments: [1, "Bobby"]) XCTFail() return .commit diff --git a/Tests/GRDBTests/DatabaseSavepointTests.swift b/Tests/GRDBTests/DatabaseSavepointTests.swift index bc727f28eb..b2fe86d256 100644 --- a/Tests/GRDBTests/DatabaseSavepointTests.swift +++ b/Tests/GRDBTests/DatabaseSavepointTests.swift @@ -101,7 +101,7 @@ class DatabaseSavepointTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() let observer = Observer() dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { @@ -130,7 +130,7 @@ class DatabaseSavepointTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() let observer = Observer() dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { @@ -159,7 +159,7 @@ class DatabaseSavepointTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() let observer = Observer() dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { @@ -196,7 +196,7 @@ class DatabaseSavepointTests: GRDBTestCase { try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } observer.reset() - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { @@ -233,7 +233,7 @@ class DatabaseSavepointTests: GRDBTestCase { try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } observer.reset() - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { @@ -271,7 +271,7 @@ class DatabaseSavepointTests: GRDBTestCase { try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } observer.reset() - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { diff --git a/Tests/GRDBTests/DatabaseTests.swift b/Tests/GRDBTests/DatabaseTests.swift index 064deb00a3..6b37ab2ad1 100644 --- a/Tests/GRDBTests/DatabaseTests.swift +++ b/Tests/GRDBTests/DatabaseTests.swift @@ -602,7 +602,7 @@ class DatabaseTests : GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() try db.inSavepoint { .commit } try db.inTransaction { .commit } try db.inTransaction(.immediate) { .commit } @@ -610,7 +610,7 @@ class DatabaseTests : GRDBTestCase { } try db.readOnly { - sqlQueries.removeAll() + clearSQLQueries() try db.inSavepoint { .commit } try db.inTransaction { .commit } XCTAssertEqual(Set(sqlQueries), ["BEGIN DEFERRED TRANSACTION", "COMMIT TRANSACTION"]) diff --git a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift index 85c573377a..5ed9f6bb08 100644 --- a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift @@ -357,7 +357,7 @@ extension DatabaseUUIDEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } @@ -377,7 +377,7 @@ extension DatabaseUUIDEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } @@ -397,7 +397,7 @@ extension DatabaseUUIDEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } diff --git a/Tests/GRDBTests/DerivableRequestTests.swift b/Tests/GRDBTests/DerivableRequestTests.swift index 1cb5f91b25..98b0c87fb2 100644 --- a/Tests/GRDBTests/DerivableRequestTests.swift +++ b/Tests/GRDBTests/DerivableRequestTests.swift @@ -180,7 +180,7 @@ class DerivableRequestTests: GRDBTestCase { .forKey("fullName"))) // ... for one table - sqlQueries.removeAll() + clearSQLQueries() let authorNames = try Author.all() .orderByFullName() .fetchAll(db) @@ -192,7 +192,7 @@ class DerivableRequestTests: GRDBTestCase { "firstName" COLLATE swiftLocalizedCaseInsensitiveCompare """) - sqlQueries.removeAll() + clearSQLQueries() let reversedAuthorNames = try Author.all() .orderByFullName() .reversed() @@ -205,7 +205,7 @@ class DerivableRequestTests: GRDBTestCase { "firstName" COLLATE swiftLocalizedCaseInsensitiveCompare DESC """) - sqlQueries.removeAll() + clearSQLQueries() _ /* unorderedAuthors */ = try Author.all() .orderByFullName() .unordered() @@ -214,7 +214,7 @@ class DerivableRequestTests: GRDBTestCase { SELECT * FROM "author" """) - sqlQueries.removeAll() + clearSQLQueries() _ /* stableOrderAuthors */ = try Author.all() .withStableOrder() .fetchAll(db) @@ -222,7 +222,7 @@ class DerivableRequestTests: GRDBTestCase { SELECT * FROM "author" ORDER BY "id" """) - sqlQueries.removeAll() + clearSQLQueries() _ /* stableOrderAuthors */ = try Author.all() .orderByFullName() .withStableOrder() @@ -232,7 +232,7 @@ class DerivableRequestTests: GRDBTestCase { """) // ... for one view - sqlQueries.removeAll() + clearSQLQueries() _ /* authorViewNames */ = try Table("authorView").all() .order(Column("fullName")) .fetchAll(db) @@ -241,7 +241,7 @@ class DerivableRequestTests: GRDBTestCase { ORDER BY "fullName" """) - sqlQueries.removeAll() + clearSQLQueries() _ /* reversedAuthorViewNames */ = try Table("authorView").all() .order(Column("fullName")) .reversed() @@ -251,7 +251,7 @@ class DerivableRequestTests: GRDBTestCase { ORDER BY "fullName" DESC """) - sqlQueries.removeAll() + clearSQLQueries() _ /* unorderedAuthorViews */ = try Table("authorView").all() .order(Column("fullName")) .unordered() @@ -260,7 +260,7 @@ class DerivableRequestTests: GRDBTestCase { SELECT * FROM "authorView" """) - sqlQueries.removeAll() + clearSQLQueries() _ /* stableOrderAuthorViews */ = try Table("authorView").all() .withStableOrder() .fetchAll(db) @@ -268,7 +268,7 @@ class DerivableRequestTests: GRDBTestCase { SELECT * FROM "authorView" ORDER BY 1, 2, 3, 4, 5 """) - sqlQueries.removeAll() + clearSQLQueries() _ /* stableOrderAuthorViews */ = try Table("authorView").all() .order(Column("fullName")) .withStableOrder() @@ -278,7 +278,7 @@ class DerivableRequestTests: GRDBTestCase { """) // ... for two tables (2) - sqlQueries.removeAll() + clearSQLQueries() let bookTitles = try Book .joining(required: Book.author.orderByFullName()) .orderByTitle() @@ -294,7 +294,7 @@ class DerivableRequestTests: GRDBTestCase { "author"."firstName" COLLATE swiftLocalizedCaseInsensitiveCompare """) - sqlQueries.removeAll() + clearSQLQueries() let reversedBookTitles = try Book .joining(required: Book.author.orderByFullName()) .orderByTitle() @@ -311,7 +311,7 @@ class DerivableRequestTests: GRDBTestCase { "author"."firstName" COLLATE swiftLocalizedCaseInsensitiveCompare DESC """) - sqlQueries.removeAll() + clearSQLQueries() _ /* unorderedBooks */ = try Book .joining(required: Book.author.orderByFullName()) .orderByTitle() @@ -322,7 +322,7 @@ class DerivableRequestTests: GRDBTestCase { JOIN "author" ON "author"."id" = "book"."authorId" """) - sqlQueries.removeAll() + clearSQLQueries() _ /* stableOrderBooks */ = try Book .joining(required: Book.author.orderByFullName()) .orderByTitle() @@ -346,7 +346,7 @@ class DerivableRequestTests: GRDBTestCase { try libraryMigrator.migrate(dbQueue) try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let request = Author.all().selectCountry() let authorCountries = try Set(String.fetchAll(db, request)) XCTAssertEqual(authorCountries, ["FR", "US"]) @@ -356,7 +356,7 @@ class DerivableRequestTests: GRDBTestCase { } do { - sqlQueries.removeAll() + clearSQLQueries() let request = Book.including(required: Book.author.selectCountry()) _ = try Row.fetchAll(db, request) XCTAssertEqual(lastSQLQuery, """ @@ -373,7 +373,7 @@ class DerivableRequestTests: GRDBTestCase { try libraryMigrator.migrate(dbQueue) try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let frenchBookTitles = try Book.all() .filter(authorCountry: "FR") .order(Column("title")) @@ -389,7 +389,7 @@ class DerivableRequestTests: GRDBTestCase { } do { - sqlQueries.removeAll() + clearSQLQueries() let frenchAuthorFullNames = try Author .joining(required: Author.books.filter(authorCountry: "FR")) .order(Column("firstName")) @@ -436,7 +436,7 @@ class DerivableRequestTests: GRDBTestCase { // matchingFts4 do { - sqlQueries.removeAll() + clearSQLQueries() let title = try Book.all() .matchingFts4(FTS3Pattern(rawPattern: "moby dick")) .fetchOne(db) @@ -448,7 +448,7 @@ class DerivableRequestTests: GRDBTestCase { LIMIT 1 """)) - sqlQueries.removeAll() + clearSQLQueries() let fullName = try Author .joining(required: Author.books.matchingFts4(FTS3Pattern(rawPattern: "moby dick"))) .fetchOne(db) @@ -465,7 +465,7 @@ class DerivableRequestTests: GRDBTestCase { #if SQLITE_ENABLE_FTS5 // matchingFts5 do { - sqlQueries.removeAll() + clearSQLQueries() let title = try Book.all() .matchingFts5(FTS3Pattern(rawPattern: "cote swann")) .fetchOne(db) @@ -477,7 +477,7 @@ class DerivableRequestTests: GRDBTestCase { LIMIT 1 """)) - sqlQueries.removeAll() + clearSQLQueries() let fullName = try Author .joining(required: Author.books.matchingFts5(FTS3Pattern(rawPattern: "cote swann"))) .fetchOne(db) diff --git a/Tests/GRDBTests/FTS3RecordTests.swift b/Tests/GRDBTests/FTS3RecordTests.swift index b37c08bb54..c1c5168686 100644 --- a/Tests/GRDBTests/FTS3RecordTests.swift +++ b/Tests/GRDBTests/FTS3RecordTests.swift @@ -102,14 +102,14 @@ class FTS3RecordTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let pattern = try FTS3Pattern(rawPattern: "Herman Melville") XCTAssertEqual(try Book.matching(pattern).fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\" WHERE \"books\" MATCH 'Herman Melville'")) } do { - sqlQueries = [] + clearSQLQueries() XCTAssertEqual(try Book.fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\"")) } diff --git a/Tests/GRDBTests/FTS4RecordTests.swift b/Tests/GRDBTests/FTS4RecordTests.swift index 646c3852ef..1c81bc6a49 100644 --- a/Tests/GRDBTests/FTS4RecordTests.swift +++ b/Tests/GRDBTests/FTS4RecordTests.swift @@ -102,14 +102,14 @@ class FTS4RecordTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let pattern = try FTS3Pattern(rawPattern: "Herman Melville") XCTAssertEqual(try Book.matching(pattern).fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\" WHERE \"books\" MATCH 'Herman Melville'")) } do { - sqlQueries = [] + clearSQLQueries() XCTAssertEqual(try Book.fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\"")) } diff --git a/Tests/GRDBTests/FTS5RecordTests.swift b/Tests/GRDBTests/FTS5RecordTests.swift index b25b0a0135..6656b2ef92 100644 --- a/Tests/GRDBTests/FTS5RecordTests.swift +++ b/Tests/GRDBTests/FTS5RecordTests.swift @@ -101,14 +101,14 @@ class FTS5RecordTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let pattern = FTS5Pattern(matchingAllTokensIn: "Herman Melville")! XCTAssertEqual(try Book.matching(pattern).fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\" WHERE \"books\" MATCH 'herman melville'")) } do { - sqlQueries = [] + clearSQLQueries() XCTAssertEqual(try Book.fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\"")) } diff --git a/Tests/GRDBTests/ForeignKeyDefinitionTests.swift b/Tests/GRDBTests/ForeignKeyDefinitionTests.swift index d6bacfc5a6..8a4940476c 100644 --- a/Tests/GRDBTests/ForeignKeyDefinitionTests.swift +++ b/Tests/GRDBTests/ForeignKeyDefinitionTests.swift @@ -20,7 +20,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent") @@ -99,7 +99,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child", options: .ifNotExists) { t in t.column("a") t.belongsTo("parent") @@ -155,7 +155,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").unique() @@ -197,7 +197,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", indexed: false) @@ -239,7 +239,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").notNull() @@ -295,7 +295,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", onDelete: .cascade, onUpdate: .setNull, deferred: true) @@ -411,7 +411,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent") @@ -494,7 +494,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child", options: .ifNotExists) { t in t.column("a") t.belongsTo("parent") @@ -556,7 +556,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").unique() @@ -604,7 +604,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", indexed: false) @@ -652,7 +652,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").notNull() @@ -714,7 +714,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", onDelete: .cascade, onUpdate: .setNull, deferred: true) @@ -757,7 +757,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { func testTable_belongsTo_singleColumnPrimaryKey_autoreference_singular() throws { try makeDatabaseQueue().inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "employee") { t in t.autoIncrementedPrimaryKey("id") t.column("a") @@ -786,7 +786,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } do { - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "node") { t in t.primaryKey { t.column("code") } t.column("a") @@ -820,7 +820,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { func testTable_belongsTo_singleColumnPrimaryKey_autoreference_plural() throws { try makeDatabaseQueue().inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "employees") { t in t.autoIncrementedPrimaryKey("id") t.column("a") @@ -849,7 +849,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } do { - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "nodes") { t in t.primaryKey { t.column("code") } t.column("a") @@ -913,7 +913,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent") @@ -1025,7 +1025,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child", options: .ifNotExists) { t in t.column("a") t.belongsTo("parent") @@ -1107,7 +1107,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").unique() @@ -1189,7 +1189,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", indexed: false) @@ -1257,7 +1257,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").notNull() @@ -1339,7 +1339,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", onDelete: .cascade, onUpdate: .setNull, deferred: true) diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index 837cf89b46..d1f52ac492 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -64,10 +64,12 @@ class GRDBTestCase: XCTestCase { // The default path for database pool directory private var dbDirectoryPath: String! - // Populated by default configuration - @LockedBox var sqlQueries: [String] = [] + let _sqlQueriesMutex: Mutex<[String]> = Mutex([]) - // Populated by default configuration + // Automatically updated by default dbConfiguration + var sqlQueries: [String] { _sqlQueriesMutex.load() } + + // Automatically updated by default dbConfiguration var lastSQLQuery: String? { sqlQueries.last } override func setUp() { @@ -112,9 +114,11 @@ class GRDBTestCase: XCTestCase { } } - dbConfiguration.prepareDatabase { db in + dbConfiguration.prepareDatabase { [_sqlQueriesMutex] db in db.trace { event in - self.sqlQueries.append(event.expandedDescription) + _sqlQueriesMutex.withLock { + $0.append(event.expandedDescription) + } } #if GRDBCIPHER_USE_ENCRYPTION @@ -122,7 +126,7 @@ class GRDBTestCase: XCTestCase { #endif } - sqlQueries = [] + clearSQLQueries() } override func tearDown() { @@ -130,6 +134,10 @@ class GRDBTestCase: XCTestCase { do { try FileManager.default.removeItem(atPath: dbDirectoryPath) } catch { } } + func clearSQLQueries() { + _sqlQueriesMutex.store([]) + } + func assertNoError(file: StaticString = #file, line: UInt = #line, _ test: () throws -> Void) { do { try test() diff --git a/Tests/GRDBTests/MutablePersistableRecordChangesTests.swift b/Tests/GRDBTests/MutablePersistableRecordChangesTests.swift index f530149f8a..e3a3485037 100644 --- a/Tests/GRDBTests/MutablePersistableRecordChangesTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordChangesTests.swift @@ -319,14 +319,14 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { try record.insert(db) do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } @@ -335,7 +335,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = nil } @@ -346,7 +346,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" @@ -391,14 +391,14 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { try record.insert(db) do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } @@ -407,7 +407,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = nil } @@ -418,7 +418,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" @@ -457,14 +457,14 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { try record.insert(db) do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } @@ -473,7 +473,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = nil } @@ -484,7 +484,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" @@ -519,14 +519,14 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { try record.insert(db) do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } @@ -535,7 +535,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = nil } @@ -546,7 +546,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" diff --git a/Tests/GRDBTests/MutablePersistableRecordTests.swift b/Tests/GRDBTests/MutablePersistableRecordTests.swift index 7348cbbbb6..a5da3c08d4 100644 --- a/Tests/GRDBTests/MutablePersistableRecordTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordTests.swift @@ -1282,7 +1282,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) @@ -1332,7 +1332,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(name: "Arthur") let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement)! @@ -1368,7 +1368,7 @@ extension MutablePersistableRecordTests { do { // Test onConflict: .ignore - sqlQueries.removeAll() + clearSQLQueries() var player = FullPlayer(id: 1, name: "Barbara", score: 100) try XCTAssertTrue(player.exists(db)) let score = try player.insertAndFetch(db, onConflict: .ignore, selection: [Column("score")]) { (statement: Statement) in @@ -1444,7 +1444,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) @@ -1482,7 +1482,7 @@ extension MutablePersistableRecordTests { do { var partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) - sqlQueries.removeAll() + clearSQLQueries() let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" @@ -1519,7 +1519,7 @@ extension MutablePersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(id: 1, name: "Arthur") let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) @@ -1570,7 +1570,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(name: "Arthur") let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) @@ -1608,7 +1608,7 @@ extension MutablePersistableRecordTests { do { var partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) - sqlQueries.removeAll() + clearSQLQueries() let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) } @@ -1645,7 +1645,7 @@ extension MutablePersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(id: 1, name: "Arthur") let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) @@ -2405,7 +2405,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var player = FullPlayer(id: 1, name: "Arthur", score: 1000) let upsertedPlayer = try player.upsertAndFetch(db) @@ -2449,7 +2449,7 @@ extension MutablePersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() var player = FullPlayer(id: 1, name: "Barbara", score: 100) let upsertedPlayer = try player.upsertAndFetch(db) @@ -2508,7 +2508,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try partialPlayer.upsertAndFetch(db, as: FullPlayer.self) diff --git a/Tests/GRDBTests/Mutex.swift b/Tests/GRDBTests/Mutex.swift new file mode 100644 index 0000000000..1fb69c2e22 --- /dev/null +++ b/Tests/GRDBTests/Mutex.swift @@ -0,0 +1,54 @@ +import Foundation + +/// A Mutex protects a value with an NSLock. +/// +/// We'll replace it with the SE-0433 Mutex when it is available. +/// +final class Mutex { + private var _value: T + private var lock = NSLock() + + init(_ value: T) { + _value = value + } + + /// Runs the provided closure while holding a lock on the value. + /// + /// - parameter body: A closure that can modify the value. + func withLock(_ body: (inout T) throws -> U) rethrows -> U { + lock.lock() + defer { lock.unlock() } + return try body(&_value) + } +} + +// Inspired by +extension Mutex { + func load() -> T { + withLock { $0 } + } + + func store(_ value: T) { + withLock { $0 = value } + } +} + +extension Mutex where T: Numeric { + @discardableResult + func increment() -> T { + withLock { n in + n += 1 + return n + } + } + + @discardableResult + func decrement() -> T { + withLock { n in + n -= 1 + return n + } + } +} + +extension Mutex: @unchecked Sendable where T: Sendable { } diff --git a/Tests/GRDBTests/PersistableRecordTests.swift b/Tests/GRDBTests/PersistableRecordTests.swift index 5c9bc6761b..c391e27da0 100644 --- a/Tests/GRDBTests/PersistableRecordTests.swift +++ b/Tests/GRDBTests/PersistableRecordTests.swift @@ -1340,7 +1340,7 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) @@ -1389,7 +1389,7 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(name: "Arthur") let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement)! @@ -1442,7 +1442,7 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) @@ -1479,7 +1479,7 @@ extension PersistableRecordTests { do { let partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) - sqlQueries.removeAll() + clearSQLQueries() let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" @@ -1515,7 +1515,7 @@ extension PersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(id: 1, name: "Arthur") let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) @@ -1565,7 +1565,7 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(name: "Arthur") let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) @@ -1602,7 +1602,7 @@ extension PersistableRecordTests { do { let partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) - sqlQueries.removeAll() + clearSQLQueries() let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) } @@ -1638,7 +1638,7 @@ extension PersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(id: 1, name: "Arthur") let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) @@ -1974,7 +1974,7 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let player = FullPlayer(id: 1, name: "Arthur", score: 1000) let upsertedPlayer = try player.upsertAndFetch(db) @@ -2018,7 +2018,7 @@ extension PersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() let player = FullPlayer(id: 1, name: "Barbara", score: 100) let upsertedPlayer = try player.upsertAndFetch(db) @@ -2077,7 +2077,7 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try partialPlayer.upsertAndFetch(db, as: FullPlayer.self) diff --git a/Tests/GRDBTests/TableDefinitionTests.swift b/Tests/GRDBTests/TableDefinitionTests.swift index 1079b39d61..3c138d6e59 100644 --- a/Tests/GRDBTests/TableDefinitionTests.swift +++ b/Tests/GRDBTests/TableDefinitionTests.swift @@ -204,7 +204,7 @@ class TableDefinitionTests: GRDBTestCase { func testColumnIndexed() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "test") { t in t.column("a", .integer).indexed() t.column("b", .integer).indexed() @@ -219,7 +219,7 @@ class TableDefinitionTests: GRDBTestCase { func testColumnIndexedInheritsIfNotExistsFlag() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "test", options: [.ifNotExists]) { t in t.column("a", .integer).indexed() t.column("b", .integer).indexed() @@ -733,7 +733,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("a", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "test") { t in t.add(column: "b", .text) t.add(column: "c", .integer).notNull().defaults(to: 1) @@ -760,7 +760,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("a", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "hiddenRowIdTable") { t in t.add(column: "ref").references("hiddenRowIdTable") } @@ -775,7 +775,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("a", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "explicitPrimaryKey") { t in t.add(column: "ref").references("explicitPrimaryKey") } @@ -816,7 +816,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("a", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "test") { t in t.rename(column: "a", to: "b") t.add(column: "c") @@ -870,7 +870,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("c", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "test") { t in t.add(column: "d", .integer).generatedAs(sql: "a*abs(b)", .virtual) t.add(column: "e", .text).generatedAs(sql: "substr(c,b,b+1)", .virtual) @@ -904,7 +904,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("b", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "test") { t in t.drop(column: "b") } diff --git a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift index 6d01e8f7a0..737220992c 100644 --- a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift @@ -386,7 +386,7 @@ class TableRecordQueryInterfaceRequestTests: GRDBTestCase { try XCTAssertFalse(Player.exists(db, id: 1)) XCTAssertEqual(lastSQLQuery, "SELECT EXISTS (SELECT * FROM \"player\" WHERE \"id\" = 1)") - sqlQueries.removeAll() + clearSQLQueries() try XCTAssertFalse(Player.exists(db, id: nil)) XCTAssertNil(lastSQLQuery) // Database not hit diff --git a/Tests/GRDBTests/TableRecordUpdateTests.swift b/Tests/GRDBTests/TableRecordUpdateTests.swift index dd375bc8d3..ac7bf5f84b 100644 --- a/Tests/GRDBTests/TableRecordUpdateTests.swift +++ b/Tests/GRDBTests/TableRecordUpdateTests.swift @@ -480,7 +480,7 @@ class TableRecordUpdateTests: GRDBTestCase { func testUpdateAllWithoutAssignmentDoesNotAccessTheDatabase() throws { try makeDatabaseQueue().write { db in try Player.createTable(db) - sqlQueries.removeAll() + clearSQLQueries() try XCTAssertEqual(Player.updateAll(db, []), 0) try XCTAssertEqual(Player.all().updateAll(db, []), 0) XCTAssert(sqlQueries.isEmpty) diff --git a/Tests/GRDBTests/TableTests.swift b/Tests/GRDBTests/TableTests.swift index 7278fa11f3..fa7325771c 100644 --- a/Tests/GRDBTests/TableTests.swift +++ b/Tests/GRDBTests/TableTests.swift @@ -830,7 +830,7 @@ class TableTests: GRDBTestCase { DELETE FROM "country" WHERE "code" = 'FR' """) - sqlQueries.removeAll() + clearSQLQueries() try Table("country").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit @@ -939,7 +939,7 @@ class TableTests: GRDBTestCase { SELECT EXISTS (SELECT * FROM "country" WHERE "code" = 'FR') """) - sqlQueries.removeAll() + clearSQLQueries() try XCTAssertFalse(Table("country").exists(db, id: nil)) XCTAssertNil(lastSQLQuery) // Database not hit } diff --git a/Tests/GRDBTests/ValueObservationPrintTests.swift b/Tests/GRDBTests/ValueObservationPrintTests.swift index 74e2450be1..c2940c757b 100644 --- a/Tests/GRDBTests/ValueObservationPrintTests.swift +++ b/Tests/GRDBTests/ValueObservationPrintTests.swift @@ -4,9 +4,10 @@ import Dispatch class ValueObservationPrintTests: GRDBTestCase { class TestStream: TextOutputStream { - @LockedBox var strings: [String] = [] + private var stringsMutex: Mutex<[String]> = Mutex([]) + var strings: [String] { stringsMutex.load() } func write(_ string: String) { - strings.append(string) + stringsMutex.withLock { $0.append(string) } } } diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index 326d536476..c947a44d63 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -174,9 +174,10 @@ class ValueObservationTests: GRDBTestCase { func testTrackingExplicitRegion() throws { class TestStream: TextOutputStream { - @LockedBox var strings: [String] = [] + private var stringsMutex: Mutex<[String]> = Mutex([]) + var strings: [String] { stringsMutex.load() } func write(_ string: String) { - strings.append(string) + stringsMutex.withLock { $0.append(string) } } } From b2286aaaabb2869337a6ec5831bc2048e9563bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 25 Aug 2024 13:07:22 +0200 Subject: [PATCH 022/160] DatabaseCursor has a primary associated type --- GRDB/Core/Statement.swift | 2 +- TODO.md | 2 +- Tests/GRDBTests/DatabaseCursorTests.swift | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index 698855b19d..3956887093 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -719,7 +719,7 @@ extension Statement { /// [`sqlite3_reset`](https://www.sqlite.org/c3ref/reset.html) when the cursor /// is created, and when it is deallocated. Don't share the same prepared /// statement between two cursors! -public protocol DatabaseCursor: Cursor { +public protocol DatabaseCursor: Cursor { /// Must be initialized to false. var _isDone: Bool { get set } diff --git a/TODO.md b/TODO.md index e2e85fa2b1..06c27e3f09 100644 --- a/TODO.md +++ b/TODO.md @@ -128,7 +128,7 @@ - [ ] GRDB7: Sendable: SQL (ac33856f) - [ ] GRDB7: Split Row.swift (2ce8a619) - [ ] GRDB7: Cleanup ValueReducer (6c73b1c5) -- [ ] GRDB7: DatabaseCursor has a primary associated type (b11c5dd2) +- [X] GRDB7: DatabaseCursor has a primary associated type (b11c5dd2) - [ ] GRDB7: Enable Strict Concurrency Checks (6aa43ded) - [ ] GRDB7: Sendable: OrderedDictionary (e022c35b) - [ ] GRDB7: Rename ReadWriteBox to ReadWriteLock (7f5205ef) diff --git a/Tests/GRDBTests/DatabaseCursorTests.swift b/Tests/GRDBTests/DatabaseCursorTests.swift index d18191488f..19e53f3700 100644 --- a/Tests/GRDBTests/DatabaseCursorTests.swift +++ b/Tests/GRDBTests/DatabaseCursorTests.swift @@ -213,6 +213,15 @@ class DatabaseCursorTests: GRDBTestCase { // } } + // This test passes if it compiles + func testAssociatedType() throws { + func accept(_ cursor: some DatabaseCursor) { } + func useCursor(_ db: Database) throws { + let cursor = try String.fetchCursor(db, sql: "SELECT 'foo'") + accept(cursor) + } + } + // For profiling tests let profilingSQL = """ WITH RECURSIVE From c4507c5a0019e98fef5da2f7a07d8049c380f67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 25 Aug 2024 13:51:11 +0200 Subject: [PATCH 023/160] .editorconfig --- .editorconfig | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..d82189eac3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*.swift] +indent_style = space +indent_size = 4 +tab_width = 4 +end_of_line = lf +insert_final_newline = true +max_line_length = 76 +trim_trailing_whitespace = true From 1fe4df943879d3441f17230877ba5ee5c2465586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 28 Jul 2024 13:59:23 +0200 Subject: [PATCH 024/160] [BREAKING] databaseDataEncodingStrategy depends on the column --- GRDB/Documentation.docc/JSON.md | 4 +- .../Request/RequestProtocols.swift | 65 ++++++++++++------- GRDB/Record/EncodableRecord+Encodable.swift | 4 +- GRDB/Record/EncodableRecord.swift | 15 +++-- README.md | 2 +- .../DatabaseDataEncodingStrategyTests.swift | 8 ++- 6 files changed, 64 insertions(+), 34 deletions(-) diff --git a/GRDB/Documentation.docc/JSON.md b/GRDB/Documentation.docc/JSON.md index 34fdba1257..499da4ef40 100644 --- a/GRDB/Documentation.docc/JSON.md +++ b/GRDB/Documentation.docc/JSON.md @@ -92,7 +92,9 @@ struct Team: Codable { extension Team: FetchableRecord, PersistableRecord { // Support SQLite JSON functions and operators // by storing JSON data as database text: - static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { + .text + } } ``` diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index 71413dda7f..03334eef9f 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -443,45 +443,64 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { // `deleteAll(_:ids:)` etc. if let recordType = RowDecoder.self as? any EncodableRecord.Type { if Sequence.Element.self == Data.self || Sequence.Element.self == Optional.self { - let strategy = recordType.databaseDataEncodingStrategy - let keys = keys.compactMap { ($0 as! Data?).flatMap(strategy.encode)?.databaseValue } - return filter(rawKeys: keys) + let datas = keys.compactMap { ($0 as! Data?) } + if datas.isEmpty { + // Don't hit the database + return none() + } + + return filterWhenConnected(keys: { [databaseTableName] db in + let primaryKey = try db.primaryKey(databaseTableName) + GRDBPrecondition( + primaryKey.columns.count == 1, + "Requesting by key requires a single-column primary key in the table \(databaseTableName)") + let column = primaryKey.columns[0] + let strategy = recordType.databaseDataEncodingStrategy(for: column) + let expressions = datas.map { strategy.encode($0).sqlExpression } + return expressions + }) } else if Sequence.Element.self == Date.self || Sequence.Element.self == Optional.self { + let dates = keys.compactMap { ($0 as! Date?) } + if dates.isEmpty { + // Don't hit the database + return none() + } let strategy = recordType.databaseDateEncodingStrategy - let keys = keys.compactMap { ($0 as! Date?).flatMap(strategy.encode)?.databaseValue } - return filter(rawKeys: keys) + let expressions = dates.map { strategy.encode($0).sqlExpression } + return filterWhenConnected(keys: { _ in expressions }) } else if Sequence.Element.self == UUID.self || Sequence.Element.self == Optional.self { + let uuids = keys.compactMap { ($0 as! UUID?) } + if uuids.isEmpty { + // Don't hit the database + return none() + } let strategy = recordType.databaseUUIDEncodingStrategy - let keys = keys.map { ($0 as! UUID?).map(strategy.encode)?.databaseValue } - return filter(rawKeys: keys) + let expressions = uuids.map { strategy.encode($0).sqlExpression } + return filterWhenConnected(keys: { _ in expressions }) } } - return filter(rawKeys: keys) + let expressions = keys.map { $0.sqlExpression } + if expressions.isEmpty { + // Don't hit the database + return none() + } + return filterWhenConnected(keys: { _ in expressions }) } /// Creates a request filtered by primary key. /// /// // SELECT * FROM player WHERE ... id IN (1, 2, 3) - /// let request = try Player...filter(rawKeys: [1, 2, 3]) + /// let request = try Player...filterWhenConnected(keys: { db in [1, 2, 3] }) /// /// - parameter keys: A collection of primary keys - func filter(rawKeys: Keys) -> Self - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { - // Don't bother removing NULLs. We'd lose CPU cycles, and this does not - // change the SQLite results anyway. - let expressions = rawKeys.map { - $0.databaseValue.sqlExpression - } - - if expressions.isEmpty { - // Don't hit the database - return none() - } - + fileprivate func filterWhenConnected(keys: @escaping @Sendable (Database) throws -> [SQLExpression]) -> Self { let databaseTableName = self.databaseTableName return filterWhenConnected { db in + // Don't bother removing NULLs. We'd lose CPU cycles, and this does not + // change the SQLite results anyway. + let expressions = try keys(db) + let primaryKey = try db.primaryKey(databaseTableName) GRDBPrecondition( primaryKey.columns.count == 1, diff --git a/GRDB/Record/EncodableRecord+Encodable.swift b/GRDB/Record/EncodableRecord+Encodable.swift index 8ead9397bc..de1653d984 100644 --- a/GRDB/Record/EncodableRecord+Encodable.swift +++ b/GRDB/Record/EncodableRecord+Encodable.swift @@ -118,7 +118,9 @@ private class RecordEncoder: Encoder { fileprivate func encode(_ value: T, forKey key: any CodingKey) throws where T: Encodable { if let data = value as? Data { - persist(Record.databaseDataEncodingStrategy.encode(data), forKey: key) + let column = keyEncodingStrategy.column(forKey: key) + let dbValue = Record.databaseDataEncodingStrategy(for: column).encode(data) + _persistenceContainer[column] = dbValue } else if let date = value as? Date { persist(Record.databaseDateEncodingStrategy.encode(date), forKey: key) } else if let uuid = value as? UUID { diff --git a/GRDB/Record/EncodableRecord.swift b/GRDB/Record/EncodableRecord.swift index b1015c751f..d980741b59 100644 --- a/GRDB/Record/EncodableRecord.swift +++ b/GRDB/Record/EncodableRecord.swift @@ -20,8 +20,7 @@ import Foundation // For JSONEncoder /// ### Configuring Persistence for the Standard Encodable Protocol /// /// - ``databaseColumnEncodingStrategy-5sx4v`` -/// - ``databaseDataEncodingStrategy-9y0c7`` -/// - ``databaseDateEncodingStrategy-2gtc1`` +/// - ``databaseDataEncodingStrategy(for:)`` /// - ``databaseEncodingUserInfo-8upii`` /// - ``databaseJSONEncoder(for:)-6x62c`` /// - ``databaseUUIDEncodingStrategy-2t96q`` @@ -127,13 +126,15 @@ public protocol EncodableRecord { /// /// ```swift /// struct Player: EncodableRecord, Encodable { - /// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text + /// static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { + /// .text + /// } /// /// // Encoded as SQL text. Data must contain valid UTF8 bytes. /// var jsonData: Data /// } /// ``` - static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { get } + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy /// The strategy for encoding `Date` columns. /// @@ -221,7 +222,7 @@ extension EncodableRecord { /// Returns the default strategy for encoding `Data` columns: /// ``DatabaseDataEncodingStrategy/deferredToData``. - public static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { + public static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { .deferredToData } @@ -460,7 +461,9 @@ extension Row { /// /// ```swift /// struct Player: EncodableRecord, Encodable { -/// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text +/// static func databaseDataEncodingStrategy(for column: Column) -> DatabaseDataEncodingStrategy { +/// .text +/// } /// /// // Encoded as SQL text. Data must contain valid UTF8 bytes. /// var jsonData: Data diff --git a/README.md b/README.md index a27d198d7d..8ae61c047d 100644 --- a/README.md +++ b/README.md @@ -2705,7 +2705,7 @@ protocol FetchableRecord { } protocol EncodableRecord { - static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { get } + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get } static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get } } diff --git a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift index f6e4758821..567affede7 100644 --- a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift @@ -19,7 +19,9 @@ private enum StrategyCustom: StrategyProvider { } private struct RecordWithData: EncodableRecord, Encodable { - static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { Strategy.strategy } + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { + Strategy.strategy + } var data: Data } @@ -29,7 +31,9 @@ extension RecordWithData: Identifiable { } private struct RecordWithOptionalData: EncodableRecord, Encodable { - static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { Strategy.strategy } + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { + Strategy.strategy + } var data: Data? } From 7a8a9a936352f9cece15b763333d99073d33ff6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 28 Jul 2024 14:08:19 +0200 Subject: [PATCH 025/160] [BREAKING] databaseDateEncodingStrategy depends on the column --- GRDB/QueryInterface/Request/RequestProtocols.swift | 14 +++++++++++--- GRDB/Record/EncodableRecord+Encodable.swift | 4 +++- GRDB/Record/EncodableRecord.swift | 13 +++++++++---- README.md | 2 +- .../DatabaseDateEncodingStrategyTests.swift | 8 ++++++-- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index 03334eef9f..9a07ce2646 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -465,9 +465,17 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { // Don't hit the database return none() } - let strategy = recordType.databaseDateEncodingStrategy - let expressions = dates.map { strategy.encode($0).sqlExpression } - return filterWhenConnected(keys: { _ in expressions }) + + return filterWhenConnected(keys: { [databaseTableName] db in + let primaryKey = try db.primaryKey(databaseTableName) + GRDBPrecondition( + primaryKey.columns.count == 1, + "Requesting by key requires a single-column primary key in the table \(databaseTableName)") + let column = primaryKey.columns[0] + let strategy = recordType.databaseDateEncodingStrategy(for: column) + let expressions = dates.map { strategy.encode($0).sqlExpression } + return expressions + }) } else if Sequence.Element.self == UUID.self || Sequence.Element.self == Optional.self { let uuids = keys.compactMap { ($0 as! UUID?) } if uuids.isEmpty { diff --git a/GRDB/Record/EncodableRecord+Encodable.swift b/GRDB/Record/EncodableRecord+Encodable.swift index de1653d984..024df5197c 100644 --- a/GRDB/Record/EncodableRecord+Encodable.swift +++ b/GRDB/Record/EncodableRecord+Encodable.swift @@ -122,7 +122,9 @@ private class RecordEncoder: Encoder { let dbValue = Record.databaseDataEncodingStrategy(for: column).encode(data) _persistenceContainer[column] = dbValue } else if let date = value as? Date { - persist(Record.databaseDateEncodingStrategy.encode(date), forKey: key) + let column = keyEncodingStrategy.column(forKey: key) + let dbValue = Record.databaseDateEncodingStrategy(for: column).encode(date) + _persistenceContainer[column] = dbValue } else if let uuid = value as? UUID { persist(Record.databaseUUIDEncodingStrategy.encode(uuid), forKey: key) } else if let value = value as? any DatabaseValueConvertible { diff --git a/GRDB/Record/EncodableRecord.swift b/GRDB/Record/EncodableRecord.swift index d980741b59..d780101f3c 100644 --- a/GRDB/Record/EncodableRecord.swift +++ b/GRDB/Record/EncodableRecord.swift @@ -21,6 +21,7 @@ import Foundation // For JSONEncoder /// /// - ``databaseColumnEncodingStrategy-5sx4v`` /// - ``databaseDataEncodingStrategy(for:)`` +/// - ``databaseDateEncodingStrategy(for:)`` /// - ``databaseEncodingUserInfo-8upii`` /// - ``databaseJSONEncoder(for:)-6x62c`` /// - ``databaseUUIDEncodingStrategy-2t96q`` @@ -146,13 +147,15 @@ public protocol EncodableRecord { /// /// ```swift /// struct Player: EncodableRecord, Encodable { - /// static let databaseDateEncodingStrategy = DatabaseDateEncodingStrategy.timeIntervalSince1970 + /// static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy { + /// .timeIntervalSince1970 + /// } /// /// // Encoded as an epoch timestamp /// var creationDate: Date /// } /// ``` - static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get } + static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy /// The strategy for encoding `UUID` columns. /// @@ -228,7 +231,7 @@ extension EncodableRecord { /// Returns the default strategy for encoding `Date` columns: /// ``DatabaseDateEncodingStrategy/deferredToDate``. - public static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { + public static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy { .deferredToDate } @@ -505,7 +508,9 @@ public enum DatabaseDataEncodingStrategy { /// /// ```swift /// struct Player: EncodableRecord, Encodable { -/// static let databaseDateEncodingStrategy = DatabaseDateEncodingStrategy.timeIntervalSince1970 +/// static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy {` +/// .timeIntervalSince1970 +/// } /// /// // Encoded as an epoch timestamp /// var creationDate: Date diff --git a/README.md b/README.md index 8ae61c047d..619efcc3d9 100644 --- a/README.md +++ b/README.md @@ -2706,7 +2706,7 @@ protocol FetchableRecord { protocol EncodableRecord { static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy - static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get } + static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get } } ``` diff --git a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift index 4aec3e6107..4d32142727 100644 --- a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift @@ -46,7 +46,9 @@ private enum StrategyCustom: StrategyProvider { } private struct RecordWithDate: EncodableRecord, Encodable { - static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { Strategy.strategy } + static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy { + Strategy.strategy + } var date: Date } @@ -56,7 +58,9 @@ extension RecordWithDate: Identifiable { } private struct RecordWithOptionalDate: EncodableRecord, Encodable { - static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { Strategy.strategy } + static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy { + Strategy.strategy + } var date: Date? } From bcd6e03d7a249054635c018c874b5bb56f96bfb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 28 Jul 2024 14:55:44 +0200 Subject: [PATCH 026/160] [BREAKING] databaseUUIDEncodingStrategy depends on the column --- .../Request/RequestProtocols.swift | 14 +++++++++++--- GRDB/Record/EncodableRecord+Encodable.swift | 4 +++- GRDB/Record/EncodableRecord.swift | 16 ++++++++++------ README.md | 7 +++++-- .../DatabaseUUIDEncodingStrategyTests.swift | 10 ++++++++-- 5 files changed, 37 insertions(+), 14 deletions(-) diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index 9a07ce2646..1803a4cfbc 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -482,9 +482,17 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { // Don't hit the database return none() } - let strategy = recordType.databaseUUIDEncodingStrategy - let expressions = uuids.map { strategy.encode($0).sqlExpression } - return filterWhenConnected(keys: { _ in expressions }) + + return filterWhenConnected(keys: { [databaseTableName] db in + let primaryKey = try db.primaryKey(databaseTableName) + GRDBPrecondition( + primaryKey.columns.count == 1, + "Requesting by key requires a single-column primary key in the table \(databaseTableName)") + let column = primaryKey.columns[0] + let strategy = recordType.databaseUUIDEncodingStrategy(for: column) + let expressions = uuids.map { strategy.encode($0).sqlExpression } + return expressions + }) } } diff --git a/GRDB/Record/EncodableRecord+Encodable.swift b/GRDB/Record/EncodableRecord+Encodable.swift index 024df5197c..28782323fb 100644 --- a/GRDB/Record/EncodableRecord+Encodable.swift +++ b/GRDB/Record/EncodableRecord+Encodable.swift @@ -126,7 +126,9 @@ private class RecordEncoder: Encoder { let dbValue = Record.databaseDateEncodingStrategy(for: column).encode(date) _persistenceContainer[column] = dbValue } else if let uuid = value as? UUID { - persist(Record.databaseUUIDEncodingStrategy.encode(uuid), forKey: key) + let column = keyEncodingStrategy.column(forKey: key) + let dbValue = Record.databaseUUIDEncodingStrategy(for: column).encode(uuid) + _persistenceContainer[column] = dbValue } else if let value = value as? any DatabaseValueConvertible { // Prefer DatabaseValueConvertible encoding over Decodable. persist(value.databaseValue, forKey: key) diff --git a/GRDB/Record/EncodableRecord.swift b/GRDB/Record/EncodableRecord.swift index d780101f3c..fb71cce645 100644 --- a/GRDB/Record/EncodableRecord.swift +++ b/GRDB/Record/EncodableRecord.swift @@ -22,9 +22,9 @@ import Foundation // For JSONEncoder /// - ``databaseColumnEncodingStrategy-5sx4v`` /// - ``databaseDataEncodingStrategy(for:)`` /// - ``databaseDateEncodingStrategy(for:)`` -/// - ``databaseEncodingUserInfo-8upii`` /// - ``databaseJSONEncoder(for:)-6x62c`` -/// - ``databaseUUIDEncodingStrategy-2t96q`` +/// - ``databaseUUIDEncodingStrategy(for:)`` +/// - ``databaseEncodingUserInfo-8upii`` /// - ``DatabaseColumnEncodingStrategy`` /// - ``DatabaseDataEncodingStrategy`` /// - ``DatabaseDateEncodingStrategy`` @@ -167,13 +167,15 @@ public protocol EncodableRecord { /// /// ```swift /// struct Player: EncodableRecord, Encodable { - /// static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString + /// static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { + /// .uppercaseString + /// } /// /// // Encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" /// var uuid: UUID /// } /// ``` - static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get } + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy /// The strategy for converting coding keys to column names. /// @@ -237,7 +239,7 @@ extension EncodableRecord { /// Returns the default strategy for encoding `UUID` columns: /// ``DatabaseUUIDEncodingStrategy/deferredToUUID``. - public static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { + public static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { .deferredToUUID } @@ -587,7 +589,9 @@ public enum DatabaseDateEncodingStrategy { /// /// ```swift /// struct Player: EncodableRecord, Encodable { -/// static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString +/// static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy {` +/// .uppercaseString +/// } /// /// // Encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" /// var uuid: UUID diff --git a/README.md b/README.md index 619efcc3d9..189935b4f1 100644 --- a/README.md +++ b/README.md @@ -2707,7 +2707,7 @@ protocol FetchableRecord { protocol EncodableRecord { static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy - static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get } + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy } ``` @@ -2727,7 +2727,10 @@ So make sure that those are properly encoded in your requests. For example: ```swift struct Player: Codable, FetchableRecord, PersistableRecord, Identifiable { // UUIDs are stored as strings - static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { + .uppercaseString + } + var id: UUID ... } diff --git a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift index 5ed9f6bb08..c5c652ac28 100644 --- a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift @@ -19,7 +19,10 @@ private enum StrategyLowercaseString: StrategyProvider { } private struct RecordWithUUID: EncodableRecord, Encodable { - static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { Strategy.strategy } + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { + Strategy.strategy + } + var uuid: UUID } @@ -29,7 +32,10 @@ extension RecordWithUUID: Identifiable { } private struct RecordWithOptionalUUID: EncodableRecord, Encodable { - static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { Strategy.strategy } + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { + Strategy.strategy + } + var uuid: UUID? } From 95ca9f5bf3aa6c745c9aef4ff7bf2a192c035326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 28 Jul 2024 14:58:08 +0200 Subject: [PATCH 027/160] [BREAKING] databaseDataDecodingStrategy depends on the column --- GRDB/Record/FetchableRecord+Decodable.swift | 18 +++++--- GRDB/Record/FetchableRecord.swift | 28 ++++++------ README.md | 2 +- .../DatabaseDataDecodingStrategyTests.swift | 10 ++++- .../FetchableRecordDecodableTests.swift | 43 +++++++++++++++++++ 5 files changed, 79 insertions(+), 22 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 9fbf0cfabf..cf2f25a92c 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -61,7 +61,7 @@ extension FetchableRecord where Self: Decodable { /// The behavior of the decoder depends on the decoded type. See: /// /// - ``FetchableRecord/databaseColumnDecodingStrategy-6uefz`` -/// - ``FetchableRecord/databaseDataDecodingStrategy-71bh1`` +/// - ``FetchableRecord/databaseDataDecodingStrategy(for:)`` /// - ``FetchableRecord/databaseDateDecodingStrategy-78y03`` /// - ``FetchableRecord/databaseDecodingUserInfo-77jim`` /// - ``FetchableRecord/databaseJSONDecoder(for:)-7lmxd`` @@ -285,9 +285,9 @@ private struct _RowDecoder: Decoder { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. if type == Data.self { - return try R.databaseDataDecodingStrategy.decodeIfPresent( - fromRow: row, - atUncheckedIndex: index) as! T? + return try R + .databaseDataDecodingStrategy(for: column) + .decodeIfPresent(fromRow: row, atUncheckedIndex: index) as! T? } else if type == Date.self { return try R.databaseDateDecodingStrategy.decodeIfPresent( fromRow: row, @@ -334,7 +334,9 @@ private struct _RowDecoder: Decoder { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. if type == Data.self { - return try R.databaseDataDecodingStrategy.decode(fromRow: row, atUncheckedIndex: index) as! T + return try R + .databaseDataDecodingStrategy(for: column) + .decode(fromRow: row, atUncheckedIndex: index) as! T } else if type == Date.self { return try R.databaseDateDecodingStrategy.decode(fromRow: row, atUncheckedIndex: index) as! T } else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type { @@ -652,9 +654,11 @@ extension ColumnDecoder: SingleValueDecodingContainer { func decode(_ type: String.Type) throws -> String { try row.decode(atIndex: columnIndex) } func decode(_ type: T.Type) throws -> T where T: Decodable { - // TODO: not tested if type == Data.self { - return try R.databaseDataDecodingStrategy.decode(fromRow: row, atUncheckedIndex: columnIndex) as! T + let columnName = row.impl.columnName(atUncheckedIndex: columnIndex) + return try R + .databaseDataDecodingStrategy(for: columnName) + .decode(fromRow: row, atUncheckedIndex: columnIndex) as! T } else if type == Date.self { return try R.databaseDateDecodingStrategy.decode(fromRow: row, atUncheckedIndex: columnIndex) as! T } else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type { diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index 90dd6669ca..f52782a3bc 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -89,10 +89,10 @@ import Foundation /// ### Configuring Row Decoding for the Standard Decodable Protocol /// /// - ``databaseColumnDecodingStrategy-6uefz`` -/// - ``databaseDataDecodingStrategy-71bh1`` +/// - ``databaseDataDecodingStrategy(for:)`` /// - ``databaseDateDecodingStrategy-78y03`` -/// - ``databaseDecodingUserInfo-77jim`` /// - ``databaseJSONDecoder(for:)-7lmxd`` +/// - ``databaseDecodingUserInfo-77jim`` /// - ``DatabaseColumnDecodingStrategy`` /// - ``DatabaseDataDecodingStrategy`` /// - ``DatabaseDateDecodingStrategy`` @@ -165,18 +165,20 @@ public protocol FetchableRecord { /// /// ```swift /// struct Player: FetchableRecord, Decodable { - /// static let databaseDataDecodingStrategy = DatabaseDataDecodingStrategy.custom { dbValue - /// guard let base64Data = Data.fromDatabaseValue(dbValue) else { - /// return nil + /// static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { + /// .custom { dbValue + /// guard let base64Data = Data.fromDatabaseValue(dbValue) else { + /// return nil + /// } + /// return Data(base64Encoded: base64Data) /// } - /// return Data(base64Encoded: base64Data) /// } /// /// // Decoded from both database base64 strings and blobs /// var myData: Data /// } /// ``` - static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { get } + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy /// The strategy for decoding `Date` columns. /// @@ -243,7 +245,7 @@ extension FetchableRecord { /// The default strategy for decoding `Data` columns is /// ``DatabaseDataDecodingStrategy/deferredToData``. - public static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { + public static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { .deferredToData } @@ -865,11 +867,13 @@ extension RecordCursor: Sendable { } /// /// ```swift /// struct Player: FetchableRecord, Decodable { -/// static let databaseDataDecodingStrategy = DatabaseDataDecodingStrategy.custom { dbValue -/// guard let base64Data = Data.fromDatabaseValue(dbValue) else { -/// return nil +/// static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { +/// .custom { dbValue +/// guard let base64Data = Data.fromDatabaseValue(dbValue) else { +/// return nil +/// } +/// return Data(base64Encoded: base64Data) /// } -/// return Data(base64Encoded: base64Data) /// } /// /// // Decoded from both database base64 strings and blobs diff --git a/README.md b/README.md index 189935b4f1..b2f7791dfb 100644 --- a/README.md +++ b/README.md @@ -2700,7 +2700,7 @@ Those behaviors can be overridden: ```swift protocol FetchableRecord { - static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { get } + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { get } } diff --git a/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift index 8ff1da13b2..a11493297e 100644 --- a/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift @@ -20,12 +20,18 @@ private enum StrategyCustom: StrategyProvider { } private struct RecordWithData: FetchableRecord, Decodable { - static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { Strategy.strategy } + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { + Strategy.strategy + } + var data: Data } private struct RecordWithOptionalData: FetchableRecord, Decodable { - static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { Strategy.strategy } + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { + Strategy.strategy + } + var data: Data? } diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index 8c7d0c195c..59f6054fbb 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -121,6 +121,49 @@ extension FetchableRecordDecodableTests { } } + func testSingleValueDataProperty() throws { + struct Value : Decodable { + let data: Data + + init(from decoder: Decoder) throws { + data = try decoder.singleValueContainer().decode(Data.self) + } + } + + struct Struct : FetchableRecord, Decodable { + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { + if column == "value" { + return .custom { _ in Data([1, 2, 3]) } + } else { + return .deferredToData + } + } + let value: Value + let optionalValue: Value? + } + + do { + // No null values + let s = try Struct(row: ["value": "foo", "optionalValue": "bar"]) + XCTAssertEqual(s.value.data, Data([1, 2, 3])) + XCTAssertEqual(s.optionalValue?.data, Data([98, 97, 114])) + } + + do { + // Null values + let s = try Struct(row: ["value": "foo", "optionalValue": nil]) + XCTAssertEqual(s.value.data, Data([1, 2, 3])) + XCTAssertNil(s.optionalValue) + } + + do { + // Missing and extra values + let s = try Struct(row: ["value": "foo", "ignored": "?"]) + XCTAssertEqual(s.value.data, Data([1, 2, 3])) + XCTAssertNil(s.optionalValue) + } + } + func testNonTrivialSingleValueDecodableProperty() throws { struct NestedValue : Decodable { let string: String From 7747211691aa36408a6a46c97a8e84014af06219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Tue, 30 Jul 2024 08:31:12 +0200 Subject: [PATCH 028/160] [BREAKING] databaseDateDecodingStrategy depends on the column --- GRDB/Record/FetchableRecord+Decodable.swift | 17 +++++--- GRDB/Record/FetchableRecord.swift | 14 +++--- README.md | 2 +- .../DatabaseDateDecodingStrategyTests.swift | 10 ++++- .../FetchableRecordDecodableTests.swift | 43 +++++++++++++++++++ 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index cf2f25a92c..8edca5d322 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -62,7 +62,7 @@ extension FetchableRecord where Self: Decodable { /// /// - ``FetchableRecord/databaseColumnDecodingStrategy-6uefz`` /// - ``FetchableRecord/databaseDataDecodingStrategy(for:)`` -/// - ``FetchableRecord/databaseDateDecodingStrategy-78y03`` +/// - ``FetchableRecord/databaseDateDecodingStrategy(for:)`` /// - ``FetchableRecord/databaseDecodingUserInfo-77jim`` /// - ``FetchableRecord/databaseJSONDecoder(for:)-7lmxd`` public class FetchableRecordDecoder { @@ -289,9 +289,9 @@ private struct _RowDecoder: Decoder { .databaseDataDecodingStrategy(for: column) .decodeIfPresent(fromRow: row, atUncheckedIndex: index) as! T? } else if type == Date.self { - return try R.databaseDateDecodingStrategy.decodeIfPresent( - fromRow: row, - atUncheckedIndex: index) as! T? + return try R + .databaseDateDecodingStrategy(for: column) + .decodeIfPresent(fromRow: row, atUncheckedIndex: index) as! T? } else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type { return try type.fastDecodeIfPresent(fromRow: row, atUncheckedIndex: index) as! T? } else if let type = T.self as? any DatabaseValueConvertible.Type { @@ -338,7 +338,9 @@ private struct _RowDecoder: Decoder { .databaseDataDecodingStrategy(for: column) .decode(fromRow: row, atUncheckedIndex: index) as! T } else if type == Date.self { - return try R.databaseDateDecodingStrategy.decode(fromRow: row, atUncheckedIndex: index) as! T + return try R + .databaseDateDecodingStrategy(for: column) + .decode(fromRow: row, atUncheckedIndex: index) as! T } else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type { return try type.fastDecode(fromRow: row, atUncheckedIndex: index) as! T } else if let type = T.self as? any DatabaseValueConvertible.Type { @@ -660,7 +662,10 @@ extension ColumnDecoder: SingleValueDecodingContainer { .databaseDataDecodingStrategy(for: columnName) .decode(fromRow: row, atUncheckedIndex: columnIndex) as! T } else if type == Date.self { - return try R.databaseDateDecodingStrategy.decode(fromRow: row, atUncheckedIndex: columnIndex) as! T + let columnName = row.impl.columnName(atUncheckedIndex: columnIndex) + return try R + .databaseDateDecodingStrategy(for: columnName) + .decode(fromRow: row, atUncheckedIndex: columnIndex) as! T } else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type { return try type.fastDecode(fromRow: row, atUncheckedIndex: columnIndex) as! T } else if let type = T.self as? any DatabaseValueConvertible.Type { diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index f52782a3bc..bf1c92fbde 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -90,7 +90,7 @@ import Foundation /// /// - ``databaseColumnDecodingStrategy-6uefz`` /// - ``databaseDataDecodingStrategy(for:)`` -/// - ``databaseDateDecodingStrategy-78y03`` +/// - ``databaseDateDecodingStrategy(for:)`` /// - ``databaseJSONDecoder(for:)-7lmxd`` /// - ``databaseDecodingUserInfo-77jim`` /// - ``DatabaseColumnDecodingStrategy`` @@ -190,13 +190,15 @@ public protocol FetchableRecord { /// /// ```swift /// struct Player: FetchableRecord, Decodable { - /// static let databaseDateDecodingStrategy = DatabaseDateDecodingStrategy.timeIntervalSince1970 + /// static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { + /// .timeIntervalSince1970 + /// } /// /// // Decoded from an epoch timestamp /// var creationDate: Date /// } /// ``` - static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { get } + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy /// The strategy for converting column names to coding keys. /// @@ -251,7 +253,7 @@ extension FetchableRecord { /// The default strategy for decoding `Date` columns is /// ``DatabaseDateDecodingStrategy/deferredToDate``. - public static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { + public static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { .deferredToDate } @@ -901,7 +903,9 @@ public enum DatabaseDataDecodingStrategy { /// For example: /// /// struct Player: FetchableRecord, Decodable { -/// static let databaseDateDecodingStrategy = DatabaseDateDecodingStrategy.timeIntervalSince1970 +/// static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { +/// .timeIntervalSince1970 +/// } /// /// var name: String /// var registrationDate: Date // decoded from epoch timestamp diff --git a/README.md b/README.md index b2f7791dfb..1d6213e65f 100644 --- a/README.md +++ b/README.md @@ -2701,7 +2701,7 @@ Those behaviors can be overridden: ```swift protocol FetchableRecord { static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy - static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { get } + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy } protocol EncodableRecord { diff --git a/Tests/GRDBTests/DatabaseDateDecodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDateDecodingStrategyTests.swift index 5f920fe736..c813d377fb 100644 --- a/Tests/GRDBTests/DatabaseDateDecodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDateDecodingStrategyTests.swift @@ -47,12 +47,18 @@ private enum StrategyCustom: StrategyProvider { } private struct RecordWithDate: FetchableRecord, Decodable { - static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { Strategy.strategy } + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { + Strategy.strategy + } + var date: Date } private struct RecordWithOptionalDate: FetchableRecord, Decodable { - static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { Strategy.strategy } + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { + Strategy.strategy + } + var date: Date? } diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index 59f6054fbb..f62e864087 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -164,6 +164,49 @@ extension FetchableRecordDecodableTests { } } + func testSingleValueDateProperty() throws { + struct Value : Decodable { + let date: Date + + init(from decoder: Decoder) throws { + date = try decoder.singleValueContainer().decode(Date.self) + } + } + + struct Struct : FetchableRecord, Decodable { + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { + if column == "value" { + return .custom { _ in Date(timeIntervalSince1970: 0) } + } else { + return .deferredToDate + } + } + let value: Value + let optionalValue: Value? + } + + do { + // No null values + let s = try Struct(row: ["value": "foo", "optionalValue": "2001-01-01 00:00:00"]) + XCTAssertEqual(s.value.date, Date(timeIntervalSince1970: 0)) + XCTAssertEqual(s.optionalValue?.date, Date(timeIntervalSinceReferenceDate: 0)) + } + + do { + // Null values + let s = try Struct(row: ["value": "foo", "optionalValue": nil]) + XCTAssertEqual(s.value.date, Date(timeIntervalSince1970: 0)) + XCTAssertNil(s.optionalValue) + } + + do { + // Missing and extra values + let s = try Struct(row: ["value": "foo", "ignored": "?"]) + XCTAssertEqual(s.value.date, Date(timeIntervalSince1970: 0)) + XCTAssertNil(s.optionalValue) + } + } + func testNonTrivialSingleValueDecodableProperty() throws { struct NestedValue : Decodable { let string: String From ad74526f6a816f9ad7649b865fc5830b892cf7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 25 Aug 2024 14:50:42 +0200 Subject: [PATCH 029/160] BusyCallback and BusyMode are Sendable --- GRDB/Core/Database.swift | 4 ++-- TODO.md | 4 ++-- Tests/GRDBTests/DatabaseQueueConcurrencyTests.swift | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 68956eaa1f..8f3eeaffe6 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1831,7 +1831,7 @@ extension Database { // MARK: - Database-Related Types /// See BusyMode and - public typealias BusyCallback = (_ numberOfTries: Int) -> Bool + public typealias BusyCallback = @Sendable (_ numberOfTries: Int) -> Bool /// When there are several connections to a database, a connection may try /// to access the database while it is locked by another connection. @@ -1859,7 +1859,7 @@ extension Database { /// - /// - /// - - public enum BusyMode { + public enum BusyMode: Sendable { /// The `SQLITE_BUSY` error is immediately returned to the connection /// that tries to access the locked database. case immediateError diff --git a/TODO.md b/TODO.md index 06c27e3f09..96f71aa68e 100644 --- a/TODO.md +++ b/TODO.md @@ -92,8 +92,8 @@ - [X] GRDB7/BREAKING: Stop exporting SQLite (679d6463) - [X] GRDB7/BREAKING: Remove Configuration.defaultTransactionKind (2661ff46) - [X] GRDB7: Replace LockedBox with Mutex (00ccab06) -- [ ] GRDB7: Sendable: BusyCallback (e0d8e20b) -- [ ] GRDB7: Sendable: BusyMode (e0d8e20b) +- [X] GRDB7: Sendable: BusyCallback (e0d8e20b) +- [X] GRDB7: Sendable: BusyMode (e0d8e20b) - [ ] GRDB7: Sendable: TransactionClock (f7dc72a5) - [ ] GRDB7: Sendable: Configuration (54ffb21f) - [ ] GRDB7: Sendable: DatabaseDataEncodingStrategy (264d7fb5) diff --git a/Tests/GRDBTests/DatabaseQueueConcurrencyTests.swift b/Tests/GRDBTests/DatabaseQueueConcurrencyTests.swift index a051152887..c525f9ef7b 100644 --- a/Tests/GRDBTests/DatabaseQueueConcurrencyTests.swift +++ b/Tests/GRDBTests/DatabaseQueueConcurrencyTests.swift @@ -244,9 +244,9 @@ class ConcurrencyTests: GRDBTestCase { // BEGIN EXCLUSIVE TRANSACTION // COMMIT - var busyCallbackCalled = false + let busyCallbackCalledMutex = Mutex(false) self.busyCallback = { n in - busyCallbackCalled = true + busyCallbackCalledMutex.store(true) s2.signal() return true } @@ -283,7 +283,7 @@ class ConcurrencyTests: GRDBTestCase { _ = group.wait(timeout: .distantFuture) - XCTAssertTrue(busyCallbackCalled) + XCTAssertTrue(busyCallbackCalledMutex.load()) } func testReaderDuringDefaultTransaction() throws { @@ -384,9 +384,9 @@ class ConcurrencyTests: GRDBTestCase { // COMMIT // COMMIT - var busyCallbackCalled = false + let busyCallbackCalledMutex = Mutex(false) self.busyCallback = { n in - busyCallbackCalled = true + busyCallbackCalledMutex.store(true) s3.signal() return true } @@ -431,6 +431,6 @@ class ConcurrencyTests: GRDBTestCase { _ = group.wait(timeout: .distantFuture) - XCTAssertTrue(busyCallbackCalled) + XCTAssertTrue(busyCallbackCalledMutex.load()) } } From 0457a6262e1e64d91c7297bf41e8afb40c7922c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Mar 2024 16:26:20 +0100 Subject: [PATCH 030/160] TransactionClock is Sendable --- GRDB/Core/TransactionClock.swift | 8 ++-- TODO.md | 2 +- Tests/GRDBTests/TransactionDateTests.swift | 48 +++++++++------------- 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/GRDB/Core/TransactionClock.swift b/GRDB/Core/TransactionClock.swift index d878cd4504..b467ce7240 100644 --- a/GRDB/Core/TransactionClock.swift +++ b/GRDB/Core/TransactionClock.swift @@ -10,7 +10,7 @@ import Foundation /// /// - ``DefaultTransactionClock`` /// - ``CustomTransactionClock`` -public protocol TransactionClock { +public protocol TransactionClock: Sendable { /// Returns the date of the current transaction. /// /// This function is called whenever a transaction starts - precisely @@ -36,7 +36,7 @@ extension TransactionClock where Self == CustomTransactionClock { /// /// It is also called when the ``Database/transactionDate`` property is /// called, and the database connection is not in a transaction. - public static func custom(_ now: @escaping (Database) throws -> Date) -> Self { + public static func custom(_ now: @escaping @Sendable (Database) throws -> Date) -> Self { CustomTransactionClock(now) } } @@ -53,9 +53,9 @@ public struct DefaultTransactionClock: TransactionClock { /// A custom transaction clock. public struct CustomTransactionClock: TransactionClock { - let _now: (Database) throws -> Date + let _now: @Sendable (Database) throws -> Date - public init(_ now: @escaping (Database) throws -> Date) { + public init(_ now: @escaping @Sendable (Database) throws -> Date) { self._now = now } diff --git a/TODO.md b/TODO.md index 96f71aa68e..ebe34c75be 100644 --- a/TODO.md +++ b/TODO.md @@ -94,7 +94,7 @@ - [X] GRDB7: Replace LockedBox with Mutex (00ccab06) - [X] GRDB7: Sendable: BusyCallback (e0d8e20b) - [X] GRDB7: Sendable: BusyMode (e0d8e20b) -- [ ] GRDB7: Sendable: TransactionClock (f7dc72a5) +- [X] GRDB7: Sendable: TransactionClock (f7dc72a5) - [ ] GRDB7: Sendable: Configuration (54ffb21f) - [ ] GRDB7: Sendable: DatabaseDataEncodingStrategy (264d7fb5) - [ ] GRDB7: Sendable: DatabaseDateEncodingStrategy (264d7fb5) diff --git a/Tests/GRDBTests/TransactionDateTests.swift b/Tests/GRDBTests/TransactionDateTests.swift index 5f2707223e..59bec6641f 100644 --- a/Tests/GRDBTests/TransactionDateTests.swift +++ b/Tests/GRDBTests/TransactionDateTests.swift @@ -8,9 +8,9 @@ class TransactionDateTests: GRDBTestCase { Date(), Date.distantFuture, ] - var dateIterator = dates.makeIterator() + let dateIteratorMutex = Mutex(dates.makeIterator()) dbConfiguration.transactionClock = .custom { _ in - dateIterator.next()! + dateIteratorMutex.withLock { $0.next()! } } var collectedDates: [Date] = [] @@ -28,9 +28,9 @@ class TransactionDateTests: GRDBTestCase { Date(), Date.distantFuture, ] - var dateIterator = dates.makeIterator() + let dateIteratorMutex = Mutex(dates.makeIterator()) dbConfiguration.transactionClock = .custom { _ in - dateIterator.next()! + dateIteratorMutex.withLock { $0.next()! } } var collectedDates: [Date] = [] @@ -51,9 +51,9 @@ class TransactionDateTests: GRDBTestCase { Date(), Date.distantFuture, ] - var dateIterator = dates.makeIterator() + let dateIteratorMutex = Mutex(dates.makeIterator()) dbConfiguration.transactionClock = .custom { _ in - dateIterator.next()! + dateIteratorMutex.withLock { $0.next()! } } var collectedDates: [Date] = [] @@ -74,9 +74,9 @@ class TransactionDateTests: GRDBTestCase { Date(), Date.distantFuture, ] - var dateIterator = dates.makeIterator() + let dateIteratorMutex = Mutex(dates.makeIterator()) dbConfiguration.transactionClock = .custom { _ in - dateIterator.next()! + dateIteratorMutex.withLock { $0.next()! } } var collectedDates: [Date] = [] @@ -107,8 +107,7 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + dbConfiguration.transactionClock = .custom { _ in .distantPast } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -119,7 +118,6 @@ class TransactionDateTests: GRDBTestCase { } } - currentDate = Date.distantPast try dbQueue.write { db in do { var player = Player(name: "Arthur") @@ -152,8 +150,8 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + let currentDate = Mutex(Date.distantPast) + dbConfiguration.transactionClock = .custom { _ in currentDate.load() } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -164,14 +162,13 @@ class TransactionDateTests: GRDBTestCase { } } - currentDate = Date.distantPast try dbQueue.write { db in var player = Player(name: "Arthur") try player.insert(db) } let newTransactionDate = Date() - currentDate = newTransactionDate + currentDate.store(newTransactionDate) try dbQueue.write { db in var player = try Player.find(db, key: 1) @@ -198,8 +195,8 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + let currentDate = Mutex(Date.distantPast) + dbConfiguration.transactionClock = .custom { _ in currentDate.load() } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -210,14 +207,13 @@ class TransactionDateTests: GRDBTestCase { } } - currentDate = Date.distantPast try dbQueue.write { db in var player = Player(name: "Arthur") try player.insert(db) } let newTransactionDate = Date() - currentDate = newTransactionDate + currentDate.store(newTransactionDate) try dbQueue.write { db in var player = try Player.find(db, key: 1) @@ -251,8 +247,8 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + let currentDate = Mutex(Date.distantPast) + dbConfiguration.transactionClock = .custom { _ in currentDate.load() } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -267,7 +263,7 @@ class TransactionDateTests: GRDBTestCase { } let newTransactionDate = Date() - currentDate = newTransactionDate + currentDate.store(newTransactionDate) try dbQueue.write { db in var player = try Player.find(db, key: 1) try player.touch(db) @@ -303,8 +299,7 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + dbConfiguration.transactionClock = .custom { _ in .distantPast } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -315,7 +310,6 @@ class TransactionDateTests: GRDBTestCase { } } - currentDate = Date.distantPast try dbQueue.write { db in var player = Player(name: "Arthur", isInserted: false) try player.insert(db) @@ -354,8 +348,7 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + dbConfiguration.transactionClock = .custom { _ in .distantPast } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -366,7 +359,6 @@ class TransactionDateTests: GRDBTestCase { } } - currentDate = Date.distantPast try dbQueue.write { db in let player = Player(name: "Arthur") try player.insert(db) From 8e860e671ba06613cc3b9085219731096506a5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Mar 2024 16:39:51 +0100 Subject: [PATCH 031/160] Configuration is Sendable --- GRDB/Core/Configuration.swift | 28 +++- TODO.md | 2 +- .../DatabaseConfigurationTests.swift | 32 ++--- .../DatabasePoolReleaseMemoryTests.swift | 121 ++++++++---------- .../DatabaseQueueReleaseMemoryTests.swift | 48 +++---- Tests/GRDBTests/DatabaseQueueTests.swift | 2 +- Tests/GRDBTests/DatabaseTraceTests.swift | 12 +- Tests/GRDBTests/FTS4TableBuilderTests.swift | 12 +- Tests/GRDBTests/GRDBTestCase.swift | 2 +- 9 files changed, 125 insertions(+), 134 deletions(-) diff --git a/GRDB/Core/Configuration.swift b/GRDB/Core/Configuration.swift index 17a50eff8e..059bbb86f9 100644 --- a/GRDB/Core/Configuration.swift +++ b/GRDB/Core/Configuration.swift @@ -10,7 +10,7 @@ import SQLite3 import Dispatch import Foundation -public struct Configuration { +public struct Configuration: Sendable { // MARK: - Misc options @@ -195,7 +195,7 @@ public struct Configuration { // MARK: - Managing SQLite Connections - private var setups: [(Database) throws -> Void] = [] + private var setups: [@Sendable (Database) throws -> Void] = [] /// Defines a function to run whenever an SQLite connection is opened. /// @@ -232,7 +232,7 @@ public struct Configuration { /// /// On newly created databases files, ``DatabasePool`` activates the WAL /// mode after the preparation functions have run. - public mutating func prepareDatabase(_ setup: @escaping (Database) throws -> Void) { + public mutating func prepareDatabase(_ setup: @escaping @Sendable (Database) throws -> Void) { setups.append(setup) } @@ -432,9 +432,25 @@ public struct Configuration { /// through a `SerializedDatabase`. var threadingMode = Database.ThreadingMode.default - var SQLiteConnectionDidOpen: (() -> Void)? - var SQLiteConnectionWillClose: ((SQLiteConnection) -> Void)? - var SQLiteConnectionDidClose: (() -> Void)? + private(set) var SQLiteConnectionDidOpen: (@Sendable () -> Void)? + private(set) var SQLiteConnectionWillClose: (@Sendable (SQLiteConnection) -> Void)? + private(set) var SQLiteConnectionDidClose: (@Sendable () -> Void)? + + // Workaround https://github.com/apple/swift/issues/72727 + mutating func onConnectionDidOpen(_ callback: @escaping @Sendable () -> Void) { + SQLiteConnectionDidOpen = callback + } + + // Workaround https://github.com/apple/swift/issues/72727 + mutating func onConnectionWillClose(_ callback: @escaping @Sendable (SQLiteConnection) -> Void) { + SQLiteConnectionWillClose = callback + } + + // Workaround https://github.com/apple/swift/issues/72727 + mutating func onConnectionDidClose(_ callback: @escaping @Sendable () -> Void) { + SQLiteConnectionDidClose = callback + } + var SQLiteOpenFlags: CInt { var flags = readonly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE) if sqlite3_libversion_number() >= 3037000 { diff --git a/TODO.md b/TODO.md index ebe34c75be..42d463d3e4 100644 --- a/TODO.md +++ b/TODO.md @@ -95,7 +95,7 @@ - [X] GRDB7: Sendable: BusyCallback (e0d8e20b) - [X] GRDB7: Sendable: BusyMode (e0d8e20b) - [X] GRDB7: Sendable: TransactionClock (f7dc72a5) -- [ ] GRDB7: Sendable: Configuration (54ffb21f) +- [X] GRDB7: Sendable: Configuration (54ffb21f) - [ ] GRDB7: Sendable: DatabaseDataEncodingStrategy (264d7fb5) - [ ] GRDB7: Sendable: DatabaseDateEncodingStrategy (264d7fb5) - [ ] GRDB7: Sendable: DatabaseColumnEncodingStrategy (264d7fb5) diff --git a/Tests/GRDBTests/DatabaseConfigurationTests.swift b/Tests/GRDBTests/DatabaseConfigurationTests.swift index fccf86e9da..7c64c91e5f 100644 --- a/Tests/GRDBTests/DatabaseConfigurationTests.swift +++ b/Tests/GRDBTests/DatabaseConfigurationTests.swift @@ -15,40 +15,40 @@ class DatabaseConfigurationTests: GRDBTestCase { func testPrepareDatabase() throws { // prepareDatabase is called when connection opens - var connectionCount = 0 + let connectionCountMutex = Mutex(0) var configuration = Configuration() configuration.prepareDatabase { db in - connectionCount += 1 + connectionCountMutex.increment() } _ = try DatabaseQueue(configuration: configuration) - XCTAssertEqual(connectionCount, 1) + XCTAssertEqual(connectionCountMutex.load(), 1) _ = try makeDatabaseQueue(configuration: configuration) - XCTAssertEqual(connectionCount, 2) + XCTAssertEqual(connectionCountMutex.load(), 2) let pool = try makeDatabasePool(configuration: configuration) - XCTAssertEqual(connectionCount, 3) + XCTAssertEqual(connectionCountMutex.load(), 3) try pool.read { _ in } - XCTAssertEqual(connectionCount, 4) + XCTAssertEqual(connectionCountMutex.load(), 4) try pool.makeSnapshot().read { _ in } - XCTAssertEqual(connectionCount, 5) + XCTAssertEqual(connectionCountMutex.load(), 5) #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try pool.makeSnapshotPool().read { _ in } - XCTAssertEqual(connectionCount, 6) + XCTAssertEqual(connectionCountMutex.load(), 6) #endif } func testPrepareDatabaseError() throws { struct TestError: Error { } - var error: TestError? + let errorMutex: Mutex = Mutex(nil) var configuration = Configuration() configuration.prepareDatabase { db in - if let error { + if let error = errorMutex.load() { throw error } } @@ -56,36 +56,36 @@ class DatabaseConfigurationTests: GRDBTestCase { // TODO: what about in-memory DatabaseQueue??? do { - error = TestError() + errorMutex.store(TestError()) _ = try makeDatabaseQueue(configuration: configuration) XCTFail("Expected TestError") } catch is TestError { } do { - error = TestError() + errorMutex.store(TestError()) _ = try makeDatabasePool(configuration: configuration) XCTFail("Expected TestError") } catch is TestError { } do { - error = nil + errorMutex.store(nil) let pool = try makeDatabasePool(configuration: configuration) do { - error = TestError() + errorMutex.store(TestError()) try pool.read { _ in } XCTFail("Expected TestError") } catch is TestError { } do { - error = TestError() + errorMutex.store(TestError()) _ = try pool.makeSnapshot() XCTFail("Expected TestError") } catch is TestError { } #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) do { - error = TestError() + errorMutex.store(TestError()) _ = try pool.makeSnapshotPool() XCTFail("Expected TestError") } catch is TestError { } diff --git a/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift b/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift index 09dfde4057..afa8bd32a5 100644 --- a/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift +++ b/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift @@ -13,21 +13,16 @@ import XCTest class DatabasePoolReleaseMemoryTests: GRDBTestCase { func testDatabasePoolDeinitClosesAllConnections() throws { - let countQueue = DispatchQueue(label: "GRDB") - var openConnectionCount = 0 - var totalOpenConnectionCount = 0 - - dbConfiguration.SQLiteConnectionDidOpen = { - countQueue.sync { - totalOpenConnectionCount += 1 - openConnectionCount += 1 - } + let openConnectionCountMutex = Mutex(0) + let totalOpenConnectionCountMutex = Mutex(0) + + dbConfiguration.onConnectionDidOpen { + totalOpenConnectionCountMutex.increment() + openConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - countQueue.sync { - openConnectionCount -= 1 - } + dbConfiguration.onConnectionDidClose { + openConnectionCountMutex.decrement() } // write & read @@ -44,19 +39,19 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { } // One reader, one writer - XCTAssertEqual(totalOpenConnectionCount, 2) + XCTAssertEqual(totalOpenConnectionCountMutex.load(), 2) // All connections are closed - XCTAssertEqual(openConnectionCount, 0) + XCTAssertEqual(openConnectionCountMutex.load(), 0) } - + #if os(iOS) func testDatabasePoolReleasesMemoryOnPressureEvent() throws { // Create a database pool, and expect a reader connection to be closed let expectation = self.expectation(description: "Reader connection closed") var configuration = Configuration() - configuration.SQLiteConnectionWillClose = { conn in + configuration.onConnectionWillClose { conn in if sqlite3_db_readonly(conn, nil) != 0 { expectation.fulfill() } @@ -76,7 +71,7 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { waitForExpectations(timeout: 0.5) } } - + func testDatabasePoolDoesNotReleaseMemoryOnPressureEventIfDisabled() throws { // Create a database pool, and do not expect any reader connection to be closed let expectation = self.expectation(description: "Reader connection closed") @@ -84,7 +79,7 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { var configuration = Configuration() configuration.automaticMemoryManagement = false - configuration.SQLiteConnectionWillClose = { conn in + configuration.onConnectionWillClose { conn in if sqlite3_db_readonly(conn, nil) != 0 { expectation.fulfill() } @@ -126,26 +121,21 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { // Cleanup semaphore.signal() } - + #endif - + func test_DatabasePool_releaseMemory_closes_reader_connections() throws { // A complicated test setup that opens multiple reader connections. - let countQueue = DispatchQueue(label: "GRDB") - var openConnectionCount = 0 - var totalOpenConnectionCount = 0 - - dbConfiguration.SQLiteConnectionDidOpen = { - countQueue.sync { - totalOpenConnectionCount += 1 - openConnectionCount += 1 - } + let openConnectionCountMutex = Mutex(0) + let totalOpenConnectionCountMutex = Mutex(0) + + dbConfiguration.onConnectionDidOpen { + totalOpenConnectionCountMutex.increment() + openConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - countQueue.sync { - openConnectionCount -= 1 - } + dbConfiguration.onConnectionDidClose { + openConnectionCountMutex.decrement() } let dbPool = try makeDatabasePool() @@ -201,74 +191,69 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { } // Two readers, one writer - XCTAssertEqual(totalOpenConnectionCount, 3) + XCTAssertEqual(totalOpenConnectionCountMutex.load(), 3) // Writer is still open - XCTAssertEqual(openConnectionCount, 1) + XCTAssertEqual(openConnectionCountMutex.load(), 1) } func test_DatabasePool_releaseMemory_closes_reader_connections_when_persistentReadOnlyConnections_is_false() throws { - var persistentConnectionCount = 0 + let persistentConnectionCountMutex = Mutex(0) - dbConfiguration.SQLiteConnectionDidOpen = { - persistentConnectionCount += 1 + dbConfiguration.onConnectionDidOpen { + persistentConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - persistentConnectionCount -= 1 + dbConfiguration.onConnectionDidClose { + persistentConnectionCountMutex.decrement() } dbConfiguration.persistentReadOnlyConnections = false let dbPool = try makeDatabasePool() - XCTAssertEqual(persistentConnectionCount, 1) // writer + XCTAssertEqual(persistentConnectionCountMutex.load(), 1) // writer try dbPool.read { _ in } - XCTAssertEqual(persistentConnectionCount, 2) // writer + reader + XCTAssertEqual(persistentConnectionCountMutex.load(), 2) // writer + reader dbPool.releaseMemory() - XCTAssertEqual(persistentConnectionCount, 1) // writer + XCTAssertEqual(persistentConnectionCountMutex.load(), 1) // writer } func test_DatabasePool_releaseMemory_does_not_close_reader_connections_when_persistentReadOnlyConnections_is_true() throws { - var persistentConnectionCount = 0 + let persistentConnectionCountMutex = Mutex(0) - dbConfiguration.SQLiteConnectionDidOpen = { - persistentConnectionCount += 1 + dbConfiguration.onConnectionDidOpen { + persistentConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - persistentConnectionCount -= 1 + dbConfiguration.onConnectionDidClose { + persistentConnectionCountMutex.decrement() } dbConfiguration.persistentReadOnlyConnections = true let dbPool = try makeDatabasePool() - XCTAssertEqual(persistentConnectionCount, 1) // writer + XCTAssertEqual(persistentConnectionCountMutex.load(), 1) // writer try dbPool.read { _ in } - XCTAssertEqual(persistentConnectionCount, 2) // writer + reader + XCTAssertEqual(persistentConnectionCountMutex.load(), 2) // writer + reader dbPool.releaseMemory() - XCTAssertEqual(persistentConnectionCount, 2) // writer + reader + XCTAssertEqual(persistentConnectionCountMutex.load(), 2) // writer + reader } - + func testBlocksRetainConnection() throws { - let countQueue = DispatchQueue(label: "GRDB") - var openConnectionCount = 0 - var totalOpenConnectionCount = 0 - - dbConfiguration.SQLiteConnectionDidOpen = { - countQueue.sync { - totalOpenConnectionCount += 1 - openConnectionCount += 1 - } + let openConnectionCountMutex = Mutex(0) + let totalOpenConnectionCountMutex = Mutex(0) + + dbConfiguration.onConnectionDidOpen { + totalOpenConnectionCountMutex.increment() + openConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - countQueue.sync { - openConnectionCount -= 1 - } + dbConfiguration.onConnectionDidClose { + openConnectionCountMutex.decrement() } // Block 1 Block 2 @@ -311,10 +296,10 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { } // one writer, one reader - XCTAssertEqual(totalOpenConnectionCount, 2) + XCTAssertEqual(totalOpenConnectionCountMutex.load(), 2) // All connections are closed - XCTAssertEqual(openConnectionCount, 0) + XCTAssertEqual(openConnectionCountMutex.load(), 0) } func testStatementDoNotRetainDatabaseConnection() throws { diff --git a/Tests/GRDBTests/DatabaseQueueReleaseMemoryTests.swift b/Tests/GRDBTests/DatabaseQueueReleaseMemoryTests.swift index 7c5b9428eb..5a828b9e84 100644 --- a/Tests/GRDBTests/DatabaseQueueReleaseMemoryTests.swift +++ b/Tests/GRDBTests/DatabaseQueueReleaseMemoryTests.swift @@ -4,21 +4,16 @@ import XCTest class DatabaseQueueReleaseMemoryTests: GRDBTestCase { func testDatabaseQueueDeinitClosesConnection() throws { - let countQueue = DispatchQueue(label: "GRDB") - var openConnectionCount = 0 - var totalOpenConnectionCount = 0 + let openConnectionCountMutex = Mutex(0) + let totalOpenConnectionCountMutex = Mutex(0) - dbConfiguration.SQLiteConnectionDidOpen = { - countQueue.sync { - totalOpenConnectionCount += 1 - openConnectionCount += 1 - } + dbConfiguration.onConnectionDidOpen { + totalOpenConnectionCountMutex.increment() + openConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - countQueue.sync { - openConnectionCount -= 1 - } + dbConfiguration.onConnectionDidClose { + openConnectionCountMutex.decrement() } do { @@ -27,28 +22,23 @@ class DatabaseQueueReleaseMemoryTests: GRDBTestCase { } // One reader, one writer - XCTAssertEqual(totalOpenConnectionCount, 1) + XCTAssertEqual(totalOpenConnectionCountMutex.load(), 1) // All connections are closed - XCTAssertEqual(openConnectionCount, 0) + XCTAssertEqual(openConnectionCountMutex.load(), 0) } - + func testBlocksRetainConnection() throws { - let countQueue = DispatchQueue(label: "GRDB") - var openConnectionCount = 0 - var totalOpenConnectionCount = 0 + let openConnectionCountMutex = Mutex(0) + let totalOpenConnectionCountMutex = Mutex(0) - dbConfiguration.SQLiteConnectionDidOpen = { - countQueue.sync { - totalOpenConnectionCount += 1 - openConnectionCount += 1 - } + dbConfiguration.onConnectionDidOpen { + totalOpenConnectionCountMutex.increment() + openConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - countQueue.sync { - openConnectionCount -= 1 - } + dbConfiguration.onConnectionDidClose { + openConnectionCountMutex.decrement() } // Block 1 Block 2 @@ -91,10 +81,10 @@ class DatabaseQueueReleaseMemoryTests: GRDBTestCase { } // one writer - XCTAssertEqual(totalOpenConnectionCount, 1) + XCTAssertEqual(totalOpenConnectionCountMutex.load(), 1) // All connections are closed - XCTAssertEqual(openConnectionCount, 0) + XCTAssertEqual(openConnectionCountMutex.load(), 0) } func testStatementDoNotRetainDatabaseConnection() throws { diff --git a/Tests/GRDBTests/DatabaseQueueTests.swift b/Tests/GRDBTests/DatabaseQueueTests.swift index 66f4cd5554..bf0bb5371f 100644 --- a/Tests/GRDBTests/DatabaseQueueTests.swift +++ b/Tests/GRDBTests/DatabaseQueueTests.swift @@ -378,7 +378,7 @@ class DatabaseQueueTests: GRDBTestCase { } let parallelWritesCount = 50 - DispatchQueue.concurrentPerform(iterations: parallelWritesCount) { index in + DispatchQueue.concurrentPerform(iterations: parallelWritesCount) { [configuration] index in let dbQueue = try! makeDatabaseQueue(filename: "test", configuration: configuration) try! dbQueue.write { db in _ = try Table("test").fetchCount(db) diff --git a/Tests/GRDBTests/DatabaseTraceTests.swift b/Tests/GRDBTests/DatabaseTraceTests.swift index df9d73119e..8ad6eedc76 100644 --- a/Tests/GRDBTests/DatabaseTraceTests.swift +++ b/Tests/GRDBTests/DatabaseTraceTests.swift @@ -114,11 +114,11 @@ class DatabaseTraceTests : GRDBTestCase { } func testTraceFromConfigurationWithDefaultOptions() throws { - var events: [String] = [] + let eventsMutex: Mutex<[String]> = Mutex([]) var configuration = Configuration() configuration.prepareDatabase { db in db.trace { event in - events.append("SQL: \(event)") + eventsMutex.withLock { $0.append("SQL: \(event)") } } } let dbQueue = try makeDatabaseQueue(configuration: configuration) @@ -127,19 +127,19 @@ class DatabaseTraceTests : GRDBTestCase { CREATE table t(a); INSERT INTO t (a) VALUES (?) """, arguments: [1]) - XCTAssertEqual(events.suffix(2), [ + XCTAssertEqual(eventsMutex.load().suffix(2), [ "SQL: CREATE table t(a)", "SQL: INSERT INTO t (a) VALUES (?)"]) } } func testTraceFromConfigurationWithPublicStatementArguments() throws { - var events: [String] = [] + let eventsMutex: Mutex<[String]> = Mutex([]) var configuration = Configuration() configuration.publicStatementArguments = true configuration.prepareDatabase { db in db.trace { event in - events.append("SQL: \(event)") + eventsMutex.withLock { $0.append("SQL: \(event)") } } } let dbQueue = try makeDatabaseQueue(configuration: configuration) @@ -148,7 +148,7 @@ class DatabaseTraceTests : GRDBTestCase { CREATE table t(a); INSERT INTO t (a) VALUES (?) """, arguments: [1]) - XCTAssertEqual(events.suffix(2), [ + XCTAssertEqual(eventsMutex.load().suffix(2), [ "SQL: CREATE table t(a)", "SQL: INSERT INTO t (a) VALUES (1)"]) } diff --git a/Tests/GRDBTests/FTS4TableBuilderTests.swift b/Tests/GRDBTests/FTS4TableBuilderTests.swift index ad7287b4d2..20b19384c1 100644 --- a/Tests/GRDBTests/FTS4TableBuilderTests.swift +++ b/Tests/GRDBTests/FTS4TableBuilderTests.swift @@ -272,16 +272,16 @@ class FTS4TableBuilderTests: GRDBTestCase { func testFTS4Compression() throws { // Based on https://github.com/groue/GRDB.swift/issues/369 - var compressCalled = false - var uncompressCalled = false + let compressCalledMutex = Mutex(false) + let uncompressCalledMutex = Mutex(false) dbConfiguration.prepareDatabase { db in db.add(function: DatabaseFunction("zipit", argumentCount: 1, pure: true, function: { dbValues in - compressCalled = true + compressCalledMutex.store(true) return dbValues[0] })) db.add(function: DatabaseFunction("unzipit", argumentCount: 1, pure: true, function: { dbValues in - uncompressCalled = true + uncompressCalledMutex.store(true) return dbValues[0] })) } @@ -296,12 +296,12 @@ class FTS4TableBuilderTests: GRDBTestCase { assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts4(content, compress=\"zipit\", uncompress=\"unzipit\")") try db.execute(sql: "INSERT INTO documents (content) VALUES (?)", arguments: ["abc"]) - XCTAssertTrue(compressCalled) + XCTAssertTrue(compressCalledMutex.load()) } try dbPool.read { db in _ = try Row.fetchOne(db, sql: "SELECT * FROM documents") - XCTAssertTrue(uncompressCalled) + XCTAssertTrue(uncompressCalledMutex.load()) } } } diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index d1f52ac492..7fe7ac6b3d 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -84,7 +84,7 @@ class GRDBTestCase: XCTestCase { dbConfiguration = Configuration() // Test that database are deallocated in a clean state - dbConfiguration.SQLiteConnectionWillClose = { sqliteConnection in + dbConfiguration.onConnectionWillClose { sqliteConnection in // https://www.sqlite.org/capi3ref.html#sqlite3_close: // > If sqlite3_close_v2() is called on a database connection that still // > has outstanding prepared statements, BLOB handles, and/or From 876a0870ee62ff22da06f8a3391e1ce2b387a4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Mar 2024 16:44:14 +0100 Subject: [PATCH 032/160] Coding strategies are Sendable --- GRDB/Record/EncodableRecord.swift | 12 ++++++------ GRDB/Record/FetchableRecord.swift | 12 ++++++------ TODO.md | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/GRDB/Record/EncodableRecord.swift b/GRDB/Record/EncodableRecord.swift index fb71cce645..9e8e80e2c2 100644 --- a/GRDB/Record/EncodableRecord.swift +++ b/GRDB/Record/EncodableRecord.swift @@ -474,7 +474,7 @@ extension Row { /// var jsonData: Data /// } /// ``` -public enum DatabaseDataEncodingStrategy { +public enum DatabaseDataEncodingStrategy: Sendable { /// Encodes `Data` columns as SQL blob. case deferredToData @@ -482,7 +482,7 @@ public enum DatabaseDataEncodingStrategy { case text /// Encodes `Data` column as the result of the user-provided function. - case custom((Data) -> (any DatabaseValueConvertible)?) + case custom(@Sendable (Data) -> (any DatabaseValueConvertible)?) func encode(_ data: Data) -> DatabaseValue { switch self { @@ -518,7 +518,7 @@ public enum DatabaseDataEncodingStrategy { /// var creationDate: Date /// } /// ``` -public enum DatabaseDateEncodingStrategy { +public enum DatabaseDateEncodingStrategy: Sendable { /// The strategy that uses formatting from the Date structure. /// /// It encodes dates using the format "YYYY-MM-DD HH:MM:SS.SSS" in the @@ -548,7 +548,7 @@ public enum DatabaseDateEncodingStrategy { case formatted(DateFormatter) /// Encodes the result of the user-provided function - case custom((Date) -> (any DatabaseValueConvertible)?) + case custom(@Sendable (Date) -> (any DatabaseValueConvertible)?) private static let iso8601Formatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() @@ -638,7 +638,7 @@ public enum DatabaseUUIDEncodingStrategy: Sendable { /// var playerID: String /// } /// ``` -public enum DatabaseColumnEncodingStrategy { +public enum DatabaseColumnEncodingStrategy: Sendable { /// A key encoding strategy that doesn’t change key names during encoding. case useDefaultKeys @@ -646,7 +646,7 @@ public enum DatabaseColumnEncodingStrategy { case convertToSnakeCase /// A key encoding strategy defined by the closure you supply. - case custom((any CodingKey) -> String) + case custom(@Sendable (any CodingKey) -> String) func column(forKey key: some CodingKey) -> String { switch self { diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index bf1c92fbde..cde368dce7 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -882,7 +882,7 @@ extension RecordCursor: Sendable { } /// var myData: Data /// } /// ``` -public enum DatabaseDataDecodingStrategy { +public enum DatabaseDataDecodingStrategy: Sendable { /// Decodes `Data` columns from SQL blobs and UTF8 text. case deferredToData @@ -891,7 +891,7 @@ public enum DatabaseDataDecodingStrategy { /// If the database value does not contain a suitable value, the function /// must return nil (GRDB will interpret this nil result as a conversion /// error, and react accordingly). - case custom((DatabaseValue) -> Data?) + case custom(@Sendable (DatabaseValue) -> Data?) } // MARK: - DatabaseDateDecodingStrategy @@ -910,7 +910,7 @@ public enum DatabaseDataDecodingStrategy { /// var name: String /// var registrationDate: Date // decoded from epoch timestamp /// } -public enum DatabaseDateDecodingStrategy { +public enum DatabaseDateDecodingStrategy: Sendable { /// The strategy that uses formatting from the Date structure. /// /// It decodes numeric values as a number of seconds since Epoch @@ -951,7 +951,7 @@ public enum DatabaseDateDecodingStrategy { /// If the database value does not contain a suitable value, the function /// must return nil (GRDB will interpret this nil result as a conversion /// error, and react accordingly). - case custom((DatabaseValue) -> Date?) + case custom(@Sendable (DatabaseValue) -> Date?) } // MARK: - DatabaseColumnDecodingStrategy @@ -968,7 +968,7 @@ public enum DatabaseDateDecodingStrategy { /// // Decoded from the player_id column /// var playerID: Int /// } -public enum DatabaseColumnDecodingStrategy { +public enum DatabaseColumnDecodingStrategy: Sendable { /// A key decoding strategy that doesn’t change key names during decoding. case useDefaultKeys @@ -976,7 +976,7 @@ public enum DatabaseColumnDecodingStrategy { case convertFromSnakeCase /// A key decoding strategy defined by the closure you supply. - case custom((String) -> CodingKey) + case custom(@Sendable (String) -> CodingKey) func key(forColumn column: String) -> K? { switch self { diff --git a/TODO.md b/TODO.md index 42d463d3e4..4886d14111 100644 --- a/TODO.md +++ b/TODO.md @@ -96,12 +96,12 @@ - [X] GRDB7: Sendable: BusyMode (e0d8e20b) - [X] GRDB7: Sendable: TransactionClock (f7dc72a5) - [X] GRDB7: Sendable: Configuration (54ffb21f) -- [ ] GRDB7: Sendable: DatabaseDataEncodingStrategy (264d7fb5) -- [ ] GRDB7: Sendable: DatabaseDateEncodingStrategy (264d7fb5) -- [ ] GRDB7: Sendable: DatabaseColumnEncodingStrategy (264d7fb5) -- [ ] GRDB7: Sendable: DatabaseDataDecodingStrategy (264d7fb5) -- [ ] GRDB7: Sendable: DatabaseDateDecodingStrategy (264d7fb5) -- [ ] GRDB7: Sendable: DatabaseColumnDecodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseDataEncodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseDateEncodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseColumnEncodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseDataDecodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseDateDecodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseColumnDecodingStrategy (264d7fb5) - [X] GRDB7/BREAKING: Remove DatabaseFuture and concurrentRead (05f7d3c8) - [ ] GRDB7: Sendable: DatabaseFunction (6e691fe7) - [ ] GRDB7: Sendable: DatabaseMigrator (22114ad4) From b752229c375d7b651b6b8d6fc9f49bd68df357b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 10 Feb 2024 19:48:21 +0100 Subject: [PATCH 033/160] DatabaseFunction is Sendable --- GRDB/Core/DatabaseFunction.swift | 12 ++++++------ TODO.md | 2 +- Tests/GRDBTests/DatabaseFunctionTests.swift | 6 +++--- Tests/GRDBTests/SelectStatementTests.swift | 10 +++++----- Tests/GRDBTests/UpdateStatementTests.swift | 7 +++---- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/GRDB/Core/DatabaseFunction.swift b/GRDB/Core/DatabaseFunction.swift index 2fb1301f5f..d7f8354013 100644 --- a/GRDB/Core/DatabaseFunction.swift +++ b/GRDB/Core/DatabaseFunction.swift @@ -29,7 +29,7 @@ import SQLite3 /// - ``localizedUppercase`` /// - ``lowercase`` /// - ``uppercase`` -public final class DatabaseFunction: Hashable { +public final class DatabaseFunction: Hashable, Sendable { // SQLite identifies functions by (name + argument count) private struct Identity: Hashable { let name: String @@ -82,11 +82,11 @@ public final class DatabaseFunction: Hashable { _ name: String, argumentCount: Int? = nil, pure: Bool = false, - function: @escaping ([DatabaseValue]) throws -> (any DatabaseValueConvertible)?) + function: @escaping @Sendable ([DatabaseValue]) throws -> (any DatabaseValueConvertible)?) { self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1) self.isPure = pure - self.kind = .function{ (argc, argv) in + self.kind = .function { (argc, argv) in let arguments = (0.. - private enum Kind { + private enum Kind: Sendable { /// A regular function: SELECT f(1) - case function((CInt, UnsafeMutablePointer?) throws -> (any DatabaseValueConvertible)?) + case function(@Sendable (CInt, UnsafeMutablePointer?) throws -> (any DatabaseValueConvertible)?) /// An aggregate: SELECT f(foo) FROM bar GROUP BY baz - case aggregate(() -> any DatabaseAggregate) + case aggregate(@Sendable () -> any DatabaseAggregate) /// Feeds the `pApp` parameter of sqlite3_create_function_v2 /// diff --git a/TODO.md b/TODO.md index 4886d14111..ef373f309a 100644 --- a/TODO.md +++ b/TODO.md @@ -103,7 +103,7 @@ - [X] GRDB7: Sendable: DatabaseDateDecodingStrategy (264d7fb5) - [X] GRDB7: Sendable: DatabaseColumnDecodingStrategy (264d7fb5) - [X] GRDB7/BREAKING: Remove DatabaseFuture and concurrentRead (05f7d3c8) -- [ ] GRDB7: Sendable: DatabaseFunction (6e691fe7) +- [X] GRDB7: Sendable: DatabaseFunction (6e691fe7) - [ ] GRDB7: Sendable: DatabaseMigrator (22114ad4) - [ ] GRDB7: Not Sendable: FilterCursor (b26e9709) - [ ] GRDB7: Sendable: RowAdapter (d138af26) diff --git a/Tests/GRDBTests/DatabaseFunctionTests.swift b/Tests/GRDBTests/DatabaseFunctionTests.swift index 8d252c6a83..4c670bbd8a 100644 --- a/Tests/GRDBTests/DatabaseFunctionTests.swift +++ b/Tests/GRDBTests/DatabaseFunctionTests.swift @@ -347,13 +347,13 @@ class DatabaseFunctionTests: GRDBTestCase { func testFunctionsAreClosures() throws { let dbQueue = try makeDatabaseQueue() - var x = 123 + let mutex = Mutex(123) let fn = DatabaseFunction("f", argumentCount: 0) { dbValues in - return x + return mutex.load() } try dbQueue.inDatabase { db in db.add(function: fn) - x = 321 + mutex.store(321) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT f()")!, 321) } } diff --git a/Tests/GRDBTests/SelectStatementTests.swift b/Tests/GRDBTests/SelectStatementTests.swift index b3263d651f..95d8d39c49 100644 --- a/Tests/GRDBTests/SelectStatementTests.swift +++ b/Tests/GRDBTests/SelectStatementTests.swift @@ -139,20 +139,20 @@ class SelectStatementTests : GRDBTestCase { func testCachedSelectStatementStepFailure() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - var needsThrow = false + let needsThrowMutex = Mutex(false) db.add(function: DatabaseFunction("bomb", argumentCount: 0, pure: false) { _ in - if needsThrow { + if needsThrowMutex.load() { throw DatabaseError(message: "boom") } return "success" }) let sql = "SELECT bomb()" - needsThrow = false + needsThrowMutex.store(false) XCTAssertEqual(try String.fetchAll(db.cachedStatement(sql: sql)), ["success"]) do { - needsThrow = true + needsThrowMutex.store(true) _ = try String.fetchAll(db.cachedStatement(sql: sql)) XCTFail() } catch let error as DatabaseError { @@ -162,7 +162,7 @@ class SelectStatementTests : GRDBTestCase { XCTAssertEqual(error.description, "SQLite error 1: boom - while executing `\(sql)`") } - needsThrow = false + needsThrowMutex.store(false) XCTAssertEqual(try String.fetchAll(db.cachedStatement(sql: sql)), ["success"]) } } diff --git a/Tests/GRDBTests/UpdateStatementTests.swift b/Tests/GRDBTests/UpdateStatementTests.swift index 64b9c23c38..6a3233fdc8 100644 --- a/Tests/GRDBTests/UpdateStatementTests.swift +++ b/Tests/GRDBTests/UpdateStatementTests.swift @@ -178,17 +178,16 @@ class UpdateStatementTests : GRDBTestCase { func testUpdateStatementAcceptsSelectQueriesAndConsumeAllRows() throws { let dbQueue = try makeDatabaseQueue() - var index = 0 + let indexMutex = Mutex(0) try dbQueue.inDatabase { db in db.add(function: DatabaseFunction("seq", argumentCount: 0, pure: false) { _ in - defer { index += 1 } - return index + indexMutex.increment() }) try db.execute(sql: "SELECT seq() UNION ALL SELECT seq() UNION ALL SELECT seq()") let statement = try db.makeStatement(sql: "SELECT seq() UNION ALL SELECT seq() UNION ALL SELECT seq()") try statement.execute() } - XCTAssertEqual(index, 3 + 3) + XCTAssertEqual(indexMutex.load(), 3 + 3) } func testExecuteNothing() throws { From 300e5fec6b0364dbe660d27c54a235bb696209b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 25 Aug 2024 15:39:57 +0200 Subject: [PATCH 034/160] DatabaseFunction is Identifiable --- GRDB/Core/DatabaseFunction.swift | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/GRDB/Core/DatabaseFunction.swift b/GRDB/Core/DatabaseFunction.swift index d7f8354013..3731db9b49 100644 --- a/GRDB/Core/DatabaseFunction.swift +++ b/GRDB/Core/DatabaseFunction.swift @@ -29,16 +29,20 @@ import SQLite3 /// - ``localizedUppercase`` /// - ``lowercase`` /// - ``uppercase`` -public final class DatabaseFunction: Hashable, Sendable { - // SQLite identifies functions by (name + argument count) - private struct Identity: Hashable { +public final class DatabaseFunction: Hashable, Identifiable, Sendable { + /// The identifier of an SQLite function. + /// + /// SQLite identifies functions by their name and argument count. + public struct ID: Hashable, Sendable { let name: String let nArg: CInt // -1 for variadic functions } - /// The name of the SQL function - public var name: String { identity.name } - private let identity: Identity + /// The name of the SQL function. + public var name: String { id.name } + + /// The identifier of the SQL function. + public let id: ID let isPure: Bool private let kind: Kind private var eTextRep: CInt { (SQLITE_UTF8 | (isPure ? SQLITE_DETERMINISTIC : 0)) } @@ -84,7 +88,7 @@ public final class DatabaseFunction: Hashable, Sendable { pure: Bool = false, function: @escaping @Sendable ([DatabaseValue]) throws -> (any DatabaseValueConvertible)?) { - self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1) + self.id = ID(name: name, nArg: argumentCount.map(CInt.init) ?? -1) self.isPure = pure self.kind = .function { (argc, argv) in let arguments = (0.. Bool { - lhs.identity == rhs.identity + lhs.id == rhs.id } } From a5806a002045dbb39e8596d426e6295df8f11780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 25 Aug 2024 15:28:17 +0200 Subject: [PATCH 035/160] [BREAKING] DatabaseFunction is no longer Hashable --- GRDB/Core/Database.swift | 6 +++--- GRDB/Core/DatabaseFunction.swift | 13 +------------ 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 8f3eeaffe6..1a18d265ad 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -361,7 +361,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib private var trace: ((TraceEvent) -> Void)? /// The registered custom SQL functions. - private var functions = Set() + private var functions: [DatabaseFunction.ID: DatabaseFunction] = [:] /// The registered custom SQL collations. private var collations = Set() @@ -707,13 +707,13 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// let dbPool = try DatabasePool(path: ..., configuration: config) /// ``` public func add(function: DatabaseFunction) { - functions.update(with: function) + functions[function.id] = function function.install(in: self) } /// Removes a custom SQL function. public func remove(function: DatabaseFunction) { - functions.remove(function) + functions.removeValue(forKey: function.id) function.uninstall(in: self) } diff --git a/GRDB/Core/DatabaseFunction.swift b/GRDB/Core/DatabaseFunction.swift index 3731db9b49..9e2c03ed2f 100644 --- a/GRDB/Core/DatabaseFunction.swift +++ b/GRDB/Core/DatabaseFunction.swift @@ -29,7 +29,7 @@ import SQLite3 /// - ``localizedUppercase`` /// - ``lowercase`` /// - ``uppercase`` -public final class DatabaseFunction: Hashable, Identifiable, Sendable { +public final class DatabaseFunction: Identifiable, Sendable { /// The identifier of an SQLite function. /// /// SQLite identifies functions by their name and argument count. @@ -428,17 +428,6 @@ public final class DatabaseFunction: Hashable, Identifiable, Sendable { } } -extension DatabaseFunction { - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - - /// Two functions are equal if they share the same name and arity. - public static func == (lhs: DatabaseFunction, rhs: DatabaseFunction) -> Bool { - lhs.id == rhs.id - } -} - /// The protocol for custom SQLite aggregates. /// /// For example: From 405312f52e3bbb6ba0f7c5a604309fa840162b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 10 Feb 2024 19:49:07 +0100 Subject: [PATCH 036/160] DatabaseCollation is Sendable --- GRDB/Core/DatabaseCollation.swift | 6 +++--- TODO.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GRDB/Core/DatabaseCollation.swift b/GRDB/Core/DatabaseCollation.swift index 38c0dba681..19ae4baed1 100644 --- a/GRDB/Core/DatabaseCollation.swift +++ b/GRDB/Core/DatabaseCollation.swift @@ -29,10 +29,10 @@ import Foundation /// - ``localizedCompare`` /// - ``localizedStandardCompare`` /// - ``unicodeCompare`` -public final class DatabaseCollation { +public final class DatabaseCollation: Sendable { /// The name of the collation. public let name: String - let function: (CInt, UnsafeRawPointer?, CInt, UnsafeRawPointer?) -> ComparisonResult + let function: @Sendable (CInt, UnsafeRawPointer?, CInt, UnsafeRawPointer?) -> ComparisonResult /// Creates a collation. /// @@ -49,7 +49,7 @@ public final class DatabaseCollation { /// - parameters: /// - name: The collation name. /// - function: A function that compares two strings. - public init(_ name: String, function: @escaping (String, String) -> ComparisonResult) { + public init(_ name: String, function: @escaping @Sendable (String, String) -> ComparisonResult) { self.name = name self.function = { (length1, buffer1, length2, buffer2) in // Buffers are not C strings: they do not end with \0. diff --git a/TODO.md b/TODO.md index ef373f309a..31cae197ed 100644 --- a/TODO.md +++ b/TODO.md @@ -108,7 +108,7 @@ - [ ] GRDB7: Not Sendable: FilterCursor (b26e9709) - [ ] GRDB7: Sendable: RowAdapter (d138af26) - [ ] GRDB7: Sendable: ValueObservationScheduler (8429eb68) -- [ ] GRDB7: Sendable: DatabaseCollation (4d9d67dd) +- [X] GRDB7: Sendable: DatabaseCollation (4d9d67dd) - [ ] GRDB7: Sendable: LogErrorFunction (f362518d) - [ ] GRDB7: Sendable: ReadWriteBox (57a86a0e) - [ ] GRDB7: Sendable: Pool (f13b2d2e) From eedd165e5ba57c87345f0f349703ec7cfe5a4e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 25 Aug 2024 15:34:35 +0200 Subject: [PATCH 037/160] DatabaseCollation is Identifiable --- GRDB/Core/DatabaseCollation.swift | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/GRDB/Core/DatabaseCollation.swift b/GRDB/Core/DatabaseCollation.swift index 19ae4baed1..792531da92 100644 --- a/GRDB/Core/DatabaseCollation.swift +++ b/GRDB/Core/DatabaseCollation.swift @@ -29,7 +29,33 @@ import Foundation /// - ``localizedCompare`` /// - ``localizedStandardCompare`` /// - ``unicodeCompare`` -public final class DatabaseCollation: Sendable { +public final class DatabaseCollation: Identifiable, Sendable { + /// The identifier of an SQLite collation. + /// + /// SQLite identifies collations by their name (case insensitive). + public struct ID: Hashable { + var name: String + + // Collation equality is based on the sqlite3_strnicmp SQLite function. + // (see https://www.sqlite.org/c3ref/create_collation.html). Computing + // a hash value that honors the Swift Hashable contract (value equality + // implies hash equality) is thus non trivial. But it's not that + // important, since this hashValue is only used when one adds + // or removes a collation from a database connection. + public func hash(into hasher: inout Hasher) { + hasher.combine(0) + } + + /// Two collations are equal if they share the same name (case insensitive) + public static func == (lhs: Self, rhs: Self) -> Bool { + // See + return sqlite3_stricmp(lhs.name, rhs.name) == 0 + } + } + + /// The identifier of the collation. + public var id: ID { ID(name: name) } + /// The name of the collation. public let name: String let function: @Sendable (CInt, UnsafeRawPointer?, CInt, UnsafeRawPointer?) -> ComparisonResult From c1f59dfa2a5ad995a3b43fa6e232a0b9c93fe38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 25 Aug 2024 15:36:09 +0200 Subject: [PATCH 038/160] [BREAKING] DatabaseCollation is no longer Hashable --- GRDB/Core/Database.swift | 6 +++--- GRDB/Core/DatabaseCollation.swift | 18 ------------------ 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 1a18d265ad..dec9d1273b 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -364,7 +364,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib private var functions: [DatabaseFunction.ID: DatabaseFunction] = [:] /// The registered custom SQL collations. - private var collations = Set() + private var collations: [DatabaseCollation.ID: DatabaseCollation] = [:] /// Support for `beginReadOnly()` and `endReadOnly()`. private var readOnlyDepth = 0 @@ -734,7 +734,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// let dbPool = try DatabasePool(path: ..., configuration: config) /// ``` public func add(collation: DatabaseCollation) { - collations.update(with: collation) + collations[collation.id] = collation let collationPointer = Unmanaged.passUnretained(collation).toOpaque() let code = sqlite3_create_collation_v2( sqliteConnection, @@ -753,7 +753,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// Removes a collation. public func remove(collation: DatabaseCollation) { - collations.remove(collation) + collations.removeValue(forKey: collation.id) sqlite3_create_collation_v2( sqliteConnection, collation.name, diff --git a/GRDB/Core/DatabaseCollation.swift b/GRDB/Core/DatabaseCollation.swift index 792531da92..6748874c8b 100644 --- a/GRDB/Core/DatabaseCollation.swift +++ b/GRDB/Core/DatabaseCollation.swift @@ -93,21 +93,3 @@ public final class DatabaseCollation: Identifiable, Sendable { } } } - -extension DatabaseCollation: Hashable { - // Collation equality is based on the sqlite3_strnicmp SQLite function. - // (see https://www.sqlite.org/c3ref/create_collation.html). Computing - // a hash value that honors the Swift Hashable contract (value equality - // implies hash equality) is thus non trivial. But it's not that - // important, since this hashValue is only used when one adds - // or removes a collation from a database connection. - public func hash(into hasher: inout Hasher) { - hasher.combine(0) - } - - /// Two collations are equal if they share the same name (case insensitive) - public static func == (lhs: DatabaseCollation, rhs: DatabaseCollation) -> Bool { - // See - return sqlite3_stricmp(lhs.name, rhs.name) == 0 - } -} From 55cdb4e9a975f603e2542a155e1d7864228de662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 11 Feb 2024 14:01:57 +0100 Subject: [PATCH 039/160] DatabaseMigrator is Sendable --- GRDB/Migration/DatabaseMigrator.swift | 4 ++-- GRDB/Migration/Migration.swift | 4 ++-- TODO.md | 2 +- Tests/GRDBTests/DatabaseMigratorTests.swift | 8 ++++---- Tests/GRDBTests/RecordEditedTests.swift | 6 ++++-- .../RecordMinimalNonOptionalPrimaryKeySingleTests.swift | 6 ++++-- Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift | 6 ++++-- Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift | 6 ++++-- Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift | 6 ++++-- Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift | 6 ++++-- Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift | 6 ++++-- Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift | 6 ++++-- Tests/GRDBTests/RecordPrimaryKeySingleTests.swift | 6 ++++-- ...imaryKeySingleWithReplaceConflictResolutionTests.swift | 6 ++++-- Tests/GRDBTests/RecordSubClassTests.swift | 6 ++++-- Tests/GRDBTests/RecordWithColumnNameManglingTests.swift | 6 ++++-- Tests/GRDBTests/TransactionObserverTests.swift | 2 +- 17 files changed, 58 insertions(+), 34 deletions(-) diff --git a/GRDB/Migration/DatabaseMigrator.swift b/GRDB/Migration/DatabaseMigrator.swift index 2bb9d8517f..00c759c220 100644 --- a/GRDB/Migration/DatabaseMigrator.swift +++ b/GRDB/Migration/DatabaseMigrator.swift @@ -40,7 +40,7 @@ import Foundation /// - ``completedMigrations(_:)`` /// - ``hasBeenSuperseded(_:)`` /// - ``hasCompletedMigrations(_:)`` -public struct DatabaseMigrator { +public struct DatabaseMigrator: Sendable { /// Controls how a migration handle foreign keys constraints. public enum ForeignKeyChecks: Sendable { /// The migration runs with disabled foreign keys. @@ -207,7 +207,7 @@ public struct DatabaseMigrator { public mutating func registerMigration( _ identifier: String, foreignKeyChecks: ForeignKeyChecks = .deferred, - migrate: @escaping (Database) throws -> Void) + migrate: @escaping @Sendable (Database) throws -> Void) { let migrationChecks: Migration.ForeignKeyChecks switch foreignKeyChecks { diff --git a/GRDB/Migration/Migration.swift b/GRDB/Migration/Migration.swift index 36676d017f..5a100bdec7 100644 --- a/GRDB/Migration/Migration.swift +++ b/GRDB/Migration/Migration.swift @@ -1,5 +1,5 @@ /// An internal struct that defines a migration. -struct Migration { +struct Migration: Sendable { enum ForeignKeyChecks { case deferred case immediate @@ -8,7 +8,7 @@ struct Migration { let identifier: String var foreignKeyChecks: ForeignKeyChecks - let migrate: (Database) throws -> Void + let migrate: @Sendable (Database) throws -> Void func run(_ db: Database) throws { if try Bool.fetchOne(db, sql: "PRAGMA foreign_keys") ?? false { diff --git a/TODO.md b/TODO.md index 31cae197ed..ba700e44fe 100644 --- a/TODO.md +++ b/TODO.md @@ -104,7 +104,7 @@ - [X] GRDB7: Sendable: DatabaseColumnDecodingStrategy (264d7fb5) - [X] GRDB7/BREAKING: Remove DatabaseFuture and concurrentRead (05f7d3c8) - [X] GRDB7: Sendable: DatabaseFunction (6e691fe7) -- [ ] GRDB7: Sendable: DatabaseMigrator (22114ad4) +- [X] GRDB7: Sendable: DatabaseMigrator (22114ad4) - [ ] GRDB7: Not Sendable: FilterCursor (b26e9709) - [ ] GRDB7: Sendable: RowAdapter (d138af26) - [ ] GRDB7: Sendable: ValueObservationScheduler (8429eb68) diff --git a/Tests/GRDBTests/DatabaseMigratorTests.swift b/Tests/GRDBTests/DatabaseMigratorTests.swift index c117b5729f..a9efa088d0 100644 --- a/Tests/GRDBTests/DatabaseMigratorTests.swift +++ b/Tests/GRDBTests/DatabaseMigratorTests.swift @@ -128,7 +128,7 @@ class DatabaseMigratorTests : GRDBTestCase { } let expectation = self.expectation(description: "") - migrator.asyncMigrate(writer, completion: { dbResult in + migrator.asyncMigrate(writer, completion: { [migrator2] dbResult in // No migration error let db = try! dbResult.get() @@ -795,13 +795,13 @@ class DatabaseMigratorTests : GRDBTestCase { var migrator = DatabaseMigrator() migrator.eraseDatabaseOnSchemaChange = true - var witness = 1 + let mutex = Mutex(0) migrator.registerMigration("1") { db in + let value = mutex.increment() try db.execute(sql: """ CREATE TABLE t1(id INTEGER PRIMARY KEY); INSERT INTO t1(id) VALUES (?) - """, arguments: [witness]) - witness += 1 + """, arguments: [value]) } let dbQueue = try makeDatabaseQueue() diff --git a/Tests/GRDBTests/RecordEditedTests.swift b/Tests/GRDBTests/RecordEditedTests.swift index 31316f5c0f..64573f4c4d 100644 --- a/Tests/GRDBTests/RecordEditedTests.swift +++ b/Tests/GRDBTests/RecordEditedTests.swift @@ -15,7 +15,7 @@ private class Person : Record { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE persons ( id INTEGER PRIMARY KEY, @@ -131,7 +131,9 @@ class RecordEditedTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createPerson", migrate: Person.setup) + migrator.registerMigration("createPerson") { + try Person.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift index 21896ae5b3..a77404913f 100644 --- a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift @@ -13,7 +13,7 @@ private class MinimalNonOptionalPrimaryKeySingle: Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: "CREATE TABLE minimalSingles (id TEXT NOT NULL PRIMARY KEY)") } @@ -48,7 +48,9 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createMinimalNonOptionalPrimaryKeySingle", migrate: MinimalNonOptionalPrimaryKeySingle.setup) + migrator.registerMigration("createMinimalNonOptionalPrimaryKeySingle") { + try MinimalNonOptionalPrimaryKeySingle.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift index 37a577b216..ff462cb6ae 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift @@ -12,7 +12,7 @@ class MinimalRowID : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: "CREATE TABLE minimalRowIDs (id INTEGER PRIMARY KEY)") } @@ -52,7 +52,9 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createMinimalRowID", migrate: MinimalRowID.setup) + migrator.registerMigration("createMinimalRowID") { + try MinimalRowID.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift index 028d6ea6d0..9139d0cfc4 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift @@ -11,7 +11,7 @@ class MinimalSingle: Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: "CREATE TABLE minimalSingles (UUID TEXT NOT NULL PRIMARY KEY)") } @@ -49,7 +49,9 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createMinimalSingle", migrate: MinimalSingle.setup) + migrator.registerMigration("createMinimalSingle") { + try MinimalSingle.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift index d2388e1d94..cc4de060e1 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift @@ -16,7 +16,7 @@ private class Person : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE persons ( creationDate TEXT NOT NULL, @@ -84,7 +84,9 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createPerson", migrate: Person.setup) + migrator.registerMigration("createPerson") { + try Person.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift b/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift index f8b8c8e109..cf792c6865 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift @@ -14,7 +14,7 @@ private class Citizenship : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE citizenships ( personName TEXT NOT NULL, @@ -61,7 +61,9 @@ class RecordPrimaryKeyMultipleTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createCitizenship", migrate: Citizenship.setup) + migrator.registerMigration("createCitizenship") { + try Citizenship.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift b/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift index 8582a5e488..a96bd496e1 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift @@ -14,7 +14,7 @@ private class Item : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE items ( name TEXT, @@ -58,7 +58,9 @@ class RecordPrimaryKeyNoneTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createItem", migrate: Item.setup) + migrator.registerMigration("createItem") { + try Item.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift index eb9023cf24..dabb5c9084 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift @@ -16,7 +16,7 @@ private class Person : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE persons ( id INTEGER PRIMARY KEY, @@ -78,7 +78,9 @@ class RecordPrimaryKeyRowIDTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createPerson", migrate: Person.setup) + migrator.registerMigration("createPerson") { + try Person.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift index 69664f7295..ca70ff5b44 100644 --- a/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift @@ -12,7 +12,7 @@ class Pet : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE pets ( UUID TEXT NOT NULL PRIMARY KEY, @@ -51,7 +51,9 @@ class RecordPrimaryKeySingleTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createPet", migrate: Pet.setup) + migrator.registerMigration("createPet") { + try Pet.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift b/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift index e1444cdf31..8451feeab9 100644 --- a/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift @@ -11,7 +11,7 @@ class Email : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE emails ( email TEXT NOT NULL PRIMARY KEY ON CONFLICT REPLACE, @@ -50,7 +50,9 @@ class RecordPrimaryKeySingleWithReplaceConflictResolutionTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createEmail", migrate: Email.setup) + migrator.registerMigration("createEmail") { + try Email.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordSubClassTests.swift b/Tests/GRDBTests/RecordSubClassTests.swift index fc3d8dc3fc..ce82183f51 100644 --- a/Tests/GRDBTests/RecordSubClassTests.swift +++ b/Tests/GRDBTests/RecordSubClassTests.swift @@ -15,7 +15,7 @@ private class Person : Record { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE persons ( id INTEGER PRIMARY KEY, @@ -105,7 +105,9 @@ class RecordSubClassTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createPerson", migrate: Person.setup) + migrator.registerMigration("createPerson") { + try Person.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordWithColumnNameManglingTests.swift b/Tests/GRDBTests/RecordWithColumnNameManglingTests.swift index 0ce7021e02..e9dba83836 100644 --- a/Tests/GRDBTests/RecordWithColumnNameManglingTests.swift +++ b/Tests/GRDBTests/RecordWithColumnNameManglingTests.swift @@ -13,7 +13,7 @@ class BadlyMangledStuff : Record { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: "CREATE TABLE stuffs (id INTEGER PRIMARY KEY, name TEXT)") } @@ -48,7 +48,9 @@ class RecordWithColumnNameManglingTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createBadlyMangledStuff", migrate: BadlyMangledStuff.setup) + migrator.registerMigration("createBadlyMangledStuff") { + try BadlyMangledStuff.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/TransactionObserverTests.swift b/Tests/GRDBTests/TransactionObserverTests.swift index 0730f42f29..cedbb23020 100644 --- a/Tests/GRDBTests/TransactionObserverTests.swift +++ b/Tests/GRDBTests/TransactionObserverTests.swift @@ -1384,7 +1384,7 @@ class TransactionObserverTests: GRDBTestCase { dbQueue.add(transactionObserver: observer) try dbQueue.writeWithoutTransaction { db in - try MinimalRowID.setup(inDatabase: db) + try MinimalRowID.setup(db) let record = MinimalRowID() try record.save(db) From 961f6c41760e69abb2d57c492463329f5082782a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Mar 2024 18:45:22 +0100 Subject: [PATCH 040/160] FilterCursor can't be made Sendable --- GRDB/Core/Cursor.swift | 4 ++++ TODO.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/GRDB/Core/Cursor.swift b/GRDB/Core/Cursor.swift index 5ce6dc7c03..f2bed78002 100644 --- a/GRDB/Core/Cursor.swift +++ b/GRDB/Core/Cursor.swift @@ -909,6 +909,10 @@ public final class FilterCursor { } } +// Explicit non-conformance to Sendable. +@available(*, unavailable) +extension FilterCursor: Sendable { } + extension FilterCursor: Cursor { public func next() throws -> Base.Element? { while let element = try base.next() { diff --git a/TODO.md b/TODO.md index ba700e44fe..4302cc33d6 100644 --- a/TODO.md +++ b/TODO.md @@ -105,7 +105,7 @@ - [X] GRDB7/BREAKING: Remove DatabaseFuture and concurrentRead (05f7d3c8) - [X] GRDB7: Sendable: DatabaseFunction (6e691fe7) - [X] GRDB7: Sendable: DatabaseMigrator (22114ad4) -- [ ] GRDB7: Not Sendable: FilterCursor (b26e9709) +- [X] GRDB7: Not Sendable: FilterCursor (b26e9709) - [ ] GRDB7: Sendable: RowAdapter (d138af26) - [ ] GRDB7: Sendable: ValueObservationScheduler (8429eb68) - [X] GRDB7: Sendable: DatabaseCollation (4d9d67dd) From 7aff0b97c7890495844a05c334c8a14ddb82e4ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Mar 2024 18:59:20 +0100 Subject: [PATCH 041/160] RowAdapter is Sendable --- GRDB/Core/RowAdapter.swift | 6 +++--- TODO.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index a077e58152..86c42a38d8 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -318,7 +318,7 @@ extension Statement: _RowLayout { /// - ``RenameColumnAdapter`` /// - ``ScopeAdapter`` /// - ``SuffixRowAdapter`` -public protocol RowAdapter { +public protocol RowAdapter: Sendable { /// You never call this method directly. It is called for you whenever an /// adapter has to be applied. /// @@ -656,11 +656,11 @@ struct ChainedAdapter: RowAdapter { /// print(Array(adaptedRow.columnNames)) /// ``` public struct RenameColumnAdapter: RowAdapter { - let transform: (String) -> String + let transform: @Sendable (String) -> String /// Creates a `RenameColumnAdapter` adapter that renames columns according to the /// provided transform function. - public init(_ transform: @escaping (String) -> String) { + public init(_ transform: @escaping @Sendable (String) -> String) { self.transform = transform } diff --git a/TODO.md b/TODO.md index 4302cc33d6..132ca83bc9 100644 --- a/TODO.md +++ b/TODO.md @@ -106,7 +106,7 @@ - [X] GRDB7: Sendable: DatabaseFunction (6e691fe7) - [X] GRDB7: Sendable: DatabaseMigrator (22114ad4) - [X] GRDB7: Not Sendable: FilterCursor (b26e9709) -- [ ] GRDB7: Sendable: RowAdapter (d138af26) +- [X] GRDB7: Sendable: RowAdapter (d138af26) - [ ] GRDB7: Sendable: ValueObservationScheduler (8429eb68) - [X] GRDB7: Sendable: DatabaseCollation (4d9d67dd) - [ ] GRDB7: Sendable: LogErrorFunction (f362518d) From 5333dbeff1a094509acd430983abef74cf606b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Mar 2024 19:36:05 +0100 Subject: [PATCH 042/160] LogErrorFunction is Sendable --- GRDB/Core/Database.swift | 30 ++++++++++++++++++--- TODO.md | 2 +- Tests/GRDBTests/DatabasePoolTests.swift | 4 +-- Tests/GRDBTests/DatabaseQueueTests.swift | 2 +- Tests/GRDBTests/DatabaseSnapshotTests.swift | 2 +- Tests/GRDBTests/GRDBTestCase.swift | 14 +++++----- 6 files changed, 39 insertions(+), 15 deletions(-) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index dec9d1273b..fb0715e4e6 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -122,8 +122,14 @@ let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_ /// - ``logError`` /// - ``releaseMemory()`` /// - ``trace(options:_:)`` +/// +/// ### Supporting Types +/// +/// - ``BusyCallback`` +/// - ``BusyMode`` /// - ``CheckpointMode`` /// - ``DatabaseBackupProgress`` +/// - ``LogErrorFunction`` /// - ``StorageClass`` /// - ``TraceEvent`` /// - ``TracingOptions`` @@ -143,8 +149,26 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// The error logging function. /// + /// SQLite can be configured to invoke a callback function containing + /// an error code and a terse error message whenever anomalies occur. + /// + /// This global error callback must be configured early in the lifetime + /// of your application: + /// + /// ```swift + /// Database.logError = { (resultCode, message) in + /// NSLog("%@", "SQLite error \(resultCode): \(message)") + /// } + /// ``` + /// + /// - warning: Database.logError must be set before any database + /// connection is opened. This includes the connections that your + /// application opens with GRDB, but also connections opened by + /// other tools, such as third-party libraries. Setting it after a + /// connection has been opened is an SQLite misuse, and has no effect. + /// /// Related SQLite documentation: - public static var logError: LogErrorFunction? = nil { + nonisolated(unsafe) public static var logError: LogErrorFunction? = nil { didSet { if logError != nil { _registerErrorLogCallback { (_, code, message) in @@ -1830,7 +1854,7 @@ extension Database { // MARK: - Database-Related Types - /// See BusyMode and + /// See ``BusyMode`` and public typealias BusyCallback = @Sendable (_ numberOfTries: Int) -> Bool /// When there are several connections to a database, a connection may try @@ -2019,7 +2043,7 @@ extension Database { } /// An error log function that takes an error code and message. - public typealias LogErrorFunction = (_ resultCode: ResultCode, _ message: String) -> Void + public typealias LogErrorFunction = @Sendable (_ resultCode: ResultCode, _ message: String) -> Void /// An SQLite storage class. /// diff --git a/TODO.md b/TODO.md index 132ca83bc9..c9ed6dceb5 100644 --- a/TODO.md +++ b/TODO.md @@ -109,7 +109,7 @@ - [X] GRDB7: Sendable: RowAdapter (d138af26) - [ ] GRDB7: Sendable: ValueObservationScheduler (8429eb68) - [X] GRDB7: Sendable: DatabaseCollation (4d9d67dd) -- [ ] GRDB7: Sendable: LogErrorFunction (f362518d) +- [X] GRDB7: Sendable: LogErrorFunction (f362518d) - [ ] GRDB7: Sendable: ReadWriteBox (57a86a0e) - [ ] GRDB7: Sendable: Pool (f13b2d2e) - [ ] GRDB7: Sendable: OnDemandFuture fulfill (2aabc4c1) diff --git a/Tests/GRDBTests/DatabasePoolTests.swift b/Tests/GRDBTests/DatabasePoolTests.swift index 2e42bb5ccd..21b76f64ea 100644 --- a/Tests/GRDBTests/DatabasePoolTests.swift +++ b/Tests/GRDBTests/DatabasePoolTests.swift @@ -397,7 +397,7 @@ class DatabasePoolTests: GRDBTestCase { XCTFail("Expected Error") } catch DatabaseError.SQLITE_BUSY { } } - XCTAssert(lastMessage!.contains("unfinalized statement: SELECT * FROM sqlite_master")) + XCTAssert(lastSQLiteDiagnostic!.message.contains("unfinalized statement: SELECT * FROM sqlite_master")) // Database is not closed: no error try dbPool.write { db in @@ -427,7 +427,7 @@ class DatabasePoolTests: GRDBTestCase { // // The first comes from GRDB, and the second, depending on the SQLite // version, from `sqlite3_close_v2()`. Write the test so that it always pass: - XCTAssert(lastMessage!.contains("unfinalized statement")) + XCTAssert(lastSQLiteDiagnostic!.message.contains("unfinalized statement")) // Database is in a zombie state. // In the zombie state, access throws SQLITE_MISUSE diff --git a/Tests/GRDBTests/DatabaseQueueTests.swift b/Tests/GRDBTests/DatabaseQueueTests.swift index bf0bb5371f..3f748f06ea 100644 --- a/Tests/GRDBTests/DatabaseQueueTests.swift +++ b/Tests/GRDBTests/DatabaseQueueTests.swift @@ -458,7 +458,7 @@ class DatabaseQueueTests: GRDBTestCase { XCTFail("Expected Error") } catch DatabaseError.SQLITE_BUSY { } } - XCTAssert(lastMessage!.contains("unfinalized statement: SELECT * FROM sqlite_master")) + XCTAssert(lastSQLiteDiagnostic!.message.contains("unfinalized statement: SELECT * FROM sqlite_master")) // Database is not closed: no error try dbQueue.inDatabase { db in diff --git a/Tests/GRDBTests/DatabaseSnapshotTests.swift b/Tests/GRDBTests/DatabaseSnapshotTests.swift index 6730a8984f..1cb422ecdd 100644 --- a/Tests/GRDBTests/DatabaseSnapshotTests.swift +++ b/Tests/GRDBTests/DatabaseSnapshotTests.swift @@ -424,7 +424,7 @@ class DatabaseSnapshotTests: GRDBTestCase { XCTFail("Expected Error") } catch DatabaseError.SQLITE_BUSY { } } - XCTAssert(lastMessage!.contains("unfinalized statement: SELECT * FROM sqlite_master")) + XCTAssert(lastSQLiteDiagnostic!.message.contains("unfinalized statement: SELECT * FROM sqlite_master")) // Database is not closed: no error try snapshot.read { db in diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index 7fe7ac6b3d..a4151604ed 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -12,15 +12,15 @@ import XCTest @testable import GRDB // Support for Database.logError -var lastResultCode: ResultCode? = nil -var lastMessage: String? = nil +struct SQLiteDiagnostic { + var resultCode: ResultCode + var message: String +} +private let lastSQLiteDiagnosticMutex = Mutex(nil) +var lastSQLiteDiagnostic: SQLiteDiagnostic? { lastSQLiteDiagnosticMutex.load() } let logErrorSetup: Void = { - let lock = NSLock() Database.logError = { (resultCode, message) in - lock.lock() - defer { lock.unlock() } - lastResultCode = resultCode - lastMessage = message + lastSQLiteDiagnosticMutex.store(SQLiteDiagnostic(resultCode: resultCode, message: message)) } }() From e93e4c5a31335d3334ed9acec0c911bd45e51182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 11 Feb 2024 11:52:26 +0100 Subject: [PATCH 043/160] ReadWriteBox is Sendable --- GRDB/Utils/ReadWriteBox.swift | 3 ++- TODO.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/GRDB/Utils/ReadWriteBox.swift b/GRDB/Utils/ReadWriteBox.swift index 1037ef8b39..2c0cd88f8c 100644 --- a/GRDB/Utils/ReadWriteBox.swift +++ b/GRDB/Utils/ReadWriteBox.swift @@ -3,7 +3,8 @@ import Dispatch /// A ReadWriteBox grants multiple readers and single-writer guarantees on a /// value. It is backed by a concurrent DispatchQueue. @propertyWrapper -final class ReadWriteBox { +final class ReadWriteBox: @unchecked Sendable { + // @unchecked because `_wrappedValue` is protected by `queue` private var _wrappedValue: T private var queue: DispatchQueue diff --git a/TODO.md b/TODO.md index c9ed6dceb5..84ccdb2ec8 100644 --- a/TODO.md +++ b/TODO.md @@ -110,7 +110,7 @@ - [ ] GRDB7: Sendable: ValueObservationScheduler (8429eb68) - [X] GRDB7: Sendable: DatabaseCollation (4d9d67dd) - [X] GRDB7: Sendable: LogErrorFunction (f362518d) -- [ ] GRDB7: Sendable: ReadWriteBox (57a86a0e) +- [X] GRDB7: Sendable: ReadWriteBox (57a86a0e) - [ ] GRDB7: Sendable: Pool (f13b2d2e) - [ ] GRDB7: Sendable: OnDemandFuture fulfill (2aabc4c1) - [ ] GRDB7: Sendable: WALSnapshotTransaction (7fd34012) From b587afba0e65d726e8996ba3c5bd03b505fd5994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 11 Feb 2024 11:52:45 +0100 Subject: [PATCH 044/160] Pool is Sendable --- GRDB/Core/DatabasePool.swift | 6 ++-- GRDB/Core/DatabaseSnapshotPool.swift | 19 +++++------ GRDB/Utils/Pool.swift | 50 ++++++++++++++++++---------- TODO.md | 2 +- Tests/GRDBTests/PoolTests.swift | 4 ++- 5 files changed, 46 insertions(+), 35 deletions(-) diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index e7e4ff6098..e6019cd724 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -62,17 +62,15 @@ public final class DatabasePool { // an opened transaction. readerConfiguration.allowsUnsafeTransactions = false - var readerCount = 0 readerPool = Pool( maximumCount: configuration.maximumReaderCount, qos: configuration.readQoS, - makeElement: { - readerCount += 1 // protected by Pool (TODO: document this protection behavior) + makeElement: { [readerConfiguration] index in return try SerializedDatabase( path: path, configuration: readerConfiguration, defaultLabel: "GRDB.DatabasePool", - purpose: "reader.\(readerCount)") + purpose: "reader.\(index)") }) // Set up journal mode unless readonly diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index 525fa1c745..935f3ccf47 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -130,6 +130,7 @@ public final class DatabaseSnapshotPool { /// `db` is used. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public init(_ db: Database, configuration: Configuration? = nil) throws { + let path = db.path var configuration = Self.configure(configuration ?? db.configuration) // Acquire and hold WAL snapshot @@ -138,7 +139,7 @@ public final class DatabaseSnapshotPool { } var holderConfig = Configuration() holderConfig.allowsUnsafeTransactions = true - snapshotHolder = try DatabaseQueue(path: db.path, configuration: holderConfig) + snapshotHolder = try DatabaseQueue(path: path, configuration: holderConfig) try snapshotHolder.inDatabase { db in try db.beginTransaction(.deferred) try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1") @@ -158,20 +159,18 @@ public final class DatabaseSnapshotPool { } self.configuration = configuration - self.path = db.path + self.path = path self.walSnapshot = walSnapshot - var readerCount = 0 readerPool = Pool( maximumCount: configuration.maximumReaderCount, qos: configuration.readQoS, - makeElement: { - readerCount += 1 // protected by Pool (TODO: document this protection behavior) + makeElement: { [configuration] index in return try SerializedDatabase( - path: db.path, + path: path, configuration: configuration, defaultLabel: "GRDB.DatabaseSnapshotPool", - purpose: "snapshot.\(readerCount)") + purpose: "snapshot.\(index)") }) } @@ -219,17 +218,15 @@ public final class DatabaseSnapshotPool { self.path = path self.walSnapshot = walSnapshot - var readerCount = 0 readerPool = Pool( maximumCount: configuration.maximumReaderCount, qos: configuration.readQoS, - makeElement: { - readerCount += 1 // protected by Pool (TODO: document this protection behavior) + makeElement: { [configuration] index in return try SerializedDatabase( path: path, configuration: configuration, defaultLabel: "GRDB.DatabaseSnapshotPool", - purpose: "snapshot.\(readerCount)") + purpose: "snapshot.\(index)") }) } diff --git a/GRDB/Utils/Pool.swift b/GRDB/Utils/Pool.swift index 6e1014c75c..0a8be5338d 100644 --- a/GRDB/Utils/Pool.swift +++ b/GRDB/Utils/Pool.swift @@ -35,8 +35,9 @@ import Dispatch /// got 2 /// got 1 /// got 3 -final class Pool { - private class Item { +final class Pool: Sendable { + private class Item: @unchecked Sendable { + // @unchecked because `isAvailable` is protected by `Pool.content`. let element: T var isAvailable: Bool @@ -46,8 +47,19 @@ final class Pool { } } - private let makeElement: () throws -> T - @ReadWriteBox private var items: [Item] = [] + private struct Content { + var items: [Item] + + /// The number of created items. May become greater than the number + /// of elements in items, as some items are destroyed and other + /// are created. + var createdCount = 0 + } + + typealias ElementAndRelease = (element: T, release: @Sendable (PoolCompletion) -> Void) + + private let makeElement: @Sendable (Int) throws -> T + private let content = ReadWriteBox(wrappedValue: Content(items: [], createdCount: 0)) private let itemsSemaphore: DispatchSemaphore // limits the number of elements private let itemsGroup: DispatchGroup // knows when no element is used private let barrierQueue: DispatchQueue @@ -59,11 +71,12 @@ final class Pool { /// - maximumCount: The maximum number of elements. /// - qos: The quality of service of asynchronous accesses. /// - makeElement: A function that creates an element. It is called - /// on demand. + /// on demand. Its argument is the index of the created elements + /// (1, then 2, etc). init( maximumCount: Int, qos: DispatchQoS = .unspecified, - makeElement: @escaping () throws -> T) + makeElement: @escaping @Sendable (_ index: Int) throws -> T) { GRDBPrecondition(maximumCount > 0, "Pool size must be at least 1") self.makeElement = makeElement @@ -75,19 +88,20 @@ final class Pool { /// Returns a tuple (element, release) /// Client must call release(), only once, after the element has been used. - func get() throws -> (element: T, release: (PoolCompletion) -> Void) { + func get() throws -> ElementAndRelease { try barrierQueue.sync { itemsSemaphore.wait() itemsGroup.enter() do { - let item = try $items.update { items -> Item in - if let item = items.first(where: \.isAvailable) { + let item = try content.update { content -> Item in + if let item = content.items.first(where: \.isAvailable) { item.isAvailable = false return item } else { - let element = try makeElement() + content.createdCount += 1 + let element = try makeElement(content.createdCount) let item = Item(element: element, isAvailable: false) - items.append(item) + content.items.append(item) return item } } @@ -107,7 +121,7 @@ final class Pool { /// /// - important: The `execute` argument is executed in a serial dispatch /// queue, so make sure you use the element asynchronously. - func asyncGet(_ execute: @escaping (Result<(element: T, release: (PoolCompletion) -> Void), Error>) -> Void) { + func asyncGet(_ execute: @escaping (Result) -> Void) { // Inspired by https://khanlou.com/2016/04/the-GCD-handbook/ // > We wait on the semaphore in the serial queue, which means that // > we’ll have at most one blocked thread when we reach maximum @@ -128,7 +142,7 @@ final class Pool { } private func release(_ item: Item, completion: PoolCompletion) { - $items.update { items in + content.update { content in switch completion { case .reuse: // This is why Item is a class, not a struct: so that we can @@ -136,8 +150,8 @@ final class Pool { item.isAvailable = true case .discard: // Discard should be rare: perform lookup. - if let index = items.firstIndex(where: { $0 === item }) { - items.remove(at: index) + if let index = content.items.firstIndex(where: { $0 === item }) { + content.items.remove(at: index) } } } @@ -148,8 +162,8 @@ final class Pool { /// Performs a block on each pool element, available or not. /// The block is run is some arbitrary dispatch queue. func forEach(_ body: (T) throws -> Void) rethrows { - try $items.read { items in - for item in items { + try content.read { content in + for item in content.items { try body(item.element) } } @@ -158,7 +172,7 @@ final class Pool { /// Removes all elements from the pool. /// Currently used elements won't be reused. func removeAll() { - items = [] + content.update { $0.items.removeAll() } } /// Blocks until no element is used, and runs the `barrier` function before diff --git a/TODO.md b/TODO.md index 84ccdb2ec8..a0d7dd972a 100644 --- a/TODO.md +++ b/TODO.md @@ -111,7 +111,7 @@ - [X] GRDB7: Sendable: DatabaseCollation (4d9d67dd) - [X] GRDB7: Sendable: LogErrorFunction (f362518d) - [X] GRDB7: Sendable: ReadWriteBox (57a86a0e) -- [ ] GRDB7: Sendable: Pool (f13b2d2e) +- [X] GRDB7: Sendable: Pool (f13b2d2e) - [ ] GRDB7: Sendable: OnDemandFuture fulfill (2aabc4c1) - [ ] GRDB7: Sendable: WALSnapshotTransaction (7fd34012) - [ ] GRDB7: sending closures for SerializedDatabase diff --git a/Tests/GRDBTests/PoolTests.swift b/Tests/GRDBTests/PoolTests.swift index 29c8fcaf73..dbce4868bd 100644 --- a/Tests/GRDBTests/PoolTests.swift +++ b/Tests/GRDBTests/PoolTests.swift @@ -5,7 +5,9 @@ class PoolTests: XCTestCase { /// Returns a Pool whose elements are incremented integers: 1, 2, 3... private func makeCounterPool(maximumCount: Int) -> Pool { let count = ReadWriteBox(wrappedValue: 0) - return Pool(maximumCount: maximumCount, makeElement: count.increment) + return Pool(maximumCount: maximumCount, makeElement: { _ in + count.increment() + }) } func testElementsAreReused() throws { From b0c572507504b2d311e97c4780c048756cb361ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 31 Mar 2024 07:54:07 +0200 Subject: [PATCH 045/160] OnDemandFuture fulfill is Sendable --- GRDB/Core/DatabaseReader.swift | 8 +++----- GRDB/Utils/OnDemandFuture.swift | 10 +++++++--- TODO.md | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index ca694be0d7..37d63fc661 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -555,11 +555,9 @@ extension DatabaseReader { value: @escaping (Database) throws -> Output) -> DatabasePublishers.Read { - Deferred { - Future { fulfill in - self.asyncRead { dbResult in - fulfill(dbResult.flatMap { db in Result { try value(db) } }) - } + OnDemandFuture { fulfill in + self.asyncRead { dbResult in + fulfill(dbResult.flatMap { db in Result { try value(db) } }) } } .receiveValues(on: scheduler) diff --git a/GRDB/Utils/OnDemandFuture.swift b/GRDB/Utils/OnDemandFuture.swift index e1d8dff6ca..645da8d199 100644 --- a/GRDB/Utils/OnDemandFuture.swift +++ b/GRDB/Utils/OnDemandFuture.swift @@ -14,9 +14,12 @@ import Foundation /// Both two extra scheduling guarantees are used by GRDB in order to be /// able to spawn concurrent database reads right from the database writer /// queue, and fulfill GRDB preconditions. +/// +/// OnDemandFuture also adds Sendable requirements that avoid +/// compiler warnings. @available(iOS 13, macOS 10.15, tvOS 13, *) struct OnDemandFuture: Publisher { - typealias Promise = (Result) -> Void + typealias Promise = @Sendable (Result) -> Void typealias Output = Output typealias Failure = Failure fileprivate let attemptToFulfill: (@escaping Promise) -> Void @@ -34,8 +37,9 @@ struct OnDemandFuture: Publisher { } @available(iOS 13, macOS 10.15, tvOS 13, *) -private class OnDemandFutureSubscription: Subscription { - typealias Promise = (Result) -> Void +private class OnDemandFutureSubscription: Subscription, @unchecked Sendable { + // @unchecked because `state` is protected with `lock`. + typealias Promise = @Sendable (Result) -> Void private enum State { case waitingForDemand(downstream: Downstream, attemptToFulfill: (@escaping Promise) -> Void) diff --git a/TODO.md b/TODO.md index a0d7dd972a..82fb1a5b0c 100644 --- a/TODO.md +++ b/TODO.md @@ -112,7 +112,7 @@ - [X] GRDB7: Sendable: LogErrorFunction (f362518d) - [X] GRDB7: Sendable: ReadWriteBox (57a86a0e) - [X] GRDB7: Sendable: Pool (f13b2d2e) -- [ ] GRDB7: Sendable: OnDemandFuture fulfill (2aabc4c1) +- [X] GRDB7: Sendable: OnDemandFuture fulfill (2aabc4c1) - [ ] GRDB7: Sendable: WALSnapshotTransaction (7fd34012) - [ ] GRDB7: sending closures for SerializedDatabase - [ ] GRDB7: sending closures for ValueObservationScheduler From 1036371dff933deade0cadbd0d61d46f35faaa7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 25 Aug 2024 17:42:39 +0200 Subject: [PATCH 046/160] DatabasePromise is Sendable --- .../Request/Association/Association.swift | 24 ++++++++++++++----- .../Request/QueryInterfaceRequest.swift | 12 +++++----- .../Request/RequestProtocols.swift | 24 ++++++++++++++----- GRDB/QueryInterface/SQL/DatabasePromise.swift | 8 +++---- GRDB/QueryInterface/SQL/SQLRelation.swift | 16 ++++++------- TODO.md | 2 +- 6 files changed, 55 insertions(+), 31 deletions(-) diff --git a/GRDB/QueryInterface/Request/Association/Association.swift b/GRDB/QueryInterface/Request/Association/Association.swift index debe9590a3..b52aa31222 100644 --- a/GRDB/QueryInterface/Request/Association/Association.swift +++ b/GRDB/QueryInterface/Request/Association/Association.swift @@ -173,7 +173,9 @@ extension Association { // SelectionRequest conformance extension Association { - public func selectWhenConnected(_ selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self { + public func selectWhenConnected( + _ selection: @escaping @Sendable (Database) throws -> [any SQLSelectable] + ) -> Self { withDestinationRelation { relation in relation = relation.selectWhenConnected { db in try selection(db).map(\.sqlSelection) @@ -181,7 +183,9 @@ extension Association { } } - public func annotatedWhenConnected(with selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self { + public func annotatedWhenConnected( + with selection: @escaping @Sendable (Database) throws -> [any SQLSelectable] + ) -> Self { withDestinationRelation { relation in relation = relation.annotatedWhenConnected { db in try selection(db).map(\.sqlSelection) @@ -192,7 +196,9 @@ extension Association { // FilteredRequest conformance extension Association { - public func filterWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self { + public func filterWhenConnected( + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible + ) -> Self { withDestinationRelation { relation in relation = relation.filterWhenConnected { db in try predicate(db).sqlExpression @@ -203,7 +209,9 @@ extension Association { // OrderedRequest conformance extension Association { - public func orderWhenConnected(_ orderings: @escaping (Database) throws -> [any SQLOrderingTerm]) -> Self { + public func orderWhenConnected( + _ orderings: @escaping @Sendable (Database) throws -> [any SQLOrderingTerm] + ) -> Self { withDestinationRelation { relation in relation = relation.orderWhenConnected { db in try orderings(db).map(\.sqlOrdering) @@ -239,7 +247,9 @@ extension Association { // AggregatingRequest conformance extension Association { - public func groupWhenConnected(_ expressions: @escaping (Database) throws -> [any SQLExpressible]) -> Self { + public func groupWhenConnected( + _ expressions: @escaping @Sendable (Database) throws -> [any SQLExpressible] + ) -> Self { withDestinationRelation { relation in relation = relation.groupWhenConnected { db in try expressions(db).map(\.sqlExpression) @@ -247,7 +257,9 @@ extension Association { } } - public func havingWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self { + public func havingWhenConnected( + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible + ) -> Self { withDestinationRelation { relation in relation = relation.havingWhenConnected { db in try predicate(db).sqlExpression diff --git a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift index 54826e1488..e4dccaaa63 100644 --- a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift @@ -118,7 +118,7 @@ extension QueryInterfaceRequest: FetchRequest { extension QueryInterfaceRequest: SelectionRequest { public func selectWhenConnected( - _ selection: @escaping (Database) throws -> [any SQLSelectable]) + _ selection: @escaping @Sendable (Database) throws -> [any SQLSelectable]) -> Self { with { @@ -282,7 +282,7 @@ extension QueryInterfaceRequest: SelectionRequest { } public func annotatedWhenConnected( - with selection: @escaping (Database) throws -> [any SQLSelectable]) + with selection: @escaping @Sendable (Database) throws -> [any SQLSelectable]) -> Self { with { @@ -295,7 +295,7 @@ extension QueryInterfaceRequest: SelectionRequest { extension QueryInterfaceRequest: FilteredRequest { public func filterWhenConnected( - _ predicate: @escaping (Database) throws -> any SQLExpressible) + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible) -> Self { with { @@ -308,7 +308,7 @@ extension QueryInterfaceRequest: FilteredRequest { extension QueryInterfaceRequest: OrderedRequest { public func orderWhenConnected( - _ orderings: @escaping (Database) throws -> [any SQLOrderingTerm]) + _ orderings: @escaping @Sendable (Database) throws -> [any SQLOrderingTerm]) -> Self { with { @@ -355,7 +355,7 @@ extension QueryInterfaceRequest: OrderedRequest { extension QueryInterfaceRequest: AggregatingRequest { public func groupWhenConnected( - _ expressions: @escaping (Database) throws -> [any SQLExpressible]) + _ expressions: @escaping @Sendable (Database) throws -> [any SQLExpressible]) -> Self { with { @@ -366,7 +366,7 @@ extension QueryInterfaceRequest: AggregatingRequest { } public func havingWhenConnected( - _ predicate: @escaping (Database) throws -> any SQLExpressible) + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible) -> Self { with { diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index 1803a4cfbc..030ad91eec 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -58,7 +58,9 @@ public protocol SelectionRequest { /// /// - parameter selection: A closure that accepts a database connection and /// returns an array of result columns. - func selectWhenConnected(_ selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self + func selectWhenConnected( + _ selection: @escaping @Sendable (Database) throws -> [any SQLSelectable] + ) -> Self /// Appends result columns to the selected columns. /// @@ -78,7 +80,9 @@ public protocol SelectionRequest { /// /// - parameter selection: A closure that accepts a database connection and /// returns an array of result columns. - func annotatedWhenConnected(with selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self + func annotatedWhenConnected( + with selection: @escaping @Sendable (Database) throws -> [any SQLSelectable] + ) -> Self } extension SelectionRequest { @@ -240,7 +244,9 @@ public protocol FilteredRequest { /// /// - parameter predicate: A closure that accepts a database connection and /// returns a boolean SQL expression. - func filterWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self + func filterWhenConnected( + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible + ) -> Self } extension FilteredRequest { @@ -764,7 +770,9 @@ public protocol AggregatingRequest { /// /// - parameter expressions: A closure that accepts a database connection /// and returns an array of SQL expressions. - func groupWhenConnected(_ expressions: @escaping (Database) throws -> [any SQLExpressible]) -> Self + func groupWhenConnected( + _ expressions: @escaping @Sendable (Database) throws -> [any SQLExpressible] + ) -> Self /// Filters the aggregated groups with a boolean SQL expression. /// @@ -788,7 +796,9 @@ public protocol AggregatingRequest { /// /// - parameter predicate: A closure that accepts a database connection and /// returns a boolean SQL expression. - func havingWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self + func havingWhenConnected( + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible + ) -> Self } extension AggregatingRequest { @@ -969,7 +979,9 @@ public protocol OrderedRequest { /// /// - parameter orderings: A closure that accepts a database connection and /// returns an array of SQL ordering terms. - func orderWhenConnected(_ orderings: @escaping (Database) throws -> [any SQLOrderingTerm]) -> Self + func orderWhenConnected( + _ orderings: @escaping @Sendable (Database) throws -> [any SQLOrderingTerm] + ) -> Self /// Returns a request with reversed ordering. /// diff --git a/GRDB/QueryInterface/SQL/DatabasePromise.swift b/GRDB/QueryInterface/SQL/DatabasePromise.swift index 9d497b5846..7fb7c3edeb 100644 --- a/GRDB/QueryInterface/SQL/DatabasePromise.swift +++ b/GRDB/QueryInterface/SQL/DatabasePromise.swift @@ -23,20 +23,20 @@ /// see SQLRelation.filterPromise. struct DatabasePromise { /// Returns the resolved value. - let resolve: (Database) throws -> T + let resolve: @Sendable (Database) throws -> T /// Creates a promise that resolves to a value. - init(value: T) { + init(value: T) where T: Sendable { self.resolve = { _ in value } } /// Creates a promise from a closure. - init(_ resolve: @escaping (Database) throws -> T) { + init(_ resolve: @escaping @Sendable (Database) throws -> T) { self.resolve = resolve } /// Returns a promise whose value is transformed by the given closure. - func map(_ transform: @escaping (T) throws -> U) -> DatabasePromise { + func map(_ transform: @escaping @Sendable (T) throws -> U) -> DatabasePromise { DatabasePromise { db in try transform(resolve(db)) } diff --git a/GRDB/QueryInterface/SQL/SQLRelation.swift b/GRDB/QueryInterface/SQL/SQLRelation.swift index 4488cc96f4..d9b1b0a29a 100644 --- a/GRDB/QueryInterface/SQL/SQLRelation.swift +++ b/GRDB/QueryInterface/SQL/SQLRelation.swift @@ -187,7 +187,7 @@ extension SQLRelation { /// Convenience factory methods which selects all rows from a table. static func all( fromTable tableName: String, - selection: @escaping (Database) -> [SQLSelection] = { _ in [.allColumns] }) + selection: @escaping @Sendable (Database) -> [SQLSelection] = { _ in [.allColumns] }) -> Self { SQLRelation( @@ -197,7 +197,7 @@ extension SQLRelation { } extension SQLRelation: Refinable { - func selectWhenConnected(_ selection: @escaping (Database) throws -> [SQLSelection]) -> Self { + func selectWhenConnected(_ selection: @escaping @Sendable (Database) throws -> [SQLSelection]) -> Self { with { $0.selectionPromise = DatabasePromise(selection) } @@ -228,7 +228,7 @@ extension SQLRelation: Refinable { } } - func annotatedWhenConnected(with selection: @escaping (Database) throws -> [SQLSelection]) -> Self { + func annotatedWhenConnected(with selection: @escaping @Sendable (Database) throws -> [SQLSelection]) -> Self { with { let old = $0.selectionPromise $0.selectionPromise = DatabasePromise { db in @@ -242,7 +242,7 @@ extension SQLRelation: Refinable { annotatedWhenConnected(with: { _ in selection }) } - func filterWhenConnected(_ predicate: @escaping (Database) throws -> SQLExpression) -> Self { + func filterWhenConnected(_ predicate: @escaping @Sendable (Database) throws -> SQLExpression) -> Self { with { if let old = $0.filterPromise { $0.filterPromise = DatabasePromise { db in @@ -259,7 +259,7 @@ extension SQLRelation: Refinable { filterWhenConnected { _ in predicate } } - func orderWhenConnected(_ orderings: @escaping (Database) throws -> [SQLOrdering]) -> Self { + func orderWhenConnected(_ orderings: @escaping @Sendable (Database) throws -> [SQLOrdering]) -> Self { with { $0.ordering = SQLRelation.Ordering(orderings: orderings) } @@ -313,13 +313,13 @@ extension SQLRelation: Refinable { } } - func groupWhenConnected(_ expressions: @escaping (Database) throws -> [SQLExpression]) -> Self { + func groupWhenConnected(_ expressions: @escaping @Sendable (Database) throws -> [SQLExpression]) -> Self { with { $0.groupPromise = DatabasePromise(expressions) } } - func havingWhenConnected(_ predicate: @escaping (Database) throws -> SQLExpression) -> Self { + func havingWhenConnected(_ predicate: @escaping @Sendable (Database) throws -> SQLExpression) -> Self { with { if let old = $0.havingExpressionPromise { $0.havingExpressionPromise = DatabasePromise { db in @@ -740,7 +740,7 @@ extension SQLRelation { isReversed: false) } - init(orderings: @escaping (Database) throws -> [SQLOrdering]) { + init(orderings: @escaping @Sendable (Database) throws -> [SQLOrdering]) { self.init( elements: [.terms(DatabasePromise(orderings))], isReversed: false) diff --git a/TODO.md b/TODO.md index 82fb1a5b0c..cfa2d0b4c3 100644 --- a/TODO.md +++ b/TODO.md @@ -122,7 +122,7 @@ - [ ] GRDB7: Sendable closures for writePublisher - [ ] GRDB7: Sendable closures for readPublisher - [ ] GRDB7: Not Sendable: fts5customtokenizer, fts5tokenizer, fts5wrappertokenizer -- [ ] GRDB7: Sendable: DatabasePromise (05899228, 5a2c15b8) +- [X] GRDB7: Sendable: DatabasePromise (05899228, 5a2c15b8) - [ ] GRDB7: Sendable: TableAlias (f2b0b186) - [ ] GRDB7: Sendable: SQLRelation (9545bf70) - [ ] GRDB7: Sendable: SQL (ac33856f) From 0fdd910b0847dcc008f0814c0319c35ada56b769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 31 Mar 2024 11:31:36 +0200 Subject: [PATCH 047/160] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Make=20TableAlias?= =?UTF-8?q?=20Sendable=20even=20if=20it=20is=20not=20yet.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift | 4 +++- TODO.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift b/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift index d364a6bbba..8715e0af9b 100644 --- a/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift +++ b/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift @@ -213,7 +213,9 @@ class StatementArgumentsSink { /// See ``TableRequest/aliased(_:)`` for more information and examples. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) -public class TableAlias { +public class TableAlias: @unchecked Sendable { + // This Sendable conformance is transient. TableAlias IS NOT really Sendable. + // TODO: GRDB7 Make TableAlias really Sendable private enum Impl { /// A TableAlias is undefined when it is created by the GRDB user: /// diff --git a/TODO.md b/TODO.md index cfa2d0b4c3..cced7f2678 100644 --- a/TODO.md +++ b/TODO.md @@ -123,7 +123,7 @@ - [ ] GRDB7: Sendable closures for readPublisher - [ ] GRDB7: Not Sendable: fts5customtokenizer, fts5tokenizer, fts5wrappertokenizer - [X] GRDB7: Sendable: DatabasePromise (05899228, 5a2c15b8) -- [ ] GRDB7: Sendable: TableAlias (f2b0b186) +- [X] GRDB7: Sendable: TableAlias (f2b0b186) - [ ] GRDB7: Sendable: SQLRelation (9545bf70) - [ ] GRDB7: Sendable: SQL (ac33856f) - [ ] GRDB7: Split Row.swift (2ce8a619) From 56e2ab85b7ce42a8b3567c38cc9b55ab6f8b083c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 24 Apr 2024 07:57:30 +0200 Subject: [PATCH 048/160] SQLAssociationCondition is Sendable --- GRDB/QueryInterface/Request/CommonTableExpression.swift | 6 +++--- GRDB/QueryInterface/SQL/SQLRelation.swift | 4 ++-- GRDB/QueryInterface/SQL/Table.swift | 2 +- GRDB/QueryInterface/TableRecord+Association.swift | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/GRDB/QueryInterface/Request/CommonTableExpression.swift b/GRDB/QueryInterface/Request/CommonTableExpression.swift index 8e1e5d2c65..9cbd4d2bb6 100644 --- a/GRDB/QueryInterface/Request/CommonTableExpression.swift +++ b/GRDB/QueryInterface/Request/CommonTableExpression.swift @@ -385,7 +385,7 @@ extension CommonTableExpression { /// - returns: An association to the common table expression. public func association( to cte: CommonTableExpression, - on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) + on condition: @escaping @Sendable (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation { JoinAssociation( @@ -421,7 +421,7 @@ extension CommonTableExpression { /// - returns: An association to the common table expression. public func association( to destination: Destination.Type, - on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) + on condition: @escaping @Sendable (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation where Destination: TableRecord { @@ -458,7 +458,7 @@ extension CommonTableExpression { /// - returns: An association to the common table expression. public func association( to destination: Table, - on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) + on condition: @escaping @Sendable (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation { JoinAssociation( diff --git a/GRDB/QueryInterface/SQL/SQLRelation.swift b/GRDB/QueryInterface/SQL/SQLRelation.swift index d9b1b0a29a..5633f30519 100644 --- a/GRDB/QueryInterface/SQL/SQLRelation.swift +++ b/GRDB/QueryInterface/SQL/SQLRelation.swift @@ -796,7 +796,7 @@ extension SQLRelation { /// // SELECT * FROM book WHERE author.id = 1 /// // ~~~~~~~~~~~~~ /// author.request(for: Author.books) -enum SQLAssociationCondition { +enum SQLAssociationCondition: Sendable { /// A condition based on a foreign key. case foreignKey(SQLForeignKeyCondition) @@ -814,7 +814,7 @@ enum SQLAssociationCondition { /// player[Column("id")] == bonus[Column("playerID")] /// }) /// Player.with(bonus).joining(required: association) - case expression((_ left: TableAlias, _ right: TableAlias) -> SQLExpression?) + case expression(@Sendable (_ left: TableAlias, _ right: TableAlias) -> SQLExpression?) /// The condition that does not constrain the two associated tables /// in any way. diff --git a/GRDB/QueryInterface/SQL/Table.swift b/GRDB/QueryInterface/SQL/Table.swift index 6b85f3e51c..2cc7690e55 100644 --- a/GRDB/QueryInterface/SQL/Table.swift +++ b/GRDB/QueryInterface/SQL/Table.swift @@ -1297,7 +1297,7 @@ extension Table { /// - returns: An association to the common table expression. public func association( to cte: CommonTableExpression, - on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) + on condition: @escaping @Sendable (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation { JoinAssociation( diff --git a/GRDB/QueryInterface/TableRecord+Association.swift b/GRDB/QueryInterface/TableRecord+Association.swift index 67249d4d17..6ba8dacfe6 100644 --- a/GRDB/QueryInterface/TableRecord+Association.swift +++ b/GRDB/QueryInterface/TableRecord+Association.swift @@ -339,7 +339,7 @@ extension TableRecord { /// - returns: An association to the common table expression. public static func association( to cte: CommonTableExpression, - on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) + on condition: @escaping @Sendable (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation { JoinAssociation( From 30569aba27856a077ab6b44aa2d043c1e39634bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Tue, 23 Apr 2024 08:11:20 +0200 Subject: [PATCH 049/160] OrderedDictionary is conditionally Sendable --- GRDB/Utils/OrderedDictionary.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GRDB/Utils/OrderedDictionary.swift b/GRDB/Utils/OrderedDictionary.swift index 4f7d5242b8..228f84dfc3 100644 --- a/GRDB/Utils/OrderedDictionary.swift +++ b/GRDB/Utils/OrderedDictionary.swift @@ -207,6 +207,8 @@ extension OrderedDictionary: CustomStringConvertible { } } +extension OrderedDictionary: Sendable where Key: Sendable, Value: Sendable { } + extension Dictionary { init(_ orderedDictionary: OrderedDictionary) { self = orderedDictionary.dictionary From aff662c101001802ce160f64da479f39225aef44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 31 Mar 2024 11:44:38 +0200 Subject: [PATCH 050/160] SQL is Sendable --- GRDB/Core/SQL.swift | 2 +- GRDB/QueryInterface/SQL/SQLExpression.swift | 2 +- GRDB/QueryInterface/SQL/SQLOrdering.swift | 2 +- GRDB/QueryInterface/SQL/SQLRelation.swift | 6 +++--- GRDB/QueryInterface/SQL/SQLSelection.swift | 2 +- GRDB/QueryInterface/SQL/SQLSubquery.swift | 2 +- TODO.md | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/GRDB/Core/SQL.swift b/GRDB/Core/SQL.swift index 4246cf202e..a6d2c439f4 100644 --- a/GRDB/Core/SQL.swift +++ b/GRDB/Core/SQL.swift @@ -39,7 +39,7 @@ /// /// - ``append(literal:)`` /// - ``append(sql:arguments:)`` -public struct SQL { +public struct SQL: Sendable { /// `SQL.Element` is a component of an `SQL` literal. /// /// Elements can be qualified with table aliases, and this is how `SQL` diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift index eb4d58ceb4..6203aaef54 100644 --- a/GRDB/QueryInterface/SQL/SQLExpression.swift +++ b/GRDB/QueryInterface/SQL/SQLExpression.swift @@ -34,7 +34,7 @@ /// ``` /// /// Related SQLite documentation: -public struct SQLExpression { +public struct SQLExpression: Sendable { private var impl: Impl /// The preferred interpretation of the expression in JSON diff --git a/GRDB/QueryInterface/SQL/SQLOrdering.swift b/GRDB/QueryInterface/SQL/SQLOrdering.swift index e7b18f9748..0badbcca25 100644 --- a/GRDB/QueryInterface/SQL/SQLOrdering.swift +++ b/GRDB/QueryInterface/SQL/SQLOrdering.swift @@ -12,7 +12,7 @@ /// function arguments, prefer the ``SQLOrderingTerm`` protocol. /// /// Related SQLite documentation: -public struct SQLOrdering { +public struct SQLOrdering: Sendable { private var impl: Impl private enum Impl { diff --git a/GRDB/QueryInterface/SQL/SQLRelation.swift b/GRDB/QueryInterface/SQL/SQLRelation.swift index 5633f30519..2077537b2d 100644 --- a/GRDB/QueryInterface/SQL/SQLRelation.swift +++ b/GRDB/QueryInterface/SQL/SQLRelation.swift @@ -90,7 +90,7 @@ /// // JOIN passport ON passport.citizenId = citizens.id /// // AND passport.countryCode IN ('BE', 'DE', 'FR', ...); /// Country.including(all: Country.citizens) -struct SQLRelation { +struct SQLRelation: Sendable { struct Child: Refinable { enum Kind { // Record.including(optional: association) @@ -672,7 +672,7 @@ struct SQLLimit { // MARK: - SQLSource -struct SQLSource { +struct SQLSource: Sendable { var tableName: String var alias: TableAlias? @@ -691,7 +691,7 @@ struct SQLSource { extension SQLRelation { /// SQLRelation.Ordering provides the order clause to SQLRelation. - struct Ordering { + struct Ordering: Sendable { private enum Element { case terms(DatabasePromise<[SQLOrdering]>) case ordering(SQLRelation.Ordering) diff --git a/GRDB/QueryInterface/SQL/SQLSelection.swift b/GRDB/QueryInterface/SQL/SQLSelection.swift index c4ff88045b..a0f29120d5 100644 --- a/GRDB/QueryInterface/SQL/SQLSelection.swift +++ b/GRDB/QueryInterface/SQL/SQLSelection.swift @@ -15,7 +15,7 @@ /// function arguments, prefer the ``SQLSelectable`` protocol. /// /// Related SQLite documentation: -public struct SQLSelection { +public struct SQLSelection: Sendable { private var impl: Impl /// The private implementation of the public `SQLSelection`. diff --git a/GRDB/QueryInterface/SQL/SQLSubquery.swift b/GRDB/QueryInterface/SQL/SQLSubquery.swift index fb3d595849..fdc9c3a3ac 100644 --- a/GRDB/QueryInterface/SQL/SQLSubquery.swift +++ b/GRDB/QueryInterface/SQL/SQLSubquery.swift @@ -1,7 +1,7 @@ /// An SQL subquery. /// /// `SQLSubquery` is an opaque representation of an SQL subquery. -public struct SQLSubquery { +public struct SQLSubquery: Sendable { private var impl: Impl private enum Impl { diff --git a/TODO.md b/TODO.md index cced7f2678..1fd95c8d99 100644 --- a/TODO.md +++ b/TODO.md @@ -124,8 +124,8 @@ - [ ] GRDB7: Not Sendable: fts5customtokenizer, fts5tokenizer, fts5wrappertokenizer - [X] GRDB7: Sendable: DatabasePromise (05899228, 5a2c15b8) - [X] GRDB7: Sendable: TableAlias (f2b0b186) -- [ ] GRDB7: Sendable: SQLRelation (9545bf70) -- [ ] GRDB7: Sendable: SQL (ac33856f) +- [X] GRDB7: Sendable: SQLRelation (9545bf70) +- [X] GRDB7: Sendable: SQL (ac33856f) - [ ] GRDB7: Split Row.swift (2ce8a619) - [ ] GRDB7: Cleanup ValueReducer (6c73b1c5) - [X] GRDB7: DatabaseCursor has a primary associated type (b11c5dd2) From cc5798cf1ed54c010b231d99def28d8482ba1ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 31 Mar 2024 08:51:57 +0200 Subject: [PATCH 051/160] WALSnapshotTransaction is Sendable --- GRDB/Core/DatabaseError.swift | 4 + GRDB/Core/DatabaseSnapshotPool.swift | 2 +- GRDB/Core/WALSnapshotTransaction.swift | 80 +++++++++++++------ .../Observers/ValueConcurrentObserver.swift | 15 ++-- TODO.md | 4 +- 5 files changed, 70 insertions(+), 35 deletions(-) diff --git a/GRDB/Core/DatabaseError.swift b/GRDB/Core/DatabaseError.swift index 524c18d63e..bf808dbaa5 100644 --- a/GRDB/Core/DatabaseError.swift +++ b/GRDB/Core/DatabaseError.swift @@ -415,6 +415,10 @@ extension DatabaseError { static func connectionIsClosed() -> Self { DatabaseError(resultCode: .SQLITE_MISUSE, message: "Connection is closed") } + + static func snapshotIsLost() -> Self { + DatabaseError(resultCode: .SQLITE_ABORT, message: "Snapshot is lost.") + } } // Support for `catch DatabaseError.SQLITE_XXX` diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index 935f3ccf47..715dabf72e 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -318,7 +318,7 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { return try reader.reentrantSync { db in let result = try value(db) if snapshotIsLost(db) { - throw DatabaseError(resultCode: .SQLITE_ABORT, message: "Snapshot is lost.") + throw DatabaseError.snapshotIsLost() } return result } diff --git a/GRDB/Core/WALSnapshotTransaction.swift b/GRDB/Core/WALSnapshotTransaction.swift index 4340b67e4e..eb6597fe1a 100644 --- a/GRDB/Core/WALSnapshotTransaction.swift +++ b/GRDB/Core/WALSnapshotTransaction.swift @@ -3,9 +3,28 @@ /// /// `WALSnapshotTransaction` **takes ownership** of its reader /// `SerializedDatabase` (TODO: make it a move-only type eventually). -class WALSnapshotTransaction { - private let reader: SerializedDatabase - private let release: (_ isInsideTransaction: Bool) -> Void +final class WALSnapshotTransaction: @unchecked Sendable { + // @unchecked because `databaseAccess` is protected by a mutex. + + private struct DatabaseAccess { + let reader: SerializedDatabase + let release: @Sendable (_ isInsideTransaction: Bool) -> Void + + // MUST be called only once + func commitAndRelease() { + // WALSnapshotTransaction may be deinitialized in the dispatch + // queue of its reader: allow reentrancy. + let isInsideTransaction = reader.reentrantSync(allowingLongLivedTransaction: false) { db in + try? db.commit() + return db.isInsideTransaction + } + release(isInsideTransaction) + } + } + + // TODO: consider using the serialized DispatchQueue of reader instead of a lock. + /// nil when closed + private let databaseAccessMutex: Mutex /// The state of the database at the beginning of the transaction. let walSnapshot: WALSnapshot @@ -36,10 +55,11 @@ class WALSnapshotTransaction { /// is no longer used. init( onReader reader: SerializedDatabase, - release: @escaping (_ isInsideTransaction: Bool) -> Void) + release: @escaping @Sendable (_ isInsideTransaction: Bool) -> Void) throws { assert(reader.configuration.readonly) + let databaseAccess = DatabaseAccess(reader: reader, release: release) do { // Open a long-lived transaction, and enter snapshot isolation @@ -50,44 +70,56 @@ class WALSnapshotTransaction { try db.clearSchemaCacheIfNeeded() return try WALSnapshot(db) } - self.reader = reader - self.release = release + self.databaseAccessMutex = Mutex(databaseAccess) } catch { // self is not initialized, so deinit will not run. - Self.commitAndRelease(reader: reader, release: release) + databaseAccess.commitAndRelease() throw error } } deinit { - Self.commitAndRelease(reader: reader, release: release) + close() } /// Executes database operations in the snapshot transaction, and /// returns their result after they have finished executing. - func read(_ value: (Database) throws -> T) rethrows -> T { - // We should check the validity of the snapshot, as DatabaseSnapshotPool does. - try reader.sync(value) + func read(_ value: (Database) throws -> T) throws -> T { + try databaseAccessMutex.withLock { databaseAccess in + guard let databaseAccess else { + throw DatabaseError.snapshotIsLost() + } + + // We should check the validity of the snapshot, as DatabaseSnapshotPool does. + return try databaseAccess.reader.sync(value) + } } /// Schedules database operations for execution, and /// returns immediately. - func asyncRead(_ value: @escaping (Database) -> Void) { - // We should check the validity of the snapshot, as DatabaseSnapshotPool does. - reader.async(value) + func asyncRead(_ value: @escaping @Sendable (Result) -> Void) { + databaseAccessMutex.withLock { databaseAccess in + guard let databaseAccess else { + value(.failure(DatabaseError.snapshotIsLost())) + return + } + + databaseAccess.reader.async { db in + // We should check the validity of the snapshot, as DatabaseSnapshotPool does. + // At least check if self was closed: + if self.databaseAccessMutex.load() == nil { + value(.failure(DatabaseError.snapshotIsLost())) + } + value(.success(db)) + } + } } - private static func commitAndRelease( - reader: SerializedDatabase, - release: (_ isInsideTransaction: Bool) -> Void) - { - // WALSnapshotTransaction may be deinitialized in the dispatch - // queue of its reader: allow reentrancy. - let isInsideTransaction = reader.reentrantSync(allowingLongLivedTransaction: false) { db in - try? db.commit() - return db.isInsideTransaction + func close() { + databaseAccessMutex.withLock { databaseAccess in + databaseAccess?.commitAndRelease() + databaseAccess = nil } - release(isInsideTransaction) } } #endif diff --git a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift index e57aef0983..d4cc4b0304 100644 --- a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift @@ -352,10 +352,11 @@ extension ValueConcurrentObserver { let initialFetchTransaction = try result.get() // Second async jump because that's how // `DatabasePool.asyncWALSnapshotTransaction` has to be used. - initialFetchTransaction.asyncRead { db in + initialFetchTransaction.asyncRead { dbResult in do { let fetchedValue: Reducer.Fetched let initialRegion: DatabaseRegion + let db = try dbResult.get() switch self.trackingMode { case let .constantRegion(regions): @@ -429,11 +430,9 @@ extension ValueConcurrentObserver { // checkpointed. That's why we'll keep `initialFetchTransaction` // alive until the comparison is done. // - // However, we want to release `initialFetchTransaction` as soon as + // However, we want to close `initialFetchTransaction` as soon as // possible, so that the reader connection it holds becomes - // available for other reads. It will be released when this optional - // is set to nil: - var initialFetchTransaction: WALSnapshotTransaction? = initialFetchTransaction + // available for other reads. databaseAccess.dbPool.asyncWriteWithoutTransaction { writerDB in let events = self.lock.synchronized { self.notificationCallbacks?.events } @@ -446,7 +445,7 @@ extension ValueConcurrentObserver { // Was the database modified since the initial fetch? let isModified: Bool if let currentWALSnapshot = try? WALSnapshot(writerDB) { - let ordering = initialFetchTransaction!.walSnapshot.compare(currentWALSnapshot) + let ordering = initialFetchTransaction.walSnapshot.compare(currentWALSnapshot) assert(ordering <= 0, "Unexpected snapshot ordering") isModified = ordering < 0 } else { @@ -454,9 +453,9 @@ extension ValueConcurrentObserver { isModified = true } - // Comparison done: end the WAL snapshot transaction + // Comparison done: close the WAL snapshot transaction // and release its reader connection. - initialFetchTransaction = nil + initialFetchTransaction.close() if isModified { events.databaseDidChange?() diff --git a/TODO.md b/TODO.md index 1fd95c8d99..c191648910 100644 --- a/TODO.md +++ b/TODO.md @@ -113,7 +113,7 @@ - [X] GRDB7: Sendable: ReadWriteBox (57a86a0e) - [X] GRDB7: Sendable: Pool (f13b2d2e) - [X] GRDB7: Sendable: OnDemandFuture fulfill (2aabc4c1) -- [ ] GRDB7: Sendable: WALSnapshotTransaction (7fd34012) +- [X] GRDB7: Sendable: WALSnapshotTransaction (7fd34012) - [ ] GRDB7: sending closures for SerializedDatabase - [ ] GRDB7: sending closures for ValueObservationScheduler - [ ] GRDB7: Sendable closures for ValueObservation.handleEvents @@ -130,7 +130,7 @@ - [ ] GRDB7: Cleanup ValueReducer (6c73b1c5) - [X] GRDB7: DatabaseCursor has a primary associated type (b11c5dd2) - [ ] GRDB7: Enable Strict Concurrency Checks (6aa43ded) -- [ ] GRDB7: Sendable: OrderedDictionary (e022c35b) +- [X] GRDB7: Sendable: OrderedDictionary (e022c35b) - [ ] GRDB7: Rename ReadWriteBox to ReadWriteLock (7f5205ef) - [ ] GRDB7: Sendable: DatabaseRegionConvertible (b4677ded) - [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65, 5465d056) From 47aa1f58a7ec479e0f7f003448cf62ae9f488120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Mon, 26 Aug 2024 08:36:46 +0200 Subject: [PATCH 052/160] TODO --- TODO.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/TODO.md b/TODO.md index c191648910..00feb18f0d 100644 --- a/TODO.md +++ b/TODO.md @@ -144,6 +144,20 @@ - [ ] GRDB7: doc (c0838cf9) - [ ] GRDB7/BREAKING: PersistenceContainer is Sendable (50eefa8c) - [ ] GRDB7: TableRecord.databaseSelection must be declared as a computed property (24d232aa) +- [ ] GRDB7: Sendable: Association (b06aaee4) +- [ ] GRDB7/Tests: Sendable: ValueObservationRecorder (2947b3d7) +- [ ] GRDB7: ValueObservation.print cautiously uses its stream argument (5f8b39b7) +- [ ] GRDB7/Tests: use a single and Sendable test TextOutputStream (bbb1a736) +- [ ] GRDB7: ValueObservation needs a ValueReducer, not a `_ValueReducer` (08733108) +- [ ] GRDB7: Database support for cancellation (4ddf4bca) +- [ ] GRDB7: SerializedDatabase support for async db access with support for Task cancellation (737cb149) +- [ ] GRDB7: DatabaseWriter async methods support Task cancellation (a5226501) +- [ ] GRDB7: DatabaseReader async methods support Task cancellation (10c9d311) +- [ ] GRDB7: Document that async methods can throw CancellationError (8df18fb8) +- [ ] GRDB7: Sendable: AssociationAggregate (48ad10ae) +- [?] GRDB7: Sendable: AsyncValueObservation (ce63cdfa) +- [ ] GRDB7: Sendable: DatabaseRegionObservation (b4ff52fb) +- [ ] GRDB7: DispatchQueue.asyncSending (7b075e6b) - [?] GRDB7: Change ValueObservation callback argument so that it could expose snapshots? https://github.com/groue/GRDB.swift/discussions/1523#discussioncomment-9092500 From 4222854f8b576e38aeb648277b7b91d244de3397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Tue, 27 Aug 2024 07:46:52 +0200 Subject: [PATCH 053/160] DatabaseRegionConvertible is Sendable --- GRDB/Core/DatabaseRegion.swift | 8 ++++---- GRDB/Core/FetchRequest.swift | 12 +++++++----- TODO.md | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/GRDB/Core/DatabaseRegion.swift b/GRDB/Core/DatabaseRegion.swift index fddeb77d37..06b887947e 100644 --- a/GRDB/Core/DatabaseRegion.swift +++ b/GRDB/Core/DatabaseRegion.swift @@ -416,7 +416,7 @@ private struct TableRegion: Equatable { /// ### Supporting Types /// /// - ``AnyDatabaseRegionConvertible`` -public protocol DatabaseRegionConvertible { +public protocol DatabaseRegionConvertible: Sendable { /// Returns a database region. /// /// - parameter db: A database connection. @@ -437,14 +437,14 @@ extension DatabaseRegion: DatabaseRegionConvertible { /// A type-erased DatabaseRegionConvertible public struct AnyDatabaseRegionConvertible: DatabaseRegionConvertible { - let _region: (Database) throws -> DatabaseRegion + let _region: @Sendable (Database) throws -> DatabaseRegion - public init(_ region: @escaping (Database) throws -> DatabaseRegion) { + public init(_ region: @escaping @Sendable (Database) throws -> DatabaseRegion) { _region = region } public init(_ region: some DatabaseRegionConvertible) { - _region = region.databaseRegion + _region = { try region.databaseRegion($0) } } public func databaseRegion(_ db: Database) throws -> DatabaseRegion { diff --git a/GRDB/Core/FetchRequest.swift b/GRDB/Core/FetchRequest.swift index 43115cab70..c3e24d8ba2 100644 --- a/GRDB/Core/FetchRequest.swift +++ b/GRDB/Core/FetchRequest.swift @@ -171,7 +171,9 @@ extension FetchRequest { /// /// - parameter adapter: A closure that accepts a database connection and /// returns a row adapter. - public func adapted(_ adapter: @escaping (Database) throws -> any RowAdapter) -> AdaptedFetchRequest { + public func adapted( + _ adapter: @escaping @Sendable (Database) throws -> any RowAdapter + ) -> AdaptedFetchRequest { AdaptedFetchRequest(self, adapter) } } @@ -181,11 +183,11 @@ extension FetchRequest { /// See ``FetchRequest/adapted(_:)``. public struct AdaptedFetchRequest { let base: Base - let adapter: (Database) throws -> any RowAdapter + let adapter: @Sendable (Database) throws -> any RowAdapter /// Creates an adapted request from a base request and a closure that builds /// a row adapter from a database connection. - init(_ base: Base, _ adapter: @escaping (Database) throws -> any RowAdapter) { + init(_ base: Base, _ adapter: @escaping @Sendable (Database) throws -> any RowAdapter) { self.base = base self.adapter = adapter } @@ -274,7 +276,7 @@ extension AnyFetchRequest: FetchRequest { } // Class-based type erasure, so that we preserve full type information. -private class FetchRequestEraser: FetchRequest { +private class FetchRequestEraser: FetchRequest, @unchecked Sendable { typealias RowDecoder = Void var sqlSubquery: SQLSubquery { @@ -290,7 +292,7 @@ private class FetchRequestEraser: FetchRequest { } } -private final class ConcreteFetchRequestEraser: FetchRequestEraser { +private final class ConcreteFetchRequestEraser: FetchRequestEraser, @unchecked Sendable { let request: Request init(request: Request) { diff --git a/TODO.md b/TODO.md index 00feb18f0d..0eb3932d98 100644 --- a/TODO.md +++ b/TODO.md @@ -132,7 +132,7 @@ - [ ] GRDB7: Enable Strict Concurrency Checks (6aa43ded) - [X] GRDB7: Sendable: OrderedDictionary (e022c35b) - [ ] GRDB7: Rename ReadWriteBox to ReadWriteLock (7f5205ef) -- [ ] GRDB7: Sendable: DatabaseRegionConvertible (b4677ded) +- [X] GRDB7: Sendable: DatabaseRegionConvertible (b4677ded) - [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65, 5465d056) - [ ] GRDB7: Sendable: ValueWriteOnlyObserver (ff2a7548) - [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65) From c56b52980afd9ae6ac5b19b19fa7f3f614aafc56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Tue, 27 Aug 2024 07:55:46 +0200 Subject: [PATCH 054/160] DatabaseCancellable is Sendable --- GRDB/Core/DatabaseReader.swift | 8 ++--- .../DatabaseCancellable.swift | 34 ++++++++++++------- TODO.md | 2 +- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 37d63fc661..2295f6f036 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -618,16 +618,16 @@ extension DatabaseReader { } return AnyDatabaseCancellable(cancel: { /* nothing to cancel */ }) } else { - var isCancelled = false + let cancellable = AnyDatabaseCancellable() asyncRead { dbResult in - guard !isCancelled else { return } + if cancellable.isCancelled { return } let result = dbResult.flatMap { db in Result { try observation.fetchInitialValue(db) } } scheduler.schedule { - guard !isCancelled else { return } + if cancellable.isCancelled { return } do { try onChange(result.get()) } catch { @@ -635,7 +635,7 @@ extension DatabaseReader { } } } - return AnyDatabaseCancellable(cancel: { isCancelled = true }) + return cancellable } } } diff --git a/GRDB/ValueObservation/DatabaseCancellable.swift b/GRDB/ValueObservation/DatabaseCancellable.swift index de62987917..fc904892f0 100644 --- a/GRDB/ValueObservation/DatabaseCancellable.swift +++ b/GRDB/ValueObservation/DatabaseCancellable.swift @@ -5,7 +5,7 @@ /// ### Supporting Types /// /// - ``AnyDatabaseCancellable`` -public protocol DatabaseCancellable { +public protocol DatabaseCancellable: Sendable { /// Cancel the activity. func cancel() } @@ -15,31 +15,39 @@ public protocol DatabaseCancellable { /// /// An `AnyDatabaseCancellable` instance automatically calls ``cancel()`` /// when deinitialized. -public class AnyDatabaseCancellable: DatabaseCancellable { - private var _cancel: (() -> Void)? +public final class AnyDatabaseCancellable: DatabaseCancellable { + private let cancelMutex: Mutex<(@Sendable () -> Void)?> + + var isCancelled: Bool { + cancelMutex.withLock { $0 == nil } + } + + convenience init() { + self.init(cancel: { }) + } /// Initializes the cancellable object with the given cancel-time closure. - public init(cancel: @escaping () -> Void) { - _cancel = cancel + public init(cancel: @escaping @Sendable () -> Void) { + cancelMutex = Mutex(cancel) } /// Creates a cancellable object that forwards cancellation to `base`. public convenience init(_ base: some DatabaseCancellable) { - var cancellable = Optional.some(base) self.init { - cancellable?.cancel() - cancellable = nil // Release memory + base.cancel() } } deinit { - _cancel?() + cancel() } public func cancel() { - // Don't prevent multiple concurrent calls to _cancel, because it is - // pointless. But release memory! - _cancel?() - _cancel = nil + let cancel = cancelMutex.withLock { + let cancel = $0 + $0 = nil + return cancel + } + cancel?() } } diff --git a/TODO.md b/TODO.md index 0eb3932d98..ac4245ad9a 100644 --- a/TODO.md +++ b/TODO.md @@ -136,7 +136,7 @@ - [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65, 5465d056) - [ ] GRDB7: Sendable: ValueWriteOnlyObserver (ff2a7548) - [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65) -- [ ] GRDB7: Sendable: DatabaseCancellable (2f93f00b, 8f486a5e) +- [X] GRDB7: Sendable: DatabaseCancellable (2f93f00b, 8f486a5e) - [ ] GRDB7: ValueObservation closures - [?] GRDB7: DatabasePublishers.ValueSubscription - [ ] GRDB7: Sendable: ValueObservation (93f6f982) From aac8720bc75199806780c4290d030e96a616050b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Tue, 27 Aug 2024 07:59:26 +0200 Subject: [PATCH 055/160] DatabaseRegionObservation is Sendable --- GRDB/Core/DatabaseRegion.swift | 2 +- GRDB/Core/DatabaseRegionObservation.swift | 4 ++-- TODO.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GRDB/Core/DatabaseRegion.swift b/GRDB/Core/DatabaseRegion.swift index 06b887947e..271b53f604 100644 --- a/GRDB/Core/DatabaseRegion.swift +++ b/GRDB/Core/DatabaseRegion.swift @@ -461,7 +461,7 @@ extension DatabaseRegion { } } - static func union(_ regions: [any DatabaseRegionConvertible]) -> (Database) throws -> DatabaseRegion { + static func union(_ regions: [any DatabaseRegionConvertible]) -> @Sendable (Database) throws -> DatabaseRegion { return { db in try regions.reduce(into: DatabaseRegion()) { union, region in try union.formUnion(region.databaseRegion(db)) diff --git a/GRDB/Core/DatabaseRegionObservation.swift b/GRDB/Core/DatabaseRegionObservation.swift index 3579586f81..877e14feaa 100644 --- a/GRDB/Core/DatabaseRegionObservation.swift +++ b/GRDB/Core/DatabaseRegionObservation.swift @@ -3,10 +3,10 @@ import Combine #endif import Foundation -public struct DatabaseRegionObservation { +public struct DatabaseRegionObservation: Sendable { /// A closure that is evaluated when the observation starts, and returns /// the observed database region. - var observedRegion: (Database) throws -> DatabaseRegion + var observedRegion: @Sendable (Database) throws -> DatabaseRegion } extension DatabaseRegionObservation { diff --git a/TODO.md b/TODO.md index ac4245ad9a..004238c8d4 100644 --- a/TODO.md +++ b/TODO.md @@ -156,7 +156,7 @@ - [ ] GRDB7: Document that async methods can throw CancellationError (8df18fb8) - [ ] GRDB7: Sendable: AssociationAggregate (48ad10ae) - [?] GRDB7: Sendable: AsyncValueObservation (ce63cdfa) -- [ ] GRDB7: Sendable: DatabaseRegionObservation (b4ff52fb) +- [X] GRDB7: Sendable: DatabaseRegionObservation (b4ff52fb) - [ ] GRDB7: DispatchQueue.asyncSending (7b075e6b) - [?] GRDB7: Change ValueObservation callback argument so that it could expose snapshots? https://github.com/groue/GRDB.swift/discussions/1523#discussioncomment-9092500 From 849bba28207d2304cea6bad0e92ec8ee456f6c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Tue, 27 Aug 2024 08:23:04 +0200 Subject: [PATCH 056/160] Refactor ValueReducer for Swift concurrency The fetching facet of a ValueReducer is Sendable. --- .../Extension/ValueObservation.md | 2 +- .../Observers/ValueConcurrentObserver.swift | 38 +++++++++-------- .../Observers/ValueWriteOnlyObserver.swift | 24 +++++------ GRDB/ValueObservation/Reducers/Fetch.swift | 20 ++++++--- GRDB/ValueObservation/Reducers/Map.swift | 16 ++++--- .../Reducers/RemoveDuplicates.swift | 14 +++---- GRDB/ValueObservation/Reducers/Trace.swift | 27 +++++++----- .../Reducers/ValueReducer.swift | 42 ++++++++++++++----- GRDB/ValueObservation/ValueObservation.swift | 16 +++---- TODO.md | 2 +- Tests/GRDBTests/GRDBTestCase.swift | 27 ++++++++---- 11 files changed, 140 insertions(+), 88 deletions(-) diff --git a/GRDB/Documentation.docc/Extension/ValueObservation.md b/GRDB/Documentation.docc/Extension/ValueObservation.md index 2fbf4f9e1a..5c4664e1f1 100644 --- a/GRDB/Documentation.docc/Extension/ValueObservation.md +++ b/GRDB/Documentation.docc/Extension/ValueObservation.md @@ -313,6 +313,6 @@ When needed, you can help GRDB optimize observations and reduce database content - ``handleEvents(willStart:willFetch:willTrackRegion:databaseDidChange:didReceiveValue:didFail:didCancel:)`` - ``print(_:to:)`` -### Support +### Supporting Types - ``ValueReducer`` diff --git a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift index d4cc4b0304..2671b6b095 100644 --- a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift @@ -76,25 +76,25 @@ final class ValueConcurrentObserver Reducer.Fetched { + func fetch(_ db: Database) throws -> Reducer.Fetcher.Value { try db.isolated(readOnly: true) { - try reducer._fetch(db) + try fetcher.fetch(db) } } - func fetchRecordingObservedRegion(_ db: Database) throws -> (Reducer.Fetched, DatabaseRegion) { + func fetchRecordingObservedRegion(_ db: Database) throws -> (Reducer.Fetcher.Value, DatabaseRegion) { var region = DatabaseRegion() let fetchedValue = try db.isolated(readOnly: true) { try db.recordingSelection(®ion) { - try reducer._fetch(db) + try fetcher.fetch(db) } } return try (fetchedValue, region.observableRegion(db)) @@ -170,9 +170,9 @@ final class ValueConcurrentObserver (Reducer.Fetched, DatabaseRegion) in + let fetchedValue: Reducer.Fetcher.Value + let initialRegion: DatabaseRegion + (fetchedValue, initialRegion) = try databaseAccess.dbPool.read { db in switch trackingMode { case let .constantRegion(regions): let fetchedValue = try databaseAccess.fetch(db) @@ -583,7 +587,7 @@ extension ValueConcurrentObserver { do { // Fetch - let fetchedValue: Reducer.Fetched + let fetchedValue: Reducer.Fetcher.Value let initialRegion: DatabaseRegion let db = try dbResult.get() switch self.trackingMode { @@ -643,7 +647,7 @@ extension ValueConcurrentObserver { do { try writerDB.isolated(readOnly: true) { // Fetch - let fetchedValue: Reducer.Fetched + let fetchedValue: Reducer.Fetcher.Value let observedRegion: DatabaseRegion switch self.trackingMode { case .constantRegion: @@ -822,7 +826,7 @@ extension ValueConcurrentObserver: TransactionObserver { } } - private func reduce(_ fetchResult: Result) { + private func reduce(_ fetchResult: Result) { reduceQueue.async { do { let fetchedValue = try fetchResult.get() diff --git a/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift b/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift index e7dc67bda9..3f5a1c50f6 100644 --- a/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift @@ -83,26 +83,26 @@ final class ValueWriteOnlyObserver< /// If true, database values are fetched from a read-only access. private let readOnly: Bool - /// A reducer that fetches database values. - private let reducer: Reducer + /// The fetcher that fetches database values. + private let fetcher: Reducer.Fetcher - init(writer: Writer, readOnly: Bool, reducer: Reducer) { + init(writer: Writer, readOnly: Bool, fetcher: Reducer.Fetcher) { self.writer = writer self.readOnly = readOnly - self.reducer = reducer + self.fetcher = fetcher } - func fetch(_ db: Database) throws -> Reducer.Fetched { + func fetch(_ db: Database) throws -> Reducer.Fetcher.Value { try db.isolated(readOnly: readOnly) { - try reducer._fetch(db) + try fetcher.fetch(db) } } - func fetchRecordingObservedRegion(_ db: Database) throws -> (Reducer.Fetched, DatabaseRegion) { + func fetchRecordingObservedRegion(_ db: Database) throws -> (Reducer.Fetcher.Value, DatabaseRegion) { var region = DatabaseRegion() let fetchedValue = try db.isolated(readOnly: readOnly) { try db.recordingSelection(®ion) { - try reducer._fetch(db) + try fetcher.fetch(db) } } return try (fetchedValue, region.observableRegion(db)) @@ -160,9 +160,9 @@ final class ValueWriteOnlyObserver< self.databaseAccess = DatabaseAccess( writer: writer, readOnly: readOnly, - // ValueReducer semantics guarantees that reducer._fetch + // ValueReducer semantics guarantees that the fetcher // is independent from the reducer state - reducer: reducer) + fetcher: reducer._makeFetcher()) self.notificationCallbacks = NotificationCallbacks(events: events, onChange: onChange) self.reducer = reducer self.reduceQueue = DispatchQueue( @@ -301,7 +301,7 @@ 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.Fetcher.Value? { let (events, databaseAccess) = lock.synchronized { (notificationCallbacks?.events, self.databaseAccess) } @@ -380,7 +380,7 @@ extension ValueWriteOnlyObserver: TransactionObserver { do { // Fetch - let fetchedValue: Reducer.Fetched + let fetchedValue: Reducer.Fetcher.Value switch trackingMode { case .constantRegion, .constantRegionRecordedFromSelection: diff --git a/GRDB/ValueObservation/Reducers/Fetch.swift b/GRDB/ValueObservation/Reducers/Fetch.swift index 4f9b16c88f..6ab2f1583b 100644 --- a/GRDB/ValueObservation/Reducers/Fetch.swift +++ b/GRDB/ValueObservation/Reducers/Fetch.swift @@ -1,16 +1,24 @@ extension ValueReducers { /// A `ValueReducer` that perform database fetches. public struct Fetch: ValueReducer { - private let __fetch: (Database) throws -> Value + public struct _Fetcher: _ValueReducerFetcher { + let _fetch: @Sendable (Database) throws -> Value + + public func fetch(_ db: Database) throws -> Value { + assert(db.isInsideTransaction, "Fetching in a non-isolated way is illegal") + return try _fetch(db) + } + } + + private let _fetch: @Sendable (Database) throws -> Value /// Creates a reducer which passes raw fetched values through. - init(fetch: @escaping (Database) throws -> Value) { - self.__fetch = fetch + init(fetch: @escaping @Sendable (Database) throws -> Value) { + self._fetch = fetch } - public func _fetch(_ db: Database) throws -> Value { - assert(db.isInsideTransaction, "Fetching in a non-isolated way is illegal") - return try __fetch(db) + public func _makeFetcher() -> _Fetcher { + _Fetcher(_fetch: _fetch) } public func _value(_ fetched: Value) -> Value? { diff --git a/GRDB/ValueObservation/Reducers/Map.swift b/GRDB/ValueObservation/Reducers/Map.swift index b69984b248..0b90fda7f8 100644 --- a/GRDB/ValueObservation/Reducers/Map.swift +++ b/GRDB/ValueObservation/Reducers/Map.swift @@ -26,11 +26,11 @@ extension ValueObservation { } extension ValueReducers { - /// A `ValueReducer` whose values consist of those in a `Base` reduced + /// A `ValueReducer` whose values consist of those in a `Base` reducer /// passed through a transform function. /// /// See ``ValueObservation/map(_:)``. - public struct Map: _ValueReducer { + public struct Map: ValueReducer { private var base: Base private let transform: (Base.Value) throws -> Value @@ -39,15 +39,13 @@ extension ValueReducers { self.transform = transform } - public mutating func _value(_ fetched: Base.Fetched) throws -> Value? { + public func _makeFetcher() -> Base.Fetcher { + base._makeFetcher() + } + + public mutating func _value(_ fetched: Base.Fetcher.Value) throws -> Value? { guard let value = try base._value(fetched) else { return nil } return try transform(value) } } } - -extension ValueReducers.Map: ValueReducer where Base: ValueReducer { - public func _fetch(_ db: Database) throws -> Base.Fetched { - try base._fetch(db) - } -} diff --git a/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift b/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift index 6d2e64e537..6320664083 100644 --- a/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift +++ b/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift @@ -67,7 +67,7 @@ extension ValueReducers { /// previously observed value. /// /// See ``ValueObservation/removeDuplicates()``. - public struct RemoveDuplicates: _ValueReducer { + public struct RemoveDuplicates: ValueReducer { private var base: Base private var previousValue: Base.Value? private var predicate: (Base.Value, Base.Value) -> Bool @@ -77,7 +77,11 @@ extension ValueReducers { self.predicate = predicate } - public mutating func _value(_ fetched: Base.Fetched) throws -> Base.Value? { + public func _makeFetcher() -> Base.Fetcher { + base._makeFetcher() + } + + public mutating func _value(_ fetched: Base.Fetcher.Value) throws -> Base.Value? { guard let value = try base._value(fetched) else { return nil } @@ -90,9 +94,3 @@ extension ValueReducers { } } } - -extension ValueReducers.RemoveDuplicates: ValueReducer where Base: ValueReducer { - public func _fetch(_ db: Database) throws -> Base.Fetched { - try base._fetch(db) - } -} diff --git a/GRDB/ValueObservation/Reducers/Trace.swift b/GRDB/ValueObservation/Reducers/Trace.swift index bb2db4f623..107d80de78 100644 --- a/GRDB/ValueObservation/Reducers/Trace.swift +++ b/GRDB/ValueObservation/Reducers/Trace.swift @@ -4,12 +4,26 @@ extension ValueReducers { /// /// See ``ValueObservation/handleEvents(willStart:willFetch:willTrackRegion:databaseDidChange:didReceiveValue:didFail:didCancel:)`` /// and ``ValueObservation/print(_:to:)``. - public struct Trace: _ValueReducer { + public struct Trace: ValueReducer { + public struct _Fetcher: _ValueReducerFetcher { + let base: Base.Fetcher + let willFetch: @Sendable () -> Void + + public func fetch(_ db: Database) throws -> Base.Fetcher.Value { + willFetch() + return try base.fetch(db) + } + } + var base: Base - let willFetch: () -> Void + let willFetch: @Sendable () -> Void let didReceiveValue: (Base.Value) -> Void - public mutating func _value(_ fetched: Base.Fetched) throws -> Base.Value? { + public func _makeFetcher() -> _Fetcher { + _Fetcher(base: base._makeFetcher(), willFetch: willFetch) + } + + public mutating func _value(_ fetched: Base.Fetcher.Value) throws -> Base.Value? { guard let value = try base._value(fetched) else { return nil } @@ -19,10 +33,3 @@ extension ValueReducers { } // swiftlint:enable line_length } - -extension ValueReducers.Trace: ValueReducer where Base: ValueReducer { - public func _fetch(_ db: Database) throws -> Base.Fetched { - willFetch() - return try base._fetch(db) - } -} diff --git a/GRDB/ValueObservation/Reducers/ValueReducer.swift b/GRDB/ValueObservation/Reducers/ValueReducer.swift index 0a4706057f..52cf4e848e 100644 --- a/GRDB/ValueObservation/Reducers/ValueReducer.swift +++ b/GRDB/ValueObservation/Reducers/ValueReducer.swift @@ -1,11 +1,28 @@ +// A `ValueReducer` fetches and transforms the database values +// observed by a ``ValueObservation``. +// +// It is NOT Sendable, because we need `ValueReducers.RemoveDuplicates` to +// be able to call `Equatable.==`, which IS not a Sendable function. +// Thread-safety will be assured by `ValueObservation`, which will make sure +// it does not invoke the reducer concurrently. +// +// However, we need to be able to fetch from any database dispatch queue, +// and maybe concurrently. That's why a `ValueReducer` has a Sendable facet, +// which is its `Fetcher`. + /// Implementation details of `ValueReducer`. public protocol _ValueReducer { - /// The type of fetched database values - associatedtype Fetched + /// The Sendable type that fetches database values + associatedtype Fetcher: _ValueReducerFetcher /// The type of observed values associatedtype Value + /// Returns a value that fetches database values upon changes in an + /// observed database region. The returned value method must not depend + /// on the state of the reducer. + func _makeFetcher() -> Fetcher + /// Transforms a fetched value into an eventual observed value. Returns nil /// when observer should not be notified. /// @@ -18,7 +35,14 @@ public protocol _ValueReducer { /// reducer._value(...) // MUST NOT be nil /// reducer._value(...) // MAY be nil /// reducer._value(...) // MAY be nil - mutating func _value(_ fetched: Fetched) throws -> Value? + mutating func _value(_ fetched: Fetcher.Value) throws -> Value? +} + +public protocol _ValueReducerFetcher: Sendable { + /// The type of fetched database values + associatedtype Value + + func fetch(_ db: Database) throws -> Value } /// `ValueReducer` supports ``ValueObservation``. @@ -26,17 +50,15 @@ public protocol _ValueReducer { /// A `ValueReducer` fetches and transforms the database values /// observed by a ``ValueObservation``. /// +/// Do not declare new conformances to `ValueReducer`. Only the built-in +/// conforming types are valid. +/// /// ## Topics /// -/// ### Support +/// ### Supporting Types /// /// - ``ValueReducers`` -public protocol ValueReducer: _ValueReducer { - /// Fetches database values upon changes in an observed database region. - /// - /// This method must does not depend on the state of the reducer. - func _fetch(_ db: Database) throws -> Fetched -} +public protocol ValueReducer: _ValueReducer { } /// A namespace for concrete types that adopt the ``ValueReducer`` protocol. public enum ValueReducers { } diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index ae8ca3cc78..aa8a59f6dd 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -4,7 +4,7 @@ import Combine import Dispatch import Foundation -public struct ValueObservation { +public struct ValueObservation { var events = ValueObservationEvents() /// A boolean value indicating whether the observation requires write access @@ -176,7 +176,7 @@ extension ValueObservation: Refinable { /// when ValueObservation events occur. public func handleEvents( willStart: (() -> Void)? = nil, - willFetch: (() -> Void)? = nil, + willFetch: (@Sendable () -> Void)? = nil, willTrackRegion: ((DatabaseRegion) -> Void)? = nil, databaseDidChange: (() -> Void)? = nil, didReceiveValue: ((Reducer.Value) -> Void)? = nil, @@ -267,7 +267,9 @@ extension ValueObservation: Refinable { where Reducer: ValueReducer { var reducer = makeReducer() - guard let value = try reducer._value(reducer._fetch(db)) else { + let fetcher = reducer._makeFetcher() + let fetchedValue = try fetcher.fetch(db) + guard let value = try reducer._value(fetchedValue) else { fatalError("Broken contract: reducer has no initial value") } return value @@ -731,7 +733,7 @@ extension ValueObservation { /// /// - parameter fetch: The closure that fetches the observed value. public static func trackingConstantRegion( - _ fetch: @escaping (Database) throws -> Value) + _ fetch: @escaping @Sendable (Database) throws -> Value) -> Self where Reducer == ValueReducers.Fetch { @@ -805,7 +807,7 @@ extension ValueObservation { public static func tracking( region: any DatabaseRegionConvertible, _ otherRegions: any DatabaseRegionConvertible..., - fetch: @escaping (Database) throws -> Value) + fetch: @escaping @Sendable (Database) throws -> Value) -> Self where Reducer == ValueReducers.Fetch { @@ -874,7 +876,7 @@ extension ValueObservation { /// - parameter fetch: The closure that fetches the observed value. public static func tracking( regions: [any DatabaseRegionConvertible], - fetch: @escaping (Database) throws -> Value) + fetch: @escaping @Sendable (Database) throws -> Value) -> Self where Reducer == ValueReducers.Fetch { @@ -931,7 +933,7 @@ extension ValueObservation { /// /// - parameter fetch: The closure that fetches the observed value. public static func tracking( - _ fetch: @escaping (Database) throws -> Value) + _ fetch: @escaping @Sendable (Database) throws -> Value) -> Self where Reducer == ValueReducers.Fetch { diff --git a/TODO.md b/TODO.md index 004238c8d4..dd8551bb92 100644 --- a/TODO.md +++ b/TODO.md @@ -139,7 +139,7 @@ - [X] GRDB7: Sendable: DatabaseCancellable (2f93f00b, 8f486a5e) - [ ] GRDB7: ValueObservation closures - [?] GRDB7: DatabasePublishers.ValueSubscription -- [ ] GRDB7: Sendable: ValueObservation (93f6f982) +- [X] GRDB7: Sendable: ValueObservation (93f6f982) - [?] GRDB7: Not Sendable: SharedValueObservation - [ ] GRDB7: doc (c0838cf9) - [ ] GRDB7/BREAKING: PersistenceContainer is Sendable (50eefa8c) diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index a4151604ed..ffb1f1a0b8 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -235,26 +235,39 @@ extension FetchRequest { } /// A type-erased ValueReducer. -public struct AnyValueReducer: ValueReducer { - private var __fetch: (Database) throws -> Fetched +struct AnyValueReducer: ValueReducer { + private var __fetch: @Sendable (Database) throws -> Fetched private var __value: (Fetched) -> Value? - public init( - fetch: @escaping (Database) throws -> Fetched, + init( + fetch: @escaping @Sendable (Database) throws -> Fetched, value: @escaping (Fetched) -> Value?) { self.__fetch = fetch self.__value = value } - public func _fetch(_ db: Database) throws -> Fetched { - try __fetch(db) + func _makeFetcher() -> AnyValueReducerFetcher { + AnyValueReducerFetcher(fetch: __fetch) } - public func _value(_ fetched: Fetched) -> Value? { + func _value(_ fetched: Fetched) -> Value? { __value(fetched) } } +/// A type-erased _ValueReducerFetcher. +struct AnyValueReducerFetcher: _ValueReducerFetcher { + private var _fetch: @Sendable (Database) throws -> Fetched + + init(fetch: @escaping @Sendable (Database) throws -> Fetched) { + self._fetch = fetch + } + + func fetch(_ db: Database) throws -> Fetched { + try _fetch(db) + } +} + // Assume this is correct :-/ extension XCTestExpectation: @unchecked Sendable { } From c08b1fa7d5816d324c202d693c9781ed2622ca0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 31 Aug 2024 09:58:15 +0200 Subject: [PATCH 057/160] ValueObservationScheduler is Sendable --- GRDB/ValueObservation/ValueObservationScheduler.swift | 2 +- TODO.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GRDB/ValueObservation/ValueObservationScheduler.swift b/GRDB/ValueObservation/ValueObservationScheduler.swift index d43cad7dc3..df109cfee0 100644 --- a/GRDB/ValueObservation/ValueObservationScheduler.swift +++ b/GRDB/ValueObservation/ValueObservationScheduler.swift @@ -11,7 +11,7 @@ import Foundation /// - ``immediate`` /// - ``AsyncValueObservationScheduler`` /// - ``ImmediateValueObservationScheduler`` -public protocol ValueObservationScheduler { +public protocol ValueObservationScheduler: Sendable { /// Returns whether the initial value should be immediately notified. /// /// If the result is true, then this method was called on the main thread. diff --git a/TODO.md b/TODO.md index dd8551bb92..632aa72fe1 100644 --- a/TODO.md +++ b/TODO.md @@ -107,7 +107,7 @@ - [X] GRDB7: Sendable: DatabaseMigrator (22114ad4) - [X] GRDB7: Not Sendable: FilterCursor (b26e9709) - [X] GRDB7: Sendable: RowAdapter (d138af26) -- [ ] GRDB7: Sendable: ValueObservationScheduler (8429eb68) +- [X] GRDB7: Sendable: ValueObservationScheduler (8429eb68) - [X] GRDB7: Sendable: DatabaseCollation (4d9d67dd) - [X] GRDB7: Sendable: LogErrorFunction (f362518d) - [X] GRDB7: Sendable: ReadWriteBox (57a86a0e) From b1424d16d5a81b4b388e9022d7256624f85cfc15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 31 Aug 2024 10:08:03 +0200 Subject: [PATCH 058/160] [BREAKING] PersistenceContainer is Sendable --- GRDB/QueryInterface/SQL/SQLRelation.swift | 3 - GRDB/Record/EncodableRecord.swift | 97 ++++++++----------- .../Record/MutablePersistableRecord+DAO.swift | 10 +- .../MutablePersistableRecord+Insert.swift | 2 +- .../MutablePersistableRecord+Save.swift | 2 +- .../MutablePersistableRecord+Upsert.swift | 2 +- GRDB/Record/Record.swift | 3 +- GRDB/Record/TableRecord.swift | 2 +- TODO.md | 2 +- 9 files changed, 51 insertions(+), 72 deletions(-) diff --git a/GRDB/QueryInterface/SQL/SQLRelation.swift b/GRDB/QueryInterface/SQL/SQLRelation.swift index 2077537b2d..569fb5d8ad 100644 --- a/GRDB/QueryInterface/SQL/SQLRelation.swift +++ b/GRDB/QueryInterface/SQL/SQLRelation.swift @@ -1027,9 +1027,6 @@ extension Row: ColumnAddressable { /// PersistenceContainer has columns extension PersistenceContainer: ColumnAddressable { func index(forColumn column: String) -> String? { column } - func databaseValue(at column: String) -> DatabaseValue { - self[caseInsensitive: column]?.databaseValue ?? .null - } } // MARK: - Merging diff --git a/GRDB/Record/EncodableRecord.swift b/GRDB/Record/EncodableRecord.swift index 9e8e80e2c2..dd91c7586b 100644 --- a/GRDB/Record/EncodableRecord.swift +++ b/GRDB/Record/EncodableRecord.swift @@ -258,7 +258,7 @@ extension EncodableRecord { /// database representation. public var databaseDictionary: [String: DatabaseValue] { get throws { - try Dictionary(PersistenceContainer(self).storage).mapValues { $0?.databaseValue ?? .null } + try Dictionary(uniqueKeysWithValues: PersistenceContainer(self)) } } } @@ -336,19 +336,30 @@ extension EncodableRecord { /// /// `PersistenceContainer` is the argument of the /// ``EncodableRecord/encode(to:)-k9pf`` method. -public struct PersistenceContainer { - // fileprivate for Row(_:PersistenceContainer) +public struct PersistenceContainer: Sendable { // The ordering of the OrderedDictionary helps generating always the same // SQL queries, and hit the statement cache. - fileprivate var storage: OrderedDictionary + private var storage: OrderedDictionary /// The value associated with the given column. + /// + /// The setter accepts any ``DatabaseValueConvertible`` type, but the + /// getter always returns a ``DatabaseValue``. public subscript(_ column: String) -> (any DatabaseValueConvertible)? { - get { self[caseInsensitive: column] } - set { storage.updateValue(newValue, forKey: column) } + get { + storage[CaseInsensitiveIdentifier(rawValue: column)] + } + set { + storage.updateValue( + newValue?.databaseValue ?? .null, + forKey: CaseInsensitiveIdentifier(rawValue: column)) + } } /// The value associated with the given column. + /// + /// The setter accepts any ``DatabaseValueConvertible`` type, but the + /// getter always returns a ``DatabaseValue``. public subscript(_ column: some ColumnExpression) -> (any DatabaseValueConvertible)? { get { self[column.name] } set { self[column.name] = newValue } @@ -378,66 +389,25 @@ public struct PersistenceContainer { } /// Columns stored in the container, ordered like values. - var columns: [String] { Array(storage.keys) } + var columns: [String] { storage.keys.map(\.rawValue) } /// Values stored in the container, ordered like columns. - var values: [(any DatabaseValueConvertible)?] { Array(storage.values) } - - /// Accesses the value associated with the given column, in a - /// case-insensitive fashion. - subscript(caseInsensitive column: String) -> (any DatabaseValueConvertible)? { - get { - if let value = storage[column] { - return value - } - let lowercaseColumn = column.lowercased() - for (key, value) in storage where key.lowercased() == lowercaseColumn { - return value - } - return nil - } - set { - if storage[column] != nil { - storage[column] = newValue - return - } - let lowercaseColumn = column.lowercased() - for key in storage.keys where key.lowercased() == lowercaseColumn { - storage[key] = newValue - return - } - - storage[column] = newValue - } - } + var values: [DatabaseValue] { storage.values } - // Returns nil if column is not defined - func value(forCaseInsensitiveColumn column: String) -> DatabaseValue? { - let lowercaseColumn = column.lowercased() - for (key, value) in storage where key.lowercased() == lowercaseColumn { - return value?.databaseValue ?? .null - } - return nil - } - - var isEmpty: Bool { storage.isEmpty } - - /// An iterator over the (column, value) pairs - func makeIterator() -> IndexingIterator> { - storage.makeIterator() + /// Returns ``DatabaseValue/null`` if column is not defined + func databaseValue(at column: String) -> DatabaseValue { + storage[CaseInsensitiveIdentifier(rawValue: column)] ?? .null } @usableFromInline func changesIterator(from container: PersistenceContainer) -> AnyIterator<(String, DatabaseValue)> { - var newValueIterator = makeIterator() + var newValueIterator = storage.makeIterator() return AnyIterator { // Loop until we find a change, or exhaust columns: - while let (column, newValue) = newValueIterator.next() { - let oldValue = container[caseInsensitive: column] - let oldDbValue = oldValue?.databaseValue ?? .null - let newDbValue = newValue?.databaseValue ?? .null + while let (column, newDbValue) = newValueIterator.next() { + let oldDbValue = container.storage[column] ?? .null if newDbValue != oldDbValue { - return (column, oldDbValue) + return (column.rawValue, oldDbValue) } } return nil @@ -445,13 +415,26 @@ public struct PersistenceContainer { } } +extension PersistenceContainer: RandomAccessCollection { + public typealias Index = Int + + public var startIndex: Int { storage.startIndex } + public var endIndex: Int { storage.endIndex } + + /// Returns the (column, value) pair at given index. + public subscript(position: Int) -> (String, DatabaseValue) { + let element = storage[position] + return (element.key.rawValue, element.value) + } +} + extension Row { convenience init(_ record: Record) throws { try self.init(PersistenceContainer(record)) } convenience init(_ container: PersistenceContainer) { - self.init(Dictionary(container.storage)) + self.init(impl: ArrayRowImpl(columns: container.lazy.map { ($0, $1) })) } } diff --git a/GRDB/Record/MutablePersistableRecord+DAO.swift b/GRDB/Record/MutablePersistableRecord+DAO.swift index 48b9025c82..b63c1ab3a7 100644 --- a/GRDB/Record/MutablePersistableRecord+DAO.swift +++ b/GRDB/Record/MutablePersistableRecord+DAO.swift @@ -142,7 +142,7 @@ final class DAO { // Fail early if primary key does not resolve to a database row. let primaryKeyColumns = primaryKey.columns let primaryKeyValues = primaryKeyColumns.map { - persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + persistenceContainer.databaseValue(at: $0) } if primaryKeyValues.allSatisfy({ $0.isNull }) { return nil @@ -173,7 +173,7 @@ final class DAO { } let updatedValues = updatedColumns.map { - persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + persistenceContainer.databaseValue(at: $0) } let query = UpdateQuery( @@ -193,7 +193,7 @@ final class DAO { // Fail early if primary key does not resolve to a database row. let primaryKeyColumns = primaryKey.columns let primaryKeyValues = primaryKeyColumns.map { - persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + persistenceContainer.databaseValue(at: $0) } if primaryKeyValues.allSatisfy({ $0.isNull }) { return nil @@ -212,7 +212,7 @@ final class DAO { // Fail early if primary key does not resolve to a database row. let primaryKeyColumns = primaryKey.columns let primaryKeyValues = primaryKeyColumns.map { - persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + persistenceContainer.databaseValue(at: $0) } if primaryKeyValues.allSatisfy({ $0.isNull }) { return nil @@ -229,7 +229,7 @@ final class DAO { /// Throws a RecordError.recordNotFound error func recordNotFound() throws -> Never { let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map { - ($0, persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null) + ($0, persistenceContainer.databaseValue(at: $0)) }) throw RecordError.recordNotFound( databaseTableName: databaseTableName, diff --git a/GRDB/Record/MutablePersistableRecord+Insert.swift b/GRDB/Record/MutablePersistableRecord+Insert.swift index e089cb23c1..f63d9f938d 100644 --- a/GRDB/Record/MutablePersistableRecord+Insert.swift +++ b/GRDB/Record/MutablePersistableRecord+Insert.swift @@ -525,7 +525,7 @@ extension MutablePersistableRecord { // to false in its `aroundInsert` callback. var persistenceContainer = dao.persistenceContainer if let rowIDColumn { - persistenceContainer[caseInsensitive: rowIDColumn] = rowid + persistenceContainer[rowIDColumn] = rowid } let inserted = InsertionSuccess( diff --git a/GRDB/Record/MutablePersistableRecord+Save.swift b/GRDB/Record/MutablePersistableRecord+Save.swift index 6a238ac2f9..a579884330 100644 --- a/GRDB/Record/MutablePersistableRecord+Save.swift +++ b/GRDB/Record/MutablePersistableRecord+Save.swift @@ -409,7 +409,7 @@ extension MutablePersistableRecord { let primaryKeyInfo = try db.primaryKey(databaseTableName) let container = try PersistenceContainer(db, self) let primaryKey = Dictionary(uniqueKeysWithValues: primaryKeyInfo.columns.map { - ($0, container[caseInsensitive: $0]?.databaseValue ?? .null) + ($0, container.databaseValue(at: $0)) }) if primaryKey.allSatisfy({ $0.value.isNull }) { return nil diff --git a/GRDB/Record/MutablePersistableRecord+Upsert.swift b/GRDB/Record/MutablePersistableRecord+Upsert.swift index 4374c19601..567feee500 100644 --- a/GRDB/Record/MutablePersistableRecord+Upsert.swift +++ b/GRDB/Record/MutablePersistableRecord+Upsert.swift @@ -452,7 +452,7 @@ extension MutablePersistableRecord { var persistenceContainer = dao.persistenceContainer let rowIDColumn = dao.primaryKey.rowIDColumn if let rowIDColumn { - persistenceContainer[caseInsensitive: rowIDColumn] = rowid + persistenceContainer[rowIDColumn] = rowid } let inserted = InsertionSuccess( diff --git a/GRDB/Record/Record.swift b/GRDB/Record/Record.swift index cb7394fe14..67fbf6c0d2 100644 --- a/GRDB/Record/Record.swift +++ b/GRDB/Record/Record.swift @@ -201,8 +201,7 @@ open class Record { var newValueIterator = try PersistenceContainer(self).makeIterator() return AnyIterator { // Loop until we find a change, or exhaust columns: - while let (column, newValue) = newValueIterator.next() { - let newDbValue = newValue?.databaseValue ?? .null + while let (column, newDbValue) = newValueIterator.next() { guard let oldRow, let oldDbValue: DatabaseValue = oldRow[column] else { return (column, nil) } diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index db6edc45e0..4e9cd4485d 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -763,7 +763,7 @@ extension TableRecord where Self: EncodableRecord { let container = try PersistenceContainer(db, self) let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map { - ($0, container[caseInsensitive: $0]?.databaseValue ?? .null) + ($0, container.databaseValue(at: $0)) }) return RecordError.recordNotFound( databaseTableName: databaseTableName, diff --git a/TODO.md b/TODO.md index 632aa72fe1..9bb275b6d1 100644 --- a/TODO.md +++ b/TODO.md @@ -142,7 +142,7 @@ - [X] GRDB7: Sendable: ValueObservation (93f6f982) - [?] GRDB7: Not Sendable: SharedValueObservation - [ ] GRDB7: doc (c0838cf9) -- [ ] GRDB7/BREAKING: PersistenceContainer is Sendable (50eefa8c) +- [X] GRDB7/BREAKING: PersistenceContainer is Sendable (50eefa8c) - [ ] GRDB7: TableRecord.databaseSelection must be declared as a computed property (24d232aa) - [ ] GRDB7: Sendable: Association (b06aaee4) - [ ] GRDB7/Tests: Sendable: ValueObservationRecorder (2947b3d7) From e9d0d381f6b657895b2a6dcea8d3a5f8229e0fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 31 Aug 2024 10:48:51 +0200 Subject: [PATCH 059/160] Fix concurrency warning with DispatchQueue.mainKey --- GRDB/Utils/Utils.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GRDB/Utils/Utils.swift b/GRDB/Utils/Utils.swift index 90e3ca5573..40095bba8d 100644 --- a/GRDB/Utils/Utils.swift +++ b/GRDB/Utils/Utils.swift @@ -67,8 +67,8 @@ extension Dictionary { } extension DispatchQueue { - private static var mainKey: DispatchSpecificKey<()> = { - let key = DispatchSpecificKey<()>() + private static let mainKey: DispatchSpecificKey = { + let key = DispatchSpecificKey() DispatchQueue.main.setSpecific(key: key, value: ()) return key }() From b85121d456c8091e8cf6cf8f50cd4ba0d2e6311a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 16 Jun 2024 17:53:04 +0200 Subject: [PATCH 060/160] Database support for cancellation --- GRDB/Core/Database+Statements.swift | 14 +++ GRDB/Core/Database.swift | 138 ++++++++++++++++++++++------ GRDB/Core/Statement.swift | 2 +- 3 files changed, 127 insertions(+), 27 deletions(-) diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index b83006a19d..9c5119e3b1 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -439,6 +439,7 @@ extension Database { // documentation of this method for more information). try checkForAbortedTransaction(sql: statement.sql, arguments: statement.arguments) + // Cancelled database accesses must not execute. // Suspended databases must not execute statements that create the risk // of `0xdead10cc` exception (see the documentation of this method for // more information). @@ -491,6 +492,19 @@ extension Database { // and throws the user-provided cancelled commit error. try observationBroker?.statementDidFail(statement) + if #available(iOS 13, macOS 10.15, tvOS 13, *) { + switch ResultCode(rawValue: resultCode) { + case .SQLITE_INTERRUPT, .SQLITE_ABORT: + if suspensionMutex.load().isCancelled { + // The only error that a user sees when a Task is cancelled + // is CancellationError. + throw CancellationError() + } + default: + break + } + } + // Throw statement failure throw DatabaseError( resultCode: resultCode, diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index fb0715e4e6..da6acb67ab 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -312,18 +312,29 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// `isRecordingSelectedRegion` is true. var selectedRegion = DatabaseRegion() - /// Support for `checkForAbortedTransaction()` - var isInsideTransactionBlock = false - - /// Support for `checkForSuspensionViolation(from:)` - let isSuspendedMutex = Mutex(false) - /// Support for `checkForSuspensionViolation(from:)` /// This cache is never cleared: we assume journal mode never changes. var journalModeCache: String? + // MARK: - Suspension + + struct Suspension { + /// If true, the database is suspended and should not acquire any + /// write lock in order to avoid the 0xDEAD10CC exception. + var isSuspended: Bool + + /// If true, the database access has been cancelled. + var isCancelled: Bool + } + + /// Support for `checkForSuspensionViolation(from:)` + let suspensionMutex = Mutex(Suspension(isSuspended: false, isCancelled: false)) + // MARK: - Transaction Date + /// Support for `checkForAbortedTransaction()` + var isInsideTransactionBlock = false + enum AutocommitState { case off case on @@ -635,7 +646,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib guard code == SQLITE_OK else { // So there remain some unfinalized prepared statement somewhere. if let log = Self.logError { - if code == SQLITE_BUSY { + if ResultCode(rawValue: code).primaryResultCode == .SQLITE_BUSY { // Let the user know about unfinalized statements that did // prevent the connection from closing properly. var stmt: SQLiteStatement? = sqlite3_next_stmt(sqliteConnection, nil) @@ -1149,12 +1160,12 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// /// Suspension ends with `resume()`. func suspend() { - let needsInterrupt = isSuspendedMutex.withLock { isSuspended in - if isSuspended { + let needsInterrupt = suspensionMutex.withLock { suspension in + if suspension.isSuspended { return false } - isSuspended = true + suspension.isSuspended = true return true } @@ -1179,7 +1190,37 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// /// See suspend(). func resume() { - isSuspendedMutex.store(false) + suspensionMutex.withLock { + $0.isSuspended = false + } + } + + /// Cancels the current database access. All statements but ROLLBACK + /// will throw `CancellationError`, until `uncancel()` is called. + /// + /// This method can be called from any thread. + @available(iOS 13, macOS 10.15, tvOS 13, *) + func cancel() { + let needsInterrupt = suspensionMutex.withLock { suspension in + if suspension.isCancelled { + return false + } + + suspension.isCancelled = true + return true + } + + if needsInterrupt { + interrupt() + } + } + + /// Undo `cancel()`. + @available(iOS 13, macOS 10.15, tvOS 13, *) + func uncancel() { + suspensionMutex.withLock { + $0.isCancelled = false + } } /// Support for `checkForSuspensionViolation(from:)` @@ -1203,15 +1244,51 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib return journalMode } - /// If the database is suspended, and executing the statement would lock the - /// database in a way that may trigger the [`0xdead10cc` exception](https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report), - /// this method rollbacks the current transaction and throws `SQLITE_ABORT`. + /// Prevents a statement from running, if the database is suspended, or + /// if the current database access is cancelled by Task cancellation. + /// + /// Transaction rollbacks are always allowed. For other statements: + /// + /// - When database access is cancelled, this method + /// throws `CancellationError`. + /// + /// - When database is suspensed, and if the statement would lock the + /// database in a way that may trigger the 0xDEAD10CC exception, this + /// method rollbacks the current transaction and throws `SQLITE_ABORT`. /// - /// See `suspend()` and ``Configuration/observesSuspensionNotifications``. + /// See `cancel()`, `suspend()` and + /// ``Configuration/observesSuspensionNotifications``. func checkForSuspensionViolation(from statement: Statement) throws { - let needsAbort = try isSuspendedMutex.withLock { isSuspended in - guard isSuspended else { - return false + // No reason for suspension should prevent rollbacks: + // + // - A rollback releases the write lock when the database + // is interrupted, when preventing 0xDEAD10CC. + // + // - A rollback properly closes a transaction that fails because + // it runs in a Task that was cancelled. + // + // Finally, a rollback must be run by GRDB, not by a direct call + // to `sqlite3_exec`, so that transaction observers are + // properly notified. + if statement.transactionEffect == .rollbackTransaction { + return + } + + // How should we interrupt the statement? + enum Interrupt { + case abort // Rollback and throw SQLITE_ABORT + case cancel // Throw CancellationError + } + + let interrupt: Interrupt? = try suspensionMutex.withLock { suspension in + // Check for cancellation first, so that the only error that + // a user sees when a Task is cancelled is CancellationError. + if suspension.isCancelled { + return .cancel + } + + guard suspension.isSuspended else { + return nil } if try journalMode() == "wal" && statement.isReadonly { @@ -1222,7 +1299,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib // Those are not read-only: // - INSERT ... // - BEGIN IMMEDIATE TRANSACTION - return false + return nil } if statement.releasesDatabaseLock { @@ -1231,20 +1308,29 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib // - ROLLBACK // - ROLLBACK TRANSACTION TO SAVEPOINT // - RELEASE SAVEPOINT - return false + return nil } // Assume statement can acquire a write lock: abort. - return true + return .abort } - if needsAbort { + switch interrupt { + case nil: + break + + case .cancel: + if #available(iOS 13, macOS 10.15, tvOS 13, *) { + throw CancellationError() + } else { + // GRDB bug: cancellation is a Swift concurrency feature + fatalError("Can't cancel without support for Swift concurrency") + } + + case .abort: // Attempt at releasing an eventual lock with ROLLBACk, // as explained in Database.suspend(). - // - // Use sqlite3_exec instead of `try? rollback()` in order to avoid - // an infinite loop in checkForSuspensionViolation(from:) - _ = sqlite3_exec(sqliteConnection, "ROLLBACK", nil, nil, nil) + try? rollback() throw DatabaseError( resultCode: .SQLITE_ABORT, diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index 3956887093..ada39225ad 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -35,7 +35,7 @@ extension String { } public final class Statement { - enum TransactionEffect { + enum TransactionEffect: Equatable { case beginTransaction case commitTransaction case rollbackTransaction From 40140afb8ca8ad9251361db42889659cd197b219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 16 Jun 2024 17:53:25 +0200 Subject: [PATCH 061/160] SerializedDatabase support for async db access with support for Task cancellation --- GRDB/Core/SerializedDatabase.swift | 122 +++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index f1dfe05721..faa7b1af1d 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -243,6 +243,26 @@ final class SerializedDatabase { return try block(db) } + /// Asynchrously executes the block. + @available(iOS 13, macOS 10.15, tvOS 13, *) + func execute( + _ block: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + let dbAccess = CancellableDatabaseAccess() + return try await dbAccess.withCancellableContinuation { continuation in + self.async { db in + do { + let result = try dbAccess.inDatabase(db) { + try block(db) + } + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } + } + func interrupt() { // Intentionally not scheduled in our serial queue db.interrupt() @@ -286,3 +306,105 @@ final class SerializedDatabase { // It happens the job of SerializedDatabase is precisely to provide thread-safe // access to `Database`. extension SerializedDatabase: @unchecked Sendable { } + +// MARK: - Task Cancellation Support + +@available(iOS 13, macOS 10.15, tvOS 13, *) +enum DatabaseAccessCancellationState: @unchecked Sendable { + // @unchecked Sendable because database is only accessed from its + // dispatch queue. + case notConnected + case connected(Database) + case cancelled + case expired +} + +@available(iOS 13, macOS 10.15, tvOS 13, *) +typealias CancellableDatabaseAccess = Mutex + +/// Supports Task cancellation in async database accesses. +/// +/// Usage: +/// +/// ```swift +/// let dbAccess = CancellableDatabaseAccess() +/// return try dbAccess.withCancellableContinuation { continuation in +/// asyncDatabaseAccess { db in +/// do { +/// let result = try dbAccess.inDatabase(db) { +/// // Perform database operations +/// } +/// continuation.resume(returning: result) +/// } catch { +/// continuation.resume(throwing: error) +/// } +/// } +/// } +/// ``` +@available(iOS 13, macOS 10.15, tvOS 13, *) +extension CancellableDatabaseAccess: DatabaseCancellable { + convenience init() { + self.init(.notConnected) + } + + func cancel() { + withLock { state in + switch state { + case let .connected(db): + db.cancel() + state = .cancelled + case .notConnected: + state = .cancelled + case .cancelled, .expired: + break + } + } + } + + func withCancellableContinuation( + _ fn: (UnsafeContinuation) -> Void + ) async throws -> Value { + try await withTaskCancellationHandler { + try checkCancellation() + return try await withUnsafeThrowingContinuation { continuation in + fn(continuation) + } + } onCancel: { + cancel() + } + } + + func checkCancellation() throws { + try withLock { state in + if case .cancelled = state { + throw CancellationError() + } + } + } + + /// Wraps a full database access with cancellation support. When this + /// method returns, the database is NOT cancelled. + func inDatabase(_ db: Database, _ work: () throws -> sending Value) throws -> sending Value { + try withLock { state in + switch state { + case .connected, .expired: + fatalError("Can't use a CancellableDatabaseAccess twice") + case .notConnected: + state = .connected(db) + case .cancelled: + throw CancellationError() + } + } + + defer { + withLock { state in + if case .cancelled = state { + db.uncancel() + } + state = .expired + } + } + + return try work() + } +} From 5ecf9f67fb38a946ab83f3e64e00e2f6582e29d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 1 Sep 2024 12:27:52 +0200 Subject: [PATCH 062/160] Vendor AsyncSemaphore Semaphores allow tests to precisely schedule ordering of async jobs --- GRDB.xcodeproj/project.pbxproj | 4 + GRDBCustom.xcodeproj/project.pbxproj | 4 + .../GRDBTests.xcodeproj/project.pbxproj | 6 + .../GRDBTests.xcodeproj/project.pbxproj | 6 + Tests/GRDBTests/AsyncSemaphore.swift | 256 ++++++++++++++++++ 5 files changed, 276 insertions(+) create mode 100644 Tests/GRDBTests/AsyncSemaphore.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 4185082bb9..8cdbcc0aa8 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ 563363C01C942C04000BE133 /* DatabaseReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563363BF1C942C04000BE133 /* DatabaseReader.swift */; }; 563363C41C942C37000BE133 /* DatabaseWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563363C31C942C37000BE133 /* DatabaseWriter.swift */; }; 5636E9BC1D22574100B9B05F /* FetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5636E9BB1D22574100B9B05F /* FetchRequest.swift */; }; + 563866CB2C847659004C515A /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563866CA2C847654004C515A /* AsyncSemaphore.swift */; }; 563B06AB217EF0CC00B38F35 /* ValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06AA217EF0CC00B38F35 /* ValueObservation.swift */; }; 563B06BD2185CCD300B38F35 /* ValueObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06BC2185CCD300B38F35 /* ValueObservationTests.swift */; }; 563B06C72185D29F00B38F35 /* ValueObservationReadonlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06C22185D29F00B38F35 /* ValueObservationReadonlyTests.swift */; }; @@ -518,6 +519,7 @@ 563363D41C94484E000BE133 /* DatabaseQueueReleaseMemoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueReleaseMemoryTests.swift; sourceTree = ""; }; 5634B1061CF9B970005360B9 /* TransactionObserverSavepointsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionObserverSavepointsTests.swift; sourceTree = ""; }; 5636E9BB1D22574100B9B05F /* FetchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequest.swift; sourceTree = ""; }; + 563866CA2C847654004C515A /* AsyncSemaphore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSemaphore.swift; sourceTree = ""; }; 563B06AA217EF0CC00B38F35 /* ValueObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueObservation.swift; sourceTree = ""; }; 563B06BC2185CCD300B38F35 /* ValueObservationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueObservationTests.swift; sourceTree = ""; }; 563B06C22185D29F00B38F35 /* ValueObservationReadonlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationReadonlyTests.swift; sourceTree = ""; }; @@ -1010,6 +1012,7 @@ 56176C581EACC2D8000F3F2B /* GRDBTests */ = { isa = PBXGroup; children = ( + 563866CA2C847654004C515A /* AsyncSemaphore.swift */, 56677C14241D14450050755D /* FailureTestCase.swift */, 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, 563EA3E02C7B3A22001BE0D4 /* Mutex.swift */, @@ -2115,6 +2118,7 @@ 56D496681D813086008276D7 /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */, 5623934E1DEDFEFB00A6B01F /* EnumeratedCursorTests.swift in Sources */, 568068311EBBA26100EFB8AA /* SQLRequestTests.swift in Sources */, + 563866CB2C847659004C515A /* AsyncSemaphore.swift in Sources */, 56703297212B5450007D270F /* DatabaseUUIDEncodingStrategyTests.swift in Sources */, 5676FBA622F5CAD9004717D9 /* ValueObservationRegionRecordingTests.swift in Sources */, 56D496B41D8133F8008276D7 /* DatabaseTests.swift in Sources */, diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 04d29555ff..e1b707bca6 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -309,6 +309,7 @@ 56DF0015228DDB8300D611F3 /* AssociationPrefetchingCodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0013228DDB8200D611F3 /* AssociationPrefetchingCodableRecordTests.swift */; }; 56DF0017228DDB8300D611F3 /* AssociationPrefetchingRowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0014228DDB8200D611F3 /* AssociationPrefetchingRowTests.swift */; }; 56DF37A723D77AA0009AAA05 /* Refinable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF37A623D77AA0009AAA05 /* Refinable.swift */; }; + 56DFC3AE2C84794400DFE5DC /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DFC3AD2C84794400DFE5DC /* AsyncSemaphore.swift */; }; 56E4F7F92392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7F72392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift */; }; 56E9FAC42210468500C703A8 /* SQLInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FAC32210468500C703A8 /* SQLInterpolation.swift */; }; 56EA63C9209C7F1E009715B8 /* DerivableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EA63C7209C7F1E009715B8 /* DerivableRequestTests.swift */; }; @@ -830,6 +831,7 @@ 56DF0013228DDB8200D611F3 /* AssociationPrefetchingCodableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingCodableRecordTests.swift; sourceTree = ""; }; 56DF0014228DDB8200D611F3 /* AssociationPrefetchingRowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingRowTests.swift; sourceTree = ""; }; 56DF37A623D77AA0009AAA05 /* Refinable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Refinable.swift; sourceTree = ""; }; + 56DFC3AD2C84794400DFE5DC /* AsyncSemaphore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSemaphore.swift; sourceTree = ""; }; 56E4F7F72392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAbortedTransactionTests.swift; sourceTree = ""; }; 56E8CE0C1BB4FA5600828BEC /* DatabaseValueConvertibleFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleFetchTests.swift; sourceTree = ""; }; 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertibleFetchTests.swift; sourceTree = ""; }; @@ -1032,6 +1034,7 @@ 56176C581EACC2D8000F3F2B /* GRDBTests */ = { isa = PBXGroup; children = ( + 56DFC3AD2C84794400DFE5DC /* AsyncSemaphore.swift */, 567E4207242AB3CB00CAAD2C /* FailureTestCase.swift */, 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, 563EA3E22C7B3A3A001BE0D4 /* Mutex.swift */, @@ -2358,6 +2361,7 @@ 567DAF381EAB789800FC0928 /* DatabaseLogErrorTests.swift in Sources */, 569BBA2B228DE53200478429 /* AssociationPrefetchingFetchableRecordTests.swift in Sources */, 56F34FBF24B094D2007513FC /* SQLExpressionIsConstantTests.swift in Sources */, + 56DFC3AE2C84794400DFE5DC /* AsyncSemaphore.swift in Sources */, 5670329B212B5462007D270F /* DatabaseUUIDEncodingStrategyTests.swift in Sources */, 5616B50028B5F5490052017E /* SingletonRecordTest.swift in Sources */, 56F34FC624B0A0C9007513FC /* SQLIdentifyingColumnsTests.swift in Sources */, diff --git a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj index 2058f693fb..d4e67092f7 100644 --- a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj @@ -495,6 +495,8 @@ 568C3F802A5AB36900A2309D /* ForeignKeyDefinitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568C3F7E2A5AB36900A2309D /* ForeignKeyDefinitionTests.swift */; }; 5691D97527257CE40021D540 /* AvailableElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5691D97427257CE40021D540 /* AvailableElements.swift */; }; 5691D97627257CE40021D540 /* AvailableElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5691D97427257CE40021D540 /* AvailableElements.swift */; }; + 56DFC3B32C84798300DFE5DC /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DFC3B22C84798300DFE5DC /* AsyncSemaphore.swift */; }; + 56DFC3B42C84798300DFE5DC /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DFC3B22C84798300DFE5DC /* AsyncSemaphore.swift */; }; 56F61DF0283D484700AF9884 /* getThreadsCount.c in Sources */ = {isa = PBXBuildFile; fileRef = 56F61DEE283D484700AF9884 /* getThreadsCount.c */; }; 56F61DF1283D484700AF9884 /* getThreadsCount.c in Sources */ = {isa = PBXBuildFile; fileRef = 56F61DEE283D484700AF9884 /* getThreadsCount.c */; }; 5B33E6E34F941B4C839A714F /* (null) in Frameworks */ = {isa = PBXBuildFile; }; @@ -752,6 +754,7 @@ 567B5C4A2AD32F7000629622 /* DatabaseReaderDumpTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseReaderDumpTests.swift; sourceTree = ""; }; 568C3F7E2A5AB36900A2309D /* ForeignKeyDefinitionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForeignKeyDefinitionTests.swift; sourceTree = ""; }; 5691D97427257CE40021D540 /* AvailableElements.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvailableElements.swift; sourceTree = ""; }; + 56DFC3B22C84798300DFE5DC /* AsyncSemaphore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSemaphore.swift; sourceTree = ""; }; 56F61DEC283D484700AF9884 /* GRDBTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GRDBTests-Bridging-Header.h"; sourceTree = ""; }; 56F61DEE283D484700AF9884 /* getThreadsCount.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = getThreadsCount.c; sourceTree = ""; }; 56F61DEF283D484700AF9884 /* getThreadsCount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = getThreadsCount.h; sourceTree = ""; }; @@ -884,6 +887,7 @@ 56419CAD24A54054004967E1 /* AssociationPrefetchingSQLTests.swift */, 56419D5824A54061004967E1 /* AssociationRowScopeSearchTests.swift */, 56419D6324A54062004967E1 /* AssociationTableAliasTestsSQLTests.swift */, + 56DFC3B22C84798300DFE5DC /* AsyncSemaphore.swift */, 565A27CB27871FFF00659A62 /* BackupTestCase.swift */, 567B5C272AD32A2D00629622 /* CaseInsensitiveIdentifierTests.swift */, 56419CD424A54057004967E1 /* CGFloatTests.swift */, @@ -1493,6 +1497,7 @@ 56419ED724A54063004967E1 /* ColumnInfoTests.swift in Sources */, 56419D9F24A54062004967E1 /* CompilationSubClassTests.swift in Sources */, 567B5C3F2AD32A2D00629622 /* CaseInsensitiveIdentifierTests.swift in Sources */, + 56DFC3B32C84798300DFE5DC /* AsyncSemaphore.swift in Sources */, 56419DBB24A54062004967E1 /* ValueObservationRegionRecordingTests.swift in Sources */, 56419EA924A54063004967E1 /* DatabasePoolReleaseMemoryTests.swift in Sources */, 56419E7F24A54063004967E1 /* DatabaseQueueConcurrencyTests.swift in Sources */, @@ -1742,6 +1747,7 @@ 56419ED824A54063004967E1 /* ColumnInfoTests.swift in Sources */, 56419DA024A54062004967E1 /* CompilationSubClassTests.swift in Sources */, 567B5C402AD32A2D00629622 /* CaseInsensitiveIdentifierTests.swift in Sources */, + 56DFC3B42C84798300DFE5DC /* AsyncSemaphore.swift in Sources */, 56419DBC24A54062004967E1 /* ValueObservationRegionRecordingTests.swift in Sources */, 56419EAA24A54063004967E1 /* DatabasePoolReleaseMemoryTests.swift in Sources */, 56419E8024A54063004967E1 /* DatabaseQueueConcurrencyTests.swift in Sources */, diff --git a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj index d499535f3c..1359a5a85c 100644 --- a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj @@ -497,6 +497,8 @@ 568C3F982A5AB3A800A2309D /* RecordMinimalNonOptionalPrimaryKeySingleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568C3F8A2A5AB3A800A2309D /* RecordMinimalNonOptionalPrimaryKeySingleTests.swift */; }; 5691D97227257C930021D540 /* AvailableElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5691D97127257C930021D540 /* AvailableElements.swift */; }; 5691D97327257C930021D540 /* AvailableElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5691D97127257C930021D540 /* AvailableElements.swift */; }; + 56DFC3B02C84797400DFE5DC /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DFC3AF2C84797400DFE5DC /* AsyncSemaphore.swift */; }; + 56DFC3B12C84797400DFE5DC /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DFC3AF2C84797400DFE5DC /* AsyncSemaphore.swift */; }; 56F61DF6283D4AB100AF9884 /* getThreadsCount.c in Sources */ = {isa = PBXBuildFile; fileRef = 56F61DF4283D4AB100AF9884 /* getThreadsCount.c */; }; 56F61DF7283D4AB100AF9884 /* getThreadsCount.c in Sources */ = {isa = PBXBuildFile; fileRef = 56F61DF4283D4AB100AF9884 /* getThreadsCount.c */; }; 5B33E6E34F941B4C839A714F /* (null) in Frameworks */ = {isa = PBXBuildFile; }; @@ -755,6 +757,7 @@ 568C3F892A5AB3A800A2309D /* DatabaseSnapshotPoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSnapshotPoolTests.swift; sourceTree = ""; }; 568C3F8A2A5AB3A800A2309D /* RecordMinimalNonOptionalPrimaryKeySingleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordMinimalNonOptionalPrimaryKeySingleTests.swift; sourceTree = ""; }; 5691D97127257C930021D540 /* AvailableElements.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvailableElements.swift; sourceTree = ""; }; + 56DFC3AF2C84797400DFE5DC /* AsyncSemaphore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSemaphore.swift; sourceTree = ""; }; 56F61DF2283D4AB100AF9884 /* GRDBTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GRDBTests-Bridging-Header.h"; sourceTree = ""; }; 56F61DF4283D4AB100AF9884 /* getThreadsCount.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = getThreadsCount.c; sourceTree = ""; }; 56F61DF5283D4AB100AF9884 /* getThreadsCount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = getThreadsCount.h; sourceTree = ""; }; @@ -888,6 +891,7 @@ 56419F4424A54097004967E1 /* AssociationPrefetchingSQLTests.swift */, 56419F5024A54098004967E1 /* AssociationRowScopeSearchTests.swift */, 56419F9E24A5409D004967E1 /* AssociationTableAliasTestsSQLTests.swift */, + 56DFC3AF2C84797400DFE5DC /* AsyncSemaphore.swift */, 565A27C827871FE500659A62 /* BackupTestCase.swift */, 568C3F882A5AB3A800A2309D /* CaseInsensitiveIdentifierTests.swift */, 56419F6B24A5409A004967E1 /* CGFloatTests.swift */, @@ -1499,6 +1503,7 @@ 5641A02424A540A1004967E1 /* ValueObservationPrintTests.swift in Sources */, 5641A12024A540A1004967E1 /* AssociationHasManyThroughOrderingTests.swift in Sources */, 5641A04224A540A1004967E1 /* FTS5TableBuilderTests.swift in Sources */, + 56DFC3B02C84797400DFE5DC /* AsyncSemaphore.swift in Sources */, 5641A10624A540A1004967E1 /* TableRecord+QueryInterfaceRequestTests.swift in Sources */, 5641A11A24A540A1004967E1 /* StatementColumnConvertibleFetchTests.swift in Sources */, 5641A05424A540A1004967E1 /* DatabaseWriterTests.swift in Sources */, @@ -1748,6 +1753,7 @@ 5641A02524A540A1004967E1 /* ValueObservationPrintTests.swift in Sources */, 5641A12124A540A1004967E1 /* AssociationHasManyThroughOrderingTests.swift in Sources */, 5641A04324A540A1004967E1 /* FTS5TableBuilderTests.swift in Sources */, + 56DFC3B12C84797400DFE5DC /* AsyncSemaphore.swift in Sources */, 5641A10724A540A1004967E1 /* TableRecord+QueryInterfaceRequestTests.swift in Sources */, 5641A11B24A540A1004967E1 /* StatementColumnConvertibleFetchTests.swift in Sources */, 5641A05524A540A1004967E1 /* DatabaseWriterTests.swift in Sources */, diff --git a/Tests/GRDBTests/AsyncSemaphore.swift b/Tests/GRDBTests/AsyncSemaphore.swift new file mode 100644 index 0000000000..6e8d474b30 --- /dev/null +++ b/Tests/GRDBTests/AsyncSemaphore.swift @@ -0,0 +1,256 @@ +// Vendored from + +// Copyright (C) 2022 Gwendal Roué +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import Foundation + +/// An object that controls access to a resource across multiple execution +/// contexts through use of a traditional counting semaphore. +/// +/// You increment a semaphore count by calling the ``signal()`` method, and +/// decrement a semaphore count by calling ``wait()`` or one of its variants. +/// +/// ## Topics +/// +/// ### Creating a Semaphore +/// +/// - ``init(value:)`` +/// +/// ### Signaling the Semaphore +/// +/// - ``signal()`` +/// +/// ### Waiting for the Semaphore +/// +/// - ``wait()`` +/// - ``waitUnlessCancelled()`` +@available(iOS 13, macOS 10.15, tvOS 13, *) +public final class AsyncSemaphore: @unchecked Sendable { + /// `Suspension` is the state of a task waiting for a signal. + /// + /// It is a class because instance identity helps `waitUnlessCancelled()` + /// deal with both early and late cancellation. + /// + /// We make it @unchecked Sendable in order to prevent compiler warnings: + /// instances are always protected by the semaphore's lock. + private class Suspension: @unchecked Sendable { + enum State { + /// Initial state. Next is suspendedUnlessCancelled, or cancelled. + case pending + + /// Waiting for a signal, with support for cancellation. + case suspendedUnlessCancelled(UnsafeContinuation) + + /// Waiting for a signal, with no support for cancellation. + case suspended(UnsafeContinuation) + + /// Cancelled before we have started waiting. + case cancelled + } + + var state: State + + init(state: State) { + self.state = state + } + } + + // MARK: - Internal State + + /// The semaphore value. + private var value: Int + + /// As many elements as there are suspended tasks waiting for a signal. + private var suspensions: [Suspension] = [] + + /// The lock that protects `value` and `suspensions`. + /// + /// It is recursive in order to handle cancellation (see the implementation + /// of ``waitUnlessCancelled()``). + private let _lock = NSRecursiveLock() + + // MARK: - Creating a Semaphore + + /// Creates a semaphore. + /// + /// - parameter value: The starting value for the semaphore. Do not pass a + /// value less than zero. + public init(value: Int) { + precondition(value >= 0, "AsyncSemaphore requires a value equal or greater than zero") + self.value = value + } + + deinit { + precondition(suspensions.isEmpty, "AsyncSemaphore is deallocated while some task(s) are suspended waiting for a signal.") + } + + // MARK: - Locking + + // Let's hide the locking primitive in order to avoid a compiler warning: + // + // > Instance method 'lock' is unavailable from asynchronous contexts; + // > Use async-safe scoped locking instead; this is an error in Swift 6. + // + // We're not sweeping bad stuff under the rug. We really need to protect + // our inner state (`value` and `suspension`) across the calls to + // `withUnsafeContinuation`. Unfortunately, this method introduces a + // suspension point. So we need a lock. + private func lock() { _lock.lock() } + private func unlock() { _lock.unlock() } + + // MARK: - Waiting for the Semaphore + + /// Waits for, or decrements, a semaphore. + /// + /// Decrement the counting semaphore. If the resulting value is less than + /// zero, this function suspends the current task until a signal occurs, + /// without blocking the underlying thread. Otherwise, no suspension happens. + public func wait() async { + lock() + + value -= 1 + if value >= 0 { + unlock() + return + } + + await withUnsafeContinuation { continuation in + // Register the continuation that `signal` will resume. + let suspension = Suspension(state: .suspended(continuation)) + suspensions.insert(suspension, at: 0) // FIFO + unlock() + } + } + + /// Waits for, or decrements, a semaphore, with support for cancellation. + /// + /// Decrement the counting semaphore. If the resulting value is less than + /// zero, this function suspends the current task until a signal occurs, + /// without blocking the underlying thread. Otherwise, no suspension happens. + /// + /// If the task is canceled before a signal occurs, this function + /// throws `CancellationError`. + public func waitUnlessCancelled() async throws { + lock() + + value -= 1 + if value >= 0 { + defer { unlock() } + + do { + // All code paths check for cancellation + try Task.checkCancellation() + } catch { + // Cancellation is like a signal: we don't really "consume" + // the semaphore, and restore the value. + value += 1 + throw error + } + + return + } + + // Get ready for being suspended waiting for a continuation, or for + // early cancellation. + let suspension = Suspension(state: .pending) + + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in + if case .cancelled = suspension.state { + // Early cancellation: waitUnlessCancelled() is called from + // a cancelled task, and the `onCancel` closure below + // has marked the suspension as cancelled. + // Resume with a CancellationError. + unlock() + continuation.resume(throwing: CancellationError()) + } else { + // Current task is not cancelled: register the continuation + // that `signal` will resume. + suspension.state = .suspendedUnlessCancelled(continuation) + suspensions.insert(suspension, at: 0) // FIFO + unlock() + } + } + } onCancel: { + // withTaskCancellationHandler may immediately call this block (if + // the current task is cancelled), or call it later (if the task is + // cancelled later). In the first case, we're still holding the lock, + // waiting for the continuation. In the second case, we do not hold + // the lock. Being able to handle both situations is the reason why + // we use a recursive lock. + lock() + + // We're no longer waiting for a signal + value += 1 + if let index = suspensions.firstIndex(where: { $0 === suspension }) { + suspensions.remove(at: index) + } + + if case let .suspendedUnlessCancelled(continuation) = suspension.state { + // Late cancellation: the task is cancelled while waiting + // from the semaphore. Resume with a CancellationError. + unlock() + continuation.resume(throwing: CancellationError()) + } else { + // Early cancellation: waitUnlessCancelled() is called from + // a cancelled task. + // + // The next step is the `withTaskCancellationHandler` + // operation closure right above. + suspension.state = .cancelled + unlock() + } + } + } + + // MARK: - Signaling the Semaphore + + /// Signals (increments) a semaphore. + /// + /// Increment the counting semaphore. If the previous value was less than + /// zero, this function resumes a task currently suspended in ``wait()`` + /// or ``waitUnlessCancelled()``. + /// + /// - returns: This function returns true if a suspended task is + /// resumed. Otherwise, the result is false, meaning that no task was + /// waiting for the semaphore. + @discardableResult + public func signal() -> Bool { + lock() + + value += 1 + + switch suspensions.popLast()?.state { // FIFO + case let .suspendedUnlessCancelled(continuation): + unlock() + continuation.resume() + return true + case let .suspended(continuation): + unlock() + continuation.resume() + return true + default: + unlock() + return false + } + } +} From fa54b74eba9380bb6a8eda428f7a4c83055d93c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 1 Sep 2024 12:33:03 +0200 Subject: [PATCH 063/160] [BREAKING] DatabaseWriter async methods support Task cancellation --- GRDB/Core/DatabasePool.swift | 28 ++ GRDB/Core/DatabaseQueue.swift | 14 + GRDB/Core/DatabaseWriter.swift | 202 ++++++------- GRDB/Core/SerializedDatabase.swift | 2 +- Tests/GRDBTests/DatabaseWriterTests.swift | 333 ++++++++++++++++++++++ 5 files changed, 479 insertions(+), 100 deletions(-) diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index e6019cd724..85d1ab951d 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -721,6 +721,13 @@ extension DatabasePool: DatabaseWriter { try writer.sync(updates) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func writeWithoutTransaction( + _ updates: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await writer.execute(updates) + } + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T { guard let readerPool else { @@ -731,6 +738,27 @@ extension DatabasePool: DatabaseWriter { } } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func barrierWriteWithoutTransaction( + _ updates: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + let dbAccess = CancellableDatabaseAccess() + return try await dbAccess.withCancellableContinuation { continuation in + asyncBarrierWriteWithoutTransaction { dbResult in + do { + try dbAccess.checkCancellation() + let db = try dbResult.get() + let result = try dbAccess.inDatabase(db) { + try updates(db) + } + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } + } + public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { guard let readerPool else { updates(.failure(DatabaseError.connectionIsClosed())) diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index b6cf6a5b27..3c34be6a89 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -362,11 +362,25 @@ extension DatabaseQueue: DatabaseWriter { try writer.sync(updates) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func writeWithoutTransaction( + _ updates: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await writer.execute(updates) + } + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T { try writer.sync(updates) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func barrierWriteWithoutTransaction( + _ updates: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await writer.execute(updates) + } + public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { writer.async { updates(.success($0)) } } diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index e92334047b..a1d18fbe6c 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -26,14 +26,14 @@ import Dispatch /// - ``writePublisher(receiveOn:updates:)`` /// - ``writePublisher(receiveOn:updates:thenRead:)`` /// - ``writeWithoutTransaction(_:)-4qh1w`` -/// - ``writeWithoutTransaction(_:)-tckw`` +/// - ``writeWithoutTransaction(_:)-4kzng`` /// - ``asyncWrite(_:completion:)`` /// - ``asyncWriteWithoutTransaction(_:)`` /// /// ### Exclusive Access to the Database /// /// - ``barrierWriteWithoutTransaction(_:)-280j1`` -/// - ``barrierWriteWithoutTransaction(_:)-7u4xw`` +/// - ``barrierWriteWithoutTransaction(_:)-6py5x`` /// - ``asyncBarrierWriteWithoutTransaction(_:)`` /// /// ### Reading from the Latest Committed Database State @@ -100,6 +100,40 @@ public protocol DatabaseWriter: DatabaseReader { @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails func writeWithoutTransaction(_ updates: (Database) throws -> T) rethrows -> T + /// Executes database operations, and returns their result after they have + /// finished executing. + /// + /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) + /// + /// For example: + /// + /// ```swift + /// let newPlayerCount = try await writer.writeWithoutTransaction { db in + /// try Player(name: "Arthur").insert(db) + /// return try Player.fetchCount(db) + /// } + /// ``` + /// + /// Database operations run in the writer dispatch queue, serialized + /// with all database updates performed by this `DatabaseWriter`. + /// + /// The ``Database`` argument to `updates` is valid only during the + /// execution of the closure. Do not store or return the database connection + /// for later use. + /// + /// - warning: Database operations are not wrapped in a transaction. They + /// can see changes performed by concurrent writes or writes performed by + /// other processes: two identical requests performed by the `updates` + /// closure may not return the same value. Concurrent database accesses + /// can see partial updates performed by the `updates` closure. + /// + /// - parameter updates: A closure which accesses the database. + /// - throws: The error thrown by `updates`. + @available(iOS 13, macOS 10.15, tvOS 13, *) + func writeWithoutTransaction( + _ updates: @Sendable @escaping (Database) throws -> T + ) async throws -> T + /// Executes database operations, and returns their result after they have /// finished executing. /// @@ -140,6 +174,50 @@ public protocol DatabaseWriter: DatabaseReader { @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T + /// Executes database operations, and returns their result after they have + /// finished executing. + /// + /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) + /// + /// Database operations are not executed until all currently executing + /// database accesses performed by the database writer finish executing + /// (both reads and writes). At that point, database operations are + /// executed. Once they finish, the database writer can proceed with other + /// database accesses. + /// + /// For example: + /// + /// ```swift + /// let newPlayerCount = try await writer.barrierWriteWithoutTransaction { db in + /// try Player(name: "Arthur").insert(db) + /// return try Player.fetchCount(db) + /// } + /// ``` + /// + /// Database operations run in the writer dispatch queue, serialized + /// with all database updates performed by this `DatabaseWriter`. + /// + /// The ``Database`` argument to `updates` is valid only during the + /// execution of the closure. Do not store or return the database connection + /// for later use. + /// + /// It is a programmer error to call this method from another database + /// access method. Doing so raises a "Database methods are not reentrant" + /// fatal error at runtime. + /// + /// - warning: Database operations are not wrapped in a transaction. They + /// can see changes performed by concurrent writes or writes performed by + /// other processes: two identical requests performed by the `updates` + /// closure may not return the same value. Concurrent database accesses + /// can see partial updates performed by the `updates` closure. + /// + /// - parameter updates: A closure which accesses the database. + /// - throws: The error thrown by `updates`. + @available(iOS 13, macOS 10.15, tvOS 13, *) + func barrierWriteWithoutTransaction( + _ updates: @Sendable @escaping (Database) throws -> T) + async throws -> T + /// Schedules database operations for execution, and returns immediately. /// /// Database operations are not executed until all currently executing @@ -563,104 +641,16 @@ extension DatabaseWriter { /// would happen while establishing the database access or committing /// the transaction. @available(iOS 13, macOS 10.15, tvOS 13, *) - public func write(_ updates: @Sendable @escaping (Database) throws -> T) async throws -> T { - try await withUnsafeThrowingContinuation { continuation in - asyncWrite(updates, completion: { _, result in - continuation.resume(with: result) - }) - } - } - - /// Executes database operations, and returns their result after they have - /// finished executing. - /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// - /// For example: - /// - /// ```swift - /// let newPlayerCount = try await writer.writeWithoutTransaction { db in - /// try Player(name: "Arthur").insert(db) - /// return try Player.fetchCount(db) - /// } - /// ``` - /// - /// Database operations run in the writer dispatch queue, serialized - /// with all database updates performed by this `DatabaseWriter`. - /// - /// The ``Database`` argument to `updates` is valid only during the - /// execution of the closure. Do not store or return the database connection - /// for later use. - /// - /// - warning: Database operations are not wrapped in a transaction. They - /// can see changes performed by concurrent writes or writes performed by - /// other processes: two identical requests performed by the `updates` - /// closure may not return the same value. Concurrent database accesses - /// can see partial updates performed by the `updates` closure. - /// - /// - parameter updates: A closure which accesses the database. - /// - throws: The error thrown by `updates`. - @available(iOS 13, macOS 10.15, tvOS 13, *) - public func writeWithoutTransaction(_ updates: @Sendable @escaping (Database) throws -> T) async throws -> T { - try await withUnsafeThrowingContinuation { continuation in - asyncWriteWithoutTransaction { db in - do { - try continuation.resume(returning: updates(db)) - } catch { - continuation.resume(throwing: error) - } - } - } - } - - /// Executes database operations, and returns their result after they have - /// finished executing. - /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// - /// Database operations are not executed until all currently executing - /// database accesses performed by the database writer finish executing - /// (both reads and writes). At that point, database operations are - /// executed. Once they finish, the database writer can proceed with other - /// database accesses. - /// - /// For example: - /// - /// ```swift - /// let newPlayerCount = try await writer.barrierWriteWithoutTransaction { db in - /// try Player(name: "Arthur").insert(db) - /// return try Player.fetchCount(db) - /// } - /// ``` - /// - /// Database operations run in the writer dispatch queue, serialized - /// with all database updates performed by this `DatabaseWriter`. - /// - /// The ``Database`` argument to `updates` is valid only during the - /// execution of the closure. Do not store or return the database connection - /// for later use. - /// - /// It is a programmer error to call this method from another database - /// access method. Doing so raises a "Database methods are not reentrant" - /// fatal error at runtime. - /// - /// - warning: Database operations are not wrapped in a transaction. They - /// can see changes performed by concurrent writes or writes performed by - /// other processes: two identical requests performed by the `updates` - /// closure may not return the same value. Concurrent database accesses - /// can see partial updates performed by the `updates` closure. - /// - /// - parameter updates: A closure which accesses the database. - /// - throws: The error thrown by `updates`. - @available(iOS 13, macOS 10.15, tvOS 13, *) - public func barrierWriteWithoutTransaction( - _ updates: @Sendable @escaping (Database) throws -> T) - async throws -> T - { - try await withUnsafeThrowingContinuation { continuation in - asyncBarrierWriteWithoutTransaction { dbResult in - continuation.resume(with: dbResult.flatMap { db in Result { try updates(db) } }) + public func write( + _ updates: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await writeWithoutTransaction { db in + var result: T? + try db.inTransaction { + result = try updates(db) + return .commit } + return result! } } @@ -951,11 +941,25 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.writeWithoutTransaction(updates) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func writeWithoutTransaction( + _ updates: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await base.writeWithoutTransaction(updates) + } + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T { try base.barrierWriteWithoutTransaction(updates) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func barrierWriteWithoutTransaction( + _ updates: @Sendable @escaping (Database) throws -> T) + async throws -> T { + try await base.barrierWriteWithoutTransaction(updates) + } + public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { base.asyncBarrierWriteWithoutTransaction(updates) } diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index faa7b1af1d..fed985b562 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -384,7 +384,7 @@ extension CancellableDatabaseAccess: DatabaseCancellable { /// Wraps a full database access with cancellation support. When this /// method returns, the database is NOT cancelled. - func inDatabase(_ db: Database, _ work: () throws -> sending Value) throws -> sending Value { + func inDatabase(_ db: Database, execute work: () throws -> Value) throws -> Value { try withLock { state in switch state { case .connected, .expired: diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 581f7d3070..1812637642 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -427,4 +427,337 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) } + + // MARK: - Task Cancellation + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let task = Task { + await semaphore.wait() + try await dbWriter.writeWithoutTransaction { db in + XCTFail("Should not be executed") + } + } + task.cancel() + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_statement_execution_from_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.writeWithoutTransaction { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + try db.execute(sql: "SELECT 0") + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_cursor_iteration_from_writeWithoutTransaction_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.writeWithoutTransaction { db in + let cursor = try Int.fetchCursor(db, sql: """ + SELECT 1 UNION ALL SELECT 2 + """) + _ = try cursor.next() + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + _ = try cursor.next() + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_write_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let task = Task { + await semaphore.wait() + try await dbWriter.write { db in + XCTFail("Should not be executed") + } + } + task.cancel() + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.write { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_write_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.write { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.write { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_statement_execution_from_write_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.write { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + try db.execute(sql: "SELECT 0") + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.write { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_cursor_iteration_from_write_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.write { db in + let cursor = try Int.fetchCursor(db, sql: """ + SELECT 1 UNION ALL SELECT 2 + """) + _ = try cursor.next() + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + _ = try cursor.next() + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.write { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let task = Task { + await semaphore.wait() + try await dbWriter.barrierWriteWithoutTransaction { db in + XCTFail("Should not be executed") + } + } + task.cancel() + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.barrierWriteWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_statement_execution_from_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.barrierWriteWithoutTransaction { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + try db.execute(sql: "SELECT 0") + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.barrierWriteWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_cursor_iteration_from_barrierWriteWithoutTransaction_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.barrierWriteWithoutTransaction { db in + let cursor = try Int.fetchCursor(db, sql: """ + SELECT 1 UNION ALL SELECT 2 + """) + _ = try cursor.next() + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + _ = try cursor.next() + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.barrierWriteWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } } From d6b91eec85afa2fa012d2e2b070b68c3db693792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 1 Sep 2024 13:07:49 +0200 Subject: [PATCH 064/160] [BREAKING] DatabaseReader async methods support Task cancellation --- GRDB/Core/DatabasePool.swift | 74 +++++++ GRDB/Core/DatabaseQueue.swift | 19 ++ GRDB/Core/DatabaseReader.swift | 179 +++++++++-------- GRDB/Core/DatabaseSnapshot.swift | 20 ++ GRDB/Core/DatabaseSnapshotPool.swift | 45 +++++ GRDB/Core/DatabaseWriter.swift | 14 ++ GRDB/Documentation.docc/Concurrency.md | 2 +- Tests/GRDBTests/DatabaseReaderTests.swift | 226 ++++++++++++++++++++++ 8 files changed, 486 insertions(+), 93 deletions(-) diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index 85d1ab951d..b0f5c1b1a6 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -351,6 +351,45 @@ extension DatabasePool: DatabaseReader { } } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func read( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.") + guard let readerPool else { + throw DatabaseError.connectionIsClosed() + } + + let dbAccess = CancellableDatabaseAccess() + return try await dbAccess.withCancellableContinuation { continuation in + readerPool.asyncGet { result in + do { + let (reader, releaseReader) = try result.get() + // Second async jump because that's how `Pool.async` has to be used. + reader.async { db in + defer { + try? db.commit() // Ignore commit error + releaseReader(.reuse) + } + do { + let result = try dbAccess.inDatabase(db) { + // The block isolation comes from the DEFERRED transaction. + try db.beginTransaction(.deferred) + try db.clearSchemaCacheIfNeeded() + return try value(db) + } + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } catch { + continuation.resume(throwing: error) + } + } + } + } + public func asyncRead(_ value: @escaping (Result) -> Void) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) @@ -395,6 +434,41 @@ extension DatabasePool: DatabaseReader { } } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func unsafeRead( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + guard let readerPool else { + throw DatabaseError.connectionIsClosed() + } + + let dbAccess = CancellableDatabaseAccess() + return try await dbAccess.withCancellableContinuation { continuation in + readerPool.asyncGet { result in + do { + let (reader, releaseReader) = try result.get() + // Second async jump because that's how `Pool.async` has to be used. + reader.async { db in + defer { + releaseReader(.reuse) + } + do { + let result = try dbAccess.inDatabase(db) { + try db.clearSchemaCacheIfNeeded() + return try value(db) + } + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } catch { + continuation.resume(throwing: error) + } + } + } + } + public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index 3c34be6a89..a1175feeec 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -233,6 +233,17 @@ extension DatabaseQueue: DatabaseReader { } } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func read( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await writer.execute { db in + try db.isolated(readOnly: true) { + try value(db) + } + } + } + public func asyncRead(_ value: @escaping (Result) -> Void) { writer.async { db in defer { @@ -254,10 +265,18 @@ extension DatabaseQueue: DatabaseReader { } } + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func unsafeRead(_ value: (Database) throws -> T) rethrows -> T { try writer.sync(value) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func unsafeRead( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await writer.execute(value) + } + public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { writer.async { value(.success($0)) } } diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 2295f6f036..5c52737f7c 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -22,14 +22,14 @@ import Dispatch /// ### Reading from the Database /// /// - ``read(_:)-3806d`` -/// - ``read(_:)-4w6gy`` +/// - ``read(_:)-8gyof`` /// - ``readPublisher(receiveOn:value:)`` /// - ``asyncRead(_:)`` /// /// ### Unsafe Methods /// /// - ``unsafeRead(_:)-5i7tf`` -/// - ``unsafeRead(_:)-11mk0`` +/// - ``unsafeRead(_:)-4w54s`` /// - ``unsafeReentrantRead(_:)`` /// - ``asyncUnsafeRead(_:)`` /// @@ -188,6 +188,38 @@ public protocol DatabaseReader: AnyObject, Sendable { @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails func read(_ value: (Database) throws -> T) throws -> T + /// Executes read-only database operations, and returns their result after + /// they have finished executing. + /// + /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) + /// + /// For example: + /// + /// ```swift + /// let count = try await reader.read { db in + /// try Player.fetchCount(db) + /// } + /// ``` + /// + /// Database operations are isolated in a transaction: they do not see + /// changes performed by eventual concurrent writes (even writes performed + /// by other processes). + /// + /// The database connection is read-only: attempts to write throw a + /// ``DatabaseError`` with resultCode `SQLITE_READONLY`. + /// + /// The ``Database`` argument to `value` is valid only during the execution + /// of the closure. Do not store or return the database connection for + /// later use. + /// + /// - parameter value: A closure which accesses the database. + /// - throws: The error thrown by `value`, or any ``DatabaseError`` that + /// would happen while establishing the database access. + @available(iOS 13, macOS 10.15, tvOS 13, *) + func read( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T + /// Schedules read-only database operations for execution, and /// returns immediately. /// @@ -254,6 +286,44 @@ public protocol DatabaseReader: AnyObject, Sendable { @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails func unsafeRead(_ value: (Database) throws -> T) throws -> T + /// Executes database operations, and returns their result after they have + /// finished executing. + /// + /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) + /// + /// This method is "unsafe" because the database reader does nothing more + /// than providing a database connection. When you use this method, you + /// become responsible for the thread-safety of your application, and + /// responsible for database accesses performed by other processes. See + /// for + /// more information. + /// + /// For example: + /// + /// ```swift + /// let count = try await reader.unsafeRead { db in + /// try Player.fetchCount(db) + /// } + /// ``` + /// + /// The ``Database`` argument to `value` is valid only during the execution + /// of the closure. Do not store or return the database connection for + /// later use. + /// + /// - warning: Database operations may not be wrapped in a transaction. They + /// may see changes performed by concurrent writes or writes performed by + /// other processes: two identical requests performed by the `value` + /// closure may not return the same value. + /// - warning: Attempts to write in the database may succeed. + /// + /// - parameter value: A closure which accesses the database. + /// - throws: The error thrown by `value`, or any ``DatabaseError`` that + /// would happen while establishing the database access. + @available(iOS 13, macOS 10.15, tvOS 13, *) + func unsafeRead( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T + /// Schedules database operations for execution, and returns immediately. /// /// This method is "unsafe" because the database reader does nothing more @@ -428,96 +498,6 @@ extension DatabaseReader { } } -extension DatabaseReader { - // MARK: - Asynchronous Database Access - - /// Executes read-only database operations, and returns their result after - /// they have finished executing. - /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// - /// For example: - /// - /// ```swift - /// let count = try await reader.read { db in - /// try Player.fetchCount(db) - /// } - /// ``` - /// - /// Database operations are isolated in a transaction: they do not see - /// changes performed by eventual concurrent writes (even writes performed - /// by other processes). - /// - /// The database connection is read-only: attempts to write throw a - /// ``DatabaseError`` with resultCode `SQLITE_READONLY`. - /// - /// The ``Database`` argument to `value` is valid only during the execution - /// of the closure. Do not store or return the database connection for - /// later use. - /// - /// - parameter value: A closure which accesses the database. - /// - throws: The error thrown by `value`, or any ``DatabaseError`` that - /// would happen while establishing the database access. - @available(iOS 13, macOS 10.15, tvOS 13, *) - public func read(_ value: @Sendable @escaping (Database) throws -> T) async throws -> T { - try await withUnsafeThrowingContinuation { continuation in - asyncRead { result in - do { - try continuation.resume(returning: value(result.get())) - } catch { - continuation.resume(throwing: error) - } - } - } - } - - /// Executes database operations, and returns their result after they have - /// finished executing. - /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// - /// This method is "unsafe" because the database reader does nothing more - /// than providing a database connection. When you use this method, you - /// become responsible for the thread-safety of your application, and - /// responsible for database accesses performed by other processes. See - /// for - /// more information. - /// - /// For example: - /// - /// ```swift - /// let count = try await reader.unsafeRead { db in - /// try Player.fetchCount(db) - /// } - /// ``` - /// - /// The ``Database`` argument to `value` is valid only during the execution - /// of the closure. Do not store or return the database connection for - /// later use. - /// - /// - warning: Database operations may not be wrapped in a transaction. They - /// may see changes performed by concurrent writes or writes performed by - /// other processes: two identical requests performed by the `value` - /// closure may not return the same value. - /// - warning: Attempts to write in the database may succeed. - /// - /// - parameter value: A closure which accesses the database. - /// - throws: The error thrown by `value`, or any ``DatabaseError`` that - /// would happen while establishing the database access. - @available(iOS 13, macOS 10.15, tvOS 13, *) - public func unsafeRead(_ value: @Sendable @escaping (Database) throws -> T) async throws -> T { - try await withUnsafeThrowingContinuation { continuation in - asyncUnsafeRead { result in - do { - try continuation.resume(returning: value(result.get())) - } catch { - continuation.resume(throwing: error) - } - } - } - } -} - #if canImport(Combine) extension DatabaseReader { // MARK: - Publishing Database Values @@ -676,6 +656,13 @@ extension AnyDatabaseReader: DatabaseReader { try base.read(value) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func read( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await base.read(value) + } + public func asyncRead(_ value: @escaping (Result) -> Void) { base.asyncRead(value) } @@ -685,6 +672,13 @@ extension AnyDatabaseReader: DatabaseReader { try base.unsafeRead(value) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func unsafeRead( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await base.unsafeRead(value) + } + public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { base.asyncUnsafeRead(value) } @@ -751,6 +745,7 @@ extension DatabaseSnapshotReader { } // There is no such thing as an unsafe access to a snapshot. + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func unsafeRead(_ value: (Database) throws -> T) throws -> T { try read(value) } diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 197771b24a..8a868a416a 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -146,18 +146,38 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { // MARK: - Reading from Database + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func read(_ block: (Database) throws -> T) rethrows -> T { try reader.sync(block) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func read( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await reader.execute(value) + } + public func asyncRead(_ value: @escaping (Result) -> Void) { reader.async { value(.success($0)) } } + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func unsafeRead(_ value: (Database) throws -> T) rethrows -> T { try reader.sync(value) } + // There is no such thing as an unsafe access to a snapshot. + // We can't provide this as a default implementation in + // `DatabaseSnapshotReader`, because of + // . + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func unsafeRead( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await read(value) + } + public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { reader.async { value(.success($0)) } } diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index 715dabf72e..1fc8e788d0 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -293,6 +293,40 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { } } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func read( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + guard let readerPool else { + throw DatabaseError.connectionIsClosed() + } + + let dbAccess = CancellableDatabaseAccess() + return try await dbAccess.withCancellableContinuation { continuation in + readerPool.asyncGet { result in + do { + let (reader, releaseReader) = try result.get() + // Second async jump because that's how `Pool.async` has to be used. + reader.async { db in + defer { + releaseReader(self.poolCompletion(db)) + } + do { + let result = try dbAccess.inDatabase(db) { + try value(db) + } + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } catch { + continuation.resume(throwing: error) + } + } + } + } + public func asyncRead(_ value: @escaping (Result) -> Void) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) @@ -313,6 +347,17 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { } } + // There is no such thing as an unsafe access to a snapshot. + // We can't provide this as a default implementation in + // `DatabaseSnapshotReader`, because of + // . + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func unsafeRead( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await read(value) + } + public func unsafeReentrantRead(_ value: (Database) throws -> T) throws -> T { if let reader = currentReader { return try reader.reentrantSync { db in diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index a1d18fbe6c..c6637f241e 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -905,6 +905,13 @@ extension AnyDatabaseWriter: DatabaseReader { try base.read(value) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func read( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await base.read(value) + } + public func asyncRead(_ value: @escaping (Result) -> Void) { base.asyncRead(value) } @@ -914,6 +921,13 @@ extension AnyDatabaseWriter: DatabaseReader { try base.unsafeRead(value) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + public func unsafeRead( + _ value: @Sendable @escaping (Database) throws -> T + ) async throws -> T { + try await base.unsafeRead(value) + } + public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { base.asyncUnsafeRead(value) } diff --git a/GRDB/Documentation.docc/Concurrency.md b/GRDB/Documentation.docc/Concurrency.md index 1b8050419b..475da82622 100644 --- a/GRDB/Documentation.docc/Concurrency.md +++ b/GRDB/Documentation.docc/Concurrency.md @@ -99,7 +99,7 @@ try dbQueue.write { db in } ``` - See ``DatabaseReader/read(_:)-4w6gy`` and ``DatabaseWriter/write(_:)-88g7e``. + See ``DatabaseReader/read(_:)-8gyof`` and ``DatabaseWriter/write(_:)-88g7e``. Note the identical method names: `read`, `write`. The async version is only available in async Swift functions. diff --git a/Tests/GRDBTests/DatabaseReaderTests.swift b/Tests/GRDBTests/DatabaseReaderTests.swift index 6150cd4455..df74498b32 100644 --- a/Tests/GRDBTests/DatabaseReaderTests.swift +++ b/Tests/GRDBTests/DatabaseReaderTests.swift @@ -342,6 +342,232 @@ class DatabaseReaderTests : GRDBTestCase { try test(setup(makeDatabasePool(configuration: Configuration())).makeSnapshot()) #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(setup(makeDatabasePool(configuration: Configuration())).makeSnapshotPool()) +#endif + } + + // MARK: - Task Cancellation + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_read_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let task = Task { + await semaphore.wait() + try await dbReader.read { db in + XCTFail("Should not be executed") + } + } + task.cancel() + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.read { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_statement_execution_from_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.read { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + try db.execute(sql: "SELECT 0") + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.read { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_cursor_iteration_from_read_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.read { db in + let cursor = try Int.fetchCursor(db, sql: """ + SELECT 1 UNION ALL SELECT 2 + """) + _ = try cursor.next() + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + _ = try cursor.next() + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.read { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_unsafeRead_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let task = Task { + await semaphore.wait() + try await dbReader.unsafeRead { db in + XCTFail("Should not be executed") + } + } + task.cancel() + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.unsafeRead { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_statement_execution_from_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.unsafeRead { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + try db.execute(sql: "SELECT 0") + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.unsafeRead { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_cursor_iteration_from_unsafeRead_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.unsafeRead { db in + let cursor = try Int.fetchCursor(db, sql: """ + SELECT 1 UNION ALL SELECT 2 + """) + _ = try cursor.next() + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + _ = try cursor.next() + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.unsafeRead { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) #endif } } From cb1bd8377735a00aea18a27bf2d63647397a344b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 1 Sep 2024 13:43:09 +0200 Subject: [PATCH 065/160] Document that async methods can throw CancellationError --- GRDB/Core/DatabaseReader.swift | 10 ++++++---- GRDB/Core/DatabaseWriter.swift | 14 +++++++++----- GRDB/Documentation.docc/Concurrency.md | 2 ++ GRDB/Documentation.docc/Transactions.md | 9 +++++++-- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 5c52737f7c..691af1fd16 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -213,8 +213,9 @@ public protocol DatabaseReader: AnyObject, Sendable { /// later use. /// /// - parameter value: A closure which accesses the database. - /// - throws: The error thrown by `value`, or any ``DatabaseError`` that - /// would happen while establishing the database access. + /// - throws: Any ``DatabaseError`` that happens while establishing the + /// database access, or the error thrown by `value`, or + /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) func read( _ value: @Sendable @escaping (Database) throws -> T @@ -317,8 +318,9 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - warning: Attempts to write in the database may succeed. /// /// - parameter value: A closure which accesses the database. - /// - throws: The error thrown by `value`, or any ``DatabaseError`` that - /// would happen while establishing the database access. + /// - throws: Any ``DatabaseError`` that happens while establishing the + /// database access, or the error thrown by `value`, or + /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) func unsafeRead( _ value: @Sendable @escaping (Database) throws -> T diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index c6637f241e..dd9fafb4ac 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -128,7 +128,9 @@ public protocol DatabaseWriter: DatabaseReader { /// can see partial updates performed by the `updates` closure. /// /// - parameter updates: A closure which accesses the database. - /// - throws: The error thrown by `updates`. + /// - throws: Any ``DatabaseError`` that happens while establishing the + /// database access, or the error thrown by `updates`, or + /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) func writeWithoutTransaction( _ updates: @Sendable @escaping (Database) throws -> T @@ -212,7 +214,9 @@ public protocol DatabaseWriter: DatabaseReader { /// can see partial updates performed by the `updates` closure. /// /// - parameter updates: A closure which accesses the database. - /// - throws: The error thrown by `updates`. + /// - throws: Any ``DatabaseError`` that happens while establishing the + /// database access, or the error thrown by `updates`, or + /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) func barrierWriteWithoutTransaction( _ updates: @Sendable @escaping (Database) throws -> T) @@ -637,9 +641,9 @@ extension DatabaseWriter { /// for later use. /// /// - parameter updates: A closure which accesses the database. - /// - throws: The error thrown by `updates`, or any ``DatabaseError`` that - /// would happen while establishing the database access or committing - /// the transaction. + /// - throws: Any ``DatabaseError`` that happens while establishing the + /// database access, or the error thrown by `updates`, or + /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) public func write( _ updates: @Sendable @escaping (Database) throws -> T diff --git a/GRDB/Documentation.docc/Concurrency.md b/GRDB/Documentation.docc/Concurrency.md index 475da82622..b6eef71b65 100644 --- a/GRDB/Documentation.docc/Concurrency.md +++ b/GRDB/Documentation.docc/Concurrency.md @@ -102,6 +102,8 @@ try dbQueue.write { db in See ``DatabaseReader/read(_:)-8gyof`` and ``DatabaseWriter/write(_:)-88g7e``. Note the identical method names: `read`, `write`. The async version is only available in async Swift functions. + + The async database access methods honor task cancellation. Once an async Task is cancelled, reads and writes throw `CancellationError`, and any transaction is rollbacked. - **Combine publishers** diff --git a/GRDB/Documentation.docc/Transactions.md b/GRDB/Documentation.docc/Transactions.md index 2067a1f096..469ffd9538 100644 --- a/GRDB/Documentation.docc/Transactions.md +++ b/GRDB/Documentation.docc/Transactions.md @@ -142,16 +142,21 @@ try dbQueue.writeWithoutTransaction { db } ``` -Transactions can't be left opened unless the ``Configuration/allowsUnsafeTransactions`` configuration flag is set: +Make sure all transactions opened from a database access are committed or rollbacked from that same database access, because it is a programmer error to leave an opened transaction: ```swift -// fatal error: A transaction has been left opened at the end of a database access +// fatal error: A transaction has been left +// opened at the end of a database access. try dbQueue.writeWithoutTransaction { db in try db.execute(sql: "BEGIN TRANSACTION") // <- no commit or rollback } ``` +In particular, since commits may throw an error, make sure you perform a rollback when a commit fails. + +This restriction can be left with the ``Configuration/allowsUnsafeTransactions`` configuration flag. + It is possible to ask if a transaction is currently opened: ```swift From 5b4b8a9065ee148af534a3a8bc6eb978fb6c3f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 1 Sep 2024 13:48:00 +0200 Subject: [PATCH 066/160] Async database accesses check for cancellation before returning --- GRDB/Core/SerializedDatabase.swift | 16 ++++-- Tests/GRDBTests/DatabaseReaderTests.swift | 70 +++++++++++++++++++++++ Tests/GRDBTests/DatabaseWriterTests.swift | 62 ++++++++++++++++++++ 3 files changed, 143 insertions(+), 5 deletions(-) diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index fed985b562..ede400fce6 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -396,15 +396,21 @@ extension CancellableDatabaseAccess: DatabaseCancellable { } } - defer { - withLock { state in + return try throwingFirstError { + try work() + } finally: { + let cancelled = withLock { state in if case .cancelled = state { db.uncancel() + return true + } else { + state = .expired + return false } - state = .expired + } + if cancelled { + throw CancellationError() } } - - return try work() } } diff --git a/Tests/GRDBTests/DatabaseReaderTests.swift b/Tests/GRDBTests/DatabaseReaderTests.swift index df74498b32..6777c08adc 100644 --- a/Tests/GRDBTests/DatabaseReaderTests.swift +++ b/Tests/GRDBTests/DatabaseReaderTests.swift @@ -381,6 +381,41 @@ class DatabaseReaderTests : GRDBTestCase { #endif } + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.read { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.read { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + } + @available(iOS 13, macOS 10.15, tvOS 13, *) func test_statement_execution_from_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { @@ -493,6 +528,41 @@ class DatabaseReaderTests : GRDBTestCase { #endif } + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.unsafeRead { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.unsafeRead { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + } + @available(iOS 13, macOS 10.15, tvOS 13, *) func test_statement_execution_from_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 1812637642..56fb130c68 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -460,6 +460,37 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabasePool()) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.writeWithoutTransaction { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + @available(iOS 13, macOS 10.15, tvOS 13, *) func test_statement_execution_from_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { @@ -691,6 +722,37 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabasePool()) } + @available(iOS 13, macOS 10.15, tvOS 13, *) + func test_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.barrierWriteWithoutTransaction { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.barrierWriteWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + } + @available(iOS 13, macOS 10.15, tvOS 13, *) func test_statement_execution_from_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { From 71f555496203141ef8f763e6bfca2cf135115a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 1 Sep 2024 13:50:59 +0200 Subject: [PATCH 067/160] Test cancellation handling from AnyDatabaseReader and AnyDatabaseWriter --- Tests/GRDBTests/DatabaseReaderTests.swift | 16 ++++++++++++++++ Tests/GRDBTests/DatabaseWriterTests.swift | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/Tests/GRDBTests/DatabaseReaderTests.swift b/Tests/GRDBTests/DatabaseReaderTests.swift index 6777c08adc..364bb570c9 100644 --- a/Tests/GRDBTests/DatabaseReaderTests.swift +++ b/Tests/GRDBTests/DatabaseReaderTests.swift @@ -379,6 +379,8 @@ class DatabaseReaderTests : GRDBTestCase { #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(makeDatabasePool().makeSnapshotPool()) #endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -414,6 +416,8 @@ class DatabaseReaderTests : GRDBTestCase { #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(makeDatabasePool().makeSnapshotPool()) #endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -451,6 +455,8 @@ class DatabaseReaderTests : GRDBTestCase { #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(makeDatabasePool().makeSnapshotPool()) #endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -492,6 +498,8 @@ class DatabaseReaderTests : GRDBTestCase { #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(makeDatabasePool().makeSnapshotPool()) #endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -526,6 +534,8 @@ class DatabaseReaderTests : GRDBTestCase { #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(makeDatabasePool().makeSnapshotPool()) #endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -561,6 +571,8 @@ class DatabaseReaderTests : GRDBTestCase { #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(makeDatabasePool().makeSnapshotPool()) #endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -598,6 +610,8 @@ class DatabaseReaderTests : GRDBTestCase { #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(makeDatabasePool().makeSnapshotPool()) #endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -639,5 +653,7 @@ class DatabaseReaderTests : GRDBTestCase { #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(makeDatabasePool().makeSnapshotPool()) #endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } } diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 56fb130c68..478cab6c5b 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -458,6 +458,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -489,6 +490,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -522,6 +524,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -559,6 +562,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -589,6 +593,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -620,6 +625,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -653,6 +659,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -690,6 +697,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -720,6 +728,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -751,6 +760,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -784,6 +794,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -821,5 +832,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) } } From 9357198e1bd03288e33825cbfab67a96f4670dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 5 Sep 2024 18:42:11 +0200 Subject: [PATCH 068/160] TODO --- TODO.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.md b/TODO.md index 9bb275b6d1..0db5074a64 100644 --- a/TODO.md +++ b/TODO.md @@ -158,6 +158,8 @@ - [?] GRDB7: Sendable: AsyncValueObservation (ce63cdfa) - [X] GRDB7: Sendable: DatabaseRegionObservation (b4ff52fb) - [ ] GRDB7: DispatchQueue.asyncSending (7b075e6b) +- [ ] GRDB7: Replace sequences with collection (e.g. https://github.com/tidal-music/tidal-sdk-ios/pull/39) +- [ ] GRDB7: Replace `some` DatabaseReader/Writer with `any` where possible, in order to avoid issues with accessing DatabaseContext from GRDBQuery (if the problem exists in Xcode 16) - [?] GRDB7: Change ValueObservation callback argument so that it could expose snapshots? https://github.com/groue/GRDB.swift/discussions/1523#discussioncomment-9092500 From 110d9312db639f350ab778ef1780f792db4c50e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 11:41:05 +0200 Subject: [PATCH 069/160] Exclude products folder from GRDBTests target Otherwise, we eventually have an error "multiple resources named 'PrivacyInfo.xcprivacy' in target 'GRDBTests'" --- Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Package.swift b/Package.swift index 9377585aac..22cd2d6c3e 100644 --- a/Package.swift +++ b/Package.swift @@ -70,6 +70,7 @@ let package = Package( "SPM", "generatePerformanceReport.rb", "parsePerformanceTests.rb", + "products", ], resources: [ .copy("GRDBTests/Betty.jpeg"), From 6a29d6c08170ea62843f606c81db21ce57c0e0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 12:38:05 +0200 Subject: [PATCH 070/160] [BREAKING] Prefer Collection over Sequence for filter(keys:) and derived APIs We were frequently turning those sequences into an Array in order to check their emptiness, and generally were not sure we'd iterate a sequence only once. Also use `some Sequence` and `some Collection` whenever possible. --- GRDB/Core/Cursor.swift | 8 +- GRDB/Core/Database+Schema.swift | 20 ++- GRDB/Core/Row.swift | 4 +- GRDB/Core/RowAdapter.swift | 4 +- GRDB/Core/Statement.swift | 32 ++--- GRDB/Documentation.docc/JSON.md | 2 +- GRDB/JSON/SQLJSONExpressible.swift | 23 ++-- GRDB/JSON/SQLJSONFunctions.swift | 126 ++++++++---------- .../Request/RequestProtocols.swift | 19 ++- GRDB/QueryInterface/SQL/SQLRelation.swift | 7 +- GRDB/QueryInterface/SQL/Table.swift | 33 +++-- .../SQLInterpolation+QueryInterface.swift | 12 +- .../TableRecord+QueryInterfaceRequest.swift | 13 +- GRDB/Record/FetchableRecord+TableRecord.swift | 48 +++---- GRDB/Record/FetchableRecord.swift | 6 +- .../MutablePersistableRecord+Update.swift | 56 +++----- GRDB/Record/MutablePersistableRecord.swift | 8 +- GRDB/Record/TableRecord.swift | 20 +-- GRDB/Utils/OrderedDictionary.swift | 40 +++--- TODO.md | 2 +- 20 files changed, 208 insertions(+), 275 deletions(-) diff --git a/GRDB/Core/Cursor.swift b/GRDB/Core/Cursor.swift index f2bed78002..68baeab44d 100644 --- a/GRDB/Core/Cursor.swift +++ b/GRDB/Core/Cursor.swift @@ -748,17 +748,13 @@ public final class AnyCursor: Cursor { } /// Creates a new cursor whose elements are elements of `iterator`. - public convenience init(iterator: I) - where I: IteratorProtocol, I.Element == Element - { + public convenience init(iterator: some IteratorProtocol) { var iterator = iterator self.init { iterator.next() } } /// Creates a new cursor whose elements are elements of `sequence`. - public convenience init(_ sequence: S) - where S: Sequence, S.Element == Element - { + public convenience init(_ sequence: some Sequence) { self.init(iterator: sequence.makeIterator()) } diff --git a/GRDB/Core/Database+Schema.swift b/GRDB/Core/Database+Schema.swift index 3cdf75c823..beb6712771 100644 --- a/GRDB/Core/Database+Schema.swift +++ b/GRDB/Core/Database+Schema.swift @@ -598,13 +598,11 @@ extension Database { /// try db.table("t", hasUniqueKey: ["id", "a"]) // true /// try db.table("t", hasUniqueKey: ["id", "a", "b", "c"]) // true /// ``` - public func table( + public func table( _ tableName: String, - hasUniqueKey columns: Columns) - throws -> Bool - where Columns: Sequence, Columns.Element == String - { - try columnsForUniqueKey(Array(columns), in: tableName) != nil + hasUniqueKey columns: some Collection + ) throws -> Bool { + try columnsForUniqueKey(columns, in: tableName) != nil } /// Returns the foreign keys defined on table named `tableName`. @@ -929,12 +927,10 @@ extension Database { /// returns the columns of the unique key, ordered as the matching index (or /// primary key). The case of returned columns is not guaranteed to match /// the case of input columns. - func columnsForUniqueKey( - _ columns: Columns, - in tableName: String) - throws -> [String]? - where Columns: Sequence, Columns.Element == String - { + func columnsForUniqueKey( + _ columns: some Collection, + in tableName: String + ) throws -> [String]? { let lowercasedColumns = Set(columns.map { $0.lowercased() }) if lowercasedColumns.isEmpty { // Don't hit the database for trivial case diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 5b1c130a52..f1e49e5f67 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -2395,9 +2395,7 @@ extension RowImpl { struct ArrayRowImpl: RowImpl { let columns: [(String, DatabaseValue)] - init(columns: Columns) - where Columns: Collection, Columns.Element == (String, DatabaseValue) - { + init(columns: some Collection<(String, DatabaseValue)>) { self.columns = Array(columns) } diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index 86c42a38d8..2c4b760f1e 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -183,9 +183,7 @@ public struct _LayoutedColumnMapping { /// /// // [foo:"foo" bar: "bar"] /// try Row.fetchOne(db, sql: "SELECT NULL, 'foo', 'bar'", adapter: FooBarAdapter()) - init(layoutColumns: S) - where S: Sequence, S.Element == (Int, String) - { + init(layoutColumns: some Collection<(Int, String)>) { self._layoutColumns = Array(layoutColumns) self.lowercaseColumnIndexes = Dictionary( layoutColumns diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index ada39225ad..f2b85776d6 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -865,14 +865,12 @@ func checkBindingSuccess(code: CInt, sqliteStatement: SQLiteStatement) throws { /// - parameter index: The index of the first binding. /// - parameter body: The closure to execute when arguments are bound. @usableFromInline -func withBindings( - _ bindings: C, +func withBindings( + _ bindings: some Collection, to sqliteStatement: SQLiteStatement, from index: CInt = 1, - do body: () throws -> T) -throws -> T -where C: Collection, C.Element == DatabaseValue -{ + do body: () throws -> T +) throws -> T { guard let binding = bindings.first else { return try body() } @@ -1019,10 +1017,8 @@ public struct StatementArguments: Hashable { /// let values: [(any DatabaseValueConvertible)?] = ["foo", 1, nil] /// db.execute(sql: "INSERT ... (?,?,?)", arguments: StatementArguments(values)) /// ``` - public init(_ sequence: S) - where S: Sequence, S.Element == (any DatabaseValueConvertible)? - { - values = sequence.map { $0?.databaseValue ?? .null } + public init(_ values: some Sequence<(any DatabaseValueConvertible)?>) { + self.values = values.map { $0?.databaseValue ?? .null } namedValues = .init() } @@ -1034,10 +1030,8 @@ public struct StatementArguments: Hashable { /// let values: [String] = ["foo", "bar"] /// db.execute(sql: "INSERT ... (?,?)", arguments: StatementArguments(values)) /// ``` - public init(_ sequence: S) - where S: Sequence, S.Element: DatabaseValueConvertible - { - values = sequence.map(\.databaseValue) + public init(_ values: some Sequence) { + self.values = values.map(\.databaseValue) namedValues = .init() } @@ -1078,11 +1072,11 @@ public struct StatementArguments: Hashable { /// Creates a `StatementArguments` of named arguments from a sequence of /// (key, value) pairs. - public init(_ sequence: S) - where S: Sequence, S.Element == (String, (any DatabaseValueConvertible)?) - { - namedValues = .init(minimumCapacity: sequence.underestimatedCount) - for (key, value) in sequence { + public init( + _ keysAndValues: some Sequence<(String, (any DatabaseValueConvertible)?)> + ) { + namedValues = .init(minimumCapacity: keysAndValues.underestimatedCount) + for (key, value) in keysAndValues { namedValues[key] = value?.databaseValue ?? .null } values = .init() diff --git a/GRDB/Documentation.docc/JSON.md b/GRDB/Documentation.docc/JSON.md index 499da4ef40..ad4acad302 100644 --- a/GRDB/Documentation.docc/JSON.md +++ b/GRDB/Documentation.docc/JSON.md @@ -131,7 +131,7 @@ The `->` and `->>` SQL operators are available on the ``SQLJSONExpressible`` pro ### Build new JSON values at the SQL level - ``Database/json(_:)`` -- ``Database/jsonArray(_:)-8xxe3`` +- ``Database/jsonArray(_:)-8p2p8`` - ``Database/jsonArray(_:)-469db`` - ``Database/jsonObject(_:)`` - ``Database/jsonQuote(_:)`` diff --git a/GRDB/JSON/SQLJSONExpressible.swift b/GRDB/JSON/SQLJSONExpressible.swift index 991a5dd7d5..5bde4eefe7 100644 --- a/GRDB/JSON/SQLJSONExpressible.swift +++ b/GRDB/JSON/SQLJSONExpressible.swift @@ -8,7 +8,7 @@ /// the SQL level. /// /// - When used in a JSON-building function such as -/// ``Database/jsonArray(_:)-8xxe3`` or ``Database/jsonObject(_:)``, +/// ``Database/jsonArray(_:)-8p2p8`` or ``Database/jsonObject(_:)``, /// they are parsed and interpreted as JSON, not as plain strings. /// /// To build a JSON value, create a ``JSONColumn``, or call the @@ -66,7 +66,7 @@ /// ## Build JSON objects and arrays from JSON values /// /// When used in a JSON-building function such as -/// ``Database/jsonArray(_:)-8xxe3`` or ``Database/jsonObject(_:)-5iswr``, +/// ``Database/jsonArray(_:)-8p2p8`` or ``Database/jsonObject(_:)-5iswr``, /// JSON values are parsed and interpreted as JSON, not as plain strings. /// /// In the example below, we can see how the `JSONColumn` is interpreted as @@ -224,9 +224,9 @@ extension SQLJSONExpressible { /// Related SQL documentation: /// /// - parameter paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public func jsonExtract(atPaths paths: C) -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public func jsonExtract( + atPaths paths: some Collection + ) -> SQLExpression { Database.jsonExtract(self, atPaths: paths) } @@ -335,9 +335,9 @@ extension SQLJSONExpressible { /// /// - parameter paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public func jsonExtract(atPaths paths: C) -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public func jsonExtract( + atPaths paths: some Collection + ) -> SQLExpression { Database.jsonExtract(self, atPaths: paths) } @@ -429,10 +429,9 @@ extension SQLJSONExpressible { // /// - Parameters: // /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). // @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS -// public func jsonRemove(atPaths paths: C) -// -> ColumnAssignment -// where C: Collection, C.Element: SQLExpressible -// { +// public func jsonRemove( +// atPaths paths: some Collection +// ) -> ColumnAssignment { // .init(columnName: name, value: Database.jsonRemove(self, atPaths: paths)) // } // diff --git a/GRDB/JSON/SQLJSONFunctions.swift b/GRDB/JSON/SQLJSONFunctions.swift index e573c12a24..ee360aa6ef 100644 --- a/GRDB/JSON/SQLJSONFunctions.swift +++ b/GRDB/JSON/SQLJSONFunctions.swift @@ -24,9 +24,9 @@ extension Database { /// ``` /// /// Related SQLite documentation: - public static func jsonArray(_ values: C) -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public static func jsonArray( + _ values: some Collection + ) -> SQLExpression { .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) } @@ -40,9 +40,9 @@ extension Database { /// ``` /// /// Related SQLite documentation: - public static func jsonArray(_ values: C) -> SQLExpression - where C: Collection, C.Element == any SQLExpressible - { + public static func jsonArray( + _ values: some Collection + ) -> SQLExpression { .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) } @@ -130,10 +130,10 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public static func jsonExtract(_ value: some SQLExpressible, atPaths paths: C) - -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public static func jsonExtract( + _ value: some SQLExpressible, + atPaths paths: some Collection + ) -> SQLExpression { .function("JSON_EXTRACT", [value.sqlExpression] + paths.map(\.sqlExpression)) } @@ -152,13 +152,10 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public static func jsonInsert( + public static func jsonInsert( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_INSERT", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -179,13 +176,10 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public static func jsonReplace( + public static func jsonReplace( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_REPLACE", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -206,13 +200,10 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public static func jsonSet( + public static func jsonSet( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_SET", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -241,11 +232,9 @@ extension Database { /// ``` /// /// Related SQLite documentation: - public static func jsonObject(_ elements: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + public static func jsonObject( + _ elements: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_OBJECT", elements.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -301,10 +290,10 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public static func jsonRemove(_ value: some SQLExpressible, atPaths paths: C) - -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public static func jsonRemove( + _ value: some SQLExpressible, + atPaths paths: some Collection + ) -> SQLExpression { .function("JSON_REMOVE", [value.sqlExpression] + paths.map(\.sqlExpression)) } @@ -458,9 +447,9 @@ extension Database { /// /// Related SQLite documentation: @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonArray(_ values: C) -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public static func jsonArray( + _ values: some Collection + ) -> SQLExpression { .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) } @@ -475,9 +464,9 @@ extension Database { /// /// Related SQLite documentation: @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonArray(_ values: C) -> SQLExpression - where C: Collection, C.Element == any SQLExpressible - { + public static func jsonArray( + _ values: some Collection + ) -> SQLExpression { .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) } @@ -555,10 +544,10 @@ extension Database { /// - value: A JSON value. /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonExtract(_ value: some SQLExpressible, atPaths paths: C) - -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public static func jsonExtract( + _ value: some SQLExpressible, + atPaths paths: some Collection + ) -> SQLExpression { .function("JSON_EXTRACT", [value.sqlExpression] + paths.map(\.sqlExpression)) } @@ -578,13 +567,10 @@ extension Database { /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonInsert( + public static func jsonInsert( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_INSERT", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -606,13 +592,10 @@ extension Database { /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonReplace( + public static func jsonReplace( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_REPLACE", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -634,13 +617,10 @@ extension Database { /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonSet( + public static func jsonSet( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_SET", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -670,11 +650,9 @@ extension Database { /// /// Related SQLite documentation: @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonObject(_ elements: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + public static func jsonObject( + _ elements: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_OBJECT", elements.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -733,10 +711,10 @@ extension Database { /// - value: A JSON value. /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonRemove(_ value: some SQLExpressible, atPaths paths: C) - -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public static func jsonRemove( + _ value: some SQLExpressible, + atPaths paths: some Collection + ) -> SQLExpression { .function("JSON_REMOVE", [value.sqlExpression] + paths.map(\.sqlExpression)) } diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index 030ad91eec..369c080021 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -328,7 +328,7 @@ extension FilteredRequest { /// - ``filter(ids:)`` /// - ``filter(key:)-1p9sq`` /// - ``filter(key:)-2te6v`` -/// - ``filter(keys:)-6ggt1`` +/// - ``filter(keys:)-9p9i5`` /// - ``filter(keys:)-8fbn9`` /// - ``matching(_:)-3s3zr`` /// - ``matching(_:)-7c1e8`` @@ -437,9 +437,8 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { /// ``` /// /// - parameter keys: A collection of primary keys - public func filter(keys: Sequence) - -> Self - where Sequence.Element: DatabaseValueConvertible + public func filter(keys: Keys) -> Self + where Keys: Collection, Keys.Element: DatabaseValueConvertible { // In order to encode keys in the database, we perform a runtime check // for EncodableRecord, and look for a customized encoding strategy. @@ -448,7 +447,7 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { // make it impractical to define `filter(id:)`, `fetchOne(_:key:)`, // `deleteAll(_:ids:)` etc. if let recordType = RowDecoder.self as? any EncodableRecord.Type { - if Sequence.Element.self == Data.self || Sequence.Element.self == Optional.self { + if Keys.Element.self == Data.self || Keys.Element.self == Optional.self { let datas = keys.compactMap { ($0 as! Data?) } if datas.isEmpty { // Don't hit the database @@ -465,7 +464,7 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { let expressions = datas.map { strategy.encode($0).sqlExpression } return expressions }) - } else if Sequence.Element.self == Date.self || Sequence.Element.self == Optional.self { + } else if Keys.Element.self == Date.self || Keys.Element.self == Optional.self { let dates = keys.compactMap { ($0 as! Date?) } if dates.isEmpty { // Don't hit the database @@ -482,7 +481,7 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { let expressions = dates.map { strategy.encode($0).sqlExpression } return expressions }) - } else if Sequence.Element.self == UUID.self || Sequence.Element.self == Optional.self { + } else if Keys.Element.self == UUID.self || Keys.Element.self == Optional.self { let uuids = keys.compactMap { ($0 as! UUID?) } if uuids.isEmpty { // Don't hit the database @@ -661,9 +660,7 @@ where Self: FilteredRequest, /// ``` /// /// - parameter ids: A collection of primary keys - public func filter(ids: IDS) -> Self - where IDS: Collection, IDS.Element == RowDecoder.ID - { + public func filter(ids: some Collection) -> Self { filter(keys: ids) } } @@ -1402,7 +1399,7 @@ extension JoinableRequest where Self: SelectionRequest { /// - ``TableRequest/filter(ids:)`` /// - ``TableRequest/filter(key:)-1p9sq`` /// - ``TableRequest/filter(key:)-2te6v`` -/// - ``TableRequest/filter(keys:)-6ggt1`` +/// - ``TableRequest/filter(keys:)-9p9i5`` /// - ``TableRequest/filter(keys:)-8fbn9`` /// - ``FilteredRequest/filter(literal:)`` /// - ``FilteredRequest/filter(sql:arguments:)`` diff --git a/GRDB/QueryInterface/SQL/SQLRelation.swift b/GRDB/QueryInterface/SQL/SQLRelation.swift index 569fb5d8ad..469c845bf9 100644 --- a/GRDB/QueryInterface/SQL/SQLRelation.swift +++ b/GRDB/QueryInterface/SQL/SQLRelation.swift @@ -923,10 +923,9 @@ extension JoinMapping { /// - precondition: leftRows contains all mapping left columns. /// - precondition: All rows have the same layout: a column index returned /// by `index(forColumn:)` refers to the same column in all rows. - func joinExpression(leftRows: Rows) - -> SQLExpression - where Rows: Collection, Rows.Element: ColumnAddressable - { + func joinExpression( + leftRows: some Collection + ) -> SQLExpression { guard let firstLeftRow = leftRows.first else { // We could return `false.sqlExpression`. // diff --git a/GRDB/QueryInterface/SQL/Table.swift b/GRDB/QueryInterface/SQL/Table.swift index 2cc7690e55..8e2a74f705 100644 --- a/GRDB/QueryInterface/SQL/Table.swift +++ b/GRDB/QueryInterface/SQL/Table.swift @@ -40,7 +40,7 @@ /// /// - ``deleteAll(_:)`` /// - ``deleteAll(_:ids:)`` -/// - ``deleteAll(_:keys:)-5t865`` +/// - ``deleteAll(_:keys:)-594uc`` /// - ``deleteAll(_:keys:)-28sff`` /// - ``deleteOne(_:id:)`` /// - ``deleteOne(_:key:)-404su`` @@ -69,7 +69,7 @@ /// - ``filter(ids:)`` /// - ``filter(key:)-tw3i`` /// - ``filter(key:)-4sun7`` -/// - ``filter(keys:)-85e0v`` +/// - ``filter(keys:)-5ws7f`` /// - ``filter(keys:)-qqgf`` /// - ``filter(literal:)`` /// - ``filter(sql:arguments:)`` @@ -507,10 +507,9 @@ extension Table { /// ``` /// /// - parameter keys: A collection of primary keys - public func filter(keys: Keys) - -> QueryInterfaceRequest - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { + public func filter( + keys: some Collection + ) -> QueryInterfaceRequest { all().filter(keys: keys) } @@ -775,9 +774,9 @@ extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConv /// ``` /// /// - parameter ids: A collection of primary keys - public func filter(ids: IDS) -> QueryInterfaceRequest - where IDS: Collection, IDS.Element == RowDecoder.ID - { + public func filter( + ids: some Collection + ) -> QueryInterfaceRequest { all().filter(ids: ids) } } @@ -1644,11 +1643,10 @@ extension Table { /// - keys: A sequence of primary keys. /// - returns: The number of deleted rows. @discardableResult - public func deleteAll(_ db: Database, keys: Keys) - throws -> Int - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { - let keys = Array(keys) + public func deleteAll( + _ db: Database, + keys: some Collection + ) throws -> Int { if keys.isEmpty { // Avoid hitting the database return 0 @@ -1723,9 +1721,10 @@ where RowDecoder: Identifiable, /// - ids: A collection of primary keys. /// - returns: The number of deleted rows. @discardableResult - public func deleteAll(_ db: Database, ids: IDS) throws -> Int - where IDS: Collection, IDS.Element == RowDecoder.ID - { + public func deleteAll( + _ db: Database, + ids: some Collection + ) throws -> Int { if ids.isEmpty { // Avoid hitting the database return 0 diff --git a/GRDB/QueryInterface/SQLInterpolation+QueryInterface.swift b/GRDB/QueryInterface/SQLInterpolation+QueryInterface.swift index eb161ab8bd..35a9912b12 100644 --- a/GRDB/QueryInterface/SQLInterpolation+QueryInterface.swift +++ b/GRDB/QueryInterface/SQLInterpolation+QueryInterface.swift @@ -227,9 +227,9 @@ extension SQLInterpolation { /// let request: SQLRequest = """ /// SELECT * FROM player WHERE id IN \(ids) /// """ - public mutating func appendInterpolation(_ sequence: S) - where S: Sequence, S.Element: SQLExpressible - { + public mutating func appendInterpolation( + _ sequence: some Sequence + ) { let e: [SQL.Element] = sequence.map { .expression($0.sqlExpression) } if e.isEmpty { appendLiteral("(SELECT NULL WHERE NULL)") @@ -255,9 +255,9 @@ extension SQLInterpolation { /// let request: SQLRequest = """ /// SELECT * FROM player WHERE a IN \(expressions) /// """ - public mutating func appendInterpolation(_ sequence: S) - where S: Sequence, S.Element == any SQLExpressible - { + public mutating func appendInterpolation( + _ sequence: some Sequence + ) { appendInterpolation(sequence.lazy.map(\.sqlExpression)) } diff --git a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift index 74b802feb1..1b1ed8c8e6 100644 --- a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift @@ -343,10 +343,9 @@ extension TableRecord { /// ``` /// /// - parameter keys: A collection of primary keys - public static func filter(keys: Keys) - -> QueryInterfaceRequest - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { + public static func filter( + keys: some Collection + ) -> QueryInterfaceRequest { all().filter(keys: keys) } @@ -651,9 +650,9 @@ extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// ``` /// /// - parameter ids: A collection of primary keys - public static func filter(ids: IDS) -> QueryInterfaceRequest - where IDS: Collection, IDS.Element == ID - { + public static func filter( + ids: some Collection + ) -> QueryInterfaceRequest { all().filter(ids: ids) } } diff --git a/GRDB/Record/FetchableRecord+TableRecord.swift b/GRDB/Record/FetchableRecord+TableRecord.swift index be0b839265..736ca21e4e 100644 --- a/GRDB/Record/FetchableRecord+TableRecord.swift +++ b/GRDB/Record/FetchableRecord+TableRecord.swift @@ -128,10 +128,10 @@ extension FetchableRecord where Self: TableRecord { /// - keys: A sequence of primary keys. /// - returns: A ``RecordCursor`` over fetched records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchCursor(_ db: Database, keys: Keys) - throws -> RecordCursor - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { + public static func fetchCursor( + _ db: Database, + keys: some Collection + ) throws -> RecordCursor { try filter(keys: keys).fetchCursor(db) } @@ -154,11 +154,10 @@ extension FetchableRecord where Self: TableRecord { /// - keys: A sequence of primary keys. /// - returns: An array of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchAll(_ db: Database, keys: Keys) - throws -> [Self] - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { - let keys = Array(keys) + public static func fetchAll( + _ db: Database, + keys: some Collection + ) throws -> [Self] { if keys.isEmpty { // Avoid hitting the database return [] @@ -249,10 +248,10 @@ extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseVa /// - ids: A collection of primary keys. /// - returns: A ``RecordCursor`` over fetched records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchCursor(_ db: Database, ids: IDS) - throws -> RecordCursor - where IDS: Collection, IDS.Element == ID - { + public static func fetchCursor( + _ db: Database, + ids: some Collection + ) throws -> RecordCursor { try filter(ids: ids).fetchCursor(db) } @@ -275,9 +274,10 @@ extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseVa /// - ids: A collection of primary keys. /// - returns: An array of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchAll(_ db: Database, ids: IDS) throws -> [Self] - where IDS: Collection, IDS.Element == ID - { + public static func fetchAll( + _ db: Database, + ids: some Collection + ) throws -> [Self] { if ids.isEmpty { // Avoid hitting the database return [] @@ -346,11 +346,10 @@ extension FetchableRecord where Self: TableRecord & Hashable { /// - keys: A sequence of primary keys. /// - returns: A set of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchSet(_ db: Database, keys: Keys) - throws -> Set - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { - let keys = Array(keys) + public static func fetchSet( + _ db: Database, + keys: some Collection + ) throws -> Set { if keys.isEmpty { // Avoid hitting the database return [] @@ -377,9 +376,10 @@ extension FetchableRecord where Self: TableRecord & Hashable & Identifiable, ID: /// - ids: A collection of primary keys. /// - returns: A set of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchSet(_ db: Database, ids: IDS) throws -> Set - where IDS: Collection, IDS.Element == ID - { + public static func fetchSet( + _ db: Database, + ids: some Collection + ) throws -> Set { if ids.isEmpty { // Avoid hitting the database return [] diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index cde368dce7..9a11ca665f 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -71,9 +71,9 @@ import Foundation /// - ``fetchAll(_:ids:)`` /// - ``fetchSet(_:ids:)`` /// - ``fetchOne(_:id:)`` -/// - ``fetchCursor(_:keys:)-2jrm1`` -/// - ``fetchAll(_:keys:)-4c8no`` -/// - ``fetchSet(_:keys:)-e6uy`` +/// - ``fetchCursor(_:keys:)-1x4ja`` +/// - ``fetchAll(_:keys:)-60fah`` +/// - ``fetchSet(_:keys:)-7lhcn`` /// - ``fetchOne(_:key:)-3f3hc`` /// - ``find(_:id:)`` /// - ``find(_:key:)-4kry5`` diff --git a/GRDB/Record/MutablePersistableRecord+Update.swift b/GRDB/Record/MutablePersistableRecord+Update.swift index 8875ad5750..89d81d817c 100644 --- a/GRDB/Record/MutablePersistableRecord+Update.swift +++ b/GRDB/Record/MutablePersistableRecord+Update.swift @@ -41,13 +41,11 @@ extension MutablePersistableRecord { /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. @inlinable // allow specialization so that empty callbacks are removed - public func update( + public func update( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns) - throws - where Columns: Sequence, Columns.Element == String - { + columns: some Collection + ) throws { try willSave(db) var updated: PersistenceSuccess? @@ -84,13 +82,11 @@ extension MutablePersistableRecord { /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. @inlinable // allow specialization so that empty callbacks are removed - public func update( + public func update( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns) - throws - where Columns: Sequence, Columns.Element: ColumnExpression - { + columns: some Collection + ) throws { try update(db, onConflict: conflictResolution, columns: columns.map(\.name)) } @@ -371,15 +367,13 @@ extension MutablePersistableRecord { /// primary key does not match any row in the database. /// - precondition: `selection` is not empty. @inlinable // allow specialization so that empty callbacks are removed - public func updateAndFetch( + public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns, + columns: some Collection, selection: [any SQLSelectable], - fetch: (Statement) throws -> T) - throws -> T - where Columns: Sequence, Columns.Element == String - { + fetch: (Statement) throws -> T + ) throws -> T { GRDBPrecondition(!selection.isEmpty, "Invalid empty selection") try willSave(db) @@ -432,15 +426,13 @@ extension MutablePersistableRecord { /// primary key does not match any row in the database. /// - precondition: `selection` is not empty. @inlinable // allow specialization so that empty callbacks are removed - public func updateAndFetch( + public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns, + columns: some Collection, selection: [any SQLSelectable], - fetch: (Statement) throws -> T) - throws -> T - where Columns: Sequence, Columns.Element: ColumnExpression - { + fetch: (Statement) throws -> T + ) throws -> T { try updateAndFetch( db, onConflict: conflictResolution, columns: columns.map(\.name), @@ -686,15 +678,13 @@ extension MutablePersistableRecord { /// - precondition: `selection` is not empty. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ - public func updateAndFetch( + public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns, + columns: some Collection, selection: [any SQLSelectable], - fetch: (Statement) throws -> T) - throws -> T - where Columns: Sequence, Columns.Element == String - { + fetch: (Statement) throws -> T + ) throws -> T { GRDBPrecondition(!selection.isEmpty, "Invalid empty selection") try willSave(db) @@ -748,15 +738,13 @@ extension MutablePersistableRecord { /// - precondition: `selection` is not empty. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ - public func updateAndFetch( + public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns, + columns: some Collection, selection: [any SQLSelectable], - fetch: (Statement) throws -> T) - throws -> T - where Columns: Sequence, Columns.Element: ColumnExpression - { + fetch: (Statement) throws -> T + ) throws -> T { try updateAndFetch( db, onConflict: conflictResolution, columns: columns.map(\.name), diff --git a/GRDB/Record/MutablePersistableRecord.swift b/GRDB/Record/MutablePersistableRecord.swift index 3741c06f73..a3f8d8901f 100644 --- a/GRDB/Record/MutablePersistableRecord.swift +++ b/GRDB/Record/MutablePersistableRecord.swift @@ -69,8 +69,8 @@ /// See inherited ``TableRecord`` methods for batch updates. /// /// - ``update(_:onConflict:)`` -/// - ``update(_:onConflict:columns:)-4foo1`` -/// - ``update(_:onConflict:columns:)-5hxyx`` +/// - ``update(_:onConflict:columns:)-5qfk`` +/// - ``update(_:onConflict:columns:)-9fip4`` /// - ``updateChanges(_:onConflict:from:)`` /// - ``updateChanges(_:onConflict:modify:)`` /// @@ -78,8 +78,8 @@ /// /// - ``updateAndFetch(_:onConflict:)`` /// - ``updateAndFetch(_:onConflict:as:)`` -/// - ``updateAndFetch(_:onConflict:columns:selection:fetch:)-7s7y1`` -/// - ``updateAndFetch(_:onConflict:columns:selection:fetch:)-30d2v`` +/// - ``updateAndFetch(_:onConflict:columns:selection:fetch:)-98dtr`` +/// - ``updateAndFetch(_:onConflict:columns:selection:fetch:)-9npht`` /// - ``updateAndFetch(_:onConflict:selection:fetch:)`` /// - ``updateChangesAndFetch(_:onConflict:modify:)`` /// - ``updateChangesAndFetch(_:onConflict:as:modify:)`` diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index 4e9cd4485d..709578cd59 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -41,7 +41,7 @@ import Foundation /// /// - ``deleteAll(_:)`` /// - ``deleteAll(_:ids:)`` -/// - ``deleteAll(_:keys:)-jbkm`` +/// - ``deleteAll(_:keys:)-5l3ih`` /// - ``deleteAll(_:keys:)-5s1jg`` /// - ``deleteOne(_:id:)`` /// - ``deleteOne(_:key:)-413u8`` @@ -71,7 +71,7 @@ import Foundation /// - ``filter(key:)-9ey53`` /// - ``filter(key:)-34lau`` /// - ``filter(keys:)-4hq8y`` -/// - ``filter(keys:)-7skw1`` +/// - ``filter(keys:)-s1q0`` /// - ``filter(literal:)`` /// - ``filter(sql:arguments:)`` /// - ``having(_:)`` @@ -411,11 +411,10 @@ extension TableRecord { /// - keys: A sequence of primary keys. /// - returns: The number of deleted records. @discardableResult - public static func deleteAll(_ db: Database, keys: Keys) - throws -> Int - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { - let keys = Array(keys) + public static func deleteAll( + _ db: Database, + keys: some Collection + ) throws -> Int { if keys.isEmpty { // Avoid hitting the database return 0 @@ -484,9 +483,10 @@ extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// - ids: A collection of primary keys. /// - returns: The number of deleted records. @discardableResult - public static func deleteAll(_ db: Database, ids: IDS) throws -> Int - where IDS: Collection, IDS.Element == ID - { + public static func deleteAll( + _ db: Database, + ids: some Collection + ) throws -> Int { if ids.isEmpty { // Avoid hitting the database return 0 diff --git a/GRDB/Utils/OrderedDictionary.swift b/GRDB/Utils/OrderedDictionary.swift index 228f84dfc3..ad40174e6b 100644 --- a/GRDB/Utils/OrderedDictionary.swift +++ b/GRDB/Utils/OrderedDictionary.swift @@ -114,12 +114,10 @@ struct OrderedDictionary { return OrderedDictionary(keys: keys, dictionary: dictionary) } - mutating func merge( - _ other: S, - uniquingKeysWith combine: (Value, Value) throws -> Value) - rethrows - where S: Sequence, S.Element == (Key, Value) - { + mutating func merge( + _ other: some Sequence<(Key, Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows { for (key, value) in other { if let current = self[key] { self[key] = try combine(current, value) @@ -129,12 +127,10 @@ struct OrderedDictionary { } } - mutating func merge( - _ other: S, - uniquingKeysWith combine: (Value, Value) throws -> Value) - rethrows - where S: Sequence, S.Element == (key: Key, value: Value) - { + mutating func merge( + _ other: some Sequence<(key: Key, value: Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows { for (key, value) in other { if let current = self[key] { self[key] = try combine(current, value) @@ -144,23 +140,19 @@ struct OrderedDictionary { } } - func merging( - _ other: S, - uniquingKeysWith combine: (Value, Value) throws -> Value) - rethrows -> OrderedDictionary - where S: Sequence, S.Element == (Key, Value) - { + func merging( + _ other: some Sequence<(Key, Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows -> OrderedDictionary { var result = self try result.merge(other, uniquingKeysWith: combine) return result } - func merging( - _ other: S, - uniquingKeysWith combine: (Value, Value) throws -> Value) - rethrows -> OrderedDictionary - where S: Sequence, S.Element == (key: Key, value: Value) - { + func merging( + _ other: some Sequence<(key: Key, value: Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows -> OrderedDictionary { var result = self try result.merge(other, uniquingKeysWith: combine) return result diff --git a/TODO.md b/TODO.md index 0db5074a64..522e0e0918 100644 --- a/TODO.md +++ b/TODO.md @@ -158,7 +158,7 @@ - [?] GRDB7: Sendable: AsyncValueObservation (ce63cdfa) - [X] GRDB7: Sendable: DatabaseRegionObservation (b4ff52fb) - [ ] GRDB7: DispatchQueue.asyncSending (7b075e6b) -- [ ] GRDB7: Replace sequences with collection (e.g. https://github.com/tidal-music/tidal-sdk-ios/pull/39) +- [X] GRDB7: Replace sequences with collection (e.g. https://github.com/tidal-music/tidal-sdk-ios/pull/39) - [ ] GRDB7: Replace `some` DatabaseReader/Writer with `any` where possible, in order to avoid issues with accessing DatabaseContext from GRDBQuery (if the problem exists in Xcode 16) - [?] GRDB7: Change ValueObservation callback argument so that it could expose snapshots? https://github.com/groue/GRDB.swift/discussions/1523#discussioncomment-9092500 From 840976867694f2fa6b96942fbe8bd09ed63e068c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 13:15:06 +0200 Subject: [PATCH 071/160] TODO --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 522e0e0918..95d9a5d475 100644 --- a/TODO.md +++ b/TODO.md @@ -127,7 +127,7 @@ - [X] GRDB7: Sendable: SQLRelation (9545bf70) - [X] GRDB7: Sendable: SQL (ac33856f) - [ ] GRDB7: Split Row.swift (2ce8a619) -- [ ] GRDB7: Cleanup ValueReducer (6c73b1c5) +- [X] GRDB7: Cleanup ValueReducer (6c73b1c5) - [X] GRDB7: DatabaseCursor has a primary associated type (b11c5dd2) - [ ] GRDB7: Enable Strict Concurrency Checks (6aa43ded) - [X] GRDB7: Sendable: OrderedDictionary (e022c35b) From ea4d208037f69ef3e1199e1bdb56d5b6a53d38f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 13:32:59 +0200 Subject: [PATCH 072/160] Rename ReadWriteBox to ReadWriteLock --- GRDB.xcodeproj/project.pbxproj | 8 +- .../Record/MutablePersistableRecord+DAO.swift | 86 ++++++++++--------- GRDB/Utils/Pool.swift | 12 +-- GRDB/Utils/ReadWriteBox.swift | 52 ----------- GRDB/Utils/ReadWriteLock.swift | 32 +++++++ GRDBCustom.xcodeproj/project.pbxproj | 8 +- TODO.md | 2 +- Tests/GRDBTests/PoolTests.swift | 4 +- 8 files changed, 96 insertions(+), 108 deletions(-) delete mode 100644 GRDB/Utils/ReadWriteBox.swift create mode 100644 GRDB/Utils/ReadWriteLock.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 8cdbcc0aa8..89695e6690 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -167,7 +167,7 @@ 5657AAB91D107001006283EF /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AAB81D107001006283EF /* NSData.swift */; }; 5657AB0F1D10899D006283EF /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB0E1D10899D006283EF /* URL.swift */; }; 5659F4881EA8D94E004A4992 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4871EA8D94E004A4992 /* Utils.swift */; }; - 5659F4901EA8D964004A4992 /* ReadWriteBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */; }; + 5659F4901EA8D964004A4992 /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */; }; 5659F4981EA8D989004A4992 /* Pool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4971EA8D989004A4992 /* Pool.swift */; }; 5664759A1D97D8A000FF74B8 /* SQLCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566475991D97D8A000FF74B8 /* SQLCollection.swift */; }; 566475CC1D981D5E00FF74B8 /* SQLFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566475CA1D981D5E00FF74B8 /* SQLFunctions.swift */; }; @@ -605,7 +605,7 @@ 5657AB341D108BA9006283EF /* FoundationNSURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSURLTests.swift; sourceTree = ""; }; 5657AB351D108BA9006283EF /* FoundationURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationURLTests.swift; sourceTree = ""; }; 5659F4871EA8D94E004A4992 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteBox.swift; sourceTree = ""; }; + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; 5659F4971EA8D989004A4992 /* Pool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pool.swift; sourceTree = ""; }; 565B0FEE1BBC7D980098DE03 /* FetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableRecordTests.swift; sourceTree = ""; }; 565D5D701BBC694D00DC9BD4 /* Row+FoundationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Row+FoundationTests.swift"; sourceTree = ""; }; @@ -1328,7 +1328,7 @@ 563B8FC424A1D3B9007A48C9 /* OnDemandFuture.swift */, 563EF414215F87EB007DAACD /* OrderedDictionary.swift */, 5659F4971EA8D989004A4992 /* Pool.swift */, - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */, + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */, 563B8FB424A1D029007A48C9 /* ReceiveValuesOn.swift */, 56781B0A243F86E600650A83 /* Refinable.swift */, 5659F4871EA8D94E004A4992 /* Utils.swift */, @@ -2185,7 +2185,7 @@ 563B06AB217EF0CC00B38F35 /* ValueObservation.swift in Sources */, 56D110FA28AFC97E00E64463 /* MutablePersistableRecord+DAO.swift in Sources */, 56CEB5111EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */, - 5659F4901EA8D964004A4992 /* ReadWriteBox.swift in Sources */, + 5659F4901EA8D964004A4992 /* ReadWriteLock.swift in Sources */, 566A841A2041146100E50BFD /* DatabaseSnapshot.swift in Sources */, 569EF0E2200D2D8400A9FA45 /* DatabaseRegion.swift in Sources */, 56CEB4F11EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */, diff --git a/GRDB/Record/MutablePersistableRecord+DAO.swift b/GRDB/Record/MutablePersistableRecord+DAO.swift index b63c1ab3a7..89efd4cd7d 100644 --- a/GRDB/Record/MutablePersistableRecord+DAO.swift +++ b/GRDB/Record/MutablePersistableRecord+DAO.swift @@ -273,29 +273,33 @@ private struct InsertQuery: Hashable { } extension InsertQuery { - @ReadWriteBox private static var sqlCache: [InsertQuery: String] = [:] + private static let cacheLock: ReadWriteLock<[InsertQuery: String]> = ReadWriteLock([:]) var sql: String { - if let sql = Self.sqlCache[self] { + if let sql = Self.cacheLock.read({ $0[self] }) { return sql } - let columnsSQL = insertedColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", ") - let valuesSQL = databaseQuestionMarks(count: insertedColumns.count) - let sql: String - switch onConflict { - case .abort: - sql = """ - INSERT INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ - VALUES (\(valuesSQL)) - """ - default: - sql = """ - INSERT OR \(onConflict.rawValue) \ - INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ - VALUES (\(valuesSQL)) - """ + + return Self.cacheLock.withLock { cache in + let columnsSQL = insertedColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", ") + let valuesSQL = databaseQuestionMarks(count: insertedColumns.count) + let sql: String + switch onConflict { + case .abort: + sql = """ + INSERT INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ + VALUES (\(valuesSQL)) + """ + default: + sql = """ + INSERT OR \(onConflict.rawValue) \ + INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ + VALUES (\(valuesSQL)) + """ + } + + cache[self] = sql + return sql } - Self.sqlCache[self] = sql - return sql } } @@ -309,30 +313,34 @@ private struct UpdateQuery: Hashable { } extension UpdateQuery { - @ReadWriteBox private static var sqlCache: [UpdateQuery: String] = [:] + private static let cacheLock: ReadWriteLock<[UpdateQuery: String]> = ReadWriteLock([:]) var sql: String { - if let sql = Self.sqlCache[self] { + if let sql = Self.cacheLock.read({ $0[self] }) { return sql } - let updateSQL = updatedColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: ", ") - let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ") - let sql: String - switch onConflict { - case .abort: - sql = """ - UPDATE \(tableName.quotedDatabaseIdentifier) \ - SET \(updateSQL) \ - WHERE \(whereSQL) - """ - default: - sql = """ - UPDATE OR \(onConflict.rawValue) \(tableName.quotedDatabaseIdentifier) \ - SET \(updateSQL) \ - WHERE \(whereSQL) - """ + + return Self.cacheLock.withLock { cache in + let updateSQL = updatedColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: ", ") + let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ") + let sql: String + switch onConflict { + case .abort: + sql = """ + UPDATE \(tableName.quotedDatabaseIdentifier) \ + SET \(updateSQL) \ + WHERE \(whereSQL) + """ + default: + sql = """ + UPDATE OR \(onConflict.rawValue) \(tableName.quotedDatabaseIdentifier) \ + SET \(updateSQL) \ + WHERE \(whereSQL) + """ + } + + cache[self] = sql + return sql } - Self.sqlCache[self] = sql - return sql } } diff --git a/GRDB/Utils/Pool.swift b/GRDB/Utils/Pool.swift index 0a8be5338d..0e38d4550b 100644 --- a/GRDB/Utils/Pool.swift +++ b/GRDB/Utils/Pool.swift @@ -37,7 +37,7 @@ import Dispatch /// got 3 final class Pool: Sendable { private class Item: @unchecked Sendable { - // @unchecked because `isAvailable` is protected by `Pool.content`. + // @unchecked Sendable because `isAvailable` is protected by `contentLock`. let element: T var isAvailable: Bool @@ -59,7 +59,7 @@ final class Pool: Sendable { typealias ElementAndRelease = (element: T, release: @Sendable (PoolCompletion) -> Void) private let makeElement: @Sendable (Int) throws -> T - private let content = ReadWriteBox(wrappedValue: Content(items: [], createdCount: 0)) + private let contentLock = ReadWriteLock(Content(items: [], createdCount: 0)) private let itemsSemaphore: DispatchSemaphore // limits the number of elements private let itemsGroup: DispatchGroup // knows when no element is used private let barrierQueue: DispatchQueue @@ -93,7 +93,7 @@ final class Pool: Sendable { itemsSemaphore.wait() itemsGroup.enter() do { - let item = try content.update { content -> Item in + let item = try contentLock.withLock { content -> Item in if let item = content.items.first(where: \.isAvailable) { item.isAvailable = false return item @@ -142,7 +142,7 @@ final class Pool: Sendable { } private func release(_ item: Item, completion: PoolCompletion) { - content.update { content in + contentLock.withLock { content in switch completion { case .reuse: // This is why Item is a class, not a struct: so that we can @@ -162,7 +162,7 @@ final class Pool: Sendable { /// Performs a block on each pool element, available or not. /// The block is run is some arbitrary dispatch queue. func forEach(_ body: (T) throws -> Void) rethrows { - try content.read { content in + try contentLock.read { content in for item in content.items { try body(item.element) } @@ -172,7 +172,7 @@ final class Pool: Sendable { /// Removes all elements from the pool. /// Currently used elements won't be reused. func removeAll() { - content.update { $0.items.removeAll() } + contentLock.withLock { $0.items.removeAll() } } /// Blocks until no element is used, and runs the `barrier` function before diff --git a/GRDB/Utils/ReadWriteBox.swift b/GRDB/Utils/ReadWriteBox.swift deleted file mode 100644 index 2c0cd88f8c..0000000000 --- a/GRDB/Utils/ReadWriteBox.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Dispatch - -/// A ReadWriteBox grants multiple readers and single-writer guarantees on a -/// value. It is backed by a concurrent DispatchQueue. -@propertyWrapper -final class ReadWriteBox: @unchecked Sendable { - // @unchecked because `_wrappedValue` is protected by `queue` - private var _wrappedValue: T - private var queue: DispatchQueue - - var wrappedValue: T { - get { read { $0 } } - set { update { $0 = newValue } } - } - - var projectedValue: ReadWriteBox { self } - - init(wrappedValue: T) { - _wrappedValue = wrappedValue - queue = DispatchQueue(label: "GRDB.ReadWriteBox", attributes: [.concurrent]) - } - - func read(_ block: (T) throws -> U) rethrows -> U { - try queue.sync { - try block(_wrappedValue) - } - } - - func update(_ block: (inout T) throws -> U) rethrows -> U { - try queue.sync(flags: [.barrier]) { - try block(&_wrappedValue) - } - } -} - -extension ReadWriteBox where T: Numeric { - @discardableResult - func increment() -> T { - update { n in - n += 1 - return n - } - } - - @discardableResult - func decrement() -> T { - update { n in - n -= 1 - return n - } - } -} diff --git a/GRDB/Utils/ReadWriteLock.swift b/GRDB/Utils/ReadWriteLock.swift new file mode 100644 index 0000000000..85f191efb8 --- /dev/null +++ b/GRDB/Utils/ReadWriteLock.swift @@ -0,0 +1,32 @@ +import Dispatch + +/// A ReadWriteLock grants multiple readers and single-writer guarantees on +/// a value. It is backed by a concurrent DispatchQueue. +final class ReadWriteLock { + private var _value: T + private var queue: DispatchQueue + + init(_ value: T) { + _value = value + queue = DispatchQueue(label: "GRDB.ReadWriteLock", attributes: [.concurrent]) + } + + /// Reads the value. + func read(_ body: (T) throws -> U) rethrows -> U { + try queue.sync { + try body(_value) + } + } + + /// Runs the provided closure while holding a lock on the value. + /// + /// - parameter body: A closure that can modify the value. + func withLock(_ body: (inout T) throws -> U) rethrows -> U { + try queue.sync(flags: [.barrier]) { + try body(&_value) + } + } +} + +// @unchecked because `_value` is protected by `queue` +extension ReadWriteLock: @unchecked Sendable where T: Sendable { } diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index e1b707bca6..a4ab361527 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -167,7 +167,7 @@ 5657AB611D108BA9006283EF /* FoundationNSURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB341D108BA9006283EF /* FoundationNSURLTests.swift */; }; 5657AB691D108BA9006283EF /* FoundationURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB351D108BA9006283EF /* FoundationURLTests.swift */; }; 5659F48A1EA8D94E004A4992 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4871EA8D94E004A4992 /* Utils.swift */; }; - 5659F4921EA8D964004A4992 /* ReadWriteBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */; }; + 5659F4921EA8D964004A4992 /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */; }; 5659F49A1EA8D989004A4992 /* Pool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4971EA8D989004A4992 /* Pool.swift */; }; 565EFAF11D0436CE00A8FA9D /* NumericOverflowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565EFAED1D0436CE00A8FA9D /* NumericOverflowTests.swift */; }; 5665F868203EF4640084C6C0 /* ColumnInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5665F865203EF4590084C6C0 /* ColumnInfoTests.swift */; }; @@ -635,7 +635,7 @@ 5657AB341D108BA9006283EF /* FoundationNSURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSURLTests.swift; sourceTree = ""; }; 5657AB351D108BA9006283EF /* FoundationURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationURLTests.swift; sourceTree = ""; }; 5659F4871EA8D94E004A4992 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteBox.swift; sourceTree = ""; }; + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; 5659F4971EA8D989004A4992 /* Pool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pool.swift; sourceTree = ""; }; 565B0FEE1BBC7D980098DE03 /* FetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableRecordTests.swift; sourceTree = ""; }; 565D5D701BBC694D00DC9BD4 /* Row+FoundationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Row+FoundationTests.swift"; sourceTree = ""; }; @@ -1349,7 +1349,7 @@ 563B8FBC24A1D388007A48C9 /* OnDemandFuture.swift */, 563EF41E215F8A76007DAACD /* OrderedDictionary.swift */, 5659F4971EA8D989004A4992 /* Pool.swift */, - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */, + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */, 563B8FB924A1D036007A48C9 /* ReceiveValuesOn.swift */, 56DF37A623D77AA0009AAA05 /* Refinable.swift */, 5659F4871EA8D94E004A4992 /* Utils.swift */, @@ -2009,7 +2009,7 @@ 56012B82257404A400B4925B /* CommonTableExpression.swift in Sources */, 5656A8972295BD56001FF3FF /* SQLRelation.swift in Sources */, 56D110FF28AFC9C600E64463 /* MutablePersistableRecord+DAO.swift in Sources */, - 5659F4921EA8D964004A4992 /* ReadWriteBox.swift in Sources */, + 5659F4921EA8D964004A4992 /* ReadWriteLock.swift in Sources */, 566A842D20413D9A00E50BFD /* DatabaseSnapshot.swift in Sources */, 56CEB4F31EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */, 5656A8512295BD56001FF3FF /* SQLInterpolation+QueryInterface.swift in Sources */, diff --git a/TODO.md b/TODO.md index 95d9a5d475..2bed7e01cb 100644 --- a/TODO.md +++ b/TODO.md @@ -131,7 +131,7 @@ - [X] GRDB7: DatabaseCursor has a primary associated type (b11c5dd2) - [ ] GRDB7: Enable Strict Concurrency Checks (6aa43ded) - [X] GRDB7: Sendable: OrderedDictionary (e022c35b) -- [ ] GRDB7: Rename ReadWriteBox to ReadWriteLock (7f5205ef) +- [X] GRDB7: Rename ReadWriteBox to ReadWriteLock (7f5205ef) - [X] GRDB7: Sendable: DatabaseRegionConvertible (b4677ded) - [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65, 5465d056) - [ ] GRDB7: Sendable: ValueWriteOnlyObserver (ff2a7548) diff --git a/Tests/GRDBTests/PoolTests.swift b/Tests/GRDBTests/PoolTests.swift index dbce4868bd..8729ae8171 100644 --- a/Tests/GRDBTests/PoolTests.swift +++ b/Tests/GRDBTests/PoolTests.swift @@ -4,9 +4,9 @@ import XCTest class PoolTests: XCTestCase { /// Returns a Pool whose elements are incremented integers: 1, 2, 3... private func makeCounterPool(maximumCount: Int) -> Pool { - let count = ReadWriteBox(wrappedValue: 0) + let countMutex = Mutex(0) return Pool(maximumCount: maximumCount, makeElement: { _ in - count.increment() + countMutex.increment() }) } From 7084c95cbdb1e243da2485b945fc49a1aee26398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 13:39:38 +0200 Subject: [PATCH 073/160] TODO --- TODO.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 2bed7e01cb..2bc0e5d293 100644 --- a/TODO.md +++ b/TODO.md @@ -149,11 +149,11 @@ - [ ] GRDB7: ValueObservation.print cautiously uses its stream argument (5f8b39b7) - [ ] GRDB7/Tests: use a single and Sendable test TextOutputStream (bbb1a736) - [ ] GRDB7: ValueObservation needs a ValueReducer, not a `_ValueReducer` (08733108) -- [ ] GRDB7: Database support for cancellation (4ddf4bca) -- [ ] GRDB7: SerializedDatabase support for async db access with support for Task cancellation (737cb149) -- [ ] GRDB7: DatabaseWriter async methods support Task cancellation (a5226501) -- [ ] GRDB7: DatabaseReader async methods support Task cancellation (10c9d311) -- [ ] GRDB7: Document that async methods can throw CancellationError (8df18fb8) +- [X] GRDB7: Database support for cancellation (4ddf4bca) +- [X] GRDB7: SerializedDatabase support for async db access with support for Task cancellation (737cb149) +- [X] GRDB7: DatabaseWriter async methods support Task cancellation (a5226501) +- [X] GRDB7: DatabaseReader async methods support Task cancellation (10c9d311) +- [X] GRDB7: Document that async methods can throw CancellationError (8df18fb8) - [ ] GRDB7: Sendable: AssociationAggregate (48ad10ae) - [?] GRDB7: Sendable: AsyncValueObservation (ce63cdfa) - [X] GRDB7: Sendable: DatabaseRegionObservation (b4ff52fb) From a7a9c751858d906a179db5e4d9528a2db9cb6a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 13:41:13 +0200 Subject: [PATCH 074/160] Place `@escaping` before `@Sendable` --- GRDB/Core/DatabasePool.swift | 8 ++++---- GRDB/Core/DatabaseQueue.swift | 8 ++++---- GRDB/Core/DatabaseReader.swift | 8 ++++---- GRDB/Core/DatabaseSnapshot.swift | 4 ++-- GRDB/Core/DatabaseSnapshotPool.swift | 4 ++-- GRDB/Core/DatabaseWriter.swift | 14 +++++++------- GRDB/Core/SerializedDatabase.swift | 2 +- Tests/GRDBTests/ValueObservationTests.swift | 2 +- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index b0f5c1b1a6..1302e08225 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -353,7 +353,7 @@ extension DatabasePool: DatabaseReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func read( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.") guard let readerPool else { @@ -436,7 +436,7 @@ extension DatabasePool: DatabaseReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func unsafeRead( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { guard let readerPool else { throw DatabaseError.connectionIsClosed() @@ -797,7 +797,7 @@ extension DatabasePool: DatabaseWriter { @available(iOS 13, macOS 10.15, tvOS 13, *) public func writeWithoutTransaction( - _ updates: @Sendable @escaping (Database) throws -> T + _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writer.execute(updates) } @@ -814,7 +814,7 @@ extension DatabasePool: DatabaseWriter { @available(iOS 13, macOS 10.15, tvOS 13, *) public func barrierWriteWithoutTransaction( - _ updates: @Sendable @escaping (Database) throws -> T + _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { let dbAccess = CancellableDatabaseAccess() return try await dbAccess.withCancellableContinuation { continuation in diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index a1175feeec..eb5a43f197 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -235,7 +235,7 @@ extension DatabaseQueue: DatabaseReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func read( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writer.execute { db in try db.isolated(readOnly: true) { @@ -272,7 +272,7 @@ extension DatabaseQueue: DatabaseReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func unsafeRead( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writer.execute(value) } @@ -383,7 +383,7 @@ extension DatabaseQueue: DatabaseWriter { @available(iOS 13, macOS 10.15, tvOS 13, *) public func writeWithoutTransaction( - _ updates: @Sendable @escaping (Database) throws -> T + _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writer.execute(updates) } @@ -395,7 +395,7 @@ extension DatabaseQueue: DatabaseWriter { @available(iOS 13, macOS 10.15, tvOS 13, *) public func barrierWriteWithoutTransaction( - _ updates: @Sendable @escaping (Database) throws -> T + _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writer.execute(updates) } diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 691af1fd16..06b5078363 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -218,7 +218,7 @@ public protocol DatabaseReader: AnyObject, Sendable { /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) func read( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T /// Schedules read-only database operations for execution, and @@ -323,7 +323,7 @@ public protocol DatabaseReader: AnyObject, Sendable { /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) func unsafeRead( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T /// Schedules database operations for execution, and returns immediately. @@ -660,7 +660,7 @@ extension AnyDatabaseReader: DatabaseReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func read( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.read(value) } @@ -676,7 +676,7 @@ extension AnyDatabaseReader: DatabaseReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func unsafeRead( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.unsafeRead(value) } diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 8a868a416a..3a1fa807f5 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -153,7 +153,7 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func read( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await reader.execute(value) } @@ -173,7 +173,7 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { // . @available(iOS 13, macOS 10.15, tvOS 13, *) public func unsafeRead( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await read(value) } diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index 1fc8e788d0..3203dc605e 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -295,7 +295,7 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func read( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { guard let readerPool else { throw DatabaseError.connectionIsClosed() @@ -353,7 +353,7 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { // . @available(iOS 13, macOS 10.15, tvOS 13, *) public func unsafeRead( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await read(value) } diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index dd9fafb4ac..4aafc7fe1c 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -133,7 +133,7 @@ public protocol DatabaseWriter: DatabaseReader { /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) func writeWithoutTransaction( - _ updates: @Sendable @escaping (Database) throws -> T + _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T /// Executes database operations, and returns their result after they have @@ -219,7 +219,7 @@ public protocol DatabaseWriter: DatabaseReader { /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) func barrierWriteWithoutTransaction( - _ updates: @Sendable @escaping (Database) throws -> T) + _ updates: @escaping @Sendable (Database) throws -> T) async throws -> T /// Schedules database operations for execution, and returns immediately. @@ -646,7 +646,7 @@ extension DatabaseWriter { /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) public func write( - _ updates: @Sendable @escaping (Database) throws -> T + _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writeWithoutTransaction { db in var result: T? @@ -911,7 +911,7 @@ extension AnyDatabaseWriter: DatabaseReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func read( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.read(value) } @@ -927,7 +927,7 @@ extension AnyDatabaseWriter: DatabaseReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func unsafeRead( - _ value: @Sendable @escaping (Database) throws -> T + _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.unsafeRead(value) } @@ -961,7 +961,7 @@ extension AnyDatabaseWriter: DatabaseWriter { @available(iOS 13, macOS 10.15, tvOS 13, *) public func writeWithoutTransaction( - _ updates: @Sendable @escaping (Database) throws -> T + _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.writeWithoutTransaction(updates) } @@ -973,7 +973,7 @@ extension AnyDatabaseWriter: DatabaseWriter { @available(iOS 13, macOS 10.15, tvOS 13, *) public func barrierWriteWithoutTransaction( - _ updates: @Sendable @escaping (Database) throws -> T) + _ updates: @escaping @Sendable (Database) throws -> T) async throws -> T { try await base.barrierWriteWithoutTransaction(updates) } diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index ede400fce6..b2dccb74a8 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -246,7 +246,7 @@ final class SerializedDatabase { /// Asynchrously executes the block. @available(iOS 13, macOS 10.15, tvOS 13, *) func execute( - _ block: @Sendable @escaping (Database) throws -> T + _ block: @escaping @Sendable (Database) throws -> T ) async throws -> T { let dbAccess = CancellableDatabaseAccess() return try await dbAccess.withCancellableContinuation { continuation in diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index c947a44d63..7699a94396 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -24,7 +24,7 @@ class ValueObservationTests: GRDBTestCase { @available(iOS 13, macOS 10.15, tvOS 13, *) func testValuesFromAnyDatabaseWriter(writer: any DatabaseWriter) { func observe( - fetch: @Sendable @escaping (Database) throws -> T + fetch: @escaping @Sendable (Database) throws -> T ) throws -> AsyncValueObservation { ValueObservation.tracking(fetch).values(in: writer) } From 0b6ac870e2071fff553f08fff1cef1cc6564155e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 14:42:21 +0200 Subject: [PATCH 075/160] [SENDING REGRET] ValueObservationScheduler requires Sendable closures We should be able to use sending closures instead, but DispatchQueue.async does not accept sending closures yet. --- GRDB/ValueObservation/ValueObservationScheduler.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GRDB/ValueObservation/ValueObservationScheduler.swift b/GRDB/ValueObservation/ValueObservationScheduler.swift index df109cfee0..3e4d9fdce8 100644 --- a/GRDB/ValueObservation/ValueObservationScheduler.swift +++ b/GRDB/ValueObservation/ValueObservationScheduler.swift @@ -17,11 +17,11 @@ public protocol ValueObservationScheduler: Sendable { /// If the result is true, then this method was called on the main thread. func immediateInitialValue() -> Bool - func schedule(_ action: @escaping () -> Void) + func schedule(_ action: @escaping @Sendable () -> Void) } extension ValueObservationScheduler { - func scheduleInitial(_ action: @escaping () -> Void) { + func scheduleInitial(_ action: @escaping @Sendable () -> Void) { if immediateInitialValue() { action() } else { @@ -42,7 +42,7 @@ public struct AsyncValueObservationScheduler: ValueObservationScheduler { public func immediateInitialValue() -> Bool { false } - public func schedule(_ action: @escaping () -> Void) { + public func schedule(_ action: @escaping @Sendable () -> Void) { queue.async(execute: action) } } @@ -90,7 +90,7 @@ public struct ImmediateValueObservationScheduler: ValueObservationScheduler, Sen return true } - public func schedule(_ action: @escaping () -> Void) { + public func schedule(_ action: @escaping @Sendable () -> Void) { DispatchQueue.main.async(execute: action) } } From 55560ccd90c64fcbb8723bec605931706bc89629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 14:42:50 +0200 Subject: [PATCH 076/160] [SENDING REGRET] Pool async methods require Sendable closures We should be able to use sending closures instead, but DispatchQueue.async does not accept sending closures yet. --- GRDB/Utils/Pool.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GRDB/Utils/Pool.swift b/GRDB/Utils/Pool.swift index 0e38d4550b..661c9c4d9d 100644 --- a/GRDB/Utils/Pool.swift +++ b/GRDB/Utils/Pool.swift @@ -121,7 +121,7 @@ final class Pool: Sendable { /// /// - important: The `execute` argument is executed in a serial dispatch /// queue, so make sure you use the element asynchronously. - func asyncGet(_ execute: @escaping (Result) -> Void) { + func asyncGet(_ execute: @escaping @Sendable (Result) -> Void) { // Inspired by https://khanlou.com/2016/04/the-GCD-handbook/ // > We wait on the semaphore in the serial queue, which means that // > we’ll have at most one blocked thread when we reach maximum @@ -186,7 +186,7 @@ final class Pool: Sendable { /// Asynchronously runs the `barrier` function when no element is used, and /// before any other element is dequeued. - func asyncBarrier(execute barrier: @escaping () -> Void) { + func asyncBarrier(execute barrier: @escaping @Sendable () -> Void) { barrierQueue.async(flags: [.barrier]) { self.itemsGroup.wait() barrier() From 6371dc05d5f720369f71450c9c0c7e334b91974f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 14:44:27 +0200 Subject: [PATCH 077/160] OnDemandFuture closure is Sendable It can be called at any time, when the publisher is subscribed. --- GRDB/Utils/OnDemandFuture.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GRDB/Utils/OnDemandFuture.swift b/GRDB/Utils/OnDemandFuture.swift index 645da8d199..c18dc54654 100644 --- a/GRDB/Utils/OnDemandFuture.swift +++ b/GRDB/Utils/OnDemandFuture.swift @@ -22,9 +22,9 @@ struct OnDemandFuture: Publisher { typealias Promise = @Sendable (Result) -> Void typealias Output = Output typealias Failure = Failure - fileprivate let attemptToFulfill: (@escaping Promise) -> Void + fileprivate let attemptToFulfill: @Sendable (@escaping Promise) -> Void - init(_ attemptToFulfill: @escaping (@escaping Promise) -> Void) { + init(_ attemptToFulfill: @escaping @Sendable (@escaping Promise) -> Void) { self.attemptToFulfill = attemptToFulfill } @@ -51,7 +51,7 @@ private class OnDemandFutureSubscription: Subscription, private let lock = NSRecursiveLock() // Allow re-entrancy init( - attemptToFulfill: @escaping (@escaping Promise) -> Void, + attemptToFulfill: @escaping @Sendable (@escaping Promise) -> Void, downstream: Downstream) { self.state = .waitingForDemand(downstream: downstream, attemptToFulfill: attemptToFulfill) From c122872734d2e08ebaf0417b53ae26311299ab0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 14:45:52 +0200 Subject: [PATCH 078/160] [SENDING REGRET] SerializedDatabase.async requires a Sendable closure We should be able to use a sending closure instead, but DispatchQueue.async does not accept sending closures yet. --- GRDB/Core/SerializedDatabase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index b2dccb74a8..2251dcd7ee 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -223,7 +223,7 @@ final class SerializedDatabase { } /// Schedules database operations for execution, and returns immediately. - func async(_ block: @escaping (Database) -> Void) { + func async(_ block: @escaping @Sendable (Database) -> Void) { queue.async { block(self.db) self.preconditionNoUnsafeTransactionLeft(self.db) From b3ac63b0cd1104fa2d0c02ae26b4ebadb13d95a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 14:46:31 +0200 Subject: [PATCH 079/160] [SENDING REGRET] DatabaseMigrator.asyncMigrate requires a Sendable closure We should be able to use a sending closure instead, but DispatchQueue.async does not accept sending closures yet. --- GRDB/Migration/DatabaseMigrator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GRDB/Migration/DatabaseMigrator.swift b/GRDB/Migration/DatabaseMigrator.swift index 00c759c220..700c7085f3 100644 --- a/GRDB/Migration/DatabaseMigrator.swift +++ b/GRDB/Migration/DatabaseMigrator.swift @@ -264,7 +264,7 @@ public struct DatabaseMigrator: Sendable { /// from succeeding. public func asyncMigrate( _ writer: some DatabaseWriter, - completion: @escaping (Result) -> Void) + completion: @escaping @Sendable (Result) -> Void) { writer.asyncBarrierWriteWithoutTransaction { dbResult in do { From e35fb7930856a6e2e964075f64362d60260ee2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 16:12:33 +0200 Subject: [PATCH 080/160] DatabaseRegionObservation callbacks are Sendable --- GRDB/Core/DatabaseRegionObservation.swift | 23 ++++++++++++++--------- GRDB/Utils/Utils.swift | 12 ++++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/GRDB/Core/DatabaseRegionObservation.swift b/GRDB/Core/DatabaseRegionObservation.swift index 877e14feaa..dc4d00f8f6 100644 --- a/GRDB/Core/DatabaseRegionObservation.swift +++ b/GRDB/Core/DatabaseRegionObservation.swift @@ -43,10 +43,10 @@ extension DatabaseRegionObservation { extension DatabaseRegionObservation { /// The state of a started DatabaseRegionObservation - private enum ObservationState { + private enum ObservationState: Sendable { case cancelled case pending - case started(DatabaseRegionObserver) + case started(StrongReference) } /// Starts observing the database. @@ -85,8 +85,8 @@ extension DatabaseRegionObservation { /// - returns: A DatabaseCancellable that can stop the observation. public func start( in writer: some DatabaseWriter, - onError: @escaping (Error) -> Void, - onChange: @escaping (Database) -> Void) + onError: @escaping @Sendable (Error) -> Void, + onChange: @escaping @Sendable (Database) -> Void) -> AnyDatabaseCancellable { let stateMutex = Mutex(ObservationState.pending) @@ -111,7 +111,7 @@ extension DatabaseRegionObservation { // the observer. db.add(transactionObserver: observer, extent: .observerLifetime) - state = .started(observer) + state = .started(StrongReference(observer)) } } catch { onError(error) @@ -149,10 +149,10 @@ extension DatabaseRegionObservation { private class DatabaseRegionObserver: TransactionObserver { let region: DatabaseRegion - let onChange: (Database) -> Void + let onChange: @Sendable (Database) -> Void var isChanged = false - init(region: DatabaseRegion, onChange: @escaping (Database) -> Void) { + init(region: DatabaseRegion, onChange: @escaping @Sendable (Database) -> Void) { self.region = region self.onChange = onChange } @@ -212,9 +212,14 @@ extension DatabasePublishers { } } - private class DatabaseRegionSubscription: Subscription - where Downstream.Failure == Error, Downstream.Input == Database + private class DatabaseRegionSubscription: + Subscription, @unchecked Sendable + where Downstream: Subscriber, + Downstream.Failure == Error, + Downstream.Input == Database { + // @unchecked Sendable because `cancellable` and `state` are + // protected by `lock`. private struct WaitingForDemand { let downstream: Downstream let writer: any DatabaseWriter diff --git a/GRDB/Utils/Utils.swift b/GRDB/Utils/Utils.swift index 40095bba8d..b48db435c4 100644 --- a/GRDB/Utils/Utils.swift +++ b/GRDB/Utils/Utils.swift @@ -124,6 +124,18 @@ struct PrintOutputStream: TextOutputStream { } } +/// A Sendable strong reference to an object. +/// +/// This type hides its retained object in order to provide the +/// Sendable guarantee. +final class StrongReference: @unchecked Sendable { + private let value: Value + + init(_ value: Value) { + self.value = value + } +} + /// Concatenates two functions func concat(_ rhs: (() -> Void)?, _ lhs: (() -> Void)?) -> (() -> Void)? { switch (rhs, lhs) { From 62d96ec544a407cd8963a2a320a38500d5a02035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 16:16:40 +0200 Subject: [PATCH 081/160] [SENDING REGRET] Async database accesses require Sendable closures We should be able to use sending closures instead, but DispatchQueue.async does not accept sending closures yet. --- GRDB/Core/DatabasePool.swift | 28 ++++++++++++----- GRDB/Core/DatabaseQueue.swift | 20 +++++++++--- GRDB/Core/DatabaseReader.swift | 20 +++++++++--- GRDB/Core/DatabaseSnapshot.swift | 8 +++-- GRDB/Core/DatabaseSnapshotPool.swift | 4 ++- GRDB/Core/DatabaseWriter.swift | 46 +++++++++++++++++++--------- 6 files changed, 91 insertions(+), 35 deletions(-) diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index 1302e08225..c209129571 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -390,7 +390,9 @@ extension DatabasePool: DatabaseReader { } } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) return @@ -469,7 +471,9 @@ extension DatabasePool: DatabaseReader { } } - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) return @@ -514,7 +518,9 @@ extension DatabasePool: DatabaseReader { } } - public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { + public func spawnConcurrentRead( + _ value: @escaping @Sendable (Result) -> Void + ) { asyncConcurrentRead(value) } @@ -555,7 +561,9 @@ extension DatabasePool: DatabaseReader { /// ``` /// /// - parameter value: A function that accesses the database. - public func asyncConcurrentRead(_ value: @escaping (Result) -> Void) { + public func asyncConcurrentRead( + _ value: @escaping @Sendable (Result) -> Void + ) { // Check that we're on the writer queue... writer.execute { db in // ... and that no transaction is opened. @@ -714,7 +722,9 @@ extension DatabasePool: DatabaseReader { /// /// - important: The `completion` argument is executed in a serial /// dispatch queue, so make sure you use the transaction asynchronously. - func asyncWALSnapshotTransaction(_ completion: @escaping (Result) -> Void) { + func asyncWALSnapshotTransaction( + _ completion: @escaping @Sendable (Result) -> Void + ) { guard let readerPool else { completion(.failure(DatabaseError.connectionIsClosed())) return @@ -833,7 +843,9 @@ extension DatabasePool: DatabaseWriter { } } - public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { + public func asyncBarrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Result) -> Void + ) { guard let readerPool else { updates(.failure(DatabaseError.connectionIsClosed())) return @@ -887,7 +899,9 @@ extension DatabasePool: DatabaseWriter { try writer.reentrantSync(updates) } - public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) { + public func asyncWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) -> Void + ) { writer.async(updates) } } diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index eb5a43f197..e3db4d45e3 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -244,7 +244,9 @@ extension DatabaseQueue: DatabaseReader { } } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { writer.async { db in defer { // Ignore error because we can not notify it. @@ -277,7 +279,9 @@ extension DatabaseQueue: DatabaseReader { try await writer.execute(value) } - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { writer.async { value(.success($0)) } } @@ -285,7 +289,9 @@ extension DatabaseQueue: DatabaseReader { try writer.reentrantSync(value) } - public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { + public func spawnConcurrentRead( + _ value: @escaping @Sendable (Result) -> Void + ) { // Check that we're on the writer queue... writer.execute { db in // ... and that no transaction is opened. @@ -400,7 +406,9 @@ extension DatabaseQueue: DatabaseWriter { try await writer.execute(updates) } - public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { + public func asyncBarrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Result) -> Void + ) { writer.async { updates(.success($0)) } } @@ -446,7 +454,9 @@ extension DatabaseQueue: DatabaseWriter { try writer.reentrantSync(updates) } - public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) { + public func asyncWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) -> Void + ) { writer.async(updates) } } diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 06b5078363..4360ce926d 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -247,7 +247,9 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - parameter value: A closure which accesses the database. Its argument /// is a `Result` that provides the database connection, or the failure /// that would prevent establishing the read access to the database. - func asyncRead(_ value: @escaping (Result) -> Void) + func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) /// Executes database operations, and returns their result after they have /// finished executing. @@ -357,7 +359,9 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - parameter value: A closure which accesses the database. Its argument /// is a `Result` that provides the database connection, or the failure /// that would prevent establishing the read access to the database. - func asyncUnsafeRead(_ value: @escaping (Result) -> Void) + func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) /// Executes database operations, and returns their result after they have /// finished executing. @@ -665,7 +669,9 @@ extension AnyDatabaseReader: DatabaseReader { try await base.read(value) } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { base.asyncRead(value) } @@ -681,7 +687,9 @@ extension AnyDatabaseReader: DatabaseReader { try await base.unsafeRead(value) } - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { base.asyncUnsafeRead(value) } @@ -753,7 +761,9 @@ extension DatabaseSnapshotReader { } // There is no such thing as an unsafe access to a snapshot. - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { asyncRead(value) } } diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 3a1fa807f5..af771226d4 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -158,7 +158,9 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { try await reader.execute(value) } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { reader.async { value(.success($0)) } } @@ -178,7 +180,9 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { try await read(value) } - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { reader.async { value(.success($0)) } } diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index 3203dc605e..7a9911ce68 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -327,7 +327,9 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { } } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) return diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index 4aafc7fe1c..090de74a68 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -219,8 +219,8 @@ public protocol DatabaseWriter: DatabaseReader { /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) func barrierWriteWithoutTransaction( - _ updates: @escaping @Sendable (Database) throws -> T) - async throws -> T + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T /// Schedules database operations for execution, and returns immediately. /// @@ -261,7 +261,9 @@ public protocol DatabaseWriter: DatabaseReader { /// - parameter updates: A closure which accesses the database. Its argument /// is a `Result` that provides the database connection, or the failure /// that would prevent establishing the barrier access to the database. - func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) + func asyncBarrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Result) -> Void + ) /// Schedules database operations for execution, and returns immediately. /// @@ -293,7 +295,9 @@ public protocol DatabaseWriter: DatabaseReader { /// for more information. /// /// - parameter updates: A closure which accesses the database. - func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) + func asyncWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) -> Void + ) /// Executes database operations, and returns their result after they have /// finished executing. @@ -377,7 +381,9 @@ public protocol DatabaseWriter: DatabaseReader { /// - parameter value: A closure which accesses the database. Its argument /// is a `Result` that provides the database connection, or the failure /// that would prevent establishing the read access to the database. - func spawnConcurrentRead(_ value: @escaping (Result) -> Void) + func spawnConcurrentRead( + _ value: @escaping @Sendable (Result) -> Void + ) } extension DatabaseWriter { @@ -463,9 +469,9 @@ extension DatabaseWriter { /// - parameter updates: A closure which accesses the database. /// - parameter completion: A closure called with the transaction result. public func asyncWrite( - _ updates: @escaping (Database) throws -> T, - completion: @escaping (Database, Result) -> Void) - { + _ updates: @escaping @Sendable (Database) throws -> T, + completion: @escaping @Sendable (Database, Result) -> Void + ) { asyncWriteWithoutTransaction { db in do { var result: T? @@ -916,7 +922,9 @@ extension AnyDatabaseWriter: DatabaseReader { try await base.read(value) } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { base.asyncRead(value) } @@ -932,7 +940,9 @@ extension AnyDatabaseWriter: DatabaseReader { try await base.unsafeRead(value) } - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { base.asyncUnsafeRead(value) } @@ -973,16 +983,20 @@ extension AnyDatabaseWriter: DatabaseWriter { @available(iOS 13, macOS 10.15, tvOS 13, *) public func barrierWriteWithoutTransaction( - _ updates: @escaping @Sendable (Database) throws -> T) - async throws -> T { + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T { try await base.barrierWriteWithoutTransaction(updates) } - public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { + public func asyncBarrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Result) -> Void + ) { base.asyncBarrierWriteWithoutTransaction(updates) } - public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) { + public func asyncWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) -> Void + ) { base.asyncWriteWithoutTransaction(updates) } @@ -990,7 +1004,9 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.unsafeReentrantWrite(updates) } - public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { + public func spawnConcurrentRead( + _ value: @escaping @Sendable (Result) -> Void + ) { base.spawnConcurrentRead(value) } } From 5585e47c33a6ecbddeb69b2346ff6e21f3b9c26d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 16:17:17 +0200 Subject: [PATCH 082/160] Database publishers require Sendable closures --- GRDB/Core/DatabaseReader.swift | 5 ++--- GRDB/Core/DatabaseWriter.swift | 17 +++++++---------- GRDB/Migration/DatabaseMigrator.swift | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 4360ce926d..578e57227b 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -538,9 +538,8 @@ extension DatabaseReader { @available(iOS 13, macOS 10.15, tvOS 13, *) public func readPublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, - value: @escaping (Database) throws -> Output) - -> DatabasePublishers.Read - { + value: @escaping @Sendable (Database) throws -> Output + ) -> DatabasePublishers.Read { OnDemandFuture { fulfill in self.asyncRead { dbResult in fulfill(dbResult.flatMap { db in Result { try value(db) } }) diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index 090de74a68..a8cb9826c9 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -758,9 +758,8 @@ extension DatabaseWriter { @available(iOS 13, macOS 10.15, tvOS 13, *) public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, - updates: @escaping (Database) throws -> Output) - -> DatabasePublishers.Write - { + updates: @escaping @Sendable (Database) throws -> Output + ) -> DatabasePublishers.Write { OnDemandFuture { fulfill in self.asyncWrite(updates, completion: { _, result in fulfill(result) @@ -821,13 +820,11 @@ extension DatabaseWriter { /// - parameter updates: A closure which writes in the database. /// - parameter value: A closure which reads from the database. @available(iOS 13, macOS 10.15, tvOS 13, *) - public func writePublisher( - receiveOn scheduler: S = DispatchQueue.main, - updates: @escaping (Database) throws -> T, - thenRead value: @escaping (Database, T) throws -> Output) - -> DatabasePublishers.Write - where S: Scheduler - { + public func writePublisher( + receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, + updates: @escaping @Sendable (Database) throws -> T, + thenRead value: @escaping @Sendable (Database, T) throws -> Output + ) -> DatabasePublishers.Write { OnDemandFuture { fulfill in self.asyncWriteWithoutTransaction { db in var updatesValue: T? diff --git a/GRDB/Migration/DatabaseMigrator.swift b/GRDB/Migration/DatabaseMigrator.swift index 700c7085f3..b67336b799 100644 --- a/GRDB/Migration/DatabaseMigrator.swift +++ b/GRDB/Migration/DatabaseMigrator.swift @@ -499,7 +499,7 @@ extension DatabaseMigrator { @available(iOS 13, macOS 10.15, tvOS 13, *) public func migratePublisher( _ writer: some DatabaseWriter, - receiveOn scheduler: some Scheduler = DispatchQueue.main) + receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main) -> DatabasePublishers.Migrate { DatabasePublishers.Migrate( From 28faa97a3777922a88703a2d3f4ab86dd4a553a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 15:03:40 +0200 Subject: [PATCH 083/160] Database.afterNextTransaction callbacks are Sendable --- GRDB/Core/TransactionObserver.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/GRDB/Core/TransactionObserver.swift b/GRDB/Core/TransactionObserver.swift index 99fb4dcfdf..5c7593916c 100644 --- a/GRDB/Core/TransactionObserver.swift +++ b/GRDB/Core/TransactionObserver.swift @@ -110,14 +110,17 @@ extension Database { /// - parameter onCommit: A closure executed on transaction commit. /// - parameter onRollback: A closure executed on transaction rollback. public func afterNextTransaction( - onCommit: @escaping (Database) -> Void, - onRollback: @escaping (Database) -> Void = { _ in }) + onCommit: @escaping @Sendable (Database) -> Void, + onRollback: @escaping @Sendable (Database) -> Void = { _ in }) { class TransactionHandler: TransactionObserver { - let onCommit: (Database) -> Void - let onRollback: (Database) -> Void + let onCommit: @Sendable (Database) -> Void + let onRollback: @Sendable (Database) -> Void - init(onCommit: @escaping (Database) -> Void, onRollback: @escaping (Database) -> Void) { + init( + onCommit: @escaping @Sendable (Database) -> Void, + onRollback: @escaping @Sendable (Database) -> Void + ) { self.onCommit = onCommit self.onRollback = onRollback } From 5f384a4b2249423e14ece0b6ffa665c244a3bbc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 15:44:02 +0200 Subject: [PATCH 084/160] ValueObservation is Sendable --- GRDB/Core/DatabasePool.swift | 10 ++-- GRDB/Core/DatabaseQueue.swift | 5 +- GRDB/Core/DatabaseReader.swift | 14 +++--- GRDB/Core/DatabaseSnapshot.swift | 5 +- GRDB/Core/DatabaseSnapshotPool.swift | 5 +- GRDB/Core/DatabaseWriter.swift | 10 ++-- GRDB/Utils/Mutex.swift | 29 +++++++++++ GRDB/Utils/Utils.swift | 12 +++-- .../Observers/ValueConcurrentObserver.swift | 2 +- .../Observers/ValueWriteOnlyObserver.swift | 2 +- GRDB/ValueObservation/Reducers/Map.swift | 4 +- .../Reducers/RemoveDuplicates.swift | 20 ++++++-- .../SharedValueObservation.swift | 13 +++-- GRDB/ValueObservation/ValueObservation.swift | 50 ++++++++++--------- 14 files changed, 113 insertions(+), 68 deletions(-) diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index c209129571..d5d58f4760 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -750,9 +750,8 @@ extension DatabasePool: DatabaseReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { if configuration.readonly { // The easy case: the database does not change return _addReadOnly( @@ -781,9 +780,8 @@ extension DatabasePool: DatabaseReader { private func _addConcurrent( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { assert(!configuration.readonly, "Use _addReadOnly(observation:) instead") assert(!observation.requiresWriteAccess, "Use _addWriteOnly(observation:) instead") let observer = ValueConcurrentObserver( diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index e3db4d45e3..fe4e080a07 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -321,9 +321,8 @@ extension DatabaseQueue: DatabaseReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { if configuration.readonly { // The easy case: the database does not change return _addReadOnly( diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 578e57227b..b0189876ca 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -419,8 +419,8 @@ public protocol DatabaseReader: AnyObject, Sendable { func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable } extension DatabaseReader { @@ -585,9 +585,8 @@ extension DatabaseReader { func _addReadOnly( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { if scheduler.immediateInitialValue() { do { // Perform a reentrant read, in case the observation would be @@ -699,9 +698,8 @@ extension AnyDatabaseReader: DatabaseReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { base._add( observation: observation, scheduling: scheduler, diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index af771226d4..72cdd73b6f 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -195,9 +195,8 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { _addReadOnly( observation: observation, scheduling: scheduler, diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index 7a9911ce68..1ff4791c88 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -378,9 +378,8 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable where Reducer: ValueReducer - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable where Reducer: ValueReducer { _addReadOnly(observation: observation, scheduling: scheduler, onChange: onChange) } diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index a8cb9826c9..9dba849532 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -600,9 +600,8 @@ extension DatabaseWriter { func _addWriteOnly( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { assert(!configuration.readonly, "Use _addReadOnly(observation:) instead") let observer = ValueWriteOnlyObserver( writer: self, @@ -950,9 +949,8 @@ extension AnyDatabaseWriter: DatabaseReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { base._add( observation: observation, scheduling: scheduler, diff --git a/GRDB/Utils/Mutex.swift b/GRDB/Utils/Mutex.swift index 1fb69c2e22..771a57a4a6 100644 --- a/GRDB/Utils/Mutex.swift +++ b/GRDB/Utils/Mutex.swift @@ -52,3 +52,32 @@ extension Mutex where T: Numeric { } extension Mutex: @unchecked Sendable where T: Sendable { } + +// MARK: - UnsafeSendableMutex + +/// `UnsafeSendableMutex` is a Mutex that is always Sendable. It is unsafe, +/// because it does not guarantee that its value won't escape from the +/// critical section. +/// +/// We'll replace it with the SE-0433 Mutex when it is available. +/// +/// +/// For a longer discussion about Sendable and mutexes, see +/// +final class UnsafeSendableMutex: @unchecked Sendable { + private var _value: T + private var lock = NSLock() + + init(_ value: sending T) { + _value = value + } + + /// Runs the provided closure while holding a lock on the value. + /// + /// - parameter body: A closure that can modify the value. + func withLock(_ body: (inout T) throws -> U) rethrows -> U { + lock.lock() + defer { lock.unlock() } + return try body(&_value) + } +} diff --git a/GRDB/Utils/Utils.swift b/GRDB/Utils/Utils.swift index b48db435c4..561c3b91f1 100644 --- a/GRDB/Utils/Utils.swift +++ b/GRDB/Utils/Utils.swift @@ -118,7 +118,7 @@ func throwingFirstError(execute: () throws -> T, finally: () throws -> Void) return result! } -struct PrintOutputStream: TextOutputStream { +struct PrintOutputStream: TextOutputStream, Sendable { func write(_ string: String) { Swift.print(string) } @@ -137,7 +137,10 @@ final class StrongReference: @unchecked Sendable { } /// Concatenates two functions -func concat(_ rhs: (() -> Void)?, _ lhs: (() -> Void)?) -> (() -> Void)? { +func concat( + _ rhs: (@Sendable () -> Void)?, + _ lhs: (@Sendable () -> Void)? +) -> (@Sendable () -> Void)? { switch (rhs, lhs) { case let (rhs, nil): return rhs @@ -152,7 +155,10 @@ func concat(_ rhs: (() -> Void)?, _ lhs: (() -> Void)?) -> (() -> Void)? { } /// Concatenates two functions -func concat(_ rhs: ((T) -> Void)?, _ lhs: ((T) -> Void)?) -> ((T) -> Void)? { +func concat( + _ rhs: (@Sendable (T) -> Void)?, + _ lhs: (@Sendable (T) -> Void)? +) -> (@Sendable (T) -> Void)? { switch (rhs, lhs) { case let (rhs, nil): return rhs diff --git a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift index 2671b6b095..ad3e0bd188 100644 --- a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift @@ -161,7 +161,7 @@ final class ValueConcurrentObserver Void) + onChange: @escaping @Sendable (Reducer.Value) -> Void) { // Configuration self.scheduler = scheduler diff --git a/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift b/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift index 3f5a1c50f6..c34e0ff467 100644 --- a/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift @@ -150,7 +150,7 @@ final class ValueWriteOnlyObserver< trackingMode: ValueObservationTrackingMode, reducer: Reducer, events: ValueObservationEvents, - onChange: @escaping (Reducer.Value) -> Void) + onChange: @escaping @Sendable (Reducer.Value) -> Void) { // Configuration self.scheduler = scheduler diff --git a/GRDB/ValueObservation/Reducers/Map.swift b/GRDB/ValueObservation/Reducers/Map.swift index 0b90fda7f8..1586dbd7ff 100644 --- a/GRDB/ValueObservation/Reducers/Map.swift +++ b/GRDB/ValueObservation/Reducers/Map.swift @@ -18,7 +18,7 @@ extension ValueObservation { /// /// - parameter transform: A closure that takes one value as its parameter /// and returns a new value. - public func map(_ transform: @escaping (Reducer.Value) throws -> T) + public func map(_ transform: @escaping @Sendable (Reducer.Value) throws -> T) -> ValueObservation> { mapReducer { ValueReducers.Map($0, transform) } @@ -34,7 +34,7 @@ extension ValueReducers { private var base: Base private let transform: (Base.Value) throws -> Value - init(_ base: Base, _ transform: @escaping (Base.Value) throws -> Value) { + init(_ base: Base, _ transform: @escaping @Sendable (Base.Value) throws -> Value) { self.base = base self.transform = transform } diff --git a/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift b/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift index 6320664083..07ec82f593 100644 --- a/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift +++ b/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift @@ -5,10 +5,22 @@ extension ValueObservation { /// - parameter predicate: A closure to evaluate whether two values are /// equivalent, for purposes of filtering. Return true from this closure /// to indicate that the second element is a duplicate of the first. - public func removeDuplicates(by predicate: @escaping (Reducer.Value, Reducer.Value) -> Bool) - -> ValueObservation> - { - mapReducer { ValueReducers.RemoveDuplicates($0, predicate: predicate) } + public func removeDuplicates( + by predicate: sending @escaping (Reducer.Value, Reducer.Value) -> Bool + ) -> ValueObservation> { + // The predicate is marked `sending`, which allows us to statically + // determine that it will have no other uses after this call. + // (according to ) + // + // And because `predicate` will only be used serially, in the + // reducer queue of `ValueObservation` observers, we can say that + // this is safe. + // + // Anyway if we would not accept non-sendable closures, we could + // not deal with `Equatable.==`... + nonisolated(unsafe) let predicate = predicate + + return mapReducer { ValueReducers.RemoveDuplicates($0, predicate: predicate) } } } diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index e58d01885c..1abf6998c8 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -180,10 +180,13 @@ public final class SharedValueObservation { private var lastResult: Result? private final class Client { - var onError: (Error) -> Void - var onChange: (Element) -> Void + var onError: @Sendable (Error) -> Void + var onChange: @Sendable (Element) -> Void - init(onError: @escaping (Error) -> Void, onChange: @escaping (Element) -> Void) { + init( + onError: @escaping @Sendable (Error) -> Void, + onChange: @escaping @Sendable (Element) -> Void + ) { self.onError = onError self.onChange = onChange } @@ -224,8 +227,8 @@ public final class SharedValueObservation { /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. public func start( - onError: @escaping (Error) -> Void, - onChange: @escaping (Element) -> Void) + onError: @escaping @Sendable (Error) -> Void, + onChange: @escaping @Sendable (Element) -> Void) -> AnyDatabaseCancellable { synchronized { diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index aa8a59f6dd..ff14da1a59 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -4,7 +4,7 @@ import Combine import Dispatch import Foundation -public struct ValueObservation { +public struct ValueObservation: Sendable { var events = ValueObservationEvents() /// A boolean value indicating whether the observation requires write access @@ -30,10 +30,10 @@ public struct ValueObservation { /// The reducer is created when observation starts, and is triggered upon /// each database change. - var makeReducer: () -> Reducer + var makeReducer: @Sendable () -> Reducer /// Returns a ValueObservation with a transformed reducer. - func mapReducer(_ transform: @escaping (Reducer) -> R) -> ValueObservation { + func mapReducer(_ transform: @escaping @Sendable (Reducer) -> R) -> ValueObservation { let makeReducer = self.makeReducer return ValueObservation( events: events, @@ -74,16 +74,16 @@ enum ValueObservationTrackingMode { } struct ValueObservationEvents: Refinable { - var willStart: (() -> Void)? - var willTrackRegion: ((DatabaseRegion) -> Void)? - var databaseDidChange: (() -> Void)? - var didFail: ((Error) -> Void)? - var didCancel: (() -> Void)? + var willStart: (@Sendable () -> Void)? + var willTrackRegion: (@Sendable (DatabaseRegion) -> Void)? + var databaseDidChange: (@Sendable () -> Void)? + var didFail: (@Sendable (Error) -> Void)? + var didCancel: (@Sendable () -> Void)? } -typealias ValueObservationStart = ( - _ onError: @escaping (Error) -> Void, - _ onChange: @escaping (T) -> Void) +typealias ValueObservationStart = @Sendable ( + _ onError: @escaping @Sendable (Error) -> Void, + _ onChange: @escaping @Sendable (T) -> Void) -> AnyDatabaseCancellable extension ValueObservation: Refinable { @@ -138,8 +138,8 @@ extension ValueObservation: Refinable { public func start( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main), - onError: @escaping (Error) -> Void, - onChange: @escaping (Reducer.Value) -> Void) + onError: @escaping @Sendable (Error) -> Void, + onChange: @escaping @Sendable (Reducer.Value) -> Void) -> AnyDatabaseCancellable where Reducer: ValueReducer { @@ -175,13 +175,13 @@ extension ValueObservation: Refinable { /// - returns: A `ValueObservation` that performs the specified closures /// when ValueObservation events occur. public func handleEvents( - willStart: (() -> Void)? = nil, + willStart: (@Sendable () -> Void)? = nil, willFetch: (@Sendable () -> Void)? = nil, - willTrackRegion: ((DatabaseRegion) -> Void)? = nil, - databaseDidChange: (() -> Void)? = nil, - didReceiveValue: ((Reducer.Value) -> Void)? = nil, - didFail: ((Error) -> Void)? = nil, - didCancel: (() -> Void)? = nil) + willTrackRegion: (@Sendable (DatabaseRegion) -> Void)? = nil, + databaseDidChange: (@Sendable () -> Void)? = nil, + didReceiveValue: (@Sendable (Reducer.Value) -> Void)? = nil, + didFail: (@Sendable (Error) -> Void)? = nil, + didCancel: (@Sendable () -> Void)? = nil) -> ValueObservation> { self @@ -231,10 +231,10 @@ extension ValueObservation: Refinable { /// used to log messages to other destinations. public func print( _ prefix: String = "", - to stream: TextOutputStream? = nil) + to stream: sending TextOutputStream? = nil) -> ValueObservation> { - let streamMutex = Mutex(stream ?? PrintOutputStream()) + let streamMutex = UnsafeSendableMutex(stream ?? PrintOutputStream()) let prefix = prefix.isEmpty ? "" : "\(prefix): " return handleEvents( willStart: { @@ -476,9 +476,13 @@ extension DatabasePublishers { } } - private class ValueSubscription: Subscription - where Downstream.Failure == Error + private class ValueSubscription: + Subscription, @unchecked Sendable + where Downstream: Subscriber, + Downstream.Failure == Error { + // @unchecked Sendable because `cancellable` and `state` are + // protected by `lock`. private struct WaitingForDemand { let downstream: Downstream let start: ValueObservationStart From ec5b7d96e2d4ae2cd0f9d604e2495ce866eaf934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 15:51:04 +0200 Subject: [PATCH 085/160] SharedValueObservation is Sendable --- .../SharedValueObservation.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index 1abf6998c8..d43e03013f 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -167,7 +167,8 @@ extension ValueObservation { /// let cancellable1 = ValueObservation.tracking { db in ... }.shared(in: dbQueue).start(...) /// let cancellable2 = ValueObservation.tracking { db in ... }.shared(in: dbQueue).start(...) /// ``` -public final class SharedValueObservation { +public final class SharedValueObservation: @unchecked Sendable { + // @unchecked Sendable because state is protected by `lock`. private let scheduler: any ValueObservationScheduler private let extent: SharedValueObservationExtent private let startObservation: ValueObservationStart @@ -179,9 +180,9 @@ public final class SharedValueObservation { private var cancellable: AnyDatabaseCancellable? private var lastResult: Result? - private final class Client { - var onError: @Sendable (Error) -> Void - var onChange: @Sendable (Element) -> Void + private final class Client: Sendable { + let onError: @Sendable (Error) -> Void + let onChange: @Sendable (Element) -> Void init( onError: @escaping @Sendable (Error) -> Void, @@ -231,7 +232,7 @@ public final class SharedValueObservation { onChange: @escaping @Sendable (Element) -> Void) -> AnyDatabaseCancellable { - synchronized { + withLock { // Support for reentrancy: a shared immediate observation is // started from the first value notification of that same shared // immediate observation. Yeah, users are nasty. @@ -303,7 +304,7 @@ public final class SharedValueObservation { #endif private func handleError(_ error: Error) { - synchronized { + withLock { let notifiedClients = clients // State change @@ -324,7 +325,7 @@ public final class SharedValueObservation { } private func handleChange(_ value: Element) { - synchronized { + withLock { // State change lastResult = .success(value) @@ -336,7 +337,7 @@ public final class SharedValueObservation { } private func handleCancel(_ client: Client) { - synchronized { + withLock { // State change clients.removeFirst(where: { $0 === client }) if clients.isEmpty && extent == .whileObserved { @@ -347,7 +348,7 @@ public final class SharedValueObservation { } } - private func synchronized(_ execute: () throws -> T) rethrows -> T { + private func withLock(_ execute: () throws -> T) rethrows -> T { lock.lock() defer { lock.unlock() } return try execute() From 2f240fd764a74302a8ffab9e536e261207d90581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 16:39:37 +0200 Subject: [PATCH 086/160] [SENDING REGRET] Async database access methods return Sendable results We can not tell the compiler that we can safely return `sending` results, because fetched values are in the same region as the `db` argument, according to SE-0414 (https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md#rules-for-merging-isolation-regions). ``` class NotSendable { init() { } } func fetchNotSendable(_ db: Database) -> NotSendable { fatalError() } func notOK(_ make: (Database) -> sending T) { } func ok(_ make: () -> sending T) { } func usage() { ok { NotSendable() } // Returning a task-isolated 'NotSendable' value as a 'sending' result // risks causing data races. notOK { db in fetchNotSendable(db) } } ``` Various attempts at modifying all fetching methods so that they have a `sending` return value have failed. --- GRDB/Core/DatabasePool.swift | 8 ++++---- GRDB/Core/DatabaseQueue.swift | 8 ++++---- GRDB/Core/DatabaseReader.swift | 8 ++++---- GRDB/Core/DatabaseSnapshot.swift | 4 ++-- GRDB/Core/DatabaseSnapshotPool.swift | 4 ++-- GRDB/Core/DatabaseWriter.swift | 18 +++++++++--------- GRDB/Core/SerializedDatabase.swift | 2 +- GRDB/ValueObservation/Reducers/Fetch.swift | 2 +- GRDB/ValueObservation/Reducers/Map.swift | 2 +- .../Reducers/ValueReducer.swift | 2 +- .../SharedValueObservation.swift | 2 +- GRDB/ValueObservation/ValueObservation.swift | 2 +- 12 files changed, 31 insertions(+), 31 deletions(-) diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index d5d58f4760..cddd0f4674 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -352,7 +352,7 @@ extension DatabasePool: DatabaseReader { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func read( + public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.") @@ -437,7 +437,7 @@ extension DatabasePool: DatabaseReader { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func unsafeRead( + public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { guard let readerPool else { @@ -804,7 +804,7 @@ extension DatabasePool: DatabaseWriter { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func writeWithoutTransaction( + public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writer.execute(updates) @@ -821,7 +821,7 @@ extension DatabasePool: DatabaseWriter { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func barrierWriteWithoutTransaction( + public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { let dbAccess = CancellableDatabaseAccess() diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index fe4e080a07..57e6f88f8e 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -234,7 +234,7 @@ extension DatabaseQueue: DatabaseReader { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func read( + public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writer.execute { db in @@ -273,7 +273,7 @@ extension DatabaseQueue: DatabaseReader { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func unsafeRead( + public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writer.execute(value) @@ -387,7 +387,7 @@ extension DatabaseQueue: DatabaseWriter { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func writeWithoutTransaction( + public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writer.execute(updates) @@ -399,7 +399,7 @@ extension DatabaseQueue: DatabaseWriter { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func barrierWriteWithoutTransaction( + public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writer.execute(updates) diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index b0189876ca..fc59d0981e 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -217,7 +217,7 @@ public protocol DatabaseReader: AnyObject, Sendable { /// database access, or the error thrown by `value`, or /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) - func read( + func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -324,7 +324,7 @@ public protocol DatabaseReader: AnyObject, Sendable { /// database access, or the error thrown by `value`, or /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) - func unsafeRead( + func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -661,7 +661,7 @@ extension AnyDatabaseReader: DatabaseReader { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func read( + public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.read(value) @@ -679,7 +679,7 @@ extension AnyDatabaseReader: DatabaseReader { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func unsafeRead( + public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.unsafeRead(value) diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 72cdd73b6f..9752a90f14 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -152,7 +152,7 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func read( + public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await reader.execute(value) @@ -174,7 +174,7 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { // `DatabaseSnapshotReader`, because of // . @available(iOS 13, macOS 10.15, tvOS 13, *) - public func unsafeRead( + public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await read(value) diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index 1ff4791c88..fa88f15343 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -294,7 +294,7 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func read( + public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { guard let readerPool else { @@ -354,7 +354,7 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { // `DatabaseSnapshotReader`, because of // . @available(iOS 13, macOS 10.15, tvOS 13, *) - public func unsafeRead( + public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await read(value) diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index 9dba849532..8f3fcf9407 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -132,7 +132,7 @@ public protocol DatabaseWriter: DatabaseReader { /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) - func writeWithoutTransaction( + func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -218,7 +218,7 @@ public protocol DatabaseWriter: DatabaseReader { /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) - func barrierWriteWithoutTransaction( + func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -650,7 +650,7 @@ extension DatabaseWriter { /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. @available(iOS 13, macOS 10.15, tvOS 13, *) - public func write( + public func write( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await writeWithoutTransaction { db in @@ -819,7 +819,7 @@ extension DatabaseWriter { /// - parameter updates: A closure which writes in the database. /// - parameter value: A closure which reads from the database. @available(iOS 13, macOS 10.15, tvOS 13, *) - public func writePublisher( + public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, updates: @escaping @Sendable (Database) throws -> T, thenRead value: @escaping @Sendable (Database, T) throws -> Output @@ -836,7 +836,7 @@ extension DatabaseWriter { fulfill(.failure(error)) return } - self.spawnConcurrentRead { dbResult in + self.spawnConcurrentRead { [updatesValue] dbResult in fulfill(dbResult.flatMap { db in Result { try value(db, updatesValue!) } }) } } @@ -912,7 +912,7 @@ extension AnyDatabaseWriter: DatabaseReader { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func read( + public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.read(value) @@ -930,7 +930,7 @@ extension AnyDatabaseWriter: DatabaseReader { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func unsafeRead( + public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.unsafeRead(value) @@ -965,7 +965,7 @@ extension AnyDatabaseWriter: DatabaseWriter { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func writeWithoutTransaction( + public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.writeWithoutTransaction(updates) @@ -977,7 +977,7 @@ extension AnyDatabaseWriter: DatabaseWriter { } @available(iOS 13, macOS 10.15, tvOS 13, *) - public func barrierWriteWithoutTransaction( + public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { try await base.barrierWriteWithoutTransaction(updates) diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index 2251dcd7ee..031de45927 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -245,7 +245,7 @@ final class SerializedDatabase { /// Asynchrously executes the block. @available(iOS 13, macOS 10.15, tvOS 13, *) - func execute( + func execute( _ block: @escaping @Sendable (Database) throws -> T ) async throws -> T { let dbAccess = CancellableDatabaseAccess() diff --git a/GRDB/ValueObservation/Reducers/Fetch.swift b/GRDB/ValueObservation/Reducers/Fetch.swift index 6ab2f1583b..693c934bbf 100644 --- a/GRDB/ValueObservation/Reducers/Fetch.swift +++ b/GRDB/ValueObservation/Reducers/Fetch.swift @@ -1,6 +1,6 @@ extension ValueReducers { /// A `ValueReducer` that perform database fetches. - public struct Fetch: ValueReducer { + public struct Fetch: ValueReducer { public struct _Fetcher: _ValueReducerFetcher { let _fetch: @Sendable (Database) throws -> Value diff --git a/GRDB/ValueObservation/Reducers/Map.swift b/GRDB/ValueObservation/Reducers/Map.swift index 1586dbd7ff..ba6f1ec674 100644 --- a/GRDB/ValueObservation/Reducers/Map.swift +++ b/GRDB/ValueObservation/Reducers/Map.swift @@ -30,7 +30,7 @@ extension ValueReducers { /// passed through a transform function. /// /// See ``ValueObservation/map(_:)``. - public struct Map: ValueReducer { + public struct Map: ValueReducer { private var base: Base private let transform: (Base.Value) throws -> Value diff --git a/GRDB/ValueObservation/Reducers/ValueReducer.swift b/GRDB/ValueObservation/Reducers/ValueReducer.swift index 52cf4e848e..8c7386693a 100644 --- a/GRDB/ValueObservation/Reducers/ValueReducer.swift +++ b/GRDB/ValueObservation/Reducers/ValueReducer.swift @@ -16,7 +16,7 @@ public protocol _ValueReducer { associatedtype Fetcher: _ValueReducerFetcher /// The type of observed values - associatedtype Value + associatedtype Value: Sendable /// Returns a value that fetches database values upon changes in an /// observed database region. The returned value method must not depend diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index d43e03013f..2208ae4d1b 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -167,7 +167,7 @@ extension ValueObservation { /// let cancellable1 = ValueObservation.tracking { db in ... }.shared(in: dbQueue).start(...) /// let cancellable2 = ValueObservation.tracking { db in ... }.shared(in: dbQueue).start(...) /// ``` -public final class SharedValueObservation: @unchecked Sendable { +public final class SharedValueObservation: @unchecked Sendable { // @unchecked Sendable because state is protected by `lock`. private let scheduler: any ValueObservationScheduler private let extent: SharedValueObservationExtent diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index ff14da1a59..32abcd97f8 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -336,7 +336,7 @@ extension ValueObservation { /// You build an `AsyncValueObservation` from ``ValueObservation`` or /// ``SharedValueObservation``. @available(iOS 13, macOS 10.15, tvOS 13, *) -public struct AsyncValueObservation: AsyncSequence { +public struct AsyncValueObservation: AsyncSequence { public typealias BufferingPolicy = AsyncThrowingStream.Continuation.BufferingPolicy public typealias AsyncIterator = Iterator From 4b5ec4f6cad4725171c81ba273fcf29dd0d07101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 17:48:50 +0200 Subject: [PATCH 087/160] Enable InferSendableFromCaptures upcoming feature --- Package.swift | 1 + SQLiteCustom/GRDBDeploymentTarget.xcconfig | 1 + Support/GRDBDeploymentTarget.xcconfig | 1 + 3 files changed, 3 insertions(+) diff --git a/Package.swift b/Package.swift index 22cd2d6c3e..f08e63fe4e 100644 --- a/Package.swift +++ b/Package.swift @@ -6,6 +6,7 @@ import PackageDescription var swiftSettings: [SwiftSetting] = [ .define("SQLITE_ENABLE_FTS5"), + .enableUpcomingFeature("InferSendableFromCaptures") ] var cSettings: [CSetting] = [] var dependencies: [PackageDescription.Package.Dependency] = [] diff --git a/SQLiteCustom/GRDBDeploymentTarget.xcconfig b/SQLiteCustom/GRDBDeploymentTarget.xcconfig index 3006917366..7fed5abdec 100644 --- a/SQLiteCustom/GRDBDeploymentTarget.xcconfig +++ b/SQLiteCustom/GRDBDeploymentTarget.xcconfig @@ -2,3 +2,4 @@ IPHONEOS_DEPLOYMENT_TARGET = 12.0 MACOSX_DEPLOYMENT_TARGET = 10.13 TVOS_DEPLOYMENT_TARGET = 12.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 +SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES diff --git a/Support/GRDBDeploymentTarget.xcconfig b/Support/GRDBDeploymentTarget.xcconfig index 50434f84d7..cd39973b9a 100644 --- a/Support/GRDBDeploymentTarget.xcconfig +++ b/Support/GRDBDeploymentTarget.xcconfig @@ -3,6 +3,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.13 TVOS_DEPLOYMENT_TARGET = 12.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 OTHER_SWIFT_FLAGS = $(inherited) -D SQLITE_ENABLE_FTS5 +SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES //// Compile with all opt-in APIs //GCC_PREPROCESSOR_DEFINITIONS = $(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1 From 945f398c921567df9e73afa472407733dd710e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 8 Sep 2024 18:00:37 +0200 Subject: [PATCH 088/160] Fix Sendable warnings in tests --- ...abaseAfterNextTransactionCommitTests.swift | 40 ++--- .../DatabasePoolConcurrencyTests.swift | 20 +-- Tests/GRDBTests/DatabasePoolTests.swift | 24 ++- Tests/GRDBTests/DatabaseReaderTests.swift | 6 +- .../DatabaseRegionObservationTests.swift | 32 ++-- .../SharedValueObservationTests.swift | 130 +++++++-------- .../ValueObservationPrintTests.swift | 16 +- ...ValueObservationRegionRecordingTests.swift | 28 ++-- Tests/GRDBTests/ValueObservationTests.swift | 148 ++++++++++-------- 9 files changed, 238 insertions(+), 206 deletions(-) diff --git a/Tests/GRDBTests/DatabaseAfterNextTransactionCommitTests.swift b/Tests/GRDBTests/DatabaseAfterNextTransactionCommitTests.swift index 3daa8f55cf..9e01b79a8b 100644 --- a/Tests/GRDBTests/DatabaseAfterNextTransactionCommitTests.swift +++ b/Tests/GRDBTests/DatabaseAfterNextTransactionCommitTests.swift @@ -42,7 +42,7 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.writeWithoutTransaction { db in - var commitCount = 0 + let commitCountMutex = Mutex(0) weak var deallocationWitness: Witness? = nil do { let witness = Witness() @@ -51,19 +51,19 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { db.afterNextTransaction { _ in // use witness withExtendedLifetime(witness, { }) - commitCount += 1 + commitCountMutex.increment() } } XCTAssertNotNil(deallocationWitness) - XCTAssertEqual(commitCount, 0) + XCTAssertEqual(commitCountMutex.load(), 0) try db.execute(sql: startSQL) try db.execute(sql: endSQL) switch expectedCompletion { case .commit: - XCTAssertEqual(commitCount, 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 1, "\(startSQL); \(endSQL)") case .rollback: - XCTAssertEqual(commitCount, 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 0, "\(startSQL); \(endSQL)") } XCTAssertNil(deallocationWitness) @@ -73,9 +73,9 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { } switch expectedCompletion { case .commit: - XCTAssertEqual(commitCount, 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 1, "\(startSQL); \(endSQL)") case .rollback: - XCTAssertEqual(commitCount, 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 0, "\(startSQL); \(endSQL)") } } } @@ -85,8 +85,8 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.writeWithoutTransaction { db in - var commitCount = 0 - var rollbackCount = 0 + let commitCountMutex = Mutex(0) + let rollbackCountMutex = Mutex(0) try db.execute(sql: startSQL) weak var deallocationWitness: Witness? = nil @@ -98,25 +98,25 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { onCommit: { _ in // use witness withExtendedLifetime(witness, { }) - commitCount += 1 + commitCountMutex.increment() }, onRollback: { _ in // use witness withExtendedLifetime(witness, { }) - rollbackCount += 1 + rollbackCountMutex.increment() }) } XCTAssertNotNil(deallocationWitness) - XCTAssertEqual(commitCount, 0) + XCTAssertEqual(commitCountMutex.load(), 0) try db.execute(sql: endSQL) switch expectedCompletion { case .commit: - XCTAssertEqual(commitCount, 1, "\(startSQL); \(endSQL)") - XCTAssertEqual(rollbackCount, 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(rollbackCountMutex.load(), 0, "\(startSQL); \(endSQL)") case .rollback: - XCTAssertEqual(commitCount, 0, "\(startSQL); \(endSQL)") - XCTAssertEqual(rollbackCount, 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(rollbackCountMutex.load(), 1, "\(startSQL); \(endSQL)") } XCTAssertNil(deallocationWitness) @@ -126,11 +126,11 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { } switch expectedCompletion { case .commit: - XCTAssertEqual(commitCount, 1, "\(startSQL); \(endSQL)") - XCTAssertEqual(rollbackCount, 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(rollbackCountMutex.load(), 0, "\(startSQL); \(endSQL)") case .rollback: - XCTAssertEqual(commitCount, 0, "\(startSQL); \(endSQL)") - XCTAssertEqual(rollbackCount, 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(rollbackCountMutex.load(), 1, "\(startSQL); \(endSQL)") } } } diff --git a/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift b/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift index 93eaaae3e9..7d2478545b 100644 --- a/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift +++ b/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift @@ -1078,13 +1078,13 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { func testAsyncConcurrentReadOpensATransaction() throws { let dbPool = try makeDatabasePool() - var isInsideTransaction: Bool? = nil + let isInsideTransactionMutex: Mutex = Mutex(nil) let expectation = self.expectation(description: "read") dbPool.writeWithoutTransaction { db in dbPool.asyncConcurrentRead { dbResult in do { let db = try dbResult.get() - isInsideTransaction = db.isInsideTransaction + isInsideTransactionMutex.store(db.isInsideTransaction) do { try db.execute(sql: "BEGIN DEFERRED TRANSACTION") XCTFail("Expected error") @@ -1097,7 +1097,7 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { } } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(isInsideTransaction, true) + XCTAssertEqual(isInsideTransactionMutex.load(), true) } func testAsyncConcurrentReadOutsideOfTransaction() throws { @@ -1120,14 +1120,14 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { // < // } - var count: Int? = nil + let countMutex: Mutex = Mutex(nil) let expectation = self.expectation(description: "read") try dbPool.writeWithoutTransaction { db in dbPool.asyncConcurrentRead { dbResult in do { _ = s1.wait(timeout: .distantFuture) let db = try dbResult.get() - count = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM persons")! + try countMutex.store(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM persons")!) } catch { XCTFail("Unexpected error: \(error)") } @@ -1137,14 +1137,14 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { s1.signal() } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 0) + XCTAssertEqual(countMutex.load(), 0) } func testAsyncConcurrentReadError() throws { // Necessary for this test to run as quickly as possible dbConfiguration.readonlyBusyMode = .immediateError let dbPool = try makeDatabasePool() - var readError: DatabaseError? = nil + let readErrorMutex: Mutex = Mutex(nil) let expectation = self.expectation(description: "read") try dbPool.writeWithoutTransaction { db in try db.execute(sql: "PRAGMA locking_mode=EXCLUSIVE") @@ -1156,12 +1156,12 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { XCTFail("Unexpected result: \(dbResult)") return } - readError = dbError + readErrorMutex.store(dbError) expectation.fulfill() } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(readError!.resultCode, .SQLITE_BUSY) - XCTAssertEqual(readError!.message!, "database is locked") + XCTAssertEqual(readErrorMutex.load()!.resultCode, .SQLITE_BUSY) + XCTAssertEqual(readErrorMutex.load()!.message!, "database is locked") } } diff --git a/Tests/GRDBTests/DatabasePoolTests.swift b/Tests/GRDBTests/DatabasePoolTests.swift index f1be8493b8..e1ab57bbd5 100644 --- a/Tests/GRDBTests/DatabasePoolTests.swift +++ b/Tests/GRDBTests/DatabasePoolTests.swift @@ -237,9 +237,7 @@ class DatabasePoolTests: GRDBTestCase { let group = DispatchGroup() // The maximum number of threads we could witness - var maxThreadCount: CInt = 0 - let lock = NSLock() - + let maxThreadCountMutex: Mutex = Mutex(0) for _ in (0.. = Mutex(0) for _ in (0.. = Mutex(nil) dbReader.asyncRead { dbResult in // Make sure this block executes asynchronously semaphore.wait() do { - count = try Int.fetchOne(dbResult.get(), sql: "SELECT COUNT(*) FROM sqlite_master") + try countMutex.store(Int.fetchOne(dbResult.get(), sql: "SELECT COUNT(*) FROM sqlite_master")) } catch { XCTFail("Unexpected error: \(error)") } @@ -238,7 +238,7 @@ class DatabaseReaderTests : GRDBTestCase { semaphore.signal() waitForExpectations(timeout: 1, handler: nil) - XCTAssertNotNil(count) + XCTAssertNotNil(countMutex.load()) } try test(makeDatabaseQueue()) diff --git a/Tests/GRDBTests/DatabaseRegionObservationTests.swift b/Tests/GRDBTests/DatabaseRegionObservationTests.swift index 9d54aafbef..341616b37f 100644 --- a/Tests/GRDBTests/DatabaseRegionObservationTests.swift +++ b/Tests/GRDBTests/DatabaseRegionObservationTests.swift @@ -27,12 +27,12 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: .fullDatabase) - var count = 0 + let countMutex = Mutex(0) let cancellable = observation.start( in: dbQueue, onError: { XCTFail("Unexpected error: \($0)") }, onChange: { db in - count += 1 + countMutex.increment() notificationExpectation.fulfill() }) @@ -49,7 +49,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 3) + XCTAssertEqual(countMutex.load(), 3) } } @@ -96,12 +96,12 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: request1, request2) - var count = 0 + let countMutex = Mutex(0) let cancellable = observation.start( in: dbQueue, onError: { XCTFail("Unexpected error: \($0)") }, onChange: { db in - count += 1 + countMutex.increment() notificationExpectation.fulfill() }) @@ -118,7 +118,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 3) + XCTAssertEqual(countMutex.load(), 3) } } @@ -138,12 +138,12 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: [request1, request2]) - var count = 0 + let countMutex = Mutex(0) let cancellable = observation.start( in: dbQueue, onError: { XCTFail("Unexpected error: \($0)") }, onChange: { db in - count += 1 + countMutex.increment() notificationExpectation.fulfill() }) @@ -160,7 +160,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 3) + XCTAssertEqual(countMutex.load(), 3) } } @@ -174,13 +174,13 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: SQLRequest(sql: "SELECT * FROM t ORDER BY id")) - var count = 0 + let countMutex = Mutex(0) do { let cancellable = observation.start( in: dbQueue, onError: { XCTFail("Unexpected error: \($0)") }, onChange: { db in - count += 1 + countMutex.increment() notificationExpectation.fulfill() }) @@ -199,7 +199,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 2) + XCTAssertEqual(countMutex.load(), 2) } func testDatabaseRegionExtentNextTransaction() throws { @@ -212,14 +212,14 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: SQLRequest(sql: "SELECT * FROM t ORDER BY id")) - var count = 0 - var cancellable: AnyDatabaseCancellable? + let countMutex = Mutex(0) + nonisolated(unsafe) var cancellable: AnyDatabaseCancellable? cancellable = observation.start( in: dbQueue, onError: { XCTFail("Unexpected error: \($0)") }, onChange: { db in cancellable?.cancel() - count += 1 + countMutex.increment() notificationExpectation.fulfill() }) @@ -233,7 +233,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 1) + XCTAssertEqual(countMutex.load(), 1) } } diff --git a/Tests/GRDBTests/SharedValueObservationTests.swift b/Tests/GRDBTests/SharedValueObservationTests.swift index 572f362ff3..dc9baa1f9c 100644 --- a/Tests/GRDBTests/SharedValueObservationTests.swift +++ b/Tests/GRDBTests/SharedValueObservationTests.swift @@ -39,12 +39,12 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated withExtendedLifetime(sharedObservation) { sharedObservation in do { - var value: Int? + let valueMutex: Mutex = Mutex(nil) let cancellable = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) + onChange: { value in valueMutex.store(value) }) - XCTAssertEqual(value, 0) + XCTAssertEqual(valueMutex.load(), 0) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) cancellable.cancel() @@ -52,12 +52,12 @@ class SharedValueObservationTests: GRDBTestCase { } do { - var value: Int? + let valueMutex: Mutex = Mutex(nil) let cancellable = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) + onChange: { value in valueMutex.store(value) }) - XCTAssertEqual(value, 0) + XCTAssertEqual(valueMutex.load(), 0) XCTAssertEqual(log.flush(), []) cancellable.cancel() @@ -91,21 +91,21 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated withExtendedLifetime(sharedObservation) { sharedObservation in do { - var value1: Int? - var value2: Int? + let value1Mutex: Mutex = Mutex(nil) + let value2Mutex: Mutex = Mutex(nil) let cancellable1 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, onChange: { value in - value1 = value + value1Mutex.store(value) _ = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, onChange: { value in - value2 = value + value2Mutex.store(value) }) }) - XCTAssertEqual(value1, 0) - XCTAssertEqual(value2, 0) + XCTAssertEqual(value1Mutex.load(), 0) + XCTAssertEqual(value2Mutex.load(), 0) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) cancellable1.cancel() @@ -175,12 +175,12 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated withExtendedLifetime(sharedObservation) { sharedObservation in do { - var value: Int? + let valueMutex: Mutex = Mutex(nil) let cancellable = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) + onChange: { value in valueMutex.store(value) }) - XCTAssertEqual(value, 0) + XCTAssertEqual(valueMutex.load(), 0) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) cancellable.cancel() @@ -188,12 +188,12 @@ class SharedValueObservationTests: GRDBTestCase { } do { - var value: Int? + let valueMutex: Mutex = Mutex(nil) let cancellable = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) + onChange: { value in valueMutex.store(value) }) - XCTAssertEqual(value, 0) + XCTAssertEqual(valueMutex.load(), 0) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) cancellable.cancel() @@ -226,21 +226,21 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated withExtendedLifetime(sharedObservation) { sharedObservation in - var value1: Int? - var value2: Int? + let value1Mutex: Mutex = Mutex(nil) + let value2Mutex: Mutex = Mutex(nil) let cancellable1 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, onChange: { value in - value1 = value + value1Mutex.store(value) _ = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, onChange: { value in - value2 = value + value2Mutex.store(value) }) }) - XCTAssertEqual(value1, 0) - XCTAssertEqual(value2, 0) + XCTAssertEqual(value1Mutex.load(), 0) + XCTAssertEqual(value2Mutex.load(), 0) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) cancellable1.cancel() @@ -273,40 +273,40 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated try withExtendedLifetime(sharedObservation) { sharedObservation in // --- Start observation 1 - var values1: [Int] = [] + let values1Mutex: Mutex<[Int]> = Mutex([]) let exp1 = expectation(description: "") exp1.expectedFulfillmentCount = 2 exp1.assertForOverFulfill = false let cancellable1 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values1.append($0) + onChange: { value in + values1Mutex.withLock { $0.append(value) } exp1.fulfill() }) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} wait(for: [exp1], timeout: 1) - XCTAssertEqual(values1, [0, 1]) + XCTAssertEqual(values1Mutex.load(), [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 values2Mutex: Mutex<[Int]> = Mutex([]) let exp2 = expectation(description: "") exp2.expectedFulfillmentCount = 2 exp2.assertForOverFulfill = false let cancellable2 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values2.append($0) + onChange: { value in + values2Mutex.withLock { $0.append(value) } 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(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2]) XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) // --- Stop observation 1 @@ -314,22 +314,22 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), []) // --- Start observation 3 - var values3: [Int] = [] + let values3Mutex: Mutex<[Int]> = Mutex([]) let exp3 = expectation(description: "") exp3.expectedFulfillmentCount = 2 exp3.assertForOverFulfill = false let cancellable3 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values3.append($0) + onChange: { value in + values3Mutex.withLock { $0.append(value) } 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(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2, 3]) + XCTAssertEqual(values3Mutex.load(), [2, 3]) XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 3"]) // --- Stop observation 2 @@ -355,7 +355,7 @@ class SharedValueObservationTests: GRDBTestCase { } let log = Log() - var sharedObservation: SharedValueObservation? = ValueObservation + nonisolated(unsafe) var sharedObservation: SharedValueObservation? = ValueObservation .tracking(Table("player").fetchCount) .print(to: log) .shared( @@ -452,40 +452,40 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated try withExtendedLifetime(sharedObservation) { sharedObservation in // --- Start observation 1 - var values1: [Int] = [] + let values1Mutex: Mutex<[Int]> = Mutex([]) let exp1 = expectation(description: "") exp1.expectedFulfillmentCount = 2 exp1.assertForOverFulfill = false let cancellable1 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values1.append($0) + onChange: { value in + values1Mutex.withLock { $0.append(value) } exp1.fulfill() }) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} wait(for: [exp1], timeout: 1) - XCTAssertEqual(values1, [0, 1]) + XCTAssertEqual(values1Mutex.load(), [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 values2Mutex: Mutex<[Int]> = Mutex([]) let exp2 = expectation(description: "") exp2.expectedFulfillmentCount = 2 exp2.assertForOverFulfill = false let cancellable2 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values2.append($0) + onChange: { value in + values2Mutex.withLock { $0.append(value) } 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(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2]) XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) // --- Stop observation 1 @@ -493,22 +493,22 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), []) // --- Start observation 3 - var values3: [Int] = [] + let values3Mutex: Mutex<[Int]> = Mutex([]) let exp3 = expectation(description: "") exp3.expectedFulfillmentCount = 2 exp3.assertForOverFulfill = false let cancellable3 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values3.append($0) + onChange: { value in + values3Mutex.withLock { $0.append(value) } 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(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2, 3]) + XCTAssertEqual(values3Mutex.load(), [2, 3]) XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 3"]) // --- Stop observation 2 @@ -539,10 +539,12 @@ class SharedValueObservationTests: GRDBTestCase { } let log = Log() - var fetchError: Error? = nil + let fetchErrorMutex: Mutex = Mutex(nil) let publisher = ValueObservation .tracking { db -> Int in - if let error = fetchError { throw error } + try fetchErrorMutex.withLock { error in + if let error { throw error } + } return try Table("player").fetchCount(db) } .print(to: log) @@ -556,7 +558,7 @@ class SharedValueObservationTests: GRDBTestCase { try XCTAssertEqual(wait(for: recorder1.next(), timeout: 1), 0) try XCTAssertEqual(wait(for: recorder2.next(), timeout: 1), 0) - fetchError = TestError() + fetchErrorMutex.store(TestError()) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} if case .finished = try wait(for: recorder1.completion, timeout: 1) { XCTFail("Expected error") } @@ -573,7 +575,7 @@ class SharedValueObservationTests: GRDBTestCase { } do { - fetchError = nil + fetchErrorMutex.store(nil) let recorder = publisher.record() if case .finished = try wait(for: recorder.completion, timeout: 1) { XCTFail("Expected error") } XCTAssertEqual(log.flush(), []) @@ -595,10 +597,12 @@ class SharedValueObservationTests: GRDBTestCase { } let log = Log() - var fetchError: Error? = nil + let fetchErrorMutex: Mutex = Mutex(nil) let publisher = ValueObservation .tracking { db -> Int in - if let error = fetchError { throw error } + try fetchErrorMutex.withLock { error in + if let error { throw error } + } return try Table("player").fetchCount(db) } .print(to: log) @@ -612,7 +616,7 @@ class SharedValueObservationTests: GRDBTestCase { try XCTAssertEqual(wait(for: recorder1.next(), timeout: 1), 0) try XCTAssertEqual(wait(for: recorder2.next(), timeout: 1), 0) - fetchError = TestError() + fetchErrorMutex.store(TestError()) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} if case .finished = try wait(for: recorder1.completion, timeout: 1) { XCTFail("Expected error") } @@ -629,7 +633,7 @@ class SharedValueObservationTests: GRDBTestCase { } do { - fetchError = nil + fetchErrorMutex.store(nil) let recorder = publisher.record() try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 1"]) diff --git a/Tests/GRDBTests/ValueObservationPrintTests.swift b/Tests/GRDBTests/ValueObservationPrintTests.swift index c2940c757b..9ed1a44333 100644 --- a/Tests/GRDBTests/ValueObservationPrintTests.swift +++ b/Tests/GRDBTests/ValueObservationPrintTests.swift @@ -411,11 +411,15 @@ class ValueObservationPrintTests: GRDBTestCase { // Force DatabasePool to perform two initial fetches, because between // its first read access, and its write access that installs the // transaction observer, some write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation .trackingConstantRegion { db -> Int? in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false try dbPool.write { db in try db.execute(sql: """ INSERT INTO player DEFAULT VALUES; @@ -462,11 +466,15 @@ class ValueObservationPrintTests: GRDBTestCase { // Force DatabasePool to perform two initial fetches, because between // its first read access, and its write access that installs the // transaction observer, some write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation .trackingConstantRegion { db -> Int? in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false try dbPool.write { db in try db.execute(sql: """ INSERT INTO player DEFAULT VALUES; diff --git a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift index 90bd8beb8b..390df4af0a 100644 --- a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift +++ b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift @@ -130,24 +130,26 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { """) } - var results: [Int] = [] + let resultsMutex: Mutex<[Int]> = Mutex([]) let notificationExpectation = expectation(description: "notification") notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var regions: [DatabaseRegion] = [] + let regionsMutex: Mutex<[DatabaseRegion]> = Mutex([]) let observation = ValueObservation .tracking { db -> Int in let table = try String.fetchOne(db, sql: "SELECT name FROM source")! return try Int.fetchOne(db, sql: "SELECT IFNULL(SUM(value), 0) FROM \(table)")! } - .handleEvents(willTrackRegion: { regions.append($0) }) + .handleEvents(willTrackRegion: { region in + regionsMutex.withLock { $0.append(region) } + }) let observer = observation.start( in: dbQueue, onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - results.append(count) + resultsMutex.withLock { $0.append(count) } notificationExpectation.fulfill() }) @@ -162,9 +164,9 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results, [0, 1, 2, 3]) + XCTAssertEqual(resultsMutex.load(), [0, 1, 2, 3]) - XCTAssertEqual(regions.map(\.description), [ + XCTAssertEqual(regionsMutex.load().map(\.description), [ "a(value),source(name)", "b(value),source(name)"]) } @@ -181,25 +183,27 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { """) } - var results: [Int] = [] + let resultsMutex: Mutex<[Int]> = Mutex([]) let notificationExpectation = expectation(description: "notification") notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var regions: [DatabaseRegion] = [] + let regionsMutex: Mutex<[DatabaseRegion]> = Mutex([]) let observation = ValueObservation .tracking { db -> Int in let table = try String.fetchOne(db, sql: "SELECT name FROM source")! return try Int.fetchOne(db, sql: "SELECT IFNULL(SUM(value), 0) FROM \(table)")! } - .handleEvents(willTrackRegion: { regions.append($0) }) + .handleEvents(willTrackRegion: { region in + regionsMutex.withLock { $0.append(region) } + }) let observer = observation.start( in: dbQueue, scheduling: .async(onQueue: .main), onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - results.append(count) + resultsMutex.withLock { $0.append(count) } notificationExpectation.fulfill() }) @@ -214,9 +218,9 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results, [0, 1, 2, 3]) + XCTAssertEqual(resultsMutex.load(), [0, 1, 2, 3]) - XCTAssertEqual(regions.map(\.description), [ + XCTAssertEqual(regionsMutex.load().map(\.description), [ "a(value),source(name)", "b(value),source(name)"]) } diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index 7699a94396..08b59e1d5c 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -38,13 +38,15 @@ class ValueObservationTests: GRDBTestCase { let observation = ValueObservation.trackingConstantRegion { _ in throw TestError() } // Start observation - var error: TestError? + let errorMutex: Mutex = Mutex(nil) _ = observation.start( in: dbWriter, scheduling: .immediate, - onError: { error = $0 as? TestError }, + onError: { error in + errorMutex.store(error as? TestError) + }, onChange: { _ in }) - XCTAssertNotNil(error) + XCTAssertNotNil(errorMutex.load()) } try test(makeDatabaseQueue()) @@ -64,25 +66,25 @@ class ValueObservationTests: GRDBTestCase { notificationExpectation.expectedFulfillmentCount = 4 notificationExpectation.isInverted = true - var nextError: Error? = nil // If not null, observation throws an error + let nextErrorMutex: Mutex = Mutex(nil) // If not null, observation throws an error let observation = ValueObservation.trackingConstantRegion { _ = try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t") - if let error = nextError { - throw error + try nextErrorMutex.withLock { error in + if let error { throw error } } } // Start observation - var errorCaught = false + let errorCaughtMutex = Mutex(false) let cancellable = observation.start( in: dbWriter, onError: { _ in - errorCaught = true + errorCaughtMutex.store(true) notificationExpectation.fulfill() }, onChange: { - XCTAssertFalse(errorCaught) - nextError = TestError() + XCTAssertFalse(errorCaughtMutex.load()) + nextErrorMutex.store(TestError()) notificationExpectation.fulfill() // Trigger another change try! dbWriter.writeWithoutTransaction { db in @@ -92,7 +94,7 @@ class ValueObservationTests: GRDBTestCase { withExtendedLifetime(cancellable) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertTrue(errorCaught) + XCTAssertTrue(errorCaughtMutex.load()) } } @@ -119,12 +121,12 @@ class ValueObservationTests: 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 region: DatabaseRegion? + let regionMutex: Mutex = Mutex(nil) let expectation = self.expectation(description: "") let observation = ValueObservation - .trackingConstantRegion(request.fetchAll) - .handleEvents(willTrackRegion: { - region = $0 + .trackingConstantRegion { _ = try request.fetchAll($0) } + .handleEvents(willTrackRegion: { region in + regionMutex.store(region) expectation.fulfill() }) let observer = observation.start( @@ -133,7 +135,7 @@ class ValueObservationTests: GRDBTestCase { onChange: { _ in }) withExtendedLifetime(observer) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(region!.description, "t(id,name)") // view is NOT tracked + XCTAssertEqual(regionMutex.load()!.description, "t(id,name)") // view is NOT tracked } } @@ -152,12 +154,12 @@ class ValueObservationTests: GRDBTestCase { // Test that no pragma table is included in the observed region. // This optimization helps observation that feed from a single table. - var region: DatabaseRegion? + let regionMutex: Mutex = Mutex(nil) let expectation = self.expectation(description: "") let observation = ValueObservation - .trackingConstantRegion(request.fetchAll) - .handleEvents(willTrackRegion: { - region = $0 + .trackingConstantRegion{ _ = try request.fetchAll($0) } + .handleEvents(willTrackRegion: { region in + regionMutex.store(region) expectation.fulfill() }) let observer = observation.start( @@ -166,7 +168,7 @@ class ValueObservationTests: GRDBTestCase { onChange: { _ in }) withExtendedLifetime(observer) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(region!.description, "t(id,name)[1]") // pragma_table_xinfo is NOT tracked + XCTAssertEqual(regionMutex.load()?.description, "t(id,name)[1]") // pragma_table_xinfo is NOT tracked } } @@ -353,10 +355,14 @@ class ValueObservationTests: GRDBTestCase { // Force DatabasePool to perform two initial fetches, because between // its first read access, and its write access that installs the // transaction observer, some write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation.trackingConstantRegion { db -> Int in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false try dbPool.write { db in try db.execute(sql: """ INSERT INTO t DEFAULT VALUES; @@ -369,18 +375,18 @@ class ValueObservationTests: GRDBTestCase { let expectation = self.expectation(description: "") expectation.expectedFulfillmentCount = 2 - var observedCounts: [Int] = [] + let observedCountsMutex: Mutex<[Int]> = Mutex([]) let cancellable = observation.start( in: dbPool, scheduling: .async(onQueue: .main), onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - observedCounts.append(count) + observedCountsMutex.withLock { $0.append(count) } expectation.fulfill() }) withExtendedLifetime(cancellable) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(observedCounts, [0, 0]) + XCTAssertEqual(observedCountsMutex.load(), [0, 0]) } } @@ -393,10 +399,14 @@ class ValueObservationTests: GRDBTestCase { // Force DatabasePool to perform two initial fetches, because between // its first read access, and its write access that installs the // transaction observer, some write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation.trackingConstantRegion { db -> Int in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false try dbPool.write { db in try db.execute(sql: """ INSERT INTO t DEFAULT VALUES; @@ -409,18 +419,18 @@ class ValueObservationTests: GRDBTestCase { let expectation = self.expectation(description: "") expectation.expectedFulfillmentCount = 2 - var observedCounts: [Int] = [] + let observedCountsMutex: Mutex<[Int]> = Mutex([]) let cancellable = observation.start( in: dbPool, scheduling: .immediate, onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - observedCounts.append(count) + observedCountsMutex.withLock { $0.append(count) } expectation.fulfill() }) withExtendedLifetime(cancellable) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(observedCounts, [0, 0]) + XCTAssertEqual(observedCountsMutex.load(), [0, 0]) } } @@ -433,10 +443,14 @@ class ValueObservationTests: GRDBTestCase { // Allow pool to perform a single initial fetch, because between // its first read access, and its write access that installs the // transaction observer, no write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation.trackingConstantRegion { db -> Int in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false DispatchQueue.main.asyncAfter(deadline: .now() + 1) { try! dbPool.write { db in try db.execute(sql: """ @@ -459,18 +473,18 @@ class ValueObservationTests: GRDBTestCase { let expectation = self.expectation(description: "") expectation.expectedFulfillmentCount = expectedCounts.count - var observedCounts: [Int] = [] + let observedCountsMutex: Mutex<[Int]> = Mutex([]) let cancellable = observation.start( in: dbPool, scheduling: .async(onQueue: .main), onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - observedCounts.append(count) + observedCountsMutex.withLock { $0.append(count) } expectation.fulfill() }) withExtendedLifetime(cancellable) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(observedCounts, expectedCounts) + XCTAssertEqual(observedCountsMutex.load(), expectedCounts) } } @@ -483,10 +497,14 @@ class ValueObservationTests: GRDBTestCase { // Allow pool to perform a single initial fetch, because between // its first read access, and its write access that installs the // transaction observer, no write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation.trackingConstantRegion { db -> Int in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false DispatchQueue.main.asyncAfter(deadline: .now() + 1) { try! dbPool.write { db in try db.execute(sql: """ @@ -509,18 +527,18 @@ class ValueObservationTests: GRDBTestCase { let expectation = self.expectation(description: "") expectation.expectedFulfillmentCount = expectedCounts.count - var observedCounts: [Int] = [] + let observedCountsMutex: Mutex<[Int]> = Mutex([]) let cancellable = observation.start( in: dbPool, scheduling: .immediate, onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - observedCounts.append(count) + observedCountsMutex.withLock { $0.append(count) } expectation.fulfill() }) withExtendedLifetime(cancellable) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(observedCounts, expectedCounts) + XCTAssertEqual(observedCountsMutex.load(), expectedCounts) } } @@ -602,7 +620,7 @@ class ValueObservationTests: GRDBTestCase { try dbQueue.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } // Track reducer process - var changesCount = 0 + let changesCountMutex = Mutex(0) let notificationExpectation = expectation(description: "notification") notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 2 @@ -613,13 +631,12 @@ class ValueObservationTests: GRDBTestCase { } // Start observation and deallocate cancellable after second change - var cancellable: (any DatabaseCancellable)? + nonisolated(unsafe) var cancellable: (any DatabaseCancellable)? cancellable = observation.start( in: dbQueue, onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { _ in - changesCount += 1 - if changesCount == 2 { + if changesCountMutex.increment() == 2 { cancellable = nil } notificationExpectation.fulfill() @@ -639,7 +656,7 @@ class ValueObservationTests: GRDBTestCase { _ = cancellable waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(changesCount, 2) + XCTAssertEqual(changesCountMutex.load(), 2) } func testCancellableExplicitCancellation() throws { @@ -648,7 +665,7 @@ class ValueObservationTests: GRDBTestCase { try dbQueue.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } // Track reducer process - var changesCount = 0 + let changesCountMutex = Mutex(0) let notificationExpectation = expectation(description: "notification") notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 2 @@ -659,13 +676,12 @@ class ValueObservationTests: GRDBTestCase { } // Start observation and cancel cancellable after second change - var cancellable: (any DatabaseCancellable)! + nonisolated(unsafe) var cancellable: (any DatabaseCancellable)! cancellable = observation.start( in: dbQueue, onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { _ in - changesCount += 1 - if changesCount == 2 { + if changesCountMutex.increment() == 2 { cancellable.cancel() } notificationExpectation.fulfill() @@ -683,7 +699,7 @@ class ValueObservationTests: GRDBTestCase { } waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(changesCount, 2) + XCTAssertEqual(changesCountMutex.load(), 2) } } @@ -697,18 +713,20 @@ class ValueObservationTests: GRDBTestCase { notificationExpectation.expectedFulfillmentCount = 2 do { - var cancellable: (any DatabaseCancellable)? = nil + nonisolated(unsafe) var cancellable: (any DatabaseCancellable)? = nil _ = cancellable // Avoid "Variable 'cancellable' was written to, but never read" warning - var shouldStopObservation = false + let shouldStopObservationMutex = Mutex(false) let observation = ValueObservation( trackingMode: .nonConstantRegionRecordedFromSelection, makeReducer: { AnyValueReducer( fetch: { _ in - if shouldStopObservation { - cancellable = nil /* deallocation */ + shouldStopObservationMutex.withLock { shouldStopObservation in + if shouldStopObservation { + cancellable = nil /* deallocation */ + } + shouldStopObservation = true } - shouldStopObservation = true }, value: { _ in () }) }) @@ -741,19 +759,21 @@ class ValueObservationTests: GRDBTestCase { notificationExpectation.expectedFulfillmentCount = 2 do { - var cancellable: (any DatabaseCancellable)? = nil + nonisolated(unsafe) var cancellable: (any DatabaseCancellable)? = nil _ = cancellable // Avoid "Variable 'cancellable' was written to, but never read" warning - var shouldStopObservation = false + let shouldStopObservationMutex = Mutex(false) let observation = ValueObservation( trackingMode: .nonConstantRegionRecordedFromSelection, makeReducer: { AnyValueReducer( fetch: { _ in }, value: { _ in - if shouldStopObservation { - cancellable = nil /* deallocation right before notification */ + shouldStopObservationMutex.withLock { shouldStopObservation in + if shouldStopObservation { + cancellable = nil /* deallocation right before notification */ + } + shouldStopObservation = true } - shouldStopObservation = true return () }) }) @@ -781,13 +801,13 @@ class ValueObservationTests: GRDBTestCase { try writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } // Start observing - var counts: [Int] = [] + let countsMutex: Mutex<[Int]> = Mutex([]) let cancellable = ValueObservation .trackingConstantRegion { try Table("t").fetchCount($0) } .start(in: writer) { error in XCTFail("Unexpected error: \(error)") } onChange: { count in - counts.append(count) + countsMutex.withLock { $0.append(count) } } // Perform a write after cancellation, but before the @@ -816,7 +836,7 @@ class ValueObservationTests: GRDBTestCase { // We should not have been notified of the first write, because // it was performed after cancellation. - XCTAssertFalse(counts.contains(1)) + XCTAssertFalse(countsMutex.load().contains(1)) } try test(makeDatabaseQueue()) From c2219075cff96029e9e1788f5edfe256fe3e4096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 14 Sep 2024 13:49:30 +0200 Subject: [PATCH 089/160] TODO --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 2bc0e5d293..c0ef213840 100644 --- a/TODO.md +++ b/TODO.md @@ -116,7 +116,7 @@ - [X] GRDB7: Sendable: WALSnapshotTransaction (7fd34012) - [ ] GRDB7: sending closures for SerializedDatabase - [ ] GRDB7: sending closures for ValueObservationScheduler -- [ ] GRDB7: Sendable closures for ValueObservation.handleEvents +- [X] GRDB7: Sendable closures for ValueObservation.handleEvents - [ ] GRDB7: Not Sendable: Record (make it explicit if subclasses can be made sendable) - [ ] GRDB7: Not Sendable: databasepublishers/databaseregion, migrate, read, value, write - [ ] GRDB7: Sendable closures for writePublisher From 9501d1f936b237b41d6f032114298da352060e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 14 Sep 2024 17:24:00 +0200 Subject: [PATCH 090/160] Fix compiler warnings about SchedulingWatchdog concurrency --- GRDB/Core/SchedulingWatchdog.swift | 28 +++++++++++++++++++--------- GRDB/Core/SerializedDatabase.swift | 4 ++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/GRDB/Core/SchedulingWatchdog.swift b/GRDB/Core/SchedulingWatchdog.swift index 83cb6d17af..0081aef17f 100644 --- a/GRDB/Core/SchedulingWatchdog.swift +++ b/GRDB/Core/SchedulingWatchdog.swift @@ -21,9 +21,17 @@ import Dispatch /// /// - preconditionValidQueue() crashes whenever a database is used in an invalid /// dispatch queue. -final class SchedulingWatchdog { +final class SchedulingWatchdog: @unchecked Sendable { + // @unchecked Sendable because mutable `allowedDatabases` is only + // accessed from the serial dispatch queue the instance is attached to. + private static let watchDogKey = DispatchSpecificKey() + + /// The databases allowed in the current dispatch queue. + /// + /// MUST be accessed from the serial dispatch queue the instance is attached to. private(set) var allowedDatabases: [Database] + var databaseObservationBroker: DatabaseObservationBroker? private init(allowedDatabase database: Database) { @@ -36,10 +44,12 @@ final class SchedulingWatchdog { queue.setSpecific(key: watchDogKey, value: watchdog) } - func inheritingAllowedDatabases(from other: SchedulingWatchdog, execute body: () throws -> T) rethrows -> T { - let backup = allowedDatabases - allowedDatabases.append(contentsOf: other.allowedDatabases) - defer { allowedDatabases = backup } + /// Must be called from a DispatchQueue with an attached SchedulingWatchdog. + static func inheritingAllowedDatabases(_ allowedDatabases: [Database], execute body: () throws -> T) rethrows -> T { + let watchdog = current! + let backup = watchdog.allowedDatabases + watchdog.allowedDatabases.append(contentsOf: allowedDatabases) + defer { watchdog.allowedDatabases = backup } return try body() } @@ -58,11 +68,11 @@ final class SchedulingWatchdog { current?.allows(db) ?? false } - static var current: SchedulingWatchdog? { - DispatchQueue.getSpecific(key: watchDogKey) - } - func allows(_ db: Database) -> Bool { allowedDatabases.contains { $0 === db } } + + static var current: SchedulingWatchdog? { + DispatchQueue.getSpecific(key: watchDogKey) + } } diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index 031de45927..bcd5c953ac 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -133,7 +133,7 @@ final class SerializedDatabase { // Case 3 return try queue.sync { - try SchedulingWatchdog.current!.inheritingAllowedDatabases(from: watchdog) { + try SchedulingWatchdog.inheritingAllowedDatabases(watchdog.allowedDatabases) { defer { preconditionNoUnsafeTransactionLeft(db) } return try block(db) } @@ -209,7 +209,7 @@ final class SerializedDatabase { // Case 3 return try queue.sync { - try SchedulingWatchdog.current!.inheritingAllowedDatabases(from: watchdog) { + try SchedulingWatchdog.inheritingAllowedDatabases(watchdog.allowedDatabases) { // Since we are reentrant, a transaction may already be opened. // In this case, don't check for unsafe transaction at the end. if db.isInsideTransaction { From b079136cd51ea51de368d75dff976d36fc931391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 14 Sep 2024 17:24:16 +0200 Subject: [PATCH 091/160] Fix compiler warnings about ISO8601DateFormatter concurrency --- GRDB/Record/EncodableRecord.swift | 3 ++- GRDB/Record/FetchableRecord+Decodable.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/GRDB/Record/EncodableRecord.swift b/GRDB/Record/EncodableRecord.swift index dd91c7586b..f804f3d40c 100644 --- a/GRDB/Record/EncodableRecord.swift +++ b/GRDB/Record/EncodableRecord.swift @@ -533,7 +533,8 @@ public enum DatabaseDateEncodingStrategy: Sendable { /// Encodes the result of the user-provided function case custom(@Sendable (Date) -> (any DatabaseValueConvertible)?) - private static let iso8601Formatter: ISO8601DateFormatter = { + // Assume this non-Sendable instance can be used from multiple threads concurrently. + nonisolated(unsafe) private static let iso8601Formatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime return formatter diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 8edca5d322..a4c6e423b2 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -676,7 +676,8 @@ extension ColumnDecoder: SingleValueDecodingContainer { } } -private let iso8601Formatter: ISO8601DateFormatter = { +// Assume this non-Sendable instance can be used from multiple threads concurrently. +nonisolated(unsafe) private let iso8601Formatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime return formatter From 01308c60441a0e0e8eae38bc924f85a0b3c04f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 14 Sep 2024 17:24:30 +0200 Subject: [PATCH 092/160] Fix compiler warnings about NSCache concurrency --- GRDB/Record/TableRecord.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index 709578cd59..46e2ad3255 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -791,4 +791,10 @@ public typealias PersistenceError = RecordError /// Calculating `defaultDatabaseTableName` is somewhat expensive due to the regular expression evaluation /// /// This cache mitigates the cost of the calculation by storing the name for later retrieval -private let defaultDatabaseTableNameCache = NSCache() +/// +/// Assume this non-Sendable cache of strings can be used from multiple +/// threads concurrently, because the NSCache documentation says: +/// +/// > You can add, remove, and query items in the cache from different +/// > threads without having to lock the cache yourself. +nonisolated(unsafe) private let defaultDatabaseTableNameCache = NSCache() From d3e62dc5c9dbb19c0cf0bfc8189fdc90f897dd99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 14 Sep 2024 17:24:47 +0200 Subject: [PATCH 093/160] Fix compiler warnings about default Inflections --- GRDB/Utils/Inflections+English.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/GRDB/Utils/Inflections+English.swift b/GRDB/Utils/Inflections+English.swift index 58338ec4f2..adea88eaec 100644 --- a/GRDB/Utils/Inflections+English.swift +++ b/GRDB/Utils/Inflections+English.swift @@ -43,8 +43,12 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. extension Inflections { - /// The default inflections - public static var `default`: Inflections = { + /// The default inflections. + /// + /// This global variable is not concurrency-safe. If you modify the + /// default inflections, do it once, early in the lifetime of your + /// application, before you access query interface methods. + nonisolated(unsafe) public static var `default`: Inflections = { // Defines the standard inflection rules. These are the starting point // for new projects and are not considered complete. The current set of // inflection rules is frozen. This means, we do not change them to From 461db6c4acbe599c42270cd1b3a774c4a7ff92bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 11:12:39 +0200 Subject: [PATCH 094/160] Feed database promises with Sendable values --- .../Request/RequestProtocols.swift | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index 369c080021..d4c57fc6e5 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -104,7 +104,8 @@ extension SelectionRequest { /// .select([Column("score")]) /// ``` public func select(_ selection: [any SQLSelectable]) -> Self { - selectWhenConnected { _ in selection } + let selection = selection.map(\.sqlSelection) + return selectWhenConnected { _ in selection } } /// Defines the result columns. @@ -190,7 +191,8 @@ extension SelectionRequest { /// let request = Player.all().annotated(with: [totalScore]) /// ``` public func annotated(with selection: [any SQLSelectable]) -> Self { - annotatedWhenConnected(with: { _ in selection }) + let selection = selection.map(\.sqlSelection) + return annotatedWhenConnected(with: { _ in selection }) } /// Appends result columns to the selected columns. @@ -263,7 +265,8 @@ extension FilteredRequest { /// let request = Player.all().filter(Column("name") == name) /// ``` public func filter(_ predicate: some SQLSpecificExpressible) -> Self { - filterWhenConnected { _ in predicate } + let predicate = predicate.sqlExpression + return filterWhenConnected { _ in predicate } } /// Filters the fetched rows with an SQL string. @@ -585,6 +588,11 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { return none() } + // Turn key values into sendable DatabaseValue + let keys = keys.map { key in + key.mapValues { $0?.databaseValue ?? .null } + } + let databaseTableName = self.databaseTableName return filterWhenConnected { db in try keys @@ -816,7 +824,8 @@ extension AggregatingRequest { /// /// - parameter expressions: An array of SQL expressions. public func group(_ expressions: [any SQLExpressible]) -> Self { - groupWhenConnected { _ in expressions } + let expressions = expressions.map(\.sqlExpression) + return groupWhenConnected { _ in expressions } } /// Returns an aggregate request grouped on the given SQL expressions. @@ -891,7 +900,8 @@ extension AggregatingRequest { /// .having(max(Column("score")) > 1000) /// ``` public func having(_ predicate: some SQLExpressible) -> Self { - havingWhenConnected { _ in predicate } + let predicate = predicate.sqlExpression + return havingWhenConnected { _ in predicate } } /// Filters the aggregated groups with an SQL string. @@ -1040,7 +1050,8 @@ extension OrderedRequest { /// .order(Column("name")) /// ``` public func order(_ orderings: any SQLOrderingTerm...) -> Self { - orderWhenConnected { _ in orderings } + let orderings = orderings.map(\.sqlOrdering) + return orderWhenConnected { _ in orderings } } /// Sorts the fetched rows according to the given SQL ordering terms. @@ -1062,7 +1073,8 @@ extension OrderedRequest { /// .order([Column("name")]) /// ``` public func order(_ orderings: [any SQLOrderingTerm]) -> Self { - orderWhenConnected { _ in orderings } + let orderings = orderings.map(\.sqlOrdering) + return orderWhenConnected { _ in orderings } } /// Sorts the fetched rows according to the given SQL string. From 630daaf48aad3681529b7b0e3eaa1ab7be296698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 11:17:01 +0200 Subject: [PATCH 095/160] Fix compiler warnings about StatementArgumentsSink for literal arguments --- GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift b/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift index 8715e0af9b..4f718f3f9a 100644 --- a/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift +++ b/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift @@ -172,11 +172,14 @@ class StatementArgumentsSink { private(set) var arguments: StatementArguments private let rawSQL: Bool + // This non-Sendable instance can be used from multiple threads + // concurrently, because it never modifies its `arguments` + // mutable state. /// A sink which turns all argument values into SQL literals. /// /// The `"WHERE name = \("O'Brien")"` SQL literal is turned into the /// `WHERE name = 'O''Brien'` SQL. - static let literalValues = StatementArgumentsSink(rawSQL: true) + nonisolated(unsafe) static let literalValues = StatementArgumentsSink(rawSQL: true) private init(rawSQL: Bool) { self.arguments = [] From e68a2075faddfc5127e4f997fc1962bd2b40506b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 11:25:23 +0200 Subject: [PATCH 096/160] Fix compiler warnings when building the request for an association --- GRDB/QueryInterface/TableRecord+Association.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/GRDB/QueryInterface/TableRecord+Association.swift b/GRDB/QueryInterface/TableRecord+Association.swift index 6ba8dacfe6..e59a634a38 100644 --- a/GRDB/QueryInterface/TableRecord+Association.swift +++ b/GRDB/QueryInterface/TableRecord+Association.swift @@ -567,6 +567,19 @@ extension TableRecord where Self: EncodableRecord { fatalError("Not implemented: request association without any foreign key") case let .foreignKey(foreignKey): + // Build the sendable persistence container before building the + // request, and catch the eventual error in a Result, so that it + // is thrown later, when the request is executed. This allows + // this method to not throw: + // + // extension Player { + // // We don't want this property to have a throwing getter: + // var team: QueryInterfaceRequest { + // request(for: Player.team) + // } + // } + let persistenceContainer = Result { try PersistenceContainer(self) } + let destinationRelation = association ._sqlAssociation .with { @@ -574,7 +587,7 @@ extension TableRecord where Self: EncodableRecord { // Filter the pivot on self try foreignKey .joinMapping(db, from: Self.databaseTableName) - .joinExpression(leftRows: [PersistenceContainer(db, self)]) + .joinExpression(leftRows: [persistenceContainer.get()]) } } .destinationRelation() From 473b06eac5716dbb4f452588695747342bf715fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 11:32:06 +0200 Subject: [PATCH 097/160] Fix compiler warnings about SQLQualifiedRelation --- GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift b/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift index 17551514bd..5b37ead626 100644 --- a/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift +++ b/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift @@ -513,6 +513,9 @@ struct SQLQueryGenerator: Refinable { } } +// This type is marked as `Sendable` in order to avoid compiler warnings, +// but it should not need to be: instances are synchronously created, used, +// and discarded. /// To generate SQL, we need a "qualified" relation, where all tables, /// expressions, etc, are qualified with table aliases. /// @@ -546,8 +549,7 @@ struct SQLQueryGenerator: Refinable { /// HAVING ... -- havingExpressionPromise /// ORDER BY ... -- ordering /// LIMIT ... -- limit - -private struct SQLQualifiedRelation { +private struct SQLQualifiedRelation: Sendable { /// All aliases, including aliases of joined relations var allAliases: [TableAlias] { joins.reduce(into: [source.alias].compactMap { $0 }) { From d489178d04799877311c699877ba08c8b55b4990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 14 Sep 2024 16:40:34 +0200 Subject: [PATCH 098/160] Improve ValueObservation tests debuggability --- Tests/GRDBTests/ValueObservationRecorder.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Tests/GRDBTests/ValueObservationRecorder.swift b/Tests/GRDBTests/ValueObservationRecorder.swift index fdc799f5a6..8a895c5645 100644 --- a/Tests/GRDBTests/ValueObservationRecorder.swift +++ b/Tests/GRDBTests/ValueObservationRecorder.swift @@ -576,9 +576,10 @@ extension GRDBTestCase { func test( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, + description: String, testErrorDispatching: @escaping () -> Void) throws { - func test(writer: some DatabaseWriter) throws { + func test(writer: some DatabaseWriter, description: String) throws { try writer.write(setup) let recorder = observation.record( @@ -586,7 +587,7 @@ extension GRDBTestCase { scheduling: scheduler, onError: { _ in testErrorDispatching() }) - let (_, error) = try wait(for: recorder.failure(), timeout: 5) + let (_, error) = try wait(for: recorder.failure(), timeout: 5, description: description) if let error = error as? Failure { try testFailure(error, writer) } else { @@ -594,9 +595,9 @@ extension GRDBTestCase { } } - try test(writer: DatabaseQueue()) - try test(writer: makeDatabaseQueue()) - try test(writer: makeDatabasePool()) + try test(writer: DatabaseQueue(), description: description + " (in-memory DatabaseQueue)") + try test(writer: makeDatabaseQueue(), description: description + " (on-disk DatabaseQueue)") + try test(writer: makeDatabasePool(), description: description + " (DatabasePool)") } do { @@ -606,6 +607,7 @@ extension GRDBTestCase { try test( observation: observation, scheduling: .immediate, + description: "Immediate scheduling", testErrorDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) }) } @@ -616,6 +618,7 @@ extension GRDBTestCase { try test( observation: observation, scheduling: .async(onQueue: .main), + description: "Async on main queue scheduling", testErrorDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) }) } @@ -627,6 +630,7 @@ extension GRDBTestCase { try test( observation: observation, scheduling: .async(onQueue: queue), + description: "Async on custom queue scheduling", testErrorDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) }) } } From 29f8aa38fbb1f00d4ed9f20d6cc5bb942255b61f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 12:03:19 +0200 Subject: [PATCH 099/160] Fix compiler warnings in ValueObservation observers --- .../Observers/ValueConcurrentObserver.swift | 21 +++++++++++++------ .../Observers/ValueWriteOnlyObserver.swift | 15 +++++++------ TODO.md | 5 ++--- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift index ad3e0bd188..9cc507ddfd 100644 --- a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift @@ -18,7 +18,10 @@ import Foundation /// reducing stage. /// /// **Notify** is calling user callbacks, in case of database change or error. -final class ValueConcurrentObserver { +final class ValueConcurrentObserver: @unchecked Sendable +where Reducer: ValueReducer, + Scheduler: ValueObservationScheduler +{ // MARK: - Configuration // // Configuration is not mutable. @@ -356,7 +359,8 @@ extension ValueConcurrentObserver { // `DatabasePool.asyncWALSnapshotTransaction` has to be used. initialFetchTransaction.asyncRead { dbResult in do { - let fetchedValue: Reducer.Fetcher.Value + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue: Reducer.Fetcher.Value let initialRegion: DatabaseRegion let db = try dbResult.get() @@ -463,7 +467,8 @@ extension ValueConcurrentObserver { events.databaseDidChange?() // Fetch - let fetchedValue: Reducer.Fetcher.Value + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue: Reducer.Fetcher.Value switch self.trackingMode { case .constantRegion: @@ -587,7 +592,8 @@ extension ValueConcurrentObserver { do { // Fetch - let fetchedValue: Reducer.Fetcher.Value + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue: Reducer.Fetcher.Value let initialRegion: DatabaseRegion let db = try dbResult.get() switch self.trackingMode { @@ -647,7 +653,8 @@ extension ValueConcurrentObserver { do { try writerDB.isolated(readOnly: true) { // Fetch - let fetchedValue: Reducer.Fetcher.Value + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue: Reducer.Fetcher.Value let observedRegion: DatabaseRegion switch self.trackingMode { case .constantRegion: @@ -777,7 +784,6 @@ extension ValueConcurrentObserver: TransactionObserver { } catch { stopDatabaseObservation(writerDB) notifyError(error) - return } } } @@ -827,6 +833,9 @@ extension ValueConcurrentObserver: TransactionObserver { } private func reduce(_ fetchResult: Result) { + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchResult = fetchResult + reduceQueue.async { do { let fetchedValue = try fetchResult.get() diff --git a/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift b/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift index c34e0ff467..67d915bf04 100644 --- a/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift @@ -18,10 +18,10 @@ import Foundation /// reducing stage. /// /// **Notify** is calling user callbacks, in case of database change or error. -final class ValueWriteOnlyObserver< - Writer: DatabaseWriter, - Reducer: ValueReducer, - Scheduler: ValueObservationScheduler> +final class ValueWriteOnlyObserver: @unchecked Sendable +where Writer: DatabaseWriter, + Reducer: ValueReducer, + Scheduler: ValueObservationScheduler { // MARK: - Configuration // @@ -252,9 +252,11 @@ extension ValueWriteOnlyObserver { writer.asyncWriteWithoutTransaction { db in do { // Fetch & Start observing the database - guard let fetchedValue = try self.fetchAndStartObservation(db) else { + guard let _fetchedValue = try self.fetchAndStartObservation(db) else { return /* Cancelled */ } + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue = _fetchedValue // Reduce // @@ -380,7 +382,8 @@ extension ValueWriteOnlyObserver: TransactionObserver { do { // Fetch - let fetchedValue: Reducer.Fetcher.Value + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue: Reducer.Fetcher.Value switch trackingMode { case .constantRegion, .constantRegionRecordedFromSelection: diff --git a/TODO.md b/TODO.md index c0ef213840..a633b06ec1 100644 --- a/TODO.md +++ b/TODO.md @@ -133,9 +133,8 @@ - [X] GRDB7: Sendable: OrderedDictionary (e022c35b) - [X] GRDB7: Rename ReadWriteBox to ReadWriteLock (7f5205ef) - [X] GRDB7: Sendable: DatabaseRegionConvertible (b4677ded) -- [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65, 5465d056) -- [ ] GRDB7: Sendable: ValueWriteOnlyObserver (ff2a7548) -- [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65) +- [X] GRDB7: Sendable: ValueConcurrentObserver (87b9db65, 5465d056) +- [X] GRDB7: Sendable: ValueWriteOnlyObserver (ff2a7548) - [X] GRDB7: Sendable: DatabaseCancellable (2f93f00b, 8f486a5e) - [ ] GRDB7: ValueObservation closures - [?] GRDB7: DatabasePublishers.ValueSubscription From 3728628e5fd2dd880f702c07420f976aba9ed339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 12:08:56 +0200 Subject: [PATCH 100/160] Association is Sendable --- GRDB/QueryInterface/Request/Association/Association.swift | 2 +- GRDB/QueryInterface/SQL/SQLAssociation.swift | 2 +- TODO.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/GRDB/QueryInterface/Request/Association/Association.swift b/GRDB/QueryInterface/Request/Association/Association.swift index b52aa31222..7e6d7d0ac9 100644 --- a/GRDB/QueryInterface/Request/Association/Association.swift +++ b/GRDB/QueryInterface/Request/Association/Association.swift @@ -33,7 +33,7 @@ import Foundation /// /// - ``ForeignKey`` /// - ``Inflections`` -public protocol Association: DerivableRequest { +public protocol Association: DerivableRequest, Sendable { // OriginRowDecoder and RowDecoder inherited from DerivableRequest provide // type safety: // diff --git a/GRDB/QueryInterface/SQL/SQLAssociation.swift b/GRDB/QueryInterface/SQL/SQLAssociation.swift index 4d9a8d30c7..ea31e2f8db 100644 --- a/GRDB/QueryInterface/SQL/SQLAssociation.swift +++ b/GRDB/QueryInterface/SQL/SQLAssociation.swift @@ -49,7 +49,7 @@ /// through: Pivot1.hasMany(Pivot2.self), /// via: Pivot2.belongsTo(Destination.self))) /// Origin.including(required: association) -public struct _SQLAssociation { +public struct _SQLAssociation: Sendable { // All steps, from pivot to destination. Never empty. private(set) var steps: [SQLAssociationStep] var keyPath: [String] { steps.map(\.keyName) } diff --git a/TODO.md b/TODO.md index a633b06ec1..a1f5de8f93 100644 --- a/TODO.md +++ b/TODO.md @@ -143,7 +143,7 @@ - [ ] GRDB7: doc (c0838cf9) - [X] GRDB7/BREAKING: PersistenceContainer is Sendable (50eefa8c) - [ ] GRDB7: TableRecord.databaseSelection must be declared as a computed property (24d232aa) -- [ ] GRDB7: Sendable: Association (b06aaee4) +- [X] GRDB7: Sendable: Association (b06aaee4) - [ ] GRDB7/Tests: Sendable: ValueObservationRecorder (2947b3d7) - [ ] GRDB7: ValueObservation.print cautiously uses its stream argument (5f8b39b7) - [ ] GRDB7/Tests: use a single and Sendable test TextOutputStream (bbb1a736) From d11f5099b4938f6d692f2f0c9f5bc3af81a22dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 12:18:02 +0200 Subject: [PATCH 101/160] Record is not Sendable --- GRDB/Record/Record.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/GRDB/Record/Record.swift b/GRDB/Record/Record.swift index 67fbf6c0d2..7d0644e731 100644 --- a/GRDB/Record/Record.swift +++ b/GRDB/Record/Record.swift @@ -431,3 +431,9 @@ open class Record { extension Record: TableRecord { } extension Record: PersistableRecord { } extension Record: FetchableRecord { } + +// Explicit non-conformance to Sendable, because persistence methods mutate +// the instance (`referenceRow`). Nothing prevents a single Record instance +// from being concurrencly persisted in two distinct database connections. +@available(*, unavailable) +extension Record: Sendable { } From 157dbefdc3473690285f121c0e93cce726ea810b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 12:18:56 +0200 Subject: [PATCH 102/160] TODO --- TODO.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/TODO.md b/TODO.md index a1f5de8f93..7ca1fe6069 100644 --- a/TODO.md +++ b/TODO.md @@ -114,13 +114,13 @@ - [X] GRDB7: Sendable: Pool (f13b2d2e) - [X] GRDB7: Sendable: OnDemandFuture fulfill (2aabc4c1) - [X] GRDB7: Sendable: WALSnapshotTransaction (7fd34012) -- [ ] GRDB7: sending closures for SerializedDatabase -- [ ] GRDB7: sending closures for ValueObservationScheduler +- [-] GRDB7: sending closures for SerializedDatabase +- [-] GRDB7: sending closures for ValueObservationScheduler - [X] GRDB7: Sendable closures for ValueObservation.handleEvents -- [ ] GRDB7: Not Sendable: Record (make it explicit if subclasses can be made sendable) +- [X] GRDB7: Not Sendable: Record (make it explicit if subclasses can be made sendable) - [ ] GRDB7: Not Sendable: databasepublishers/databaseregion, migrate, read, value, write -- [ ] GRDB7: Sendable closures for writePublisher -- [ ] GRDB7: Sendable closures for readPublisher +- [X] GRDB7: Sendable closures for writePublisher +- [X] GRDB7: Sendable closures for readPublisher - [ ] GRDB7: Not Sendable: fts5customtokenizer, fts5tokenizer, fts5wrappertokenizer - [X] GRDB7: Sendable: DatabasePromise (05899228, 5a2c15b8) - [X] GRDB7: Sendable: TableAlias (f2b0b186) @@ -136,7 +136,7 @@ - [X] GRDB7: Sendable: ValueConcurrentObserver (87b9db65, 5465d056) - [X] GRDB7: Sendable: ValueWriteOnlyObserver (ff2a7548) - [X] GRDB7: Sendable: DatabaseCancellable (2f93f00b, 8f486a5e) -- [ ] GRDB7: ValueObservation closures +- [X] GRDB7: ValueObservation closures - [?] GRDB7: DatabasePublishers.ValueSubscription - [X] GRDB7: Sendable: ValueObservation (93f6f982) - [?] GRDB7: Not Sendable: SharedValueObservation From e307e9c1daa761819525148ff0af26f6082d0e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 24 Apr 2024 20:20:22 +0200 Subject: [PATCH 103/160] Improve CaseInsensitiveIdentifier documentation --- GRDB/Utils/CaseInsensitiveIdentifier.swift | 2 +- TODO.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GRDB/Utils/CaseInsensitiveIdentifier.swift b/GRDB/Utils/CaseInsensitiveIdentifier.swift index f1fd12c5de..9e80a36e2f 100644 --- a/GRDB/Utils/CaseInsensitiveIdentifier.swift +++ b/GRDB/Utils/CaseInsensitiveIdentifier.swift @@ -1,5 +1,5 @@ /// A case-preserving, case-insensitive identifier -/// that matches the ASCII version of sqlite3_stricmp +/// that intends to match the ASCII version of sqlite3_stricmp. struct CaseInsensitiveIdentifier: Hashable { private let lowercased: String let rawValue: String diff --git a/TODO.md b/TODO.md index 7ca1fe6069..479c62a5ee 100644 --- a/TODO.md +++ b/TODO.md @@ -140,7 +140,7 @@ - [?] GRDB7: DatabasePublishers.ValueSubscription - [X] GRDB7: Sendable: ValueObservation (93f6f982) - [?] GRDB7: Not Sendable: SharedValueObservation -- [ ] GRDB7: doc (c0838cf9) +- [X] GRDB7: doc (c0838cf9) - [X] GRDB7/BREAKING: PersistenceContainer is Sendable (50eefa8c) - [ ] GRDB7: TableRecord.databaseSelection must be declared as a computed property (24d232aa) - [X] GRDB7: Sendable: Association (b06aaee4) From 9e9bee8d3a1966490ba222930155e045a241877b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 14:00:30 +0200 Subject: [PATCH 104/160] TODO --- TODO.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 479c62a5ee..de09c24f3e 100644 --- a/TODO.md +++ b/TODO.md @@ -153,10 +153,10 @@ - [X] GRDB7: DatabaseWriter async methods support Task cancellation (a5226501) - [X] GRDB7: DatabaseReader async methods support Task cancellation (10c9d311) - [X] GRDB7: Document that async methods can throw CancellationError (8df18fb8) -- [ ] GRDB7: Sendable: AssociationAggregate (48ad10ae) +- [-] GRDB7: Sendable: AssociationAggregate (48ad10ae) - [?] GRDB7: Sendable: AsyncValueObservation (ce63cdfa) - [X] GRDB7: Sendable: DatabaseRegionObservation (b4ff52fb) -- [ ] GRDB7: DispatchQueue.asyncSending (7b075e6b) +- [-] GRDB7: DispatchQueue.asyncSending (7b075e6b) - [X] GRDB7: Replace sequences with collection (e.g. https://github.com/tidal-music/tidal-sdk-ios/pull/39) - [ ] GRDB7: Replace `some` DatabaseReader/Writer with `any` where possible, in order to avoid issues with accessing DatabaseContext from GRDBQuery (if the problem exists in Xcode 16) From 28455709296fafff608af10830d57b974345de76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 14:27:19 +0200 Subject: [PATCH 105/160] Fix DocC warnings --- GRDB/Core/DatabaseReader.swift | 4 ++-- GRDB/Core/DatabaseWriter.swift | 6 +++--- GRDB/Documentation.docc/Concurrency.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index fc59d0981e..dbb13bb0f4 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -22,14 +22,14 @@ import Dispatch /// ### Reading from the Database /// /// - ``read(_:)-3806d`` -/// - ``read(_:)-8gyof`` +/// - ``read(_:)-4d1da`` /// - ``readPublisher(receiveOn:value:)`` /// - ``asyncRead(_:)`` /// /// ### Unsafe Methods /// /// - ``unsafeRead(_:)-5i7tf`` -/// - ``unsafeRead(_:)-4w54s`` +/// - ``unsafeRead(_:)-5gsav`` /// - ``unsafeReentrantRead(_:)`` /// - ``asyncUnsafeRead(_:)`` /// diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index 8f3fcf9407..44fd4ad98e 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -22,18 +22,18 @@ import Dispatch /// ### Writing into the Database /// /// - ``write(_:)-76inz`` -/// - ``write(_:)-88g7e`` +/// - ``write(_:)-3db50`` /// - ``writePublisher(receiveOn:updates:)`` /// - ``writePublisher(receiveOn:updates:thenRead:)`` /// - ``writeWithoutTransaction(_:)-4qh1w`` -/// - ``writeWithoutTransaction(_:)-4kzng`` +/// - ``writeWithoutTransaction(_:)-67mri`` /// - ``asyncWrite(_:completion:)`` /// - ``asyncWriteWithoutTransaction(_:)`` /// /// ### Exclusive Access to the Database /// /// - ``barrierWriteWithoutTransaction(_:)-280j1`` -/// - ``barrierWriteWithoutTransaction(_:)-6py5x`` +/// - ``barrierWriteWithoutTransaction(_:)-48d63`` /// - ``asyncBarrierWriteWithoutTransaction(_:)`` /// /// ### Reading from the Latest Committed Database State diff --git a/GRDB/Documentation.docc/Concurrency.md b/GRDB/Documentation.docc/Concurrency.md index b6eef71b65..835199fbab 100644 --- a/GRDB/Documentation.docc/Concurrency.md +++ b/GRDB/Documentation.docc/Concurrency.md @@ -99,7 +99,7 @@ try dbQueue.write { db in } ``` - See ``DatabaseReader/read(_:)-8gyof`` and ``DatabaseWriter/write(_:)-88g7e``. + See ``DatabaseReader/read(_:)-4d1da`` and ``DatabaseWriter/write(_:)-3db50``. Note the identical method names: `read`, `write`. The async version is only available in async Swift functions. From 7707bbd1e9c8a49120b71f321b4fbc343a8d2cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 10:35:15 +0200 Subject: [PATCH 106/160] Fix Swiftlint warning --- GRDB/Core/SchedulingWatchdog.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/GRDB/Core/SchedulingWatchdog.swift b/GRDB/Core/SchedulingWatchdog.swift index 0081aef17f..3c2ba702b0 100644 --- a/GRDB/Core/SchedulingWatchdog.swift +++ b/GRDB/Core/SchedulingWatchdog.swift @@ -45,7 +45,9 @@ final class SchedulingWatchdog: @unchecked Sendable { } /// Must be called from a DispatchQueue with an attached SchedulingWatchdog. - static func inheritingAllowedDatabases(_ allowedDatabases: [Database], execute body: () throws -> T) rethrows -> T { + static func inheritingAllowedDatabases( + _ allowedDatabases: [Database], execute body: () throws -> T + ) rethrows -> T { let watchdog = current! let backup = watchdog.allowedDatabases watchdog.allowedDatabases.append(contentsOf: allowedDatabases) From e7a95cf0f7258166cc111b97fe9e17b45ed5abb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 10:36:05 +0200 Subject: [PATCH 107/160] Enable GlobalActorIsolatedTypesUsability --- Package.swift | 3 ++- SQLiteCustom/GRDBDeploymentTarget.xcconfig | 2 ++ Support/GRDBDeploymentTarget.xcconfig | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f08e63fe4e..fa9e19b323 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,8 @@ import PackageDescription var swiftSettings: [SwiftSetting] = [ .define("SQLITE_ENABLE_FTS5"), - .enableUpcomingFeature("InferSendableFromCaptures") + .enableUpcomingFeature("InferSendableFromCaptures"), + .enableUpcomingFeature("GlobalActorIsolatedTypesUsability"), ] var cSettings: [CSetting] = [] var dependencies: [PackageDescription.Package.Dependency] = [] diff --git a/SQLiteCustom/GRDBDeploymentTarget.xcconfig b/SQLiteCustom/GRDBDeploymentTarget.xcconfig index 7fed5abdec..98355ee9db 100644 --- a/SQLiteCustom/GRDBDeploymentTarget.xcconfig +++ b/SQLiteCustom/GRDBDeploymentTarget.xcconfig @@ -3,3 +3,5 @@ MACOSX_DEPLOYMENT_TARGET = 10.13 TVOS_DEPLOYMENT_TARGET = 12.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES +OTHER_SWIFT_FLAGS = $(inherited) \ + -enable-upcoming-feature GlobalActorIsolatedTypesUsability \ diff --git a/Support/GRDBDeploymentTarget.xcconfig b/Support/GRDBDeploymentTarget.xcconfig index cd39973b9a..82a2ad00a7 100644 --- a/Support/GRDBDeploymentTarget.xcconfig +++ b/Support/GRDBDeploymentTarget.xcconfig @@ -4,6 +4,8 @@ TVOS_DEPLOYMENT_TARGET = 12.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 OTHER_SWIFT_FLAGS = $(inherited) -D SQLITE_ENABLE_FTS5 SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES +OTHER_SWIFT_FLAGS = $(inherited) \ + -enable-upcoming-feature GlobalActorIsolatedTypesUsability \ //// Compile with all opt-in APIs //GCC_PREPROCESSOR_DEFINITIONS = $(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1 From dd28d9734b137be6d0403f110cb1ddc8da7d7da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 12:15:14 +0200 Subject: [PATCH 108/160] Fix GlobalActorIsolatedTypesUsability --- SQLiteCustom/GRDBDeploymentTarget.xcconfig | 3 +-- Support/GRDBDeploymentTarget.xcconfig | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/SQLiteCustom/GRDBDeploymentTarget.xcconfig b/SQLiteCustom/GRDBDeploymentTarget.xcconfig index 98355ee9db..c4f1fe1970 100644 --- a/SQLiteCustom/GRDBDeploymentTarget.xcconfig +++ b/SQLiteCustom/GRDBDeploymentTarget.xcconfig @@ -3,5 +3,4 @@ MACOSX_DEPLOYMENT_TARGET = 10.13 TVOS_DEPLOYMENT_TARGET = 12.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES -OTHER_SWIFT_FLAGS = $(inherited) \ - -enable-upcoming-feature GlobalActorIsolatedTypesUsability \ +OTHER_SWIFT_FLAGS = $(inherited) -enable-upcoming-feature GlobalActorIsolatedTypesUsability diff --git a/Support/GRDBDeploymentTarget.xcconfig b/Support/GRDBDeploymentTarget.xcconfig index 82a2ad00a7..f08af6102e 100644 --- a/Support/GRDBDeploymentTarget.xcconfig +++ b/Support/GRDBDeploymentTarget.xcconfig @@ -4,8 +4,7 @@ TVOS_DEPLOYMENT_TARGET = 12.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 OTHER_SWIFT_FLAGS = $(inherited) -D SQLITE_ENABLE_FTS5 SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES -OTHER_SWIFT_FLAGS = $(inherited) \ - -enable-upcoming-feature GlobalActorIsolatedTypesUsability \ +OTHER_SWIFT_FLAGS = $(inherited) -enable-upcoming-feature GlobalActorIsolatedTypesUsability //// Compile with all opt-in APIs //GCC_PREPROCESSOR_DEFINITIONS = $(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1 From e1a384009acf69c780e725770311e9f39ba69183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 15 Sep 2024 14:49:14 +0200 Subject: [PATCH 109/160] [BREAKING] Async sequences built from ValueObservation schedule values and errors on the cooperative thread pool by default. --- .../Extension/ValueObservation.md | 28 +- GRDB/ValueObservation/ValueObservation.swift | 6 +- .../ValueObservationScheduler.swift | 50 ++++ .../SharedValueObservationTests.swift | 246 +++++++++++++++++- Tests/GRDBTests/ValueObservationTests.swift | 77 ------ 5 files changed, 324 insertions(+), 83 deletions(-) diff --git a/GRDB/Documentation.docc/Extension/ValueObservation.md b/GRDB/Documentation.docc/Extension/ValueObservation.md index 5c4664e1f1..e60fc35a64 100644 --- a/GRDB/Documentation.docc/Extension/ValueObservation.md +++ b/GRDB/Documentation.docc/Extension/ValueObservation.md @@ -131,7 +131,7 @@ let cancellable = observation // <- Here "Fresh value" has already been printed. ``` -The other built-in scheduler ``ValueObservationScheduler/async(onQueue:)`` asynchronously schedules values and errors on the dispatch queue of your choice. Make sure you provide a serial queue, because a concurrent one such as `DispachQueue.global(qos: .default)` would mess with the ordering of fresh value notifications: +The ``ValueObservationScheduler/async(onQueue:)`` scheduler asynchronously schedules values and errors on the dispatch queue of your choice. Make sure you provide a serial queue, because a concurrent one such as `DispachQueue.global(qos: .default)` would mess with the ordering of fresh value notifications: ```swift // Async scheduling notifies all values @@ -146,6 +146,30 @@ let cancellable = observation } ``` +The ``ValueObservationScheduler/task`` scheduler asynchronously schedules values and errors on the cooperative thread pool. It is implicitly used when you turn a ValueObservation into an async sequence. You can specify it explicitly when you intend to consume a shared observation as an async sequence: + +```swift +do { + for try await players in observation.values(in: dbQueue) { + // Called on the cooperative thread pool + print("Fresh players", players) + } +} catch { + // Handle error +} + +let sharedObservation = observation.shared(in: dbQueue, scheduling: .concurrent) +do { + for try await players in sharedObservation.values() { + // Called on the cooperative thread pool + print("Fresh players", players) + } +} catch { + // Handle error +} + +``` + As described above, the `scheduling` argument controls the execution of the change and error callbacks. You also have some control on the execution of the database fetch: - With the `.immediate` scheduling, the initial fetch is always performed synchronously, on the main thread, when the observation starts, so that the initial value can be notified immediately. @@ -291,7 +315,7 @@ When needed, you can help GRDB optimize observations and reduce database content - ``publisher(in:scheduling:)`` - ``start(in:scheduling:onError:onChange:)`` -- ``values(in:scheduling:bufferingPolicy:)`` +- ``values(in:priority:bufferingPolicy:)`` - ``DatabaseCancellable`` - ``ValueObservationScheduler`` diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index 32abcd97f8..fca7619939 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -295,14 +295,14 @@ extension ValueObservation { /// ``` /// /// - parameter reader: A DatabaseReader. - /// - parameter scheduler: A ValueObservationScheduler. By default, fresh - /// values are dispatched asynchronously on the main dispatch queue. + /// - parameter scheduler: A ValueObservationScheduler. By default, + /// fresh values are dispatched on the cooperative thread pool. /// - parameter bufferingPolicy: see the documntation /// of `AsyncThrowingStream`. @available(iOS 13, macOS 10.15, tvOS 13, *) public func values( in reader: some DatabaseReader, - scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main), + scheduling scheduler: some ValueObservationScheduler = .task, bufferingPolicy: AsyncValueObservation.BufferingPolicy = .unbounded) -> AsyncValueObservation where Reducer: ValueReducer diff --git a/GRDB/ValueObservation/ValueObservationScheduler.swift b/GRDB/ValueObservation/ValueObservationScheduler.swift index 3e4d9fdce8..68ab036ebb 100644 --- a/GRDB/ValueObservation/ValueObservationScheduler.swift +++ b/GRDB/ValueObservation/ValueObservationScheduler.swift @@ -9,8 +9,11 @@ import Foundation /// /// - ``async(onQueue:)`` /// - ``immediate`` +/// - ``task`` +/// - ``task(priority:)`` /// - ``AsyncValueObservationScheduler`` /// - ``ImmediateValueObservationScheduler`` +/// - ``TaskValueObservationScheduler`` public protocol ValueObservationScheduler: Sendable { /// Returns whether the initial value should be immediately notified. /// @@ -123,3 +126,50 @@ extension ValueObservationScheduler where Self == ImmediateValueObservationSched ImmediateValueObservationScheduler() } } + +// MARK: - TaskValueObservationScheduler + +/// A scheduler that notifies all values on the cooperative thread pool. +@available(iOS 13, macOS 10.15, tvOS 13, *) +final public class TaskValueObservationScheduler: ValueObservationScheduler { + typealias Action = @Sendable () -> Void + let continuation: AsyncStream.Continuation + let task: Task + + init(priority: TaskPriority?) { + let (stream, continuation) = AsyncStream.makeStream(of: Action.self) + + self.continuation = continuation + self.task = Task(priority: priority) { + for await action in stream { + action() + } + } + } + + deinit { + task.cancel() + } + + public func immediateInitialValue() -> Bool { + false + } + + public func schedule(_ action: @escaping @Sendable () -> Void) { + continuation.yield(action) + } +} + +@available(iOS 13, macOS 10.15, tvOS 13, *) +extension ValueObservationScheduler where Self == TaskValueObservationScheduler { + /// A scheduler that notifies all values from a new `Task`. + public static var task: TaskValueObservationScheduler { + TaskValueObservationScheduler(priority: nil) + } + + /// A scheduler that notifies all values from a new `Task` with the + /// given priority. + public static func task(priority: TaskPriority) -> TaskValueObservationScheduler { + TaskValueObservationScheduler(priority: priority) + } +} diff --git a/Tests/GRDBTests/SharedValueObservationTests.swift b/Tests/GRDBTests/SharedValueObservationTests.swift index dc9baa1f9c..bf8dd893f3 100644 --- a/Tests/GRDBTests/SharedValueObservationTests.swift +++ b/Tests/GRDBTests/SharedValueObservationTests.swift @@ -525,6 +525,230 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), []) } + func test_task_observationLifetime() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + } + } + + let log = Log() + var sharedObservation: SharedValueObservation? = ValueObservation + .tracking(Table("player").fetchCount) + .print(to: log) + .shared( + in: dbQueue, + scheduling: .task, + extent: .observationLifetime) + XCTAssertEqual(log.flush(), []) + + // We want to control when the shared observation is deallocated + try withExtendedLifetime(sharedObservation) { sharedObservation in + // --- Start observation 1 + let values1Mutex: Mutex<[Int]> = Mutex([]) + let exp1 = expectation(description: "") + exp1.expectedFulfillmentCount = 2 + exp1.assertForOverFulfill = false + let cancellable1 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values1Mutex.withLock { $0.append(value) } + exp1.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp1], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1]) + XCTAssertEqual(log.flush(), [ + "start", "fetch", "tracked region: player(*)", "value: 0", + "database did change", "fetch", "value: 1"]) + + // --- Start observation 2 + let values2Mutex: Mutex<[Int]> = Mutex([]) + let exp2 = expectation(description: "") + exp2.expectedFulfillmentCount = 2 + exp2.assertForOverFulfill = false + let cancellable2 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values2Mutex.withLock { $0.append(value) } + exp2.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp2], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2]) + XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) + + // --- Stop observation 1 + cancellable1.cancel() + XCTAssertEqual(log.flush(), []) + + // --- Start observation 3 + let values3Mutex: Mutex<[Int]> = Mutex([]) + let exp3 = expectation(description: "") + exp3.expectedFulfillmentCount = 2 + exp3.assertForOverFulfill = false + let cancellable3 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values3Mutex.withLock { $0.append(value) } + exp3.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp3], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2, 3]) + XCTAssertEqual(values3Mutex.load(), [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"]) + } + +#if canImport(Combine) + func test_task_publisher() throws { + guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + throw XCTSkip("Combine is not available") + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + } + } + + let publisher = ValueObservation + .tracking(Table("player").fetchCount) + .shared(in: dbQueue, scheduling: .task) + .publisher() + + do { + let recorder = publisher.record() + try XCTAssert(recorder.availableElements.get().isEmpty) + try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 0) + 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) + try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 2) + } + } +#endif + + func test_task_whileObserved() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + } + } + + let log = Log() + var sharedObservation: SharedValueObservation? = ValueObservation + .tracking(Table("player").fetchCount) + .print(to: log) + .shared( + in: dbQueue, + scheduling: .task, + extent: .whileObserved) + XCTAssertEqual(log.flush(), []) + + // We want to control when the shared observation is deallocated + try withExtendedLifetime(sharedObservation) { sharedObservation in + // --- Start observation 1 + let values1Mutex: Mutex<[Int]> = Mutex([]) + let exp1 = expectation(description: "") + exp1.expectedFulfillmentCount = 2 + exp1.assertForOverFulfill = false + let cancellable1 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values1Mutex.withLock { $0.append(value) } + exp1.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp1], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1]) + XCTAssertEqual(log.flush(), [ + "start", "fetch", "tracked region: player(*)", "value: 0", + "database did change", "fetch", "value: 1"]) + + // --- Start observation 2 + let values2Mutex: Mutex<[Int]> = Mutex([]) + let exp2 = expectation(description: "") + exp2.expectedFulfillmentCount = 2 + exp2.assertForOverFulfill = false + let cancellable2 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values2Mutex.withLock { $0.append(value) } + exp2.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp2], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2]) + XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) + + // --- Stop observation 1 + cancellable1.cancel() + XCTAssertEqual(log.flush(), []) + + // --- Start observation 3 + let values3Mutex: Mutex<[Int]> = Mutex([]) + let exp3 = expectation(description: "") + exp3.expectedFulfillmentCount = 2 + exp3.assertForOverFulfill = false + let cancellable3 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values3Mutex.withLock { $0.append(value) } + exp3.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp3], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2, 3]) + XCTAssertEqual(values3Mutex.load(), [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 + XCTAssertEqual(log.flush(), []) + } + #if canImport(Combine) func test_error_recovery_observationLifetime() throws { guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { @@ -642,7 +866,7 @@ class SharedValueObservationTests: GRDBTestCase { #endif @available(iOS 13, macOS 10.15, tvOS 13, *) - func testAsyncAwait() async throws { + func testAsyncAwait_mainQueue() async throws { let dbQueue = try makeDatabaseQueue() try await dbQueue.write { db in try db.create(table: "player") { t in @@ -660,6 +884,26 @@ class SharedValueObservationTests: GRDBTestCase { break } } + + @available(iOS 13, macOS 10.15, tvOS 13, *) + func testAsyncAwait_task() async throws { + let dbQueue = try makeDatabaseQueue() + try await dbQueue.write { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + } + } + + let values = ValueObservation + .tracking(Table("player").fetchCount) + .shared(in: dbQueue, scheduling: .task) + .values() + + for try await value in values { + XCTAssertEqual(value, 0) + break + } + } } private class Log: TextOutputStream { diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index 08b59e1d5c..f509227fc7 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -932,44 +932,6 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(iOS 13, macOS 10.15, tvOS 13, *) - func testAsyncAwait_values_prefix_immediate_scheduling() async throws { - func test(_ writer: some DatabaseWriter) async throws { - // We need something to change - try await writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } - - let cancellationExpectation = expectation(description: "cancelled") - let task = Task { @MainActor () -> [Int] in - var counts: [Int] = [] - let observation = ValueObservation - .trackingConstantRegion(Table("t").fetchCount) - .handleEvents(didCancel: { cancellationExpectation.fulfill() }) - - for try await count in try observation.values(in: writer, scheduling: .immediate).prefix(while: { $0 <= 3 }) { - counts.append(count) - try await writer.write { try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") } - } - return counts - } - - let counts = try await task.value - XCTAssertTrue(counts.contains(0)) - XCTAssertTrue(counts.contains(where: { $0 >= 2 })) - XCTAssertEqual(counts.sorted(), counts) - - // Observation was ended -#if compiler(>=5.8) - await fulfillment(of: [cancellationExpectation], timeout: 2) -#else - wait(for: [cancellationExpectation], timeout: 2) -#endif - } - - try await AsyncTest(test).run { try DatabaseQueue() } - try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } - try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } - } - @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_values_break() async throws { func test(_ writer: some DatabaseWriter) async throws { @@ -1012,45 +974,6 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(iOS 13, macOS 10.15, tvOS 13, *) - func testAsyncAwait_values_immediate_break() async throws { - func test(_ writer: some DatabaseWriter) async throws { - // We need something to change - try await writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } - - let cancellationExpectation = expectation(description: "cancelled") - - let task = Task { @MainActor () -> [Int] in - var counts: [Int] = [] - let observation = ValueObservation - .trackingConstantRegion(Table("t").fetchCount) - .handleEvents(didCancel: { cancellationExpectation.fulfill() }) - - for try await count in observation.values(in: writer, scheduling: .immediate) { - counts.append(count) - break - } - return counts - } - - let counts = try await task.value - - // A single value was published - assertValueObservationRecordingMatch(recorded: counts, expected: [0]) - - // Observation was ended -#if compiler(>=5.8) - await fulfillment(of: [cancellationExpectation], timeout: 2) -#else - wait(for: [cancellationExpectation], timeout: 2) -#endif - } - - try await AsyncTest(test).run { try DatabaseQueue() } - try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } - try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } - } - @available(iOS 13, macOS 10.15, tvOS 13, *) func testAsyncAwait_values_cancelled() async throws { func test(_ writer: some DatabaseWriter) async throws { From bdb7d84086256e11c50fb7831fcf6d01f3813b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Mon, 16 Sep 2024 08:39:28 +0200 Subject: [PATCH 110/160] [BREAKING] ValueObservation MainActor scheduling Breaking because before iOS 13, macOS 10.15, tvOS 13, there is no longer any default scheduler --- .../Extension/ValueObservation.md | 28 ++--- .../SharedValueObservation.swift | 9 +- GRDB/ValueObservation/ValueObservation.swift | 107 +++++++++++++----- .../ValueObservationScheduler.swift | 88 ++++++++++++-- .../SharedValueObservationTests.swift | 2 + ...ValueObservationRegionRecordingTests.swift | 2 + Tests/GRDBTests/ValueObservationTests.swift | 47 ++++++++ 7 files changed, 228 insertions(+), 55 deletions(-) diff --git a/GRDB/Documentation.docc/Extension/ValueObservation.md b/GRDB/Documentation.docc/Extension/ValueObservation.md index e60fc35a64..6abaa6b28e 100644 --- a/GRDB/Documentation.docc/Extension/ValueObservation.md +++ b/GRDB/Documentation.docc/Extension/ValueObservation.md @@ -78,7 +78,7 @@ See below for the li By default, `ValueObservation` notifies a fresh value whenever any component of its fetched value is modified (any fetched column, row, etc.). This can be configured: see . -By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main dispatch queue, asynchronously. This can be configured: see . +By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main actor, asynchronously. This can be configured: see . By default, `ValueObservation` fetches a fresh value immediately after a change is committed in the database. In particular, modifying the database on the main thread triggers a fetch on the main thread as well. This behavior can be configured: see . @@ -98,40 +98,38 @@ The database observation stops when the cancellable returned by the `start` meth ## ValueObservation Scheduling -By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main dispatch queue, asynchronously: +By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main actor, asynchronously: ```swift // The default scheduling let cancellable = observation.start(in: dbQueue) { error in - // Called asynchronously on the main dispatch queue + // Called asynchronously on the main actor } onChange: { value in - // Called asynchronously on the main dispatch queue + // Called asynchronously on the main actor print("Fresh value", value) } ``` You can change this behavior by adding a `scheduling` argument to the `start()` method. -For example, the ``ValueObservationScheduler/immediate`` scheduler notifies all values on the main dispatch queue, and notifies the first one immediately when the observation starts. +For example, the ``ValueObservationMainActorScheduler/immediate`` scheduler notifies all values on the main actor, and notifies the first one immediately when the observation starts. It is very useful in graphic applications, because you can configure views right away, without waiting for the initial value to be fetched eventually. You don't have to implement any empty or loading screen, or to prevent some undesired initial animation. Take care that the user interface is not responsive during the fetch of the first value, so only use the `immediate` scheduling for very fast database requests! -The `immediate` scheduling requires that the observation starts from the main dispatch queue (a fatal error is raised otherwise): - ```swift // Immediate scheduling notifies // the initial value right on subscription. let cancellable = observation .start(in: dbQueue, scheduling: .immediate) { error in - // Called on the main dispatch queue + // Called on the main actor } onChange: { value in - // Called on the main dispatch queue + // Called on the main actor print("Fresh value", value) } // <- Here "Fresh value" has already been printed. ``` -The ``ValueObservationScheduler/async(onQueue:)`` scheduler asynchronously schedules values and errors on the dispatch queue of your choice. Make sure you provide a serial queue, because a concurrent one such as `DispachQueue.global(qos: .default)` would mess with the ordering of fresh value notifications: +The ``ValueObservationScheduler/async(onQueue:)`` scheduler asynchronously schedules values and errors on the dispatch queue of your choice. Make sure you provide a serial dispatch queue, because a concurrent one such as `DispachQueue.global(qos: .default)` would mess with the ordering of fresh value notifications: ```swift // Async scheduling notifies all values @@ -158,7 +156,7 @@ do { // Handle error } -let sharedObservation = observation.shared(in: dbQueue, scheduling: .concurrent) +let sharedObservation = observation.shared(in: dbQueue, scheduling: .task) do { for try await players in sharedObservation.values() { // Called on the cooperative thread pool @@ -172,7 +170,7 @@ do { As described above, the `scheduling` argument controls the execution of the change and error callbacks. You also have some control on the execution of the database fetch: -- With the `.immediate` scheduling, the initial fetch is always performed synchronously, on the main thread, when the observation starts, so that the initial value can be notified immediately. +- With the `.immediate` scheduling, the initial fetch is always performed synchronously, on the main actor, when the observation starts, so that the initial value can be notified immediately. - With the default `.async` scheduling, the initial fetch is always performed asynchronouly. It never blocks the main thread. @@ -314,10 +312,12 @@ When needed, you can help GRDB optimize observations and reduce database content ### Accessing Observed Values - ``publisher(in:scheduling:)`` -- ``start(in:scheduling:onError:onChange:)`` -- ``values(in:priority:bufferingPolicy:)`` +- ``start(in:scheduling:onError:onChange:)-79cet`` +- ``start(in:scheduling:onError:onChange:)-5wpgl`` +- ``values(in:scheduling:bufferingPolicy:)`` - ``DatabaseCancellable`` - ``ValueObservationScheduler`` +- ``ValueObservationMainActorScheduler`` ### Mapping Values diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index 2208ae4d1b..ce30595a1f 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -87,9 +87,9 @@ extension ValueObservation { /// main dispatch queue. You can change this behavior by providing a /// scheduler. /// - /// For example, the ``ValueObservationScheduler/immediate`` scheduler - /// notifies all values on the main dispatch queue, and notifies the first - /// one immediately when the + /// For example, the ``ValueObservationMainActorScheduler/immediate`` + /// scheduler notifies all values on the main dispatch queue, and + /// notifies the first one immediately when the /// ``SharedValueObservation/start(onError:onChange:)`` method is called. /// The `immediate` scheduling requires that the observation starts from the /// main thread (a fatal error is raised otherwise): @@ -111,9 +111,6 @@ extension ValueObservation { /// // <- here "Fresh players" is already printed. /// ``` /// - /// Note that the `.immediate` scheduler requires that the observation is - /// subscribed from the main thread. It raises a fatal error otherwise. - /// /// - parameter reader: A DatabaseReader. /// - parameter scheduler: A Scheduler. By default, fresh values are /// dispatched asynchronously on the main queue. diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index fca7619939..ac6816e661 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -102,6 +102,54 @@ extension ValueObservation: Refinable { /// try Player.fetchAll(db) /// } /// + /// let cancellable = try observation.start( + /// in: dbQueue, + /// scheduling: .async(onQueue: .main)) + /// { error in + /// // handle error + /// } onChange: { (players: [Player]) in + /// print("Fresh players: \(players)") + /// } + /// ``` + /// + /// - parameter reader: A DatabaseReader. + /// - parameter scheduler: A ValueObservationScheduler. + /// - parameter onError: The closure to execute when the + /// observation fails. + /// - parameter onChange: The closure to execute on receipt of a + /// fresh value. + /// - returns: A DatabaseCancellable that can stop the observation. + public func start( + in reader: some DatabaseReader, + scheduling scheduler: some ValueObservationScheduler, + onError: @escaping @Sendable (Error) -> Void, + onChange: @escaping @Sendable (Reducer.Value) -> Void) + -> AnyDatabaseCancellable + where Reducer: ValueReducer + { + let observation = self.with { + $0.events.didFail = concat($0.events.didFail, onError) + } + observation.events.willStart?() + return reader._add( + observation: observation, + scheduling: scheduler, + onChange: onChange) + } + + /// Starts observing the database and notifies fresh values on the + /// main actor. + /// + /// The observation lasts until the returned cancellable is cancelled + /// or deallocated. + /// + /// For example: + /// + /// ```swift + /// let observation = ValueObservation.tracking { db in + /// try Player.fetchAll(db) + /// } + /// /// let cancellable = try observation.start(in: dbQueue) { error in /// // handle error /// } onChange: { (players: [Player]) in @@ -110,14 +158,8 @@ extension ValueObservation: Refinable { /// ``` /// /// By default, fresh values are dispatched asynchronously on the - /// main dispatch queue. You can change this behavior by providing a - /// scheduler. - /// - /// For example, the ``ValueObservationScheduler/immediate`` scheduler - /// notifies all values on the main dispatch queue, and notifies the first - /// one immediately when the observation starts. The `immediate` scheduling - /// requires that the observation starts from the main dispatch queue (a - /// fatal error is raised otherwise): + /// main actor. Pass `.immediate` if the first value shoud be notified + /// immediately when the observation starts: /// /// ```swift /// let cancellable = try observation.start(in: dbQueue, scheduling: .immediate) { error in @@ -129,28 +171,37 @@ extension ValueObservation: Refinable { /// ``` /// /// - parameter reader: A DatabaseReader. - /// - parameter scheduler: A ValueObservationScheduler. By default, fresh - /// values are dispatched asynchronously on the main queue. - /// - parameter onError: The closure to execute when the observation fails. + /// - parameter scheduler: A ValueObservationMainActorScheduler. + /// By default, fresh values are dispatched asynchronously on the + /// main actor. + /// - parameter onError: The closure to execute when the + /// observation fails. /// - parameter onChange: The closure to execute on receipt of a /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. - public func start( + @available(iOS 13, macOS 10.15, tvOS 13, *) + @preconcurrency @MainActor public func start( in reader: some DatabaseReader, - scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main), - onError: @escaping @Sendable (Error) -> Void, - onChange: @escaping @Sendable (Reducer.Value) -> Void) + scheduling scheduler: some ValueObservationMainActorScheduler = .mainActor, + onError: @escaping @MainActor (Error) -> Void, + onChange: @escaping @MainActor (Reducer.Value) -> Void) -> AnyDatabaseCancellable where Reducer: ValueReducer { - let observation = self.with { - $0.events.didFail = concat($0.events.didFail, onError) - } - observation.events.willStart?() - return reader._add( - observation: observation, - scheduling: scheduler, - onChange: onChange) + let regularScheduler: some ValueObservationScheduler = scheduler + return start( + in: reader, + scheduling: regularScheduler, + onError: { error in + MainActor.assumeIsolated { + onError(error) + } + }, + onChange: { value in + MainActor.assumeIsolated { + onChange(value) + } + }) } // MARK: - Debugging @@ -416,11 +467,11 @@ extension ValueObservation { /// main dispatch queue. You can change this behavior by providing a /// scheduler. /// - /// For example, the ``ValueObservationScheduler/immediate`` scheduler - /// notifies all values on the main dispatch queue, and notifies the first - /// one immediately when the observation starts. The `immediate` scheduling - /// requires that the observation starts from the main dispatch queue (a - /// fatal error is raised otherwise): + /// For example, the ``ValueObservationMainActorScheduler/immediate`` + /// scheduler notifies all values on the main dispatch queue, and + /// notifies the first one immediately when the observation starts. The + /// `immediate` scheduling requires that the observation starts from the + /// main dispatch queue (a fatal error is raised otherwise): /// /// ```swift /// let publisher = observation.publisher(in: dbQueue, scheduling: .immediate) diff --git a/GRDB/ValueObservation/ValueObservationScheduler.swift b/GRDB/ValueObservation/ValueObservationScheduler.swift index 68ab036ebb..b4080efb32 100644 --- a/GRDB/ValueObservation/ValueObservationScheduler.swift +++ b/GRDB/ValueObservation/ValueObservationScheduler.swift @@ -9,10 +9,10 @@ import Foundation /// /// - ``async(onQueue:)`` /// - ``immediate`` +/// - ``mainActor`` /// - ``task`` /// - ``task(priority:)`` /// - ``AsyncValueObservationScheduler`` -/// - ``ImmediateValueObservationScheduler`` /// - ``TaskValueObservationScheduler`` public protocol ValueObservationScheduler: Sendable { /// Returns whether the initial value should be immediately notified. @@ -33,6 +33,29 @@ extension ValueObservationScheduler { } } +// MARK: - ValueObservationMainActorScheduler + +/// A type that determines when `ValueObservation` notifies its fresh +/// values, on the main actor. +/// +/// ## Topics +/// +/// ### Built-In Schedulers +/// +/// - ``immediate`` +/// - ``ValueObservationScheduler/mainActor`` +/// - ``ImmediateValueObservationScheduler`` +/// - ``DelayedMainActorValueObservationScheduler`` +public protocol ValueObservationMainActorScheduler: ValueObservationScheduler { + func scheduleOnMainActor(_ action: @escaping @MainActor () -> Void) +} + +extension ValueObservationMainActorScheduler { + public func schedule(_ action: @escaping @Sendable () -> Void) { + scheduleOnMainActor(action) + } +} + // MARK: - AsyncValueObservationScheduler /// A scheduler that asynchronously notifies fresh value of a `DispatchQueue`. @@ -80,10 +103,10 @@ extension ValueObservationScheduler where Self == AsyncValueObservationScheduler // MARK: - ImmediateValueObservationScheduler -/// A scheduler that notifies all values on the main `DispatchQueue`. The +/// A scheduler that notifies all values on the main actor. The /// first value is immediately notified when the `ValueObservation` /// is started. -public struct ImmediateValueObservationScheduler: ValueObservationScheduler, Sendable { +public struct ImmediateValueObservationScheduler: ValueObservationMainActorScheduler { public init() { } public func immediateInitialValue() -> Bool { @@ -93,13 +116,13 @@ public struct ImmediateValueObservationScheduler: ValueObservationScheduler, Sen return true } - public func schedule(_ action: @escaping @Sendable () -> Void) { + public func scheduleOnMainActor(_ action: @escaping @MainActor () -> Void) { DispatchQueue.main.async(execute: action) } } extension ValueObservationScheduler where Self == ImmediateValueObservationScheduler { - /// A scheduler that notifies all values on the main `DispatchQueue`. The + /// A scheduler that notifies all values on the main actor. The /// first value is immediately notified when the `ValueObservation` /// is started. /// @@ -121,7 +144,36 @@ extension ValueObservationScheduler where Self == ImmediateValueObservationSched /// ``` /// /// - important: this scheduler requires that the observation is started - /// from the main queue. A fatal error is raised otherwise. + /// from the main actor. A fatal error is raised otherwise. + public static var immediate: ImmediateValueObservationScheduler { + ImmediateValueObservationScheduler() + } +} + +extension ValueObservationMainActorScheduler where Self == ImmediateValueObservationScheduler { + /// A scheduler that notifies all values on the main actor. The + /// first value is immediately notified when the `ValueObservation` + /// is started. + /// + /// For example: + /// + /// ```swift + /// let observation = ValueObservation.tracking { db in + /// try Player.fetchAll(db) + /// } + /// + /// let cancellable = try observation.start( + /// in: dbQueue, + /// scheduling: .immediate, + /// onError: { error in ... }, + /// onChange: { (players: [Player]) in + /// print("fresh players: \(players)") + /// }) + /// // <- here "fresh players" is already printed. + /// ``` + /// + /// - important: this scheduler requires that the observation is started + /// from the main actor. A fatal error is raised otherwise. public static var immediate: ImmediateValueObservationScheduler { ImmediateValueObservationScheduler() } @@ -131,7 +183,7 @@ extension ValueObservationScheduler where Self == ImmediateValueObservationSched /// A scheduler that notifies all values on the cooperative thread pool. @available(iOS 13, macOS 10.15, tvOS 13, *) -final public class TaskValueObservationScheduler: ValueObservationScheduler { +public final class TaskValueObservationScheduler: ValueObservationScheduler { typealias Action = @Sendable () -> Void let continuation: AsyncStream.Continuation let task: Task @@ -173,3 +225,25 @@ extension ValueObservationScheduler where Self == TaskValueObservationScheduler TaskValueObservationScheduler(priority: priority) } } + +// MARK: - DelayedMainActorValueObservationScheduler + +/// A scheduler that notifies all values on the cooperative thread pool. +@available(iOS 13, macOS 10.15, tvOS 13, *) +public final class DelayedMainActorValueObservationScheduler: ValueObservationMainActorScheduler { + public func immediateInitialValue() -> Bool { + false + } + + public func scheduleOnMainActor(_ action: @escaping @MainActor () -> Void) { + DispatchQueue.main.async(execute: action) + } +} + +@available(iOS 13, macOS 10.15, tvOS 13, *) +extension ValueObservationScheduler where Self == DelayedMainActorValueObservationScheduler { + /// A scheduler that notifies all values on the main actor. + public static var mainActor: DelayedMainActorValueObservationScheduler { + DelayedMainActorValueObservationScheduler() + } +} diff --git a/Tests/GRDBTests/SharedValueObservationTests.swift b/Tests/GRDBTests/SharedValueObservationTests.swift index bf8dd893f3..2b4ff72163 100644 --- a/Tests/GRDBTests/SharedValueObservationTests.swift +++ b/Tests/GRDBTests/SharedValueObservationTests.swift @@ -525,6 +525,7 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), []) } + @available(iOS 13, macOS 10.15, tvOS 13, *) func test_task_observationLifetime() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -655,6 +656,7 @@ class SharedValueObservationTests: GRDBTestCase { } #endif + @available(iOS 13, macOS 10.15, tvOS 13, *) func test_task_whileObserved() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in diff --git a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift index 390df4af0a..54d90f9f43 100644 --- a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift +++ b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift @@ -105,6 +105,7 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { } } + @available(iOS 13, macOS 10.15, tvOS 13, *) func testTupleObservation() throws { // Here we just test that user can destructure an observed tuple. // I'm completely paranoid about tuple destructuring - I can't wrap my @@ -119,6 +120,7 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { onChange: { (int: Int, string: String) in }) // <- destructure } + @available(iOS 13, macOS 10.15, tvOS 13, *) func testVaryingRegionTrackingImmediateScheduling() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index f509227fc7..20e0626c70 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -5,6 +5,7 @@ import Dispatch class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See + @available(iOS 13, macOS 10.15, tvOS 13, *) func testStartFromAnyDatabaseReader(reader: any DatabaseReader) { _ = ValueObservation .trackingConstantRegion { _ in } @@ -13,6 +14,7 @@ class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See + @available(iOS 13, macOS 10.15, tvOS 13, *) func testStartFromAnyDatabaseWriter(writer: any DatabaseWriter) { _ = ValueObservation .trackingConstantRegion { _ in } @@ -53,6 +55,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } + @available(iOS 13, macOS 10.15, tvOS 13, *) func testErrorCompletesTheObservation() throws { struct TestError: Error { } @@ -102,6 +105,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } + @available(iOS 13, macOS 10.15, tvOS 13, *) func testViewOptimization() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { @@ -139,6 +143,7 @@ class ValueObservationTests: GRDBTestCase { } } + @available(iOS 13, macOS 10.15, tvOS 13, *) func testPragmaTableOptimization() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { @@ -174,6 +179,7 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Constant Explicit Region + @available(iOS 13, macOS 10.15, tvOS 13, *) func testTrackingExplicitRegion() throws { class TestStream: TextOutputStream { private var stringsMutex: Mutex<[String]> = Mutex([]) @@ -614,6 +620,7 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Cancellation + @available(iOS 13, macOS 10.15, tvOS 13, *) func testCancellableLifetime() throws { // We need something to change let dbQueue = try makeDatabaseQueue() @@ -659,6 +666,7 @@ class ValueObservationTests: GRDBTestCase { XCTAssertEqual(changesCountMutex.load(), 2) } + @available(iOS 13, macOS 10.15, tvOS 13, *) func testCancellableExplicitCancellation() throws { // We need something to change let dbQueue = try makeDatabaseQueue() @@ -796,6 +804,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } + @available(iOS 13, macOS 10.15, tvOS 13, *) func testIssue1550() throws { func test(_ writer: some DatabaseWriter) throws { try writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } @@ -843,6 +852,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } + @available(iOS 13, macOS 10.15, tvOS 13, *) func testIssue1209() throws { func test(_ dbWriter: some DatabaseWriter) throws { try dbWriter.write { @@ -892,6 +902,41 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } + // MARK: - Main Actor + @available(iOS 13, macOS 10.15, tvOS 13, *) + @MainActor func test_mainActor_observation() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + try db.create(table: "test") { t in + t.autoIncrementedPrimaryKey("id") + } + } + + let observation = ValueObservation.tracking { + try Table("test").fetchCount($0) + } + + var value = 0 // No mutex necessary! + let expectation = self.expectation(description: "completion") + let cancellable = observation.start( + in: dbQueue, + onError: { error in XCTFail("Unexpected error: \(error)") }, + onChange: { + value = $0 + if value == 2 { + expectation.fulfill() + } + }) + + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO test DEFAULT VALUES") + try db.execute(sql: "INSERT INTO test DEFAULT VALUES") + } + withExtendedLifetime(cancellable) { _ in + wait(for: [expectation], timeout: 2) + } + } + // MARK: - Async Await @available(iOS 13, macOS 10.15, tvOS 13, *) @@ -1156,6 +1201,7 @@ class ValueObservationTests: GRDBTestCase { } // Regression test for + @available(iOS 13, macOS 10.15, tvOS 13, *) func testIssue1362() throws { func test(_ writer: some DatabaseWriter) throws { try writer.write { try $0.execute(sql: "CREATE TABLE s(id INTEGER PRIMARY KEY AUTOINCREMENT)") } @@ -1246,6 +1292,7 @@ class ValueObservationTests: GRDBTestCase { } // Regression test for + @available(iOS 13, macOS 10.15, tvOS 13, *) func testIssue1383_async() throws { do { let dbPool = try makeDatabasePool(filename: "test") From 58a2e047364de76481f93d6ffc654f0fa15195a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 11:59:08 +0200 Subject: [PATCH 111/160] Mark ValueObservation starting methods as @preconcurrency I guess the compiler will learn to stop emitting warnings eventually. https://mjtsai.com/blog/2024/09/20/unwanted-swift-concurrency-checking/ --- GRDB/Documentation.docc/Extension/ValueObservation.md | 4 ++-- GRDB/ValueObservation/SharedValueObservation.swift | 2 +- GRDB/ValueObservation/ValueObservation.swift | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/GRDB/Documentation.docc/Extension/ValueObservation.md b/GRDB/Documentation.docc/Extension/ValueObservation.md index 6abaa6b28e..f2535e93e6 100644 --- a/GRDB/Documentation.docc/Extension/ValueObservation.md +++ b/GRDB/Documentation.docc/Extension/ValueObservation.md @@ -312,8 +312,8 @@ When needed, you can help GRDB optimize observations and reduce database content ### Accessing Observed Values - ``publisher(in:scheduling:)`` -- ``start(in:scheduling:onError:onChange:)-79cet`` -- ``start(in:scheduling:onError:onChange:)-5wpgl`` +- ``start(in:scheduling:onError:onChange:)-10vwf`` +- ``start(in:scheduling:onError:onChange:)-7z197`` - ``values(in:scheduling:bufferingPolicy:)`` - ``DatabaseCancellable`` - ``ValueObservationScheduler`` diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index ce30595a1f..b3881a59cf 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -224,7 +224,7 @@ public final class SharedValueObservation: @unchecked Sendabl /// - parameter onChange: The closure to execute on receipt of a /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. - public func start( + @preconcurrency public func start( onError: @escaping @Sendable (Error) -> Void, onChange: @escaping @Sendable (Element) -> Void) -> AnyDatabaseCancellable diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index ac6816e661..3b0de8ef9e 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -119,7 +119,7 @@ extension ValueObservation: Refinable { /// - parameter onChange: The closure to execute on receipt of a /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. - public func start( + @preconcurrency public func start( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationScheduler, onError: @escaping @Sendable (Error) -> Void, @@ -787,7 +787,7 @@ extension ValueObservation { /// ``` /// /// - parameter fetch: The closure that fetches the observed value. - public static func trackingConstantRegion( + @preconcurrency public static func trackingConstantRegion( _ fetch: @escaping @Sendable (Database) throws -> Value) -> Self where Reducer == ValueReducers.Fetch @@ -859,7 +859,7 @@ extension ValueObservation { /// - parameter otherRegions: A list of supplementary regions /// to observe. /// - parameter fetch: The closure that fetches the observed value. - public static func tracking( + @preconcurrency public static func tracking( region: any DatabaseRegionConvertible, _ otherRegions: any DatabaseRegionConvertible..., fetch: @escaping @Sendable (Database) throws -> Value) @@ -929,7 +929,7 @@ extension ValueObservation { /// /// - parameter regions: An array of observed regions. /// - parameter fetch: The closure that fetches the observed value. - public static func tracking( + @preconcurrency public static func tracking( regions: [any DatabaseRegionConvertible], fetch: @escaping @Sendable (Database) throws -> Value) -> Self @@ -987,7 +987,7 @@ extension ValueObservation { /// ``` /// /// - parameter fetch: The closure that fetches the observed value. - public static func tracking( + @preconcurrency public static func tracking( _ fetch: @escaping @Sendable (Database) throws -> Value) -> Self where Reducer == ValueReducers.Fetch From 16496366950760b829a31ec7669ad122e5202002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 12:00:53 +0200 Subject: [PATCH 112/160] TODO --- TODO.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index de09c24f3e..09cb7ee9fc 100644 --- a/TODO.md +++ b/TODO.md @@ -88,7 +88,7 @@ - [ ] What can we do with `cross-module-optimization`? See https://github.com/apple/swift-homomorphic-encryption - [X] GRDB7/BREAKING: insertAndFetch, saveAndFetch, and updateAndFetch no longer return optionals (32f41472) -- [ ] GRDB7/BREAKING: AsyncValueObservation does not need any scheduler (83c0e643) +- [X] GRDB7/BREAKING: AsyncValueObservation does not need any scheduler (83c0e643) - [X] GRDB7/BREAKING: Stop exporting SQLite (679d6463) - [X] GRDB7/BREAKING: Remove Configuration.defaultTransactionKind (2661ff46) - [X] GRDB7: Replace LockedBox with Mutex (00ccab06) @@ -158,7 +158,8 @@ - [X] GRDB7: Sendable: DatabaseRegionObservation (b4ff52fb) - [-] GRDB7: DispatchQueue.asyncSending (7b075e6b) - [X] GRDB7: Replace sequences with collection (e.g. https://github.com/tidal-music/tidal-sdk-ios/pull/39) -- [ ] GRDB7: Replace `some` DatabaseReader/Writer with `any` where possible, in order to avoid issues with accessing DatabaseContext from GRDBQuery (if the problem exists in Xcode 16) +- [ ] GRDB7: Replace `some` DatabaseReader/Writer with `any` where possible, in order to avoid issues with accessing DatabaseContext from GRDBQuery (if the problem exists in Xcode 16) +- [ ] GRDB7: bump to iOS 13, macOS 10.15, tvOS 13 (for ValueObservation support for MainActor) - [?] GRDB7: Change ValueObservation callback argument so that it could expose snapshots? https://github.com/groue/GRDB.swift/discussions/1523#discussioncomment-9092500 From 8c40592955a1bb0cc9cd24fcff2011bc2fc58457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 13:55:55 +0200 Subject: [PATCH 113/160] [BREAKING] iOS 13+ --- GRDB.swift.podspec | 2 +- GRDB/Core/Database+Statements.swift | 2 +- GRDB/Core/Database.swift | 6 ++-- GRDB/Core/DatabasePool.swift | 8 ++--- GRDB/Core/DatabasePublishers.swift | 2 +- GRDB/Core/DatabaseQueue.swift | 8 ++--- GRDB/Core/DatabaseReader.swift | 14 ++++---- GRDB/Core/DatabaseRegionObservation.swift | 6 ++-- GRDB/Core/DatabaseSnapshot.swift | 4 +-- GRDB/Core/DatabaseSnapshotPool.swift | 4 +-- GRDB/Core/DatabaseWriter.swift | 30 ++++++++-------- GRDB/Core/SerializedDatabase.swift | 8 ++--- GRDB/Dump/DumpFormats/JSONDumpFormat.swift | 2 +- GRDB/Fixits.swift | 6 ++-- GRDB/Migration/DatabaseMigrator.swift | 4 +-- .../Request/QueryInterfaceRequest.swift | 2 +- .../Request/RequestProtocols.swift | 2 +- GRDB/QueryInterface/SQL/Table.swift | 6 ++-- .../Schema/TableAlteration.swift | 2 +- .../TableRecord+QueryInterfaceRequest.swift | 2 +- GRDB/Record/FetchableRecord+TableRecord.swift | 4 +-- GRDB/Record/TableRecord.swift | 6 ++-- GRDB/Utils/OnDemandFuture.swift | 4 +-- GRDB/Utils/ReceiveValuesOn.swift | 6 ++-- .../SharedValueObservation.swift | 4 +-- GRDB/ValueObservation/ValueObservation.swift | 10 +++--- .../ValueObservationScheduler.swift | 8 ++--- Package.swift | 2 +- README.md | 2 +- SQLiteCustom/GRDBDeploymentTarget.xcconfig | 2 +- Support/GRDBDeploymentTarget.xcconfig | 2 +- Tests/CocoaPods/GRDBiOS-framework/Podfile | 2 +- .../iOS.xcodeproj/project.pbxproj | 8 ++--- Tests/CocoaPods/GRDBiOS-static/Podfile | 2 +- .../iOS.xcodeproj/project.pbxproj | 4 +-- .../AvailableElements.swift | 2 +- .../PublisherExpectations/Finished.swift | 2 +- .../PublisherExpectations/Inverted.swift | 2 +- .../PublisherExpectations/Map.swift | 4 +-- .../PublisherExpectations/Next.swift | 2 +- .../PublisherExpectations/NextOne.swift | 2 +- .../PublisherExpectations/Prefix.swift | 2 +- .../PublisherExpectations/Recording.swift | 2 +- Tests/CombineExpectations/Recorder.swift | 8 ++--- .../DatabaseReaderReadPublisherTests.swift | 12 +++---- ...abaseRegionObservationPublisherTests.swift | 4 +-- .../DatabaseWriterWritePublisherTests.swift | 26 +++++++------- Tests/GRDBCombineTests/Support.swift | 6 ++-- .../ValueObservationPublisherTests.swift | 24 ++++++------- Tests/GRDBTests/AsyncSemaphore.swift | 2 +- .../DatabaseDataEncodingStrategyTests.swift | 8 ++--- .../DatabaseDateEncodingStrategyTests.swift | 8 ++--- Tests/GRDBTests/DatabaseDumpTests.swift | 2 +- Tests/GRDBTests/DatabaseMigratorTests.swift | 12 +++---- Tests/GRDBTests/DatabaseReaderTests.swift | 22 ++++++------ .../DatabaseRegionObservationTests.swift | 2 +- .../GRDBTests/DatabaseSnapshotPoolTests.swift | 2 +- .../DatabaseUUIDEncodingStrategyTests.swift | 8 ++--- Tests/GRDBTests/DatabaseWriterTests.swift | 36 +++++++++---------- Tests/GRDBTests/JoinSupportTests.swift | 6 ++-- ...imalNonOptionalPrimaryKeySingleTests.swift | 20 +++++------ .../RecordMinimalPrimaryKeyRowIDTests.swift | 20 +++++------ .../RecordMinimalPrimaryKeySingleTests.swift | 20 +++++------ .../RecordPrimaryKeyHiddenRowIDTests.swift | 20 +++++------ .../SharedValueObservationTests.swift | 18 +++++----- Tests/GRDBTests/TableDefinitionTests.swift | 4 +-- ...bleRecord+QueryInterfaceRequestTests.swift | 2 +- Tests/GRDBTests/TableRecordDeleteTests.swift | 18 +++++----- Tests/GRDBTests/TableRecordUpdateTests.swift | 4 +-- Tests/GRDBTests/TableTests.swift | 12 +++---- ...ValueObservationRegionRecordingTests.swift | 4 +-- Tests/GRDBTests/ValueObservationTests.swift | 34 +++++++++--------- 72 files changed, 284 insertions(+), 284 deletions(-) diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index 80cf93ccb8..48182864df 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |s| s.module_name = 'GRDB' s.swift_versions = ['5.10'] - s.ios.deployment_target = '12.0' + s.ios.deployment_target = '13.0' s.osx.deployment_target = '10.13' s.watchos.deployment_target = '7.0' s.tvos.deployment_target = '12.0' diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index 9c5119e3b1..f252a6bff4 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -492,7 +492,7 @@ extension Database { // and throws the user-provided cancelled commit error. try observationBroker?.statementDidFail(statement) - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { switch ResultCode(rawValue: resultCode) { case .SQLITE_INTERRUPT, .SQLITE_ABORT: if suspensionMutex.load().isCancelled { diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index e5fd1870a5..1d19d0302c 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1199,7 +1199,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// will throw `CancellationError`, until `uncancel()` is called. /// /// This method can be called from any thread. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func cancel() { let needsInterrupt = suspensionMutex.withLock { suspension in if suspension.isCancelled { @@ -1216,7 +1216,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib } /// Undo `cancel()`. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func uncancel() { suspensionMutex.withLock { $0.isCancelled = false @@ -1320,7 +1320,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib break case .cancel: - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { throw CancellationError() } else { // GRDB bug: cancellation is a Swift concurrency feature diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index cddd0f4674..de5c5f6297 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -351,7 +351,7 @@ extension DatabasePool: DatabaseReader { } } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -436,7 +436,7 @@ extension DatabasePool: DatabaseReader { } } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -803,7 +803,7 @@ extension DatabasePool: DatabaseWriter { try writer.sync(updates) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -820,7 +820,7 @@ extension DatabasePool: DatabaseWriter { } } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabasePublishers.swift b/GRDB/Core/DatabasePublishers.swift index 88c0c48067..a21777814b 100644 --- a/GRDB/Core/DatabasePublishers.swift +++ b/GRDB/Core/DatabasePublishers.swift @@ -1,5 +1,5 @@ #if canImport(Combine) /// A namespace for database Combine publishers. -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) public enum DatabasePublishers { } #endif diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index 57e6f88f8e..90f7c6fc10 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -233,7 +233,7 @@ extension DatabaseQueue: DatabaseReader { } } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -272,7 +272,7 @@ extension DatabaseQueue: DatabaseReader { try writer.sync(value) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -386,7 +386,7 @@ extension DatabaseQueue: DatabaseWriter { try writer.sync(updates) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -398,7 +398,7 @@ extension DatabaseQueue: DatabaseWriter { try writer.sync(updates) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index dbb13bb0f4..d36fb3fd72 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -216,7 +216,7 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `value`, or /// `CancellationError` if the task is cancelled. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -323,7 +323,7 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `value`, or /// `CancellationError` if the task is cancelled. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -535,7 +535,7 @@ extension DatabaseReader { /// /// - parameter scheduler: A Combine Scheduler. /// - parameter value: A closure which accesses the database. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func readPublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, value: @escaping @Sendable (Database) throws -> Output @@ -550,7 +550,7 @@ extension DatabaseReader { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension DatabasePublishers { /// A publisher that reads from the database. /// @@ -569,7 +569,7 @@ extension DatabasePublishers { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Publisher where Failure == Error { fileprivate func eraseToReadPublisher() -> DatabasePublishers.Read { .init(upstream: eraseToAnyPublisher()) @@ -660,7 +660,7 @@ extension AnyDatabaseReader: DatabaseReader { try base.read(value) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -678,7 +678,7 @@ extension AnyDatabaseReader: DatabaseReader { try base.unsafeRead(value) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseRegionObservation.swift b/GRDB/Core/DatabaseRegionObservation.swift index dc4d00f8f6..15d9ba92c6 100644 --- a/GRDB/Core/DatabaseRegionObservation.swift +++ b/GRDB/Core/DatabaseRegionObservation.swift @@ -128,7 +128,7 @@ extension DatabaseRegionObservation { } #if canImport(Combine) -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension DatabaseRegionObservation { // MARK: - Publishing Impactful Transactions @@ -140,7 +140,7 @@ extension DatabaseRegionObservation { /// /// Do not reschedule the publisher with `receive(on:options:)` or any /// `Publisher` method that schedules publisher elements. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func publisher(in writer: some DatabaseWriter) -> DatabasePublishers.DatabaseRegion { DatabasePublishers.DatabaseRegion(self, in: writer) } @@ -186,7 +186,7 @@ private class DatabaseRegionObserver: TransactionObserver { } #if canImport(Combine) -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension DatabasePublishers { /// A publisher that tracks transactions that modify a database region. /// diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 9752a90f14..9b8e9236d6 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -151,7 +151,7 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { try reader.sync(block) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -173,7 +173,7 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { // We can't provide this as a default implementation in // `DatabaseSnapshotReader`, because of // . - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index fa88f15343..fb66e79107 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -293,7 +293,7 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { } } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -353,7 +353,7 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { // We can't provide this as a default implementation in // `DatabaseSnapshotReader`, because of // . - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index 44fd4ad98e..f717f0c56a 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -131,7 +131,7 @@ public protocol DatabaseWriter: DatabaseReader { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -217,7 +217,7 @@ public protocol DatabaseWriter: DatabaseReader { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -649,7 +649,7 @@ extension DatabaseWriter { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func write( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -666,7 +666,7 @@ extension DatabaseWriter { /// Erase the database: delete all content, drop all tables, etc. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func erase() async throws { try await writeWithoutTransaction { try $0.erase() } } @@ -677,7 +677,7 @@ extension DatabaseWriter { /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// Related SQLite documentation: - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func vacuum() async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM") } } @@ -694,7 +694,7 @@ extension DatabaseWriter { /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -709,7 +709,7 @@ extension DatabaseWriter { /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -754,7 +754,7 @@ extension DatabaseWriter { /// /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which accesses the database. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, updates: @escaping @Sendable (Database) throws -> Output @@ -818,7 +818,7 @@ extension DatabaseWriter { /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which writes in the database. /// - parameter value: A closure which reads from the database. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, updates: @escaping @Sendable (Database) throws -> T, @@ -848,7 +848,7 @@ extension DatabaseWriter { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension DatabasePublishers { /// A publisher that writes into the database. /// @@ -867,7 +867,7 @@ extension DatabasePublishers { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Publisher where Failure == Error { fileprivate func eraseToWritePublisher() -> DatabasePublishers.Write { .init(upstream: self.eraseToAnyPublisher()) @@ -911,7 +911,7 @@ extension AnyDatabaseWriter: DatabaseReader { try base.read(value) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -929,7 +929,7 @@ extension AnyDatabaseWriter: DatabaseReader { try base.unsafeRead(value) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -964,7 +964,7 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.writeWithoutTransaction(updates) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -976,7 +976,7 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.barrierWriteWithoutTransaction(updates) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index bcd5c953ac..6a64e40121 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -244,7 +244,7 @@ final class SerializedDatabase { } /// Asynchrously executes the block. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func execute( _ block: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -309,7 +309,7 @@ extension SerializedDatabase: @unchecked Sendable { } // MARK: - Task Cancellation Support -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) enum DatabaseAccessCancellationState: @unchecked Sendable { // @unchecked Sendable because database is only accessed from its // dispatch queue. @@ -319,7 +319,7 @@ enum DatabaseAccessCancellationState: @unchecked Sendable { case expired } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) typealias CancellableDatabaseAccess = Mutex /// Supports Task cancellation in async database accesses. @@ -341,7 +341,7 @@ typealias CancellableDatabaseAccess = Mutex /// } /// } /// ``` -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension CancellableDatabaseAccess: DatabaseCancellable { convenience init() { self.init(.notConnected) diff --git a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift index 027cca3791..a3a036e5ba 100644 --- a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift @@ -52,7 +52,7 @@ public struct JSONDumpFormat: Sendable { public static var defaultEncoder: JSONEncoder { // This encoder MUST NOT CHANGE, because some people rely on this format. let encoder = JSONEncoder() - if #available(iOS 13.0, macOS 10.15, tvOS 13.0, *) { + if #available(macOS 10.15, tvOS 13.0, *) { encoder.outputFormatting = .withoutEscapingSlashes } encoder.nonConformingFloatEncodingStrategy = .convertToString( diff --git a/GRDB/Fixits.swift b/GRDB/Fixits.swift index 516c650bb4..42b768ce86 100644 --- a/GRDB/Fixits.swift +++ b/GRDB/Fixits.swift @@ -119,7 +119,7 @@ extension PersistableRecord { public func performSave(_ db: Database) throws { preconditionFailure() } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension QueryInterfaceRequest where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public func selectID() -> QueryInterfaceRequest { preconditionFailure() } @@ -144,13 +144,13 @@ extension SelectionRequest { @available(*, unavailable, renamed: "SQLExpression.AssociativeBinaryOperator") public typealias SQLAssociativeBinaryOperator = SQLExpression.AssociativeBinaryOperator -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public func selectID() -> QueryInterfaceRequest { preconditionFailure() } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public static func selectID() -> QueryInterfaceRequest { preconditionFailure() } diff --git a/GRDB/Migration/DatabaseMigrator.swift b/GRDB/Migration/DatabaseMigrator.swift index b67336b799..1e8b6ce2f9 100644 --- a/GRDB/Migration/DatabaseMigrator.swift +++ b/GRDB/Migration/DatabaseMigrator.swift @@ -496,7 +496,7 @@ extension DatabaseMigrator { /// - parameter writer: A DatabaseWriter. /// where migrations should apply. /// - parameter scheduler: A Combine Scheduler. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func migratePublisher( _ writer: some DatabaseWriter, receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main) @@ -514,7 +514,7 @@ extension DatabaseMigrator { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension DatabasePublishers { /// A publisher that migrates a database. /// diff --git a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift index e4dccaaa63..8f747ff3be 100644 --- a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift @@ -646,7 +646,7 @@ extension QueryInterfaceRequest { /// - parameter db: A database connection. /// - returns: A set of deleted ids. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - @available(iOS 13, macOS 10.15, tvOS 13, *) // Identifiable + @available(macOS 10.15, tvOS 13, *) // Identifiable public func deleteAndFetchIds(_ db: Database) throws -> Set where RowDecoder: TableRecord & Identifiable, diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index d4c57fc6e5..9a51e8701c 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -631,7 +631,7 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension TableRequest where Self: FilteredRequest, Self: TypedRequest, diff --git a/GRDB/QueryInterface/SQL/Table.swift b/GRDB/QueryInterface/SQL/Table.swift index 8e2a74f705..3e7dd8180a 100644 --- a/GRDB/QueryInterface/SQL/Table.swift +++ b/GRDB/QueryInterface/SQL/Table.swift @@ -722,7 +722,7 @@ extension Table { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { /// Returns a request filtered by primary key. /// @@ -1545,7 +1545,7 @@ extension Table { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible @@ -1686,7 +1686,7 @@ extension Table { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible diff --git a/GRDB/QueryInterface/Schema/TableAlteration.swift b/GRDB/QueryInterface/Schema/TableAlteration.swift index a61e6ba8d9..f10919b231 100644 --- a/GRDB/QueryInterface/Schema/TableAlteration.swift +++ b/GRDB/QueryInterface/Schema/TableAlteration.swift @@ -129,7 +129,7 @@ public final class TableAlteration { /// /// - parameter name: the old name of the column. /// - parameter newName: the new name of the column. - @available(iOS 13, tvOS 13, *) // SQLite 3.25+ + @available(tvOS 13, *) // SQLite 3.25+ public func rename(column name: String, to newName: String) { _rename(column: name, to: newName) } diff --git a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift index 1b1ed8c8e6..ce98f1772a 100644 --- a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift @@ -604,7 +604,7 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns a request filtered by primary key. /// diff --git a/GRDB/Record/FetchableRecord+TableRecord.swift b/GRDB/Record/FetchableRecord+TableRecord.swift index 736ca21e4e..ed006713ee 100644 --- a/GRDB/Record/FetchableRecord+TableRecord.swift +++ b/GRDB/Record/FetchableRecord+TableRecord.swift @@ -216,7 +216,7 @@ extension FetchableRecord where Self: TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseValueConvertible { // MARK: Fetching by Single-Column Primary Key @@ -358,7 +358,7 @@ extension FetchableRecord where Self: TableRecord & Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension FetchableRecord where Self: TableRecord & Hashable & Identifiable, ID: DatabaseValueConvertible { /// Returns a set of records identified by their primary keys. /// diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index 46e2ad3255..cf53d3462f 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -319,7 +319,7 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns whether a record exists for this primary key. /// @@ -454,7 +454,7 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Deletes records identified by their primary keys, and returns the number /// of deleted records. @@ -774,7 +774,7 @@ extension TableRecord where Self: EncodableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns an error for a record that does not exist in the database. /// diff --git a/GRDB/Utils/OnDemandFuture.swift b/GRDB/Utils/OnDemandFuture.swift index c18dc54654..03b2ad08c6 100644 --- a/GRDB/Utils/OnDemandFuture.swift +++ b/GRDB/Utils/OnDemandFuture.swift @@ -17,7 +17,7 @@ import Foundation /// /// OnDemandFuture also adds Sendable requirements that avoid /// compiler warnings. -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) struct OnDemandFuture: Publisher { typealias Promise = @Sendable (Result) -> Void typealias Output = Output @@ -36,7 +36,7 @@ struct OnDemandFuture: Publisher { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) private class OnDemandFutureSubscription: Subscription, @unchecked Sendable { // @unchecked because `state` is protected with `lock`. typealias Promise = @Sendable (Result) -> Void diff --git a/GRDB/Utils/ReceiveValuesOn.swift b/GRDB/Utils/ReceiveValuesOn.swift index 7cbbde7c12..95fb42fe0a 100644 --- a/GRDB/Utils/ReceiveValuesOn.swift +++ b/GRDB/Utils/ReceiveValuesOn.swift @@ -11,7 +11,7 @@ import Foundation /// This scheduling guarantee is used by GRDB in order to be able /// to make promises on the scheduling of database values without surprising /// the users as in . -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) struct ReceiveValuesOn: Publisher { typealias Output = Upstream.Output typealias Failure = Upstream.Failure @@ -30,7 +30,7 @@ struct ReceiveValuesOn: Publisher { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) private class ReceiveValuesOnSubscription: Subscription, Subscriber where Upstream: Publisher, @@ -211,7 +211,7 @@ where } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Publisher { /// Specifies the scheduler on which to receive values from the publisher /// diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index b3881a59cf..6f61750094 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -292,7 +292,7 @@ public final class SharedValueObservation: @unchecked Sendabl /// print("fresh players: \(players)") /// } /// ``` - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func publisher() -> DatabasePublishers.Value { DatabasePublishers.Value { onError, onChange in self.start(onError: onError, onChange: onChange) @@ -369,7 +369,7 @@ extension SharedValueObservation { /// print("Fresh players: \(players)") /// } /// ``` - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func values(bufferingPolicy: AsyncValueObservation.BufferingPolicy = .unbounded) -> AsyncValueObservation { diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index 3b0de8ef9e..b4236838b6 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -179,7 +179,7 @@ extension ValueObservation: Refinable { /// - parameter onChange: The closure to execute on receipt of a /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) @preconcurrency @MainActor public func start( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationMainActorScheduler = .mainActor, @@ -350,7 +350,7 @@ extension ValueObservation { /// fresh values are dispatched on the cooperative thread pool. /// - parameter bufferingPolicy: see the documntation /// of `AsyncThrowingStream`. - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func values( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .task, @@ -386,7 +386,7 @@ extension ValueObservation { /// /// You build an `AsyncValueObservation` from ``ValueObservation`` or /// ``SharedValueObservation``. -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) public struct AsyncValueObservation: AsyncSequence { public typealias BufferingPolicy = AsyncThrowingStream.Continuation.BufferingPolicy public typealias AsyncIterator = Iterator @@ -488,7 +488,7 @@ extension ValueObservation { /// - parameter scheduler: A ValueObservationScheduler. By default, fresh /// values are dispatched asynchronously on the main dispatch queue. /// - returns: A Combine publisher - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) public func publisher( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main)) @@ -505,7 +505,7 @@ extension ValueObservation { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension DatabasePublishers { /// A publisher that publishes the values of a ``ValueObservation``. /// diff --git a/GRDB/ValueObservation/ValueObservationScheduler.swift b/GRDB/ValueObservation/ValueObservationScheduler.swift index b4080efb32..5188d3452c 100644 --- a/GRDB/ValueObservation/ValueObservationScheduler.swift +++ b/GRDB/ValueObservation/ValueObservationScheduler.swift @@ -182,7 +182,7 @@ extension ValueObservationMainActorScheduler where Self == ImmediateValueObserva // MARK: - TaskValueObservationScheduler /// A scheduler that notifies all values on the cooperative thread pool. -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) public final class TaskValueObservationScheduler: ValueObservationScheduler { typealias Action = @Sendable () -> Void let continuation: AsyncStream.Continuation @@ -212,7 +212,7 @@ public final class TaskValueObservationScheduler: ValueObservationScheduler { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension ValueObservationScheduler where Self == TaskValueObservationScheduler { /// A scheduler that notifies all values from a new `Task`. public static var task: TaskValueObservationScheduler { @@ -229,7 +229,7 @@ extension ValueObservationScheduler where Self == TaskValueObservationScheduler // MARK: - DelayedMainActorValueObservationScheduler /// A scheduler that notifies all values on the cooperative thread pool. -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) public final class DelayedMainActorValueObservationScheduler: ValueObservationMainActorScheduler { public func immediateInitialValue() -> Bool { false @@ -240,7 +240,7 @@ public final class DelayedMainActorValueObservationScheduler: ValueObservationMa } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension ValueObservationScheduler where Self == DelayedMainActorValueObservationScheduler { /// A scheduler that notifies all values on the main actor. public static var mainActor: DelayedMainActorValueObservationScheduler { diff --git a/Package.swift b/Package.swift index fa9e19b323..af1aace2fc 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let package = Package( name: "GRDB", defaultLocalization: "en", // for tests platforms: [ - .iOS(.v12), + .iOS(.v13), .macOS(.v10_13), .tvOS(.v12), .watchOS(.v7), diff --git a/README.md b/README.md index cdc95bbee0..be3bff834f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ **Latest release**: September 7, 2024 • [version 6.29.3](https://github.com/groue/GRDB.swift/tree/v6.29.3) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md) -**Requirements**: iOS 12.0+ / macOS 10.13+ / tvOS 12.0+ / watchOS 7.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ +**Requirements**: iOS 13.0+ / macOS 10.13+ / tvOS 12.0+ / watchOS 7.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ **Contact**: diff --git a/SQLiteCustom/GRDBDeploymentTarget.xcconfig b/SQLiteCustom/GRDBDeploymentTarget.xcconfig index c4f1fe1970..8f02cc8937 100644 --- a/SQLiteCustom/GRDBDeploymentTarget.xcconfig +++ b/SQLiteCustom/GRDBDeploymentTarget.xcconfig @@ -1,4 +1,4 @@ -IPHONEOS_DEPLOYMENT_TARGET = 12.0 +IPHONEOS_DEPLOYMENT_TARGET = 13.0 MACOSX_DEPLOYMENT_TARGET = 10.13 TVOS_DEPLOYMENT_TARGET = 12.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 diff --git a/Support/GRDBDeploymentTarget.xcconfig b/Support/GRDBDeploymentTarget.xcconfig index f08af6102e..d20702f0c6 100644 --- a/Support/GRDBDeploymentTarget.xcconfig +++ b/Support/GRDBDeploymentTarget.xcconfig @@ -1,4 +1,4 @@ -IPHONEOS_DEPLOYMENT_TARGET = 12.0 +IPHONEOS_DEPLOYMENT_TARGET = 13.0 MACOSX_DEPLOYMENT_TARGET = 10.13 TVOS_DEPLOYMENT_TARGET = 12.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 diff --git a/Tests/CocoaPods/GRDBiOS-framework/Podfile b/Tests/CocoaPods/GRDBiOS-framework/Podfile index 2c4299502a..82bf451d20 100644 --- a/Tests/CocoaPods/GRDBiOS-framework/Podfile +++ b/Tests/CocoaPods/GRDBiOS-framework/Podfile @@ -1,6 +1,6 @@ use_frameworks! target 'iOS' -platform :ios, '12.0' +platform :ios, '13.0' pod 'GRDB.swift', :path => '../../..' post_install do |installer| diff --git a/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj b/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj index 7a961bd71c..b04521c007 100644 --- a/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj @@ -251,7 +251,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -301,7 +301,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; @@ -318,7 +318,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; INFOPLIST_FILE = Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBTest; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -335,7 +335,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; INFOPLIST_FILE = Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBTest; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Tests/CocoaPods/GRDBiOS-static/Podfile b/Tests/CocoaPods/GRDBiOS-static/Podfile index 3746f208fc..f4137605f3 100644 --- a/Tests/CocoaPods/GRDBiOS-static/Podfile +++ b/Tests/CocoaPods/GRDBiOS-static/Podfile @@ -1,5 +1,5 @@ target 'iOS' -platform :ios, '12.0' +platform :ios, '13.0' pod 'GRDB.swift', :path => '../../..' post_install do |installer| diff --git a/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj b/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj index 3ee741cb0d..acfaddb1ce 100644 --- a/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj @@ -232,7 +232,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -282,7 +282,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift index 5b78e39ed4..51198282b5 100644 --- a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift +++ b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the timeout to expire, or /// the recorded publisher to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Finished.swift b/Tests/CombineExpectations/PublisherExpectations/Finished.swift index 5f866e7c9e..c9d1c8a6be 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Finished.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Finished.swift @@ -17,7 +17,7 @@ import XCTest // try wait(for: recorder.finished.inverted, timeout: 1) // } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Inverted.swift b/Tests/CombineExpectations/PublisherExpectations/Inverted.swift index 6900f834ed..ceb59955ea 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Inverted.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Inverted.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation that fails if the base expectation is fulfilled. /// diff --git a/Tests/CombineExpectations/PublisherExpectations/Map.swift b/Tests/CombineExpectations/PublisherExpectations/Map.swift index 0c6a22c4cf..e15e6607ec 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Map.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Map.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation that transforms the value of a base expectation. /// @@ -20,7 +20,7 @@ extension PublisherExpectations { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension PublisherExpectation { /// Returns a publisher expectation that transforms the value of the /// base expectation. diff --git a/Tests/CombineExpectations/PublisherExpectations/Next.swift b/Tests/CombineExpectations/PublisherExpectations/Next.swift index 70e73836fe..3fd8010dd6 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Next.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Next.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `count` elements, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/NextOne.swift b/Tests/CombineExpectations/PublisherExpectations/NextOne.swift index 6bc8ff10ed..492c2ba3b8 100644 --- a/Tests/CombineExpectations/PublisherExpectations/NextOne.swift +++ b/Tests/CombineExpectations/PublisherExpectations/NextOne.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// one element, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Prefix.swift b/Tests/CombineExpectations/PublisherExpectations/Prefix.swift index f3fcd20538..3f055e1286 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Prefix.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Prefix.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `maxLength` elements, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Recording.swift b/Tests/CombineExpectations/PublisherExpectations/Recording.swift index 2327188f69..0b88ae2731 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Recording.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Recording.swift @@ -2,7 +2,7 @@ import Combine import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. diff --git a/Tests/CombineExpectations/Recorder.swift b/Tests/CombineExpectations/Recorder.swift index 3d5be6f7b7..569066dbb2 100644 --- a/Tests/CombineExpectations/Recorder.swift +++ b/Tests/CombineExpectations/Recorder.swift @@ -13,7 +13,7 @@ import XCTest /// /// let elements = try wait(for: recorder.elements, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) public class Recorder: Subscriber { public typealias Input = Input public typealias Failure = Failure @@ -287,7 +287,7 @@ public class Recorder: Subscriber { // MARK: - Publisher Expectations -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension PublisherExpectations { /// The type of the publisher expectation returned by `Recorder.completion`. public typealias Completion = Map, Subscribers.Completion> @@ -302,7 +302,7 @@ extension PublisherExpectations { public typealias Single = Map, Input> } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Recorder { /// Returns a publisher expectation which waits for the timeout to expire, /// or the recorded publisher to complete. @@ -584,7 +584,7 @@ extension Recorder { // MARK: - Publisher + Recorder -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Publisher { /// Returns a subscribed Recorder. /// diff --git a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift index 56cb6d1e15..b45baff0e8 100644 --- a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift @@ -22,7 +22,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -128,7 +128,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // frame #71: 0x00007fff72311cc9 libdyld.dylib`start + 1 // frame #72: 0x00007fff72311cc9 libdyld.dylib`start + 1 func testReadPublisherError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -157,7 +157,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -197,7 +197,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherDefaultScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -237,7 +237,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherCustomScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -278,7 +278,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherIsReadonly() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift index 8c5e870f1c..1a91d0c933 100644 --- a/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift @@ -20,7 +20,7 @@ private struct Player: Codable, FetchableRecord, PersistableRecord { class DatabaseRegionObservationPublisherTests : XCTestCase { func testChangesNotifications() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -61,7 +61,7 @@ class DatabaseRegionObservationPublisherTests : XCTestCase { // TODO: do the same, but asynchronously. If this is too hard, update the // public API so that users can easily do it. func testPrependInitialDatabaseSync() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift b/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift index cc3903dabf..585359af26 100644 --- a/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift @@ -22,7 +22,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -49,7 +49,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherValue() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -76,7 +76,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -99,7 +99,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { } func testWritePublisherErrorRollbacksTransaction() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -132,7 +132,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -168,7 +168,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherDefaultScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -206,7 +206,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherCustomScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -247,7 +247,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // TODO: Fix flaky test with both pool and on-disk queue: // - Expectation timeout func testWriteThenReadPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -274,7 +274,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWriteThenReadPublisherIsReadonly() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -299,7 +299,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWriteThenReadPublisherWriteError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -322,7 +322,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { } func testWriteThenReadPublisherWriteErrorRollbacksTransaction() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -359,7 +359,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // TODO: Fix flaky test with both pool and on-disk queue: // - Expectation timeout func testWriteThenReadPublisherReadError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -386,7 +386,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // Regression test against deadlocks created by concurrent completion // and cancellations triggered by .switchToLatest().prefix(1) func testDeadlockPrevention() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBCombineTests/Support.swift b/Tests/GRDBCombineTests/Support.swift index 0805d0bc07..3759c94fa4 100644 --- a/Tests/GRDBCombineTests/Support.swift +++ b/Tests/GRDBCombineTests/Support.swift @@ -51,7 +51,7 @@ final class Test { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) final class AsyncTest { // Raise the repeatCount in order to help spotting flaky tests. private let repeatCount: Int @@ -100,7 +100,7 @@ final class AsyncTest { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) public func assertNoFailure( _ completion: Subscribers.Completion, file: StaticString = #file, @@ -111,7 +111,7 @@ public func assertNoFailure( } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) public func assertFailure( _ completion: Subscribers.Completion, file: StaticString = #file, diff --git a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift index fff31440fa..86c98fa669 100644 --- a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift @@ -22,7 +22,7 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Default Scheduler func testDefaultSchedulerChangesNotifications() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -64,7 +64,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDefaultSchedulerFirstValueIsEmittedAsynchronously() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -97,7 +97,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDefaultSchedulerError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -123,7 +123,7 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Immediate Scheduler func testImmediateSchedulerChangesNotifications() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -165,7 +165,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testImmediateSchedulerEmitsFirstValueSynchronously() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -201,7 +201,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testImmediateSchedulerError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -226,7 +226,7 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Demand - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) private class DemandSubscriber: Subscriber { private var subscription: Subscription? let subject = PassthroughSubject() @@ -257,7 +257,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandNoneReceivesNoElement() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -292,7 +292,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandOneReceivesOneElement() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -330,7 +330,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandOneDoesNotReceiveTwoElements() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -372,7 +372,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandTwoReceivesTwoElements() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -418,7 +418,7 @@ class ValueObservationPublisherTests : XCTestCase { /// Regression test for https://github.com/groue/GRDB.swift/issues/1194 func testIssue1194() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBTests/AsyncSemaphore.swift b/Tests/GRDBTests/AsyncSemaphore.swift index 6e8d474b30..3ac88d4959 100644 --- a/Tests/GRDBTests/AsyncSemaphore.swift +++ b/Tests/GRDBTests/AsyncSemaphore.swift @@ -43,7 +43,7 @@ import Foundation /// /// - ``wait()`` /// - ``waitUnlessCancelled()`` -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) public final class AsyncSemaphore: @unchecked Sendable { /// `Suspension` is the state of a task waiting for a signal. /// diff --git a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift index 567affede7..20061cebed 100644 --- a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift @@ -25,7 +25,7 @@ private struct RecordWithData: EncodableRecord, Enco var data: Data } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension RecordWithData: Identifiable { var id: Data { data } } @@ -37,7 +37,7 @@ private struct RecordWithOptionalData: EncodableReco var data: Data? } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension RecordWithOptionalData: Identifiable { var id: Data? { data } } @@ -154,7 +154,7 @@ extension DatabaseDataEncodingStrategyTests { } func testFilterID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } @@ -234,7 +234,7 @@ extension DatabaseDataEncodingStrategyTests { } func testDeleteID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } diff --git a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift index 4d32142727..a071a7ddbf 100644 --- a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift @@ -52,7 +52,7 @@ private struct RecordWithDate: EncodableRecord, Enco var date: Date } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension RecordWithDate: Identifiable { var id: Date { date } } @@ -64,7 +64,7 @@ private struct RecordWithOptionalDate: EncodableReco var date: Date? } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension RecordWithOptionalDate: Identifiable { var id: Date? { date } } @@ -264,7 +264,7 @@ extension DatabaseDateEncodingStrategyTests { } func testFilterID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } @@ -344,7 +344,7 @@ extension DatabaseDateEncodingStrategyTests { } func testDeleteID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } diff --git a/Tests/GRDBTests/DatabaseDumpTests.swift b/Tests/GRDBTests/DatabaseDumpTests.swift index 432177ed9d..425f1f4e62 100644 --- a/Tests/GRDBTests/DatabaseDumpTests.swift +++ b/Tests/GRDBTests/DatabaseDumpTests.swift @@ -238,7 +238,7 @@ final class DatabaseDumpTests: GRDBTestCase { // MARK: - JSON func test_json_value_formatting() throws { - guard #available(iOS 13.0, macOS 10.15, tvOS 13.0, *) else { + guard #available(macOS 10.15, tvOS 13.0, *) else { throw XCTSkip("Skip because this test relies on JSONEncoder.OutputFormatting.withoutEscapingSlashes") } diff --git a/Tests/GRDBTests/DatabaseMigratorTests.swift b/Tests/GRDBTests/DatabaseMigratorTests.swift index a9efa088d0..114e9743c2 100644 --- a/Tests/GRDBTests/DatabaseMigratorTests.swift +++ b/Tests/GRDBTests/DatabaseMigratorTests.swift @@ -41,7 +41,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testEmptyMigratorPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -153,7 +153,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testNonEmptyMigratorPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -209,7 +209,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testEmptyMigratorPublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -235,7 +235,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testNonEmptyMigratorPublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -262,7 +262,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testMigratorPublisherDefaultScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -291,7 +291,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testMigratorPublisherCustomScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBTests/DatabaseReaderTests.swift b/Tests/GRDBTests/DatabaseReaderTests.swift index 4381016351..5428c22709 100644 --- a/Tests/GRDBTests/DatabaseReaderTests.swift +++ b/Tests/GRDBTests/DatabaseReaderTests.swift @@ -49,7 +49,7 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_ReadCanRead() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -91,7 +91,7 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_ReadPreventsDatabaseModification() async throws { func test(_ dbReader: some DatabaseReader) async throws { do { @@ -135,7 +135,7 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_UnsafeReadCanRead() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -347,7 +347,7 @@ class DatabaseReaderTests : GRDBTestCase { // MARK: - Task Cancellation - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_read_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -383,7 +383,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -420,7 +420,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_statement_execution_from_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -459,7 +459,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_cursor_iteration_from_read_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -502,7 +502,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_unsafeRead_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -538,7 +538,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -575,7 +575,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_statement_execution_from_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -614,7 +614,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_cursor_iteration_from_unsafeRead_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) diff --git a/Tests/GRDBTests/DatabaseRegionObservationTests.swift b/Tests/GRDBTests/DatabaseRegionObservationTests.swift index 341616b37f..3f91a54fcb 100644 --- a/Tests/GRDBTests/DatabaseRegionObservationTests.swift +++ b/Tests/GRDBTests/DatabaseRegionObservationTests.swift @@ -9,7 +9,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { _ = observation.start(in: writer, onError: { _ in }, onChange: { _ in }) - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { _ = observation.publisher(in: writer) } } diff --git a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift index 0caf342139..c839451c05 100644 --- a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift +++ b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift @@ -229,7 +229,7 @@ final class DatabaseSnapshotPoolTests: GRDBTestCase { try XCTAssertEqual(dbPool.read(counter.value), 2) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_read_async() async throws { let dbPool = try makeDatabasePool() let counter = try Counter(dbPool: dbPool) // 0 diff --git a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift index c5c652ac28..7fb74781b8 100644 --- a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift @@ -26,7 +26,7 @@ private struct RecordWithUUID: EncodableRecord, Enco var uuid: UUID } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension RecordWithUUID: Identifiable { var id: UUID { uuid } } @@ -39,7 +39,7 @@ private struct RecordWithOptionalUUID: EncodableReco var uuid: UUID? } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension RecordWithOptionalUUID: Identifiable { var id: UUID? { uuid } } @@ -190,7 +190,7 @@ extension DatabaseUUIDEncodingStrategyTests { } func testFilterID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } @@ -309,7 +309,7 @@ extension DatabaseUUIDEncodingStrategyTests { } func testDeleteID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable not available") } diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 478cab6c5b..62b5c3e6f6 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -275,7 +275,7 @@ class DatabaseWriterTests : GRDBTestCase { try DatabaseQueue().backup(to: dbQueue) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_write() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -295,7 +295,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_writeWithoutTransaction() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -318,7 +318,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_barrierWriteWithoutTransaction() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -341,7 +341,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_erase() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -359,7 +359,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_vacuum() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -406,7 +406,7 @@ class DatabaseWriterTests : GRDBTestCase { } /// A test related to - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncWriteThenRead() async throws { /// An async read performed after an async write should see the write. func test(_ dbWriter: some DatabaseWriter) async throws { @@ -430,7 +430,7 @@ class DatabaseWriterTests : GRDBTestCase { // MARK: - Task Cancellation - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -461,7 +461,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -493,7 +493,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_statement_execution_from_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -527,7 +527,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_cursor_iteration_from_writeWithoutTransaction_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -565,7 +565,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_write_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -596,7 +596,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_write_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -628,7 +628,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_statement_execution_from_write_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -662,7 +662,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_cursor_iteration_from_write_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -700,7 +700,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -731,7 +731,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -763,7 +763,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_statement_execution_from_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -797,7 +797,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_cursor_iteration_from_barrierWriteWithoutTransaction_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) diff --git a/Tests/GRDBTests/JoinSupportTests.swift b/Tests/GRDBTests/JoinSupportTests.swift index 4b703bd71a..a3a5c9974c 100644 --- a/Tests/GRDBTests/JoinSupportTests.swift +++ b/Tests/GRDBTests/JoinSupportTests.swift @@ -92,7 +92,7 @@ private struct FlatModel: FetchableRecord { self.t5count = row.scopes[Scopes.suffix]!["t5count"] } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) static func modernAll() -> some FetchRequest { all() } @@ -138,7 +138,7 @@ private struct CodableFlatModel: FetchableRecord, Codable { var t3: T3? var t5count: Int - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) static func modernAll() -> some FetchRequest { all() } @@ -186,7 +186,7 @@ private struct CodableNestedModel: FetchableRecord, Codable { var t3: T3? var t5count: Int - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) static func modernAll() -> some FetchRequest { all() } diff --git a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift index a77404913f..64762126b0 100644 --- a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift @@ -41,7 +41,7 @@ private class MinimalNonOptionalPrimaryKeySingle: Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension MinimalNonOptionalPrimaryKeySingle: Identifiable { } class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { @@ -473,7 +473,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) @@ -512,7 +512,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) @@ -550,7 +550,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) @@ -585,7 +585,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.fetchOne(db, id: record.id)! XCTAssertTrue(fetchedRecord.id == record.id) @@ -613,7 +613,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { _ = try MinimalNonOptionalPrimaryKeySingle.find(db, key: "missing") XCTFail("Expected RecordError") @@ -653,7 +653,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) @@ -692,7 +692,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) @@ -730,7 +730,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) @@ -765,7 +765,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.filter(id: record.id).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift index ff462cb6ae..31c1d684fc 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift @@ -45,7 +45,7 @@ class MinimalRowID : Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension MinimalRowID: Identifiable { } class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { @@ -507,7 +507,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let cursor = try MinimalRowID.fetchCursor(db, ids: ids) @@ -546,7 +546,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) @@ -584,7 +584,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) @@ -619,7 +619,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalRowID.fetchOne(db, id: record.id!)! XCTAssertTrue(fetchedRecord.id == record.id) @@ -650,7 +650,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { _ = try MinimalRowID.find(db, id: -1) XCTFail("Expected RecordError") @@ -693,7 +693,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) @@ -732,7 +732,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) @@ -770,7 +770,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) @@ -805,7 +805,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalRowID.filter(id: record.id!).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift index 9139d0cfc4..0e3eb7fd41 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift @@ -39,7 +39,7 @@ class MinimalSingle: Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension MinimalSingle: Identifiable { /// Test non-optional ID type var id: String { UUID! } @@ -531,7 +531,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) @@ -572,7 +572,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) @@ -612,7 +612,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) @@ -648,7 +648,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalSingle.fetchOne(db, id: record.UUID!)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) @@ -677,7 +677,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { _ = try MinimalSingle.find(db, id: "missing") XCTFail("Expected RecordError") @@ -719,7 +719,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) @@ -760,7 +760,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) @@ -800,7 +800,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) @@ -836,7 +836,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try MinimalSingle.filter(id: record.UUID!).fetchOne(db)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) diff --git a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift index cc4de060e1..398f2d9909 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift @@ -77,7 +77,7 @@ private class Person : Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Person: Identifiable { } class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { @@ -599,7 +599,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let cursor = try Person.fetchCursor(db, ids: ids) @@ -638,7 +638,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.fetchAll(db, ids: ids) @@ -676,7 +676,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.fetchSet(db, ids: ids) @@ -714,7 +714,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try Person.fetchOne(db, id: record.id!)! XCTAssertTrue(fetchedRecord.id == record.id) @@ -751,7 +751,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { _ = try Person.find(db, id: -1) XCTFail("Expected RecordError") @@ -797,7 +797,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let cursor = try Person.filter(ids: ids).fetchCursor(db) @@ -836,7 +836,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) @@ -874,7 +874,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) @@ -912,7 +912,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { do { let fetchedRecord = try Person.filter(id: record.id!).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) diff --git a/Tests/GRDBTests/SharedValueObservationTests.swift b/Tests/GRDBTests/SharedValueObservationTests.swift index 2b4ff72163..4a61b1bb10 100644 --- a/Tests/GRDBTests/SharedValueObservationTests.swift +++ b/Tests/GRDBTests/SharedValueObservationTests.swift @@ -120,7 +120,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_immediate_publisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -397,7 +397,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_async_publisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -525,7 +525,7 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), []) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_task_observationLifetime() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -622,7 +622,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_task_publisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -656,7 +656,7 @@ class SharedValueObservationTests: GRDBTestCase { } #endif - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func test_task_whileObserved() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -753,7 +753,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_error_recovery_observationLifetime() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -811,7 +811,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_error_recovery_whileObserved() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Combine is not available") } @@ -867,7 +867,7 @@ class SharedValueObservationTests: GRDBTestCase { } #endif - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_mainQueue() async throws { let dbQueue = try makeDatabaseQueue() try await dbQueue.write { db in @@ -887,7 +887,7 @@ class SharedValueObservationTests: GRDBTestCase { } } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_task() async throws { let dbQueue = try makeDatabaseQueue() try await dbQueue.write { db in diff --git a/Tests/GRDBTests/TableDefinitionTests.swift b/Tests/GRDBTests/TableDefinitionTests.swift index 3c138d6e59..26b7ba31b1 100644 --- a/Tests/GRDBTests/TableDefinitionTests.swift +++ b/Tests/GRDBTests/TableDefinitionTests.swift @@ -806,7 +806,7 @@ class TableDefinitionTests: GRDBTestCase { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } #if !GRDBCUSTOMSQLITE && !GRDBCIPHER - guard #available(iOS 13, tvOS 13, *) else { + guard #available(tvOS 13, *) else { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } #endif @@ -834,7 +834,7 @@ class TableDefinitionTests: GRDBTestCase { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } #if !GRDBCUSTOMSQLITE && !GRDBCIPHER - guard #available(iOS 13, tvOS 13, *) else { + guard #available(tvOS 13, *) else { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } #endif diff --git a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift index 737220992c..0c28204e88 100644 --- a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift @@ -357,7 +357,7 @@ class TableRecordQueryInterfaceRequestTests: GRDBTestCase { } func testExistsIdentifiable() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, tvOS 13, *) else { throw XCTSkip("Identifiable is not available") } diff --git a/Tests/GRDBTests/TableRecordDeleteTests.swift b/Tests/GRDBTests/TableRecordDeleteTests.swift index 37a7249c1a..eb1b3f479a 100644 --- a/Tests/GRDBTests/TableRecordDeleteTests.swift +++ b/Tests/GRDBTests/TableRecordDeleteTests.swift @@ -6,7 +6,7 @@ private struct Hacker : TableRecord { var id: Int64? // Optional } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Hacker: Identifiable { } private struct Person : Codable, PersistableRecord, FetchableRecord, Hashable { @@ -16,7 +16,7 @@ private struct Person : Codable, PersistableRecord, FetchableRecord, Hashable { var email: String } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Person: Identifiable { } private struct Citizenship : TableRecord { @@ -46,7 +46,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertTrue(deleted) XCTAssertEqual(try Hacker.fetchCount(db), 0) - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [1, "Arthur"]) try XCTAssertFalse(Hacker.deleteOne(db, id: nil)) deleted = try Hacker.deleteOne(db, id: 1) @@ -62,7 +62,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(deletedCount, 2) XCTAssertEqual(try Hacker.fetchCount(db), 1) - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [2, "Barbara"]) try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [3, "Craig"]) let deletedCount = try Hacker.deleteAll(db, ids: [2, 3, 4]) @@ -85,7 +85,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertTrue(deleted) XCTAssertEqual(try Person.fetchCount(db), 0) - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [1, "Arthur", "arthur@example.com"]) deleted = try Person.deleteOne(db, id: 1) XCTAssertTrue(deleted) @@ -100,7 +100,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(deletedCount, 2) XCTAssertEqual(try Person.fetchCount(db), 1) - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [2, "Barbara", "barbara@example.com"]) try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [3, "Craig", "craig@example.com"]) let deletedCount = try Person.deleteAll(db, ids: [2, 3, 4]) @@ -190,7 +190,7 @@ class TableRecordDeleteTests: GRDBTestCase { try Person.filter(keys: [1, 2]).deleteAll(db) XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2)") - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { try Person.filter(id: 1).deleteAll(db) XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1") @@ -279,7 +279,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2) RETURNING *") #if GRDBCUSTOMSQLITE || GRDBCIPHER - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { _ = try Person.filter(id: 1).deleteAndFetchCursor(db).next() XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1 RETURNING *") @@ -364,7 +364,7 @@ class TableRecordDeleteTests: GRDBTestCase { } } - @available(iOS 13, macOS 10.15, tvOS 13, *) // Identifiable + @available(macOS 10.15, tvOS 13, *) // Identifiable func testRequestDeleteAndFetchIds() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { diff --git a/Tests/GRDBTests/TableRecordUpdateTests.swift b/Tests/GRDBTests/TableRecordUpdateTests.swift index ac7bf5f84b..b5a249eb84 100644 --- a/Tests/GRDBTests/TableRecordUpdateTests.swift +++ b/Tests/GRDBTests/TableRecordUpdateTests.swift @@ -17,7 +17,7 @@ private struct Player: Codable, PersistableRecord, FetchableRecord, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, *) +@available(macOS 10.15, tvOS 13, *) extension Player: Identifiable { } private enum Columns: String, ColumnExpression { @@ -56,7 +56,7 @@ class TableRecordUpdateTests: GRDBTestCase { UPDATE "player" SET "score" = 0 WHERE "id" IN (1, 2) """) - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { try Player.filter(id: 1).updateAll(db, assignment) XCTAssertEqual(self.lastSQLQuery, """ UPDATE "player" SET "score" = 0 WHERE "id" = 1 diff --git a/Tests/GRDBTests/TableTests.swift b/Tests/GRDBTests/TableTests.swift index fa7325771c..7576902fdc 100644 --- a/Tests/GRDBTests/TableTests.swift +++ b/Tests/GRDBTests/TableTests.swift @@ -117,7 +117,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { struct Player: Identifiable { var id: Int64 } let t = Table("player") @@ -129,7 +129,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { struct Player: Identifiable { var id: Int64? } let t = Table("player") @@ -806,7 +806,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { // Non-optional ID struct Country: Identifiable { var id: String } @@ -821,7 +821,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { // Optional ID struct Country: Identifiable { var id: String? } @@ -920,7 +920,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { // Non-optional ID struct Country: Identifiable { var id: String } @@ -930,7 +930,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, tvOS 13, *) { // Optional ID struct Country: Identifiable { var id: String? } diff --git a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift index 54d90f9f43..dff05d4c5e 100644 --- a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift +++ b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift @@ -105,7 +105,7 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { } } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testTupleObservation() throws { // Here we just test that user can destructure an observed tuple. // I'm completely paranoid about tuple destructuring - I can't wrap my @@ -120,7 +120,7 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { onChange: { (int: Int, string: String) in }) // <- destructure } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testVaryingRegionTrackingImmediateScheduling() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index 20e0626c70..142ce7249e 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -5,7 +5,7 @@ import Dispatch class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testStartFromAnyDatabaseReader(reader: any DatabaseReader) { _ = ValueObservation .trackingConstantRegion { _ in } @@ -14,7 +14,7 @@ class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testStartFromAnyDatabaseWriter(writer: any DatabaseWriter) { _ = ValueObservation .trackingConstantRegion { _ in } @@ -23,7 +23,7 @@ class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testValuesFromAnyDatabaseWriter(writer: any DatabaseWriter) { func observe( fetch: @escaping @Sendable (Database) throws -> T @@ -55,7 +55,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testErrorCompletesTheObservation() throws { struct TestError: Error { } @@ -105,7 +105,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testViewOptimization() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { @@ -143,7 +143,7 @@ class ValueObservationTests: GRDBTestCase { } } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testPragmaTableOptimization() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { @@ -179,7 +179,7 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Constant Explicit Region - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testTrackingExplicitRegion() throws { class TestStream: TextOutputStream { private var stringsMutex: Mutex<[String]> = Mutex([]) @@ -620,7 +620,7 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Cancellation - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testCancellableLifetime() throws { // We need something to change let dbQueue = try makeDatabaseQueue() @@ -666,7 +666,7 @@ class ValueObservationTests: GRDBTestCase { XCTAssertEqual(changesCountMutex.load(), 2) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testCancellableExplicitCancellation() throws { // We need something to change let dbQueue = try makeDatabaseQueue() @@ -804,7 +804,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testIssue1550() throws { func test(_ writer: some DatabaseWriter) throws { try writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } @@ -852,7 +852,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testIssue1209() throws { func test(_ dbWriter: some DatabaseWriter) throws { try dbWriter.write { @@ -903,7 +903,7 @@ class ValueObservationTests: GRDBTestCase { } // MARK: - Main Actor - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) @MainActor func test_mainActor_observation() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -939,7 +939,7 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Async Await - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_values_prefix() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -977,7 +977,7 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_values_break() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -1019,7 +1019,7 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testAsyncAwait_values_cancelled() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -1201,7 +1201,7 @@ class ValueObservationTests: GRDBTestCase { } // Regression test for - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testIssue1362() throws { func test(_ writer: some DatabaseWriter) throws { try writer.write { try $0.execute(sql: "CREATE TABLE s(id INTEGER PRIMARY KEY AUTOINCREMENT)") } @@ -1292,7 +1292,7 @@ class ValueObservationTests: GRDBTestCase { } // Regression test for - @available(iOS 13, macOS 10.15, tvOS 13, *) + @available(macOS 10.15, tvOS 13, *) func testIssue1383_async() throws { do { let dbPool = try makeDatabasePool(filename: "test") From e4a473549499ecd2cd9cdda44f9057af3da356ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 13:21:27 +0200 Subject: [PATCH 114/160] [BREAKING] tvOS 13+ --- GRDB.swift.podspec | 2 +- GRDB/Core/Database+Statements.swift | 2 +- GRDB/Core/Database.swift | 6 ++-- GRDB/Core/DatabasePool.swift | 8 ++--- GRDB/Core/DatabasePublishers.swift | 2 +- GRDB/Core/DatabaseQueue.swift | 8 ++--- GRDB/Core/DatabaseReader.swift | 14 ++++---- GRDB/Core/DatabaseRegionObservation.swift | 6 ++-- GRDB/Core/DatabaseSnapshot.swift | 4 +-- GRDB/Core/DatabaseSnapshotPool.swift | 4 +-- GRDB/Core/DatabaseWriter.swift | 30 ++++++++-------- GRDB/Core/SerializedDatabase.swift | 8 ++--- GRDB/Dump/DumpFormats/JSONDumpFormat.swift | 2 +- GRDB/Fixits.swift | 6 ++-- GRDB/Migration/DatabaseMigrator.swift | 4 +-- .../Request/QueryInterfaceRequest.swift | 2 +- .../Request/RequestProtocols.swift | 2 +- GRDB/QueryInterface/SQL/Table.swift | 6 ++-- .../Schema/TableAlteration.swift | 1 - .../TableRecord+QueryInterfaceRequest.swift | 2 +- GRDB/Record/FetchableRecord+TableRecord.swift | 4 +-- GRDB/Record/TableRecord.swift | 6 ++-- GRDB/Utils/OnDemandFuture.swift | 4 +-- GRDB/Utils/ReceiveValuesOn.swift | 6 ++-- .../SharedValueObservation.swift | 4 +-- GRDB/ValueObservation/ValueObservation.swift | 10 +++--- .../ValueObservationScheduler.swift | 8 ++--- Package.swift | 2 +- README.md | 2 +- SQLiteCustom/GRDBDeploymentTarget.xcconfig | 2 +- Support/GRDBDeploymentTarget.xcconfig | 2 +- .../AvailableElements.swift | 2 +- .../PublisherExpectations/Finished.swift | 2 +- .../PublisherExpectations/Inverted.swift | 2 +- .../PublisherExpectations/Map.swift | 4 +-- .../PublisherExpectations/Next.swift | 2 +- .../PublisherExpectations/NextOne.swift | 2 +- .../PublisherExpectations/Prefix.swift | 2 +- .../PublisherExpectations/Recording.swift | 2 +- Tests/CombineExpectations/Recorder.swift | 8 ++--- .../DatabaseReaderReadPublisherTests.swift | 12 +++---- ...abaseRegionObservationPublisherTests.swift | 4 +-- .../DatabaseWriterWritePublisherTests.swift | 26 +++++++------- Tests/GRDBCombineTests/Support.swift | 6 ++-- .../ValueObservationPublisherTests.swift | 24 ++++++------- Tests/GRDBTests/AsyncSemaphore.swift | 2 +- .../DatabaseDataEncodingStrategyTests.swift | 8 ++--- .../DatabaseDateEncodingStrategyTests.swift | 8 ++--- Tests/GRDBTests/DatabaseDumpTests.swift | 2 +- Tests/GRDBTests/DatabaseMigratorTests.swift | 12 +++---- Tests/GRDBTests/DatabaseReaderTests.swift | 22 ++++++------ .../DatabaseRegionObservationTests.swift | 2 +- .../GRDBTests/DatabaseSnapshotPoolTests.swift | 2 +- .../DatabaseUUIDEncodingStrategyTests.swift | 8 ++--- Tests/GRDBTests/DatabaseWriterTests.swift | 36 +++++++++---------- Tests/GRDBTests/JoinSupportTests.swift | 6 ++-- ...imalNonOptionalPrimaryKeySingleTests.swift | 20 +++++------ .../RecordMinimalPrimaryKeyRowIDTests.swift | 20 +++++------ .../RecordMinimalPrimaryKeySingleTests.swift | 20 +++++------ .../RecordPrimaryKeyHiddenRowIDTests.swift | 20 +++++------ .../SharedValueObservationTests.swift | 18 +++++----- Tests/GRDBTests/TableDefinitionTests.swift | 10 ------ ...bleRecord+QueryInterfaceRequestTests.swift | 2 +- Tests/GRDBTests/TableRecordDeleteTests.swift | 18 +++++----- Tests/GRDBTests/TableRecordUpdateTests.swift | 4 +-- Tests/GRDBTests/TableTests.swift | 12 +++---- ...ValueObservationRegionRecordingTests.swift | 4 +-- Tests/GRDBTests/ValueObservationTests.swift | 34 +++++++++--------- 68 files changed, 273 insertions(+), 284 deletions(-) diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index 48182864df..de695d4afa 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.ios.deployment_target = '13.0' s.osx.deployment_target = '10.13' s.watchos.deployment_target = '7.0' - s.tvos.deployment_target = '12.0' + s.tvos.deployment_target = '13.0' s.default_subspec = 'standard' s.subspec 'standard' do |ss| diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index f252a6bff4..1b2806b3ba 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -492,7 +492,7 @@ extension Database { // and throws the user-provided cancelled commit error. try observationBroker?.statementDidFail(statement) - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { switch ResultCode(rawValue: resultCode) { case .SQLITE_INTERRUPT, .SQLITE_ABORT: if suspensionMutex.load().isCancelled { diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 1d19d0302c..f6574eeabb 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1199,7 +1199,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// will throw `CancellationError`, until `uncancel()` is called. /// /// This method can be called from any thread. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func cancel() { let needsInterrupt = suspensionMutex.withLock { suspension in if suspension.isCancelled { @@ -1216,7 +1216,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib } /// Undo `cancel()`. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func uncancel() { suspensionMutex.withLock { $0.isCancelled = false @@ -1320,7 +1320,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib break case .cancel: - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { throw CancellationError() } else { // GRDB bug: cancellation is a Swift concurrency feature diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index de5c5f6297..74a67b6c47 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -351,7 +351,7 @@ extension DatabasePool: DatabaseReader { } } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -436,7 +436,7 @@ extension DatabasePool: DatabaseReader { } } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -803,7 +803,7 @@ extension DatabasePool: DatabaseWriter { try writer.sync(updates) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -820,7 +820,7 @@ extension DatabasePool: DatabaseWriter { } } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabasePublishers.swift b/GRDB/Core/DatabasePublishers.swift index a21777814b..04ca12e84b 100644 --- a/GRDB/Core/DatabasePublishers.swift +++ b/GRDB/Core/DatabasePublishers.swift @@ -1,5 +1,5 @@ #if canImport(Combine) /// A namespace for database Combine publishers. -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) public enum DatabasePublishers { } #endif diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index 90f7c6fc10..ebf822667b 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -233,7 +233,7 @@ extension DatabaseQueue: DatabaseReader { } } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -272,7 +272,7 @@ extension DatabaseQueue: DatabaseReader { try writer.sync(value) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -386,7 +386,7 @@ extension DatabaseQueue: DatabaseWriter { try writer.sync(updates) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -398,7 +398,7 @@ extension DatabaseQueue: DatabaseWriter { try writer.sync(updates) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index d36fb3fd72..ed7736c6f6 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -216,7 +216,7 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `value`, or /// `CancellationError` if the task is cancelled. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -323,7 +323,7 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `value`, or /// `CancellationError` if the task is cancelled. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -535,7 +535,7 @@ extension DatabaseReader { /// /// - parameter scheduler: A Combine Scheduler. /// - parameter value: A closure which accesses the database. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func readPublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, value: @escaping @Sendable (Database) throws -> Output @@ -550,7 +550,7 @@ extension DatabaseReader { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension DatabasePublishers { /// A publisher that reads from the database. /// @@ -569,7 +569,7 @@ extension DatabasePublishers { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Publisher where Failure == Error { fileprivate func eraseToReadPublisher() -> DatabasePublishers.Read { .init(upstream: eraseToAnyPublisher()) @@ -660,7 +660,7 @@ extension AnyDatabaseReader: DatabaseReader { try base.read(value) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -678,7 +678,7 @@ extension AnyDatabaseReader: DatabaseReader { try base.unsafeRead(value) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseRegionObservation.swift b/GRDB/Core/DatabaseRegionObservation.swift index 15d9ba92c6..4d5ab11938 100644 --- a/GRDB/Core/DatabaseRegionObservation.swift +++ b/GRDB/Core/DatabaseRegionObservation.swift @@ -128,7 +128,7 @@ extension DatabaseRegionObservation { } #if canImport(Combine) -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension DatabaseRegionObservation { // MARK: - Publishing Impactful Transactions @@ -140,7 +140,7 @@ extension DatabaseRegionObservation { /// /// Do not reschedule the publisher with `receive(on:options:)` or any /// `Publisher` method that schedules publisher elements. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func publisher(in writer: some DatabaseWriter) -> DatabasePublishers.DatabaseRegion { DatabasePublishers.DatabaseRegion(self, in: writer) } @@ -186,7 +186,7 @@ private class DatabaseRegionObserver: TransactionObserver { } #if canImport(Combine) -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension DatabasePublishers { /// A publisher that tracks transactions that modify a database region. /// diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 9b8e9236d6..36b131cea8 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -151,7 +151,7 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { try reader.sync(block) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -173,7 +173,7 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { // We can't provide this as a default implementation in // `DatabaseSnapshotReader`, because of // . - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index fb66e79107..826c7d0f68 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -293,7 +293,7 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { } } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -353,7 +353,7 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { // We can't provide this as a default implementation in // `DatabaseSnapshotReader`, because of // . - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index f717f0c56a..b1928b14ae 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -131,7 +131,7 @@ public protocol DatabaseWriter: DatabaseReader { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -217,7 +217,7 @@ public protocol DatabaseWriter: DatabaseReader { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -649,7 +649,7 @@ extension DatabaseWriter { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func write( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -666,7 +666,7 @@ extension DatabaseWriter { /// Erase the database: delete all content, drop all tables, etc. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func erase() async throws { try await writeWithoutTransaction { try $0.erase() } } @@ -677,7 +677,7 @@ extension DatabaseWriter { /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// Related SQLite documentation: - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func vacuum() async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM") } } @@ -694,7 +694,7 @@ extension DatabaseWriter { /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -709,7 +709,7 @@ extension DatabaseWriter { /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -754,7 +754,7 @@ extension DatabaseWriter { /// /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which accesses the database. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, updates: @escaping @Sendable (Database) throws -> Output @@ -818,7 +818,7 @@ extension DatabaseWriter { /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which writes in the database. /// - parameter value: A closure which reads from the database. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, updates: @escaping @Sendable (Database) throws -> T, @@ -848,7 +848,7 @@ extension DatabaseWriter { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension DatabasePublishers { /// A publisher that writes into the database. /// @@ -867,7 +867,7 @@ extension DatabasePublishers { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Publisher where Failure == Error { fileprivate func eraseToWritePublisher() -> DatabasePublishers.Write { .init(upstream: self.eraseToAnyPublisher()) @@ -911,7 +911,7 @@ extension AnyDatabaseWriter: DatabaseReader { try base.read(value) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -929,7 +929,7 @@ extension AnyDatabaseWriter: DatabaseReader { try base.unsafeRead(value) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -964,7 +964,7 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.writeWithoutTransaction(updates) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -976,7 +976,7 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.barrierWriteWithoutTransaction(updates) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index 6a64e40121..6668360116 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -244,7 +244,7 @@ final class SerializedDatabase { } /// Asynchrously executes the block. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func execute( _ block: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -309,7 +309,7 @@ extension SerializedDatabase: @unchecked Sendable { } // MARK: - Task Cancellation Support -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) enum DatabaseAccessCancellationState: @unchecked Sendable { // @unchecked Sendable because database is only accessed from its // dispatch queue. @@ -319,7 +319,7 @@ enum DatabaseAccessCancellationState: @unchecked Sendable { case expired } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) typealias CancellableDatabaseAccess = Mutex /// Supports Task cancellation in async database accesses. @@ -341,7 +341,7 @@ typealias CancellableDatabaseAccess = Mutex /// } /// } /// ``` -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension CancellableDatabaseAccess: DatabaseCancellable { convenience init() { self.init(.notConnected) diff --git a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift index a3a036e5ba..f24eaa1216 100644 --- a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift @@ -52,7 +52,7 @@ public struct JSONDumpFormat: Sendable { public static var defaultEncoder: JSONEncoder { // This encoder MUST NOT CHANGE, because some people rely on this format. let encoder = JSONEncoder() - if #available(macOS 10.15, tvOS 13.0, *) { + if #available(macOS 10.15.0, *) { encoder.outputFormatting = .withoutEscapingSlashes } encoder.nonConformingFloatEncodingStrategy = .convertToString( diff --git a/GRDB/Fixits.swift b/GRDB/Fixits.swift index 42b768ce86..453741c455 100644 --- a/GRDB/Fixits.swift +++ b/GRDB/Fixits.swift @@ -119,7 +119,7 @@ extension PersistableRecord { public func performSave(_ db: Database) throws { preconditionFailure() } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension QueryInterfaceRequest where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public func selectID() -> QueryInterfaceRequest { preconditionFailure() } @@ -144,13 +144,13 @@ extension SelectionRequest { @available(*, unavailable, renamed: "SQLExpression.AssociativeBinaryOperator") public typealias SQLAssociativeBinaryOperator = SQLExpression.AssociativeBinaryOperator -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public func selectID() -> QueryInterfaceRequest { preconditionFailure() } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public static func selectID() -> QueryInterfaceRequest { preconditionFailure() } diff --git a/GRDB/Migration/DatabaseMigrator.swift b/GRDB/Migration/DatabaseMigrator.swift index 1e8b6ce2f9..11c2ae92e4 100644 --- a/GRDB/Migration/DatabaseMigrator.swift +++ b/GRDB/Migration/DatabaseMigrator.swift @@ -496,7 +496,7 @@ extension DatabaseMigrator { /// - parameter writer: A DatabaseWriter. /// where migrations should apply. /// - parameter scheduler: A Combine Scheduler. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func migratePublisher( _ writer: some DatabaseWriter, receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main) @@ -514,7 +514,7 @@ extension DatabaseMigrator { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension DatabasePublishers { /// A publisher that migrates a database. /// diff --git a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift index 8f747ff3be..ab6a1047ae 100644 --- a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift @@ -646,7 +646,7 @@ extension QueryInterfaceRequest { /// - parameter db: A database connection. /// - returns: A set of deleted ids. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - @available(macOS 10.15, tvOS 13, *) // Identifiable + @available(macOS 10.15, *) // Identifiable public func deleteAndFetchIds(_ db: Database) throws -> Set where RowDecoder: TableRecord & Identifiable, diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index 9a51e8701c..c265069f95 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -631,7 +631,7 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension TableRequest where Self: FilteredRequest, Self: TypedRequest, diff --git a/GRDB/QueryInterface/SQL/Table.swift b/GRDB/QueryInterface/SQL/Table.swift index 3e7dd8180a..cdaf32b85c 100644 --- a/GRDB/QueryInterface/SQL/Table.swift +++ b/GRDB/QueryInterface/SQL/Table.swift @@ -722,7 +722,7 @@ extension Table { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { /// Returns a request filtered by primary key. /// @@ -1545,7 +1545,7 @@ extension Table { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible @@ -1686,7 +1686,7 @@ extension Table { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible diff --git a/GRDB/QueryInterface/Schema/TableAlteration.swift b/GRDB/QueryInterface/Schema/TableAlteration.swift index f10919b231..9b40a2d62b 100644 --- a/GRDB/QueryInterface/Schema/TableAlteration.swift +++ b/GRDB/QueryInterface/Schema/TableAlteration.swift @@ -129,7 +129,6 @@ public final class TableAlteration { /// /// - parameter name: the old name of the column. /// - parameter newName: the new name of the column. - @available(tvOS 13, *) // SQLite 3.25+ public func rename(column name: String, to newName: String) { _rename(column: name, to: newName) } diff --git a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift index ce98f1772a..fa9acff667 100644 --- a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift @@ -604,7 +604,7 @@ extension TableRecord { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns a request filtered by primary key. /// diff --git a/GRDB/Record/FetchableRecord+TableRecord.swift b/GRDB/Record/FetchableRecord+TableRecord.swift index ed006713ee..18ea1eeb23 100644 --- a/GRDB/Record/FetchableRecord+TableRecord.swift +++ b/GRDB/Record/FetchableRecord+TableRecord.swift @@ -216,7 +216,7 @@ extension FetchableRecord where Self: TableRecord { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseValueConvertible { // MARK: Fetching by Single-Column Primary Key @@ -358,7 +358,7 @@ extension FetchableRecord where Self: TableRecord & Hashable { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension FetchableRecord where Self: TableRecord & Hashable & Identifiable, ID: DatabaseValueConvertible { /// Returns a set of records identified by their primary keys. /// diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index cf53d3462f..46794444f1 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -319,7 +319,7 @@ extension TableRecord { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns whether a record exists for this primary key. /// @@ -454,7 +454,7 @@ extension TableRecord { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Deletes records identified by their primary keys, and returns the number /// of deleted records. @@ -774,7 +774,7 @@ extension TableRecord where Self: EncodableRecord { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns an error for a record that does not exist in the database. /// diff --git a/GRDB/Utils/OnDemandFuture.swift b/GRDB/Utils/OnDemandFuture.swift index 03b2ad08c6..e91d8af447 100644 --- a/GRDB/Utils/OnDemandFuture.swift +++ b/GRDB/Utils/OnDemandFuture.swift @@ -17,7 +17,7 @@ import Foundation /// /// OnDemandFuture also adds Sendable requirements that avoid /// compiler warnings. -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) struct OnDemandFuture: Publisher { typealias Promise = @Sendable (Result) -> Void typealias Output = Output @@ -36,7 +36,7 @@ struct OnDemandFuture: Publisher { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) private class OnDemandFutureSubscription: Subscription, @unchecked Sendable { // @unchecked because `state` is protected with `lock`. typealias Promise = @Sendable (Result) -> Void diff --git a/GRDB/Utils/ReceiveValuesOn.swift b/GRDB/Utils/ReceiveValuesOn.swift index 95fb42fe0a..5a309e1f50 100644 --- a/GRDB/Utils/ReceiveValuesOn.swift +++ b/GRDB/Utils/ReceiveValuesOn.swift @@ -11,7 +11,7 @@ import Foundation /// This scheduling guarantee is used by GRDB in order to be able /// to make promises on the scheduling of database values without surprising /// the users as in . -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) struct ReceiveValuesOn: Publisher { typealias Output = Upstream.Output typealias Failure = Upstream.Failure @@ -30,7 +30,7 @@ struct ReceiveValuesOn: Publisher { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) private class ReceiveValuesOnSubscription: Subscription, Subscriber where Upstream: Publisher, @@ -211,7 +211,7 @@ where } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Publisher { /// Specifies the scheduler on which to receive values from the publisher /// diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index 6f61750094..925a67a2c3 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -292,7 +292,7 @@ public final class SharedValueObservation: @unchecked Sendabl /// print("fresh players: \(players)") /// } /// ``` - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func publisher() -> DatabasePublishers.Value { DatabasePublishers.Value { onError, onChange in self.start(onError: onError, onChange: onChange) @@ -369,7 +369,7 @@ extension SharedValueObservation { /// print("Fresh players: \(players)") /// } /// ``` - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func values(bufferingPolicy: AsyncValueObservation.BufferingPolicy = .unbounded) -> AsyncValueObservation { diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index b4236838b6..84d9eda005 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -179,7 +179,7 @@ extension ValueObservation: Refinable { /// - parameter onChange: The closure to execute on receipt of a /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) @preconcurrency @MainActor public func start( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationMainActorScheduler = .mainActor, @@ -350,7 +350,7 @@ extension ValueObservation { /// fresh values are dispatched on the cooperative thread pool. /// - parameter bufferingPolicy: see the documntation /// of `AsyncThrowingStream`. - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func values( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .task, @@ -386,7 +386,7 @@ extension ValueObservation { /// /// You build an `AsyncValueObservation` from ``ValueObservation`` or /// ``SharedValueObservation``. -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) public struct AsyncValueObservation: AsyncSequence { public typealias BufferingPolicy = AsyncThrowingStream.Continuation.BufferingPolicy public typealias AsyncIterator = Iterator @@ -488,7 +488,7 @@ extension ValueObservation { /// - parameter scheduler: A ValueObservationScheduler. By default, fresh /// values are dispatched asynchronously on the main dispatch queue. /// - returns: A Combine publisher - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) public func publisher( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main)) @@ -505,7 +505,7 @@ extension ValueObservation { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension DatabasePublishers { /// A publisher that publishes the values of a ``ValueObservation``. /// diff --git a/GRDB/ValueObservation/ValueObservationScheduler.swift b/GRDB/ValueObservation/ValueObservationScheduler.swift index 5188d3452c..887ce09cf1 100644 --- a/GRDB/ValueObservation/ValueObservationScheduler.swift +++ b/GRDB/ValueObservation/ValueObservationScheduler.swift @@ -182,7 +182,7 @@ extension ValueObservationMainActorScheduler where Self == ImmediateValueObserva // MARK: - TaskValueObservationScheduler /// A scheduler that notifies all values on the cooperative thread pool. -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) public final class TaskValueObservationScheduler: ValueObservationScheduler { typealias Action = @Sendable () -> Void let continuation: AsyncStream.Continuation @@ -212,7 +212,7 @@ public final class TaskValueObservationScheduler: ValueObservationScheduler { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension ValueObservationScheduler where Self == TaskValueObservationScheduler { /// A scheduler that notifies all values from a new `Task`. public static var task: TaskValueObservationScheduler { @@ -229,7 +229,7 @@ extension ValueObservationScheduler where Self == TaskValueObservationScheduler // MARK: - DelayedMainActorValueObservationScheduler /// A scheduler that notifies all values on the cooperative thread pool. -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) public final class DelayedMainActorValueObservationScheduler: ValueObservationMainActorScheduler { public func immediateInitialValue() -> Bool { false @@ -240,7 +240,7 @@ public final class DelayedMainActorValueObservationScheduler: ValueObservationMa } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension ValueObservationScheduler where Self == DelayedMainActorValueObservationScheduler { /// A scheduler that notifies all values on the main actor. public static var mainActor: DelayedMainActorValueObservationScheduler { diff --git a/Package.swift b/Package.swift index af1aace2fc..6b53e53ff4 100644 --- a/Package.swift +++ b/Package.swift @@ -38,7 +38,7 @@ let package = Package( platforms: [ .iOS(.v13), .macOS(.v10_13), - .tvOS(.v12), + .tvOS(.v13), .watchOS(.v7), ], products: [ diff --git a/README.md b/README.md index be3bff834f..530e3d309c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ **Latest release**: September 7, 2024 • [version 6.29.3](https://github.com/groue/GRDB.swift/tree/v6.29.3) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md) -**Requirements**: iOS 13.0+ / macOS 10.13+ / tvOS 12.0+ / watchOS 7.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ +**Requirements**: iOS 13.0+ / macOS 10.13+ / tvOS 13.0+ / watchOS 7.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ **Contact**: diff --git a/SQLiteCustom/GRDBDeploymentTarget.xcconfig b/SQLiteCustom/GRDBDeploymentTarget.xcconfig index 8f02cc8937..369f90a510 100644 --- a/SQLiteCustom/GRDBDeploymentTarget.xcconfig +++ b/SQLiteCustom/GRDBDeploymentTarget.xcconfig @@ -1,6 +1,6 @@ IPHONEOS_DEPLOYMENT_TARGET = 13.0 MACOSX_DEPLOYMENT_TARGET = 10.13 -TVOS_DEPLOYMENT_TARGET = 12.0 +TVOS_DEPLOYMENT_TARGET = 13.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES OTHER_SWIFT_FLAGS = $(inherited) -enable-upcoming-feature GlobalActorIsolatedTypesUsability diff --git a/Support/GRDBDeploymentTarget.xcconfig b/Support/GRDBDeploymentTarget.xcconfig index d20702f0c6..a457235eed 100644 --- a/Support/GRDBDeploymentTarget.xcconfig +++ b/Support/GRDBDeploymentTarget.xcconfig @@ -1,6 +1,6 @@ IPHONEOS_DEPLOYMENT_TARGET = 13.0 MACOSX_DEPLOYMENT_TARGET = 10.13 -TVOS_DEPLOYMENT_TARGET = 12.0 +TVOS_DEPLOYMENT_TARGET = 13.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 OTHER_SWIFT_FLAGS = $(inherited) -D SQLITE_ENABLE_FTS5 SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES diff --git a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift index 51198282b5..7c8656f354 100644 --- a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift +++ b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the timeout to expire, or /// the recorded publisher to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Finished.swift b/Tests/CombineExpectations/PublisherExpectations/Finished.swift index c9d1c8a6be..6c773f4f3d 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Finished.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Finished.swift @@ -17,7 +17,7 @@ import XCTest // try wait(for: recorder.finished.inverted, timeout: 1) // } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Inverted.swift b/Tests/CombineExpectations/PublisherExpectations/Inverted.swift index ceb59955ea..218402af60 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Inverted.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Inverted.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation that fails if the base expectation is fulfilled. /// diff --git a/Tests/CombineExpectations/PublisherExpectations/Map.swift b/Tests/CombineExpectations/PublisherExpectations/Map.swift index e15e6607ec..2f55cc60ec 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Map.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Map.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation that transforms the value of a base expectation. /// @@ -20,7 +20,7 @@ extension PublisherExpectations { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension PublisherExpectation { /// Returns a publisher expectation that transforms the value of the /// base expectation. diff --git a/Tests/CombineExpectations/PublisherExpectations/Next.swift b/Tests/CombineExpectations/PublisherExpectations/Next.swift index 3fd8010dd6..8d511ccd8e 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Next.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Next.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `count` elements, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/NextOne.swift b/Tests/CombineExpectations/PublisherExpectations/NextOne.swift index 492c2ba3b8..6d4869a5c9 100644 --- a/Tests/CombineExpectations/PublisherExpectations/NextOne.swift +++ b/Tests/CombineExpectations/PublisherExpectations/NextOne.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// one element, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Prefix.swift b/Tests/CombineExpectations/PublisherExpectations/Prefix.swift index 3f055e1286..3089272c9b 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Prefix.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Prefix.swift @@ -1,7 +1,7 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `maxLength` elements, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Recording.swift b/Tests/CombineExpectations/PublisherExpectations/Recording.swift index 0b88ae2731..59bd76c1bf 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Recording.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Recording.swift @@ -2,7 +2,7 @@ import Combine import XCTest -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. diff --git a/Tests/CombineExpectations/Recorder.swift b/Tests/CombineExpectations/Recorder.swift index 569066dbb2..153f328a57 100644 --- a/Tests/CombineExpectations/Recorder.swift +++ b/Tests/CombineExpectations/Recorder.swift @@ -13,7 +13,7 @@ import XCTest /// /// let elements = try wait(for: recorder.elements, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) public class Recorder: Subscriber { public typealias Input = Input public typealias Failure = Failure @@ -287,7 +287,7 @@ public class Recorder: Subscriber { // MARK: - Publisher Expectations -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension PublisherExpectations { /// The type of the publisher expectation returned by `Recorder.completion`. public typealias Completion = Map, Subscribers.Completion> @@ -302,7 +302,7 @@ extension PublisherExpectations { public typealias Single = Map, Input> } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Recorder { /// Returns a publisher expectation which waits for the timeout to expire, /// or the recorded publisher to complete. @@ -584,7 +584,7 @@ extension Recorder { // MARK: - Publisher + Recorder -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Publisher { /// Returns a subscribed Recorder. /// diff --git a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift index b45baff0e8..79defbd8ca 100644 --- a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift @@ -22,7 +22,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisher() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -128,7 +128,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // frame #71: 0x00007fff72311cc9 libdyld.dylib`start + 1 // frame #72: 0x00007fff72311cc9 libdyld.dylib`start + 1 func testReadPublisherError() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -157,7 +157,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherIsAsynchronous() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -197,7 +197,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherDefaultScheduler() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -237,7 +237,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherCustomScheduler() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -278,7 +278,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherIsReadonly() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift index 1a91d0c933..10a3568b89 100644 --- a/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift @@ -20,7 +20,7 @@ private struct Player: Codable, FetchableRecord, PersistableRecord { class DatabaseRegionObservationPublisherTests : XCTestCase { func testChangesNotifications() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -61,7 +61,7 @@ class DatabaseRegionObservationPublisherTests : XCTestCase { // TODO: do the same, but asynchronously. If this is too hard, update the // public API so that users can easily do it. func testPrependInitialDatabaseSync() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift b/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift index 585359af26..d9534238cb 100644 --- a/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift @@ -22,7 +22,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisher() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -49,7 +49,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherValue() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -76,7 +76,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherError() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -99,7 +99,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { } func testWritePublisherErrorRollbacksTransaction() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -132,7 +132,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherIsAsynchronous() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -168,7 +168,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherDefaultScheduler() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -206,7 +206,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherCustomScheduler() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -247,7 +247,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // TODO: Fix flaky test with both pool and on-disk queue: // - Expectation timeout func testWriteThenReadPublisher() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -274,7 +274,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWriteThenReadPublisherIsReadonly() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -299,7 +299,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWriteThenReadPublisherWriteError() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -322,7 +322,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { } func testWriteThenReadPublisherWriteErrorRollbacksTransaction() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -359,7 +359,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // TODO: Fix flaky test with both pool and on-disk queue: // - Expectation timeout func testWriteThenReadPublisherReadError() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -386,7 +386,7 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // Regression test against deadlocks created by concurrent completion // and cancellations triggered by .switchToLatest().prefix(1) func testDeadlockPrevention() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBCombineTests/Support.swift b/Tests/GRDBCombineTests/Support.swift index 3759c94fa4..678195909d 100644 --- a/Tests/GRDBCombineTests/Support.swift +++ b/Tests/GRDBCombineTests/Support.swift @@ -51,7 +51,7 @@ final class Test { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) final class AsyncTest { // Raise the repeatCount in order to help spotting flaky tests. private let repeatCount: Int @@ -100,7 +100,7 @@ final class AsyncTest { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) public func assertNoFailure( _ completion: Subscribers.Completion, file: StaticString = #file, @@ -111,7 +111,7 @@ public func assertNoFailure( } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) public func assertFailure( _ completion: Subscribers.Completion, file: StaticString = #file, diff --git a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift index 86c98fa669..ae89d3f27b 100644 --- a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift @@ -22,7 +22,7 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Default Scheduler func testDefaultSchedulerChangesNotifications() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -64,7 +64,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDefaultSchedulerFirstValueIsEmittedAsynchronously() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -97,7 +97,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDefaultSchedulerError() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -123,7 +123,7 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Immediate Scheduler func testImmediateSchedulerChangesNotifications() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -165,7 +165,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testImmediateSchedulerEmitsFirstValueSynchronously() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -201,7 +201,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testImmediateSchedulerError() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -226,7 +226,7 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Demand - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) private class DemandSubscriber: Subscriber { private var subscription: Subscription? let subject = PassthroughSubject() @@ -257,7 +257,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandNoneReceivesNoElement() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -292,7 +292,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandOneReceivesOneElement() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -330,7 +330,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandOneDoesNotReceiveTwoElements() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -372,7 +372,7 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandTwoReceivesTwoElements() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -418,7 +418,7 @@ class ValueObservationPublisherTests : XCTestCase { /// Regression test for https://github.com/groue/GRDB.swift/issues/1194 func testIssue1194() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBTests/AsyncSemaphore.swift b/Tests/GRDBTests/AsyncSemaphore.swift index 3ac88d4959..cc6dd61097 100644 --- a/Tests/GRDBTests/AsyncSemaphore.swift +++ b/Tests/GRDBTests/AsyncSemaphore.swift @@ -43,7 +43,7 @@ import Foundation /// /// - ``wait()`` /// - ``waitUnlessCancelled()`` -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) public final class AsyncSemaphore: @unchecked Sendable { /// `Suspension` is the state of a task waiting for a signal. /// diff --git a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift index 20061cebed..58d33c7584 100644 --- a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift @@ -25,7 +25,7 @@ private struct RecordWithData: EncodableRecord, Enco var data: Data } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension RecordWithData: Identifiable { var id: Data { data } } @@ -37,7 +37,7 @@ private struct RecordWithOptionalData: EncodableReco var data: Data? } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension RecordWithOptionalData: Identifiable { var id: Data? { data } } @@ -154,7 +154,7 @@ extension DatabaseDataEncodingStrategyTests { } func testFilterID() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Identifiable not available") } @@ -234,7 +234,7 @@ extension DatabaseDataEncodingStrategyTests { } func testDeleteID() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Identifiable not available") } diff --git a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift index a071a7ddbf..0f8b7629e1 100644 --- a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift @@ -52,7 +52,7 @@ private struct RecordWithDate: EncodableRecord, Enco var date: Date } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension RecordWithDate: Identifiable { var id: Date { date } } @@ -64,7 +64,7 @@ private struct RecordWithOptionalDate: EncodableReco var date: Date? } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension RecordWithOptionalDate: Identifiable { var id: Date? { date } } @@ -264,7 +264,7 @@ extension DatabaseDateEncodingStrategyTests { } func testFilterID() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Identifiable not available") } @@ -344,7 +344,7 @@ extension DatabaseDateEncodingStrategyTests { } func testDeleteID() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Identifiable not available") } diff --git a/Tests/GRDBTests/DatabaseDumpTests.swift b/Tests/GRDBTests/DatabaseDumpTests.swift index 425f1f4e62..f05da9916e 100644 --- a/Tests/GRDBTests/DatabaseDumpTests.swift +++ b/Tests/GRDBTests/DatabaseDumpTests.swift @@ -238,7 +238,7 @@ final class DatabaseDumpTests: GRDBTestCase { // MARK: - JSON func test_json_value_formatting() throws { - guard #available(macOS 10.15, tvOS 13.0, *) else { + guard #available(macOS 10.15.0, *) else { throw XCTSkip("Skip because this test relies on JSONEncoder.OutputFormatting.withoutEscapingSlashes") } diff --git a/Tests/GRDBTests/DatabaseMigratorTests.swift b/Tests/GRDBTests/DatabaseMigratorTests.swift index 114e9743c2..700ad3381e 100644 --- a/Tests/GRDBTests/DatabaseMigratorTests.swift +++ b/Tests/GRDBTests/DatabaseMigratorTests.swift @@ -41,7 +41,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testEmptyMigratorPublisher() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -153,7 +153,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testNonEmptyMigratorPublisher() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -209,7 +209,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testEmptyMigratorPublisherIsAsynchronous() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -235,7 +235,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testNonEmptyMigratorPublisherIsAsynchronous() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -262,7 +262,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testMigratorPublisherDefaultScheduler() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -291,7 +291,7 @@ class DatabaseMigratorTests : GRDBTestCase { } func testMigratorPublisherCustomScheduler() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } diff --git a/Tests/GRDBTests/DatabaseReaderTests.swift b/Tests/GRDBTests/DatabaseReaderTests.swift index 5428c22709..d7ebdb9e1c 100644 --- a/Tests/GRDBTests/DatabaseReaderTests.swift +++ b/Tests/GRDBTests/DatabaseReaderTests.swift @@ -49,7 +49,7 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_ReadCanRead() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -91,7 +91,7 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_ReadPreventsDatabaseModification() async throws { func test(_ dbReader: some DatabaseReader) async throws { do { @@ -135,7 +135,7 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_UnsafeReadCanRead() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -347,7 +347,7 @@ class DatabaseReaderTests : GRDBTestCase { // MARK: - Task Cancellation - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_read_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -383,7 +383,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -420,7 +420,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_statement_execution_from_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -459,7 +459,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_cursor_iteration_from_read_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -502,7 +502,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_unsafeRead_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -538,7 +538,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -575,7 +575,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_statement_execution_from_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -614,7 +614,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_cursor_iteration_from_unsafeRead_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) diff --git a/Tests/GRDBTests/DatabaseRegionObservationTests.swift b/Tests/GRDBTests/DatabaseRegionObservationTests.swift index 3f91a54fcb..49d76f9eba 100644 --- a/Tests/GRDBTests/DatabaseRegionObservationTests.swift +++ b/Tests/GRDBTests/DatabaseRegionObservationTests.swift @@ -9,7 +9,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { _ = observation.start(in: writer, onError: { _ in }, onChange: { _ in }) - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { _ = observation.publisher(in: writer) } } diff --git a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift index c839451c05..3a3e26c2cd 100644 --- a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift +++ b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift @@ -229,7 +229,7 @@ final class DatabaseSnapshotPoolTests: GRDBTestCase { try XCTAssertEqual(dbPool.read(counter.value), 2) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_read_async() async throws { let dbPool = try makeDatabasePool() let counter = try Counter(dbPool: dbPool) // 0 diff --git a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift index 7fb74781b8..119953a1b6 100644 --- a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift @@ -26,7 +26,7 @@ private struct RecordWithUUID: EncodableRecord, Enco var uuid: UUID } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension RecordWithUUID: Identifiable { var id: UUID { uuid } } @@ -39,7 +39,7 @@ private struct RecordWithOptionalUUID: EncodableReco var uuid: UUID? } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension RecordWithOptionalUUID: Identifiable { var id: UUID? { uuid } } @@ -190,7 +190,7 @@ extension DatabaseUUIDEncodingStrategyTests { } func testFilterID() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Identifiable not available") } @@ -309,7 +309,7 @@ extension DatabaseUUIDEncodingStrategyTests { } func testDeleteID() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Identifiable not available") } diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 62b5c3e6f6..5d4f9e9652 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -275,7 +275,7 @@ class DatabaseWriterTests : GRDBTestCase { try DatabaseQueue().backup(to: dbQueue) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_write() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -295,7 +295,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_writeWithoutTransaction() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -318,7 +318,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_barrierWriteWithoutTransaction() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -341,7 +341,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_erase() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -359,7 +359,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_vacuum() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -406,7 +406,7 @@ class DatabaseWriterTests : GRDBTestCase { } /// A test related to - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncWriteThenRead() async throws { /// An async read performed after an async write should see the write. func test(_ dbWriter: some DatabaseWriter) async throws { @@ -430,7 +430,7 @@ class DatabaseWriterTests : GRDBTestCase { // MARK: - Task Cancellation - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -461,7 +461,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -493,7 +493,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_statement_execution_from_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -527,7 +527,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_cursor_iteration_from_writeWithoutTransaction_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -565,7 +565,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_write_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -596,7 +596,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_write_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -628,7 +628,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_statement_execution_from_write_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -662,7 +662,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_cursor_iteration_from_write_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -700,7 +700,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -731,7 +731,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -763,7 +763,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_statement_execution_from_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -797,7 +797,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_cursor_iteration_from_barrierWriteWithoutTransaction_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) diff --git a/Tests/GRDBTests/JoinSupportTests.swift b/Tests/GRDBTests/JoinSupportTests.swift index a3a5c9974c..e67523fbf7 100644 --- a/Tests/GRDBTests/JoinSupportTests.swift +++ b/Tests/GRDBTests/JoinSupportTests.swift @@ -92,7 +92,7 @@ private struct FlatModel: FetchableRecord { self.t5count = row.scopes[Scopes.suffix]!["t5count"] } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) static func modernAll() -> some FetchRequest { all() } @@ -138,7 +138,7 @@ private struct CodableFlatModel: FetchableRecord, Codable { var t3: T3? var t5count: Int - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) static func modernAll() -> some FetchRequest { all() } @@ -186,7 +186,7 @@ private struct CodableNestedModel: FetchableRecord, Codable { var t3: T3? var t5count: Int - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) static func modernAll() -> some FetchRequest { all() } diff --git a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift index 64762126b0..d3c598488c 100644 --- a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift @@ -41,7 +41,7 @@ private class MinimalNonOptionalPrimaryKeySingle: Record, Hashable { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension MinimalNonOptionalPrimaryKeySingle: Identifiable { } class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { @@ -473,7 +473,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [String] = [] let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) @@ -512,7 +512,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) @@ -550,7 +550,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) @@ -585,7 +585,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.fetchOne(db, id: record.id)! XCTAssertTrue(fetchedRecord.id == record.id) @@ -613,7 +613,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { _ = try MinimalNonOptionalPrimaryKeySingle.find(db, key: "missing") XCTFail("Expected RecordError") @@ -653,7 +653,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [String] = [] let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) @@ -692,7 +692,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) @@ -730,7 +730,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [String] = [] let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) @@ -765,7 +765,7 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.filter(id: record.id).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift index 31c1d684fc..9b4dc91874 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift @@ -45,7 +45,7 @@ class MinimalRowID : Record, Hashable { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension MinimalRowID: Identifiable { } class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { @@ -507,7 +507,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let cursor = try MinimalRowID.fetchCursor(db, ids: ids) @@ -546,7 +546,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) @@ -584,7 +584,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) @@ -619,7 +619,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let fetchedRecord = try MinimalRowID.fetchOne(db, id: record.id!)! XCTAssertTrue(fetchedRecord.id == record.id) @@ -650,7 +650,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { _ = try MinimalRowID.find(db, id: -1) XCTFail("Expected RecordError") @@ -693,7 +693,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) @@ -732,7 +732,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) @@ -770,7 +770,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) @@ -805,7 +805,7 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let fetchedRecord = try MinimalRowID.filter(id: record.id!).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift index 0e3eb7fd41..28e0a59131 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift @@ -39,7 +39,7 @@ class MinimalSingle: Record, Hashable { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension MinimalSingle: Identifiable { /// Test non-optional ID type var id: String { UUID! } @@ -531,7 +531,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let UUIDs: [String] = [] let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) @@ -572,7 +572,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) @@ -612,7 +612,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) @@ -648,7 +648,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let fetchedRecord = try MinimalSingle.fetchOne(db, id: record.UUID!)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) @@ -677,7 +677,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { _ = try MinimalSingle.find(db, id: "missing") XCTFail("Expected RecordError") @@ -719,7 +719,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let UUIDs: [String] = [] let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) @@ -760,7 +760,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) @@ -800,7 +800,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let UUIDs: [String] = [] let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) @@ -836,7 +836,7 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let fetchedRecord = try MinimalSingle.filter(id: record.UUID!).fetchOne(db)! XCTAssertTrue(fetchedRecord.UUID == record.UUID) diff --git a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift index 398f2d9909..b16e2fc3f4 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift @@ -77,7 +77,7 @@ private class Person : Record, Hashable { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Person: Identifiable { } class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { @@ -599,7 +599,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let cursor = try Person.fetchCursor(db, ids: ids) @@ -638,7 +638,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.fetchAll(db, ids: ids) @@ -676,7 +676,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.fetchSet(db, ids: ids) @@ -714,7 +714,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let fetchedRecord = try Person.fetchOne(db, id: record.id!)! XCTAssertTrue(fetchedRecord.id == record.id) @@ -751,7 +751,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { _ = try Person.find(db, id: -1) XCTFail("Expected RecordError") @@ -797,7 +797,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let cursor = try Person.filter(ids: ids).fetchCursor(db) @@ -836,7 +836,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) @@ -874,7 +874,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let ids: [Int64] = [] let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) @@ -912,7 +912,7 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { do { let fetchedRecord = try Person.filter(id: record.id!).fetchOne(db)! XCTAssertTrue(fetchedRecord.id == record.id) diff --git a/Tests/GRDBTests/SharedValueObservationTests.swift b/Tests/GRDBTests/SharedValueObservationTests.swift index 4a61b1bb10..f794c5906e 100644 --- a/Tests/GRDBTests/SharedValueObservationTests.swift +++ b/Tests/GRDBTests/SharedValueObservationTests.swift @@ -120,7 +120,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_immediate_publisher() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -397,7 +397,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_async_publisher() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -525,7 +525,7 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), []) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_task_observationLifetime() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -622,7 +622,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_task_publisher() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -656,7 +656,7 @@ class SharedValueObservationTests: GRDBTestCase { } #endif - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func test_task_whileObserved() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -753,7 +753,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_error_recovery_observationLifetime() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -811,7 +811,7 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_error_recovery_whileObserved() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Combine is not available") } @@ -867,7 +867,7 @@ class SharedValueObservationTests: GRDBTestCase { } #endif - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_mainQueue() async throws { let dbQueue = try makeDatabaseQueue() try await dbQueue.write { db in @@ -887,7 +887,7 @@ class SharedValueObservationTests: GRDBTestCase { } } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_task() async throws { let dbQueue = try makeDatabaseQueue() try await dbQueue.write { db in diff --git a/Tests/GRDBTests/TableDefinitionTests.swift b/Tests/GRDBTests/TableDefinitionTests.swift index 26b7ba31b1..12b666ca1a 100644 --- a/Tests/GRDBTests/TableDefinitionTests.swift +++ b/Tests/GRDBTests/TableDefinitionTests.swift @@ -805,11 +805,6 @@ class TableDefinitionTests: GRDBTestCase { guard sqlite3_libversion_number() >= 3025000 else { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } - #if !GRDBCUSTOMSQLITE && !GRDBCIPHER - guard #available(tvOS 13, *) else { - throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") - } - #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "test") { t in @@ -833,11 +828,6 @@ class TableDefinitionTests: GRDBTestCase { guard sqlite3_libversion_number() >= 3025000 else { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } - #if !GRDBCUSTOMSQLITE && !GRDBCIPHER - guard #available(tvOS 13, *) else { - throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") - } - #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "test") { t in diff --git a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift index 0c28204e88..4021373d54 100644 --- a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift @@ -357,7 +357,7 @@ class TableRecordQueryInterfaceRequestTests: GRDBTestCase { } func testExistsIdentifiable() throws { - guard #available(macOS 10.15, tvOS 13, *) else { + guard #available(macOS 10.15, *) else { throw XCTSkip("Identifiable is not available") } diff --git a/Tests/GRDBTests/TableRecordDeleteTests.swift b/Tests/GRDBTests/TableRecordDeleteTests.swift index eb1b3f479a..7dedbab24a 100644 --- a/Tests/GRDBTests/TableRecordDeleteTests.swift +++ b/Tests/GRDBTests/TableRecordDeleteTests.swift @@ -6,7 +6,7 @@ private struct Hacker : TableRecord { var id: Int64? // Optional } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Hacker: Identifiable { } private struct Person : Codable, PersistableRecord, FetchableRecord, Hashable { @@ -16,7 +16,7 @@ private struct Person : Codable, PersistableRecord, FetchableRecord, Hashable { var email: String } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Person: Identifiable { } private struct Citizenship : TableRecord { @@ -46,7 +46,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertTrue(deleted) XCTAssertEqual(try Hacker.fetchCount(db), 0) - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [1, "Arthur"]) try XCTAssertFalse(Hacker.deleteOne(db, id: nil)) deleted = try Hacker.deleteOne(db, id: 1) @@ -62,7 +62,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(deletedCount, 2) XCTAssertEqual(try Hacker.fetchCount(db), 1) - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [2, "Barbara"]) try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [3, "Craig"]) let deletedCount = try Hacker.deleteAll(db, ids: [2, 3, 4]) @@ -85,7 +85,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertTrue(deleted) XCTAssertEqual(try Person.fetchCount(db), 0) - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [1, "Arthur", "arthur@example.com"]) deleted = try Person.deleteOne(db, id: 1) XCTAssertTrue(deleted) @@ -100,7 +100,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(deletedCount, 2) XCTAssertEqual(try Person.fetchCount(db), 1) - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [2, "Barbara", "barbara@example.com"]) try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [3, "Craig", "craig@example.com"]) let deletedCount = try Person.deleteAll(db, ids: [2, 3, 4]) @@ -190,7 +190,7 @@ class TableRecordDeleteTests: GRDBTestCase { try Person.filter(keys: [1, 2]).deleteAll(db) XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2)") - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { try Person.filter(id: 1).deleteAll(db) XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1") @@ -279,7 +279,7 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2) RETURNING *") #if GRDBCUSTOMSQLITE || GRDBCIPHER - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { _ = try Person.filter(id: 1).deleteAndFetchCursor(db).next() XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1 RETURNING *") @@ -364,7 +364,7 @@ class TableRecordDeleteTests: GRDBTestCase { } } - @available(macOS 10.15, tvOS 13, *) // Identifiable + @available(macOS 10.15, *) // Identifiable func testRequestDeleteAndFetchIds() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { diff --git a/Tests/GRDBTests/TableRecordUpdateTests.swift b/Tests/GRDBTests/TableRecordUpdateTests.swift index b5a249eb84..787c1fb47f 100644 --- a/Tests/GRDBTests/TableRecordUpdateTests.swift +++ b/Tests/GRDBTests/TableRecordUpdateTests.swift @@ -17,7 +17,7 @@ private struct Player: Codable, PersistableRecord, FetchableRecord, Hashable { } } -@available(macOS 10.15, tvOS 13, *) +@available(macOS 10.15, *) extension Player: Identifiable { } private enum Columns: String, ColumnExpression { @@ -56,7 +56,7 @@ class TableRecordUpdateTests: GRDBTestCase { UPDATE "player" SET "score" = 0 WHERE "id" IN (1, 2) """) - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { try Player.filter(id: 1).updateAll(db, assignment) XCTAssertEqual(self.lastSQLQuery, """ UPDATE "player" SET "score" = 0 WHERE "id" = 1 diff --git a/Tests/GRDBTests/TableTests.swift b/Tests/GRDBTests/TableTests.swift index 7576902fdc..34f732b5f7 100644 --- a/Tests/GRDBTests/TableTests.swift +++ b/Tests/GRDBTests/TableTests.swift @@ -117,7 +117,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { struct Player: Identifiable { var id: Int64 } let t = Table("player") @@ -129,7 +129,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { struct Player: Identifiable { var id: Int64? } let t = Table("player") @@ -806,7 +806,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { // Non-optional ID struct Country: Identifiable { var id: String } @@ -821,7 +821,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { // Optional ID struct Country: Identifiable { var id: String? } @@ -920,7 +920,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { // Non-optional ID struct Country: Identifiable { var id: String } @@ -930,7 +930,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, tvOS 13, *) { + if #available(macOS 10.15, *) { // Optional ID struct Country: Identifiable { var id: String? } diff --git a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift index dff05d4c5e..0e3ae4432f 100644 --- a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift +++ b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift @@ -105,7 +105,7 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { } } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testTupleObservation() throws { // Here we just test that user can destructure an observed tuple. // I'm completely paranoid about tuple destructuring - I can't wrap my @@ -120,7 +120,7 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { onChange: { (int: Int, string: String) in }) // <- destructure } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testVaryingRegionTrackingImmediateScheduling() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index 142ce7249e..c72b90c5ea 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -5,7 +5,7 @@ import Dispatch class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testStartFromAnyDatabaseReader(reader: any DatabaseReader) { _ = ValueObservation .trackingConstantRegion { _ in } @@ -14,7 +14,7 @@ class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testStartFromAnyDatabaseWriter(writer: any DatabaseWriter) { _ = ValueObservation .trackingConstantRegion { _ in } @@ -23,7 +23,7 @@ class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testValuesFromAnyDatabaseWriter(writer: any DatabaseWriter) { func observe( fetch: @escaping @Sendable (Database) throws -> T @@ -55,7 +55,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testErrorCompletesTheObservation() throws { struct TestError: Error { } @@ -105,7 +105,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testViewOptimization() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { @@ -143,7 +143,7 @@ class ValueObservationTests: GRDBTestCase { } } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testPragmaTableOptimization() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { @@ -179,7 +179,7 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Constant Explicit Region - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testTrackingExplicitRegion() throws { class TestStream: TextOutputStream { private var stringsMutex: Mutex<[String]> = Mutex([]) @@ -620,7 +620,7 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Cancellation - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testCancellableLifetime() throws { // We need something to change let dbQueue = try makeDatabaseQueue() @@ -666,7 +666,7 @@ class ValueObservationTests: GRDBTestCase { XCTAssertEqual(changesCountMutex.load(), 2) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testCancellableExplicitCancellation() throws { // We need something to change let dbQueue = try makeDatabaseQueue() @@ -804,7 +804,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testIssue1550() throws { func test(_ writer: some DatabaseWriter) throws { try writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } @@ -852,7 +852,7 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testIssue1209() throws { func test(_ dbWriter: some DatabaseWriter) throws { try dbWriter.write { @@ -903,7 +903,7 @@ class ValueObservationTests: GRDBTestCase { } // MARK: - Main Actor - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) @MainActor func test_mainActor_observation() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -939,7 +939,7 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Async Await - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_values_prefix() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -977,7 +977,7 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_values_break() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -1019,7 +1019,7 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testAsyncAwait_values_cancelled() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -1201,7 +1201,7 @@ class ValueObservationTests: GRDBTestCase { } // Regression test for - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testIssue1362() throws { func test(_ writer: some DatabaseWriter) throws { try writer.write { try $0.execute(sql: "CREATE TABLE s(id INTEGER PRIMARY KEY AUTOINCREMENT)") } @@ -1292,7 +1292,7 @@ class ValueObservationTests: GRDBTestCase { } // Regression test for - @available(macOS 10.15, tvOS 13, *) + @available(macOS 10.15, *) func testIssue1383_async() throws { do { let dbPool = try makeDatabasePool(filename: "test") From 46e15616a7d1f7efeb9e3c317ad91b21a637286c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 13:54:04 +0200 Subject: [PATCH 115/160] [BREAKING] macOS 10.15+ --- GRDB.swift.podspec | 2 +- GRDB/Core/Database+Statements.swift | 30 +-- GRDB/Core/Database.swift | 9 +- GRDB/Core/DatabasePool.swift | 4 - GRDB/Core/DatabasePublishers.swift | 1 - GRDB/Core/DatabaseQueue.swift | 4 - GRDB/Core/DatabaseReader.swift | 7 - GRDB/Core/DatabaseRegionObservation.swift | 3 - GRDB/Core/DatabaseSnapshot.swift | 2 - GRDB/Core/DatabaseSnapshotPool.swift | 2 - GRDB/Core/DatabaseWriter.swift | 15 -- GRDB/Core/SerializedDatabase.swift | 4 - GRDB/Core/Statement.swift | 14 +- GRDB/Dump/DumpFormats/JSONDumpFormat.swift | 4 +- GRDB/FTS/FTS5.swift | 40 ---- GRDB/Fixits.swift | 3 - GRDB/JSON/SQLJSONExpressible.swift | 10 +- GRDB/JSON/SQLJSONFunctions.swift | 40 ++-- GRDB/Migration/DatabaseMigrator.swift | 2 - .../Request/QueryInterfaceRequest.swift | 1 - .../Request/RequestProtocols.swift | 1 - GRDB/QueryInterface/SQL/SQLExpression.swift | 2 +- GRDB/QueryInterface/SQL/Table.swift | 3 - .../TableRecord+QueryInterfaceRequest.swift | 1 - GRDB/Record/FetchableRecord+TableRecord.swift | 2 - GRDB/Record/TableRecord.swift | 3 - GRDB/Utils/OnDemandFuture.swift | 2 - GRDB/Utils/ReceiveValuesOn.swift | 3 - .../SharedValueObservation.swift | 2 - GRDB/ValueObservation/ValueObservation.swift | 5 - .../ValueObservationScheduler.swift | 4 - Package.swift | 2 +- README.md | 2 +- SQLiteCustom/GRDBDeploymentTarget.xcconfig | 2 +- Support/GRDBDeploymentTarget.xcconfig | 2 +- .../GRDBTests.xcodeproj/project.pbxproj | 4 +- Tests/CocoaPods/SQLCipher3/Podfile | 4 +- .../GRDBTests.xcodeproj/project.pbxproj | 4 +- Tests/CocoaPods/SQLCipher4/Podfile | 4 +- .../AvailableElements.swift | 1 - .../PublisherExpectations/Finished.swift | 1 - .../PublisherExpectations/Inverted.swift | 1 - .../PublisherExpectations/Map.swift | 2 - .../PublisherExpectations/Next.swift | 1 - .../PublisherExpectations/NextOne.swift | 1 - .../PublisherExpectations/Prefix.swift | 1 - .../PublisherExpectations/Recording.swift | 1 - Tests/CombineExpectations/Recorder.swift | 4 - .../CustomSQLite.xcodeproj/project.pbxproj | 4 +- .../DatabaseReaderReadPublisherTests.swift | 24 -- ...abaseRegionObservationPublisherTests.swift | 8 - .../DatabaseWriterWritePublisherTests.swift | 52 ---- Tests/GRDBCombineTests/Support.swift | 3 - .../ValueObservationPublisherTests.swift | 45 ---- Tests/GRDBTests/AsyncSemaphore.swift | 1 - Tests/GRDBTests/DatabaseCursorTests.swift | 62 +++-- .../DatabaseDataEncodingStrategyTests.swift | 10 - .../DatabaseDateEncodingStrategyTests.swift | 10 - Tests/GRDBTests/DatabaseDumpTests.swift | 4 - Tests/GRDBTests/DatabaseMigratorTests.swift | 24 -- Tests/GRDBTests/DatabaseReaderTests.swift | 11 - .../DatabaseRegionObservationTests.swift | 5 +- .../GRDBTests/DatabaseSnapshotPoolTests.swift | 1 - .../DatabaseUUIDEncodingStrategyTests.swift | 10 - Tests/GRDBTests/DatabaseWriterTests.swift | 18 -- Tests/GRDBTests/JSONColumnTests.swift | 4 +- Tests/GRDBTests/JSONExpressionsTests.swift | 50 ++-- Tests/GRDBTests/JoinSupportTests.swift | 3 - ...imalNonOptionalPrimaryKeySingleTests.swift | 189 +++++++-------- .../RecordMinimalPrimaryKeyRowIDTests.swift | 207 ++++++++-------- .../RecordMinimalPrimaryKeySingleTests.swift | 189 +++++++-------- .../RecordPrimaryKeyHiddenRowIDTests.swift | 225 ++++++++---------- .../SharedValueObservationTests.swift | 24 -- ...bleRecord+QueryInterfaceRequestTests.swift | 4 - Tests/GRDBTests/TableRecordDeleteTests.swift | 85 +++---- Tests/GRDBTests/TableRecordUpdateTests.swift | 15 +- Tests/GRDBTests/TableTests.swift | 12 +- ...ValueObservationRegionRecordingTests.swift | 2 - Tests/GRDBTests/ValueObservationTests.swift | 17 -- .../GRDBProfiling.xcodeproj/project.pbxproj | 4 +- Tests/SPM/PlainPackage/Package.swift | 6 + .../Plain.xcodeproj/project.pbxproj | 4 +- 82 files changed, 538 insertions(+), 1056 deletions(-) diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index de695d4afa..dcfc881e78 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -11,7 +11,7 @@ Pod::Spec.new do |s| s.swift_versions = ['5.10'] s.ios.deployment_target = '13.0' - s.osx.deployment_target = '10.13' + s.osx.deployment_target = '10.15' s.watchos.deployment_target = '7.0' s.tvos.deployment_target = '13.0' s.default_subspec = 'standard' diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index 1b2806b3ba..46038a83b5 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -492,17 +492,15 @@ extension Database { // and throws the user-provided cancelled commit error. try observationBroker?.statementDidFail(statement) - if #available(macOS 10.15, *) { - switch ResultCode(rawValue: resultCode) { - case .SQLITE_INTERRUPT, .SQLITE_ABORT: - if suspensionMutex.load().isCancelled { - // The only error that a user sees when a Task is cancelled - // is CancellationError. - throw CancellationError() - } - default: - break + switch ResultCode(rawValue: resultCode) { + case .SQLITE_INTERRUPT, .SQLITE_ABORT: + if suspensionMutex.load().isCancelled { + // The only error that a user sees when a Task is cancelled + // is CancellationError. + throw CancellationError() } + default: + break } // Throw statement failure @@ -554,19 +552,7 @@ struct StatementCache { // > time and probably reused many times. // // This looks like a perfect match for cached statements. - // - // However SQLITE_PREPARE_PERSISTENT was only introduced in - // SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20 - #if GRDBCUSTOMSQLITE || GRDBCIPHER let statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT)) - #else - let statement: Statement - if #available(macOS 10.14, *) { // SQLite 3.20+ - statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT)) - } else { - statement = try db.makeStatement(sql: sql) - } - #endif statements[sql] = statement return statement } diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index f6574eeabb..61354847b1 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1199,7 +1199,6 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// will throw `CancellationError`, until `uncancel()` is called. /// /// This method can be called from any thread. - @available(macOS 10.15, *) func cancel() { let needsInterrupt = suspensionMutex.withLock { suspension in if suspension.isCancelled { @@ -1216,7 +1215,6 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib } /// Undo `cancel()`. - @available(macOS 10.15, *) func uncancel() { suspensionMutex.withLock { $0.isCancelled = false @@ -1320,12 +1318,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib break case .cancel: - if #available(macOS 10.15, *) { - throw CancellationError() - } else { - // GRDB bug: cancellation is a Swift concurrency feature - fatalError("Can't cancel without support for Swift concurrency") - } + throw CancellationError() case .abort: // Attempt at releasing an eventual lock with ROLLBACk, diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index 74a67b6c47..ce1bae833e 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -351,7 +351,6 @@ extension DatabasePool: DatabaseReader { } } - @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -436,7 +435,6 @@ extension DatabasePool: DatabaseReader { } } - @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -803,7 +801,6 @@ extension DatabasePool: DatabaseWriter { try writer.sync(updates) } - @available(macOS 10.15, *) public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -820,7 +817,6 @@ extension DatabasePool: DatabaseWriter { } } - @available(macOS 10.15, *) public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabasePublishers.swift b/GRDB/Core/DatabasePublishers.swift index 04ca12e84b..c7e7a603f1 100644 --- a/GRDB/Core/DatabasePublishers.swift +++ b/GRDB/Core/DatabasePublishers.swift @@ -1,5 +1,4 @@ #if canImport(Combine) /// A namespace for database Combine publishers. -@available(macOS 10.15, *) public enum DatabasePublishers { } #endif diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index ebf822667b..37b62fde5b 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -233,7 +233,6 @@ extension DatabaseQueue: DatabaseReader { } } - @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -272,7 +271,6 @@ extension DatabaseQueue: DatabaseReader { try writer.sync(value) } - @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -386,7 +384,6 @@ extension DatabaseQueue: DatabaseWriter { try writer.sync(updates) } - @available(macOS 10.15, *) public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -398,7 +395,6 @@ extension DatabaseQueue: DatabaseWriter { try writer.sync(updates) } - @available(macOS 10.15, *) public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index ed7736c6f6..61c8d59e0b 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -216,7 +216,6 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `value`, or /// `CancellationError` if the task is cancelled. - @available(macOS 10.15, *) func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -323,7 +322,6 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `value`, or /// `CancellationError` if the task is cancelled. - @available(macOS 10.15, *) func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -535,7 +533,6 @@ extension DatabaseReader { /// /// - parameter scheduler: A Combine Scheduler. /// - parameter value: A closure which accesses the database. - @available(macOS 10.15, *) public func readPublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, value: @escaping @Sendable (Database) throws -> Output @@ -550,7 +547,6 @@ extension DatabaseReader { } } -@available(macOS 10.15, *) extension DatabasePublishers { /// A publisher that reads from the database. /// @@ -569,7 +565,6 @@ extension DatabasePublishers { } } -@available(macOS 10.15, *) extension Publisher where Failure == Error { fileprivate func eraseToReadPublisher() -> DatabasePublishers.Read { .init(upstream: eraseToAnyPublisher()) @@ -660,7 +655,6 @@ extension AnyDatabaseReader: DatabaseReader { try base.read(value) } - @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -678,7 +672,6 @@ extension AnyDatabaseReader: DatabaseReader { try base.unsafeRead(value) } - @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseRegionObservation.swift b/GRDB/Core/DatabaseRegionObservation.swift index 4d5ab11938..c0e89e4b38 100644 --- a/GRDB/Core/DatabaseRegionObservation.swift +++ b/GRDB/Core/DatabaseRegionObservation.swift @@ -128,7 +128,6 @@ extension DatabaseRegionObservation { } #if canImport(Combine) -@available(macOS 10.15, *) extension DatabaseRegionObservation { // MARK: - Publishing Impactful Transactions @@ -140,7 +139,6 @@ extension DatabaseRegionObservation { /// /// Do not reschedule the publisher with `receive(on:options:)` or any /// `Publisher` method that schedules publisher elements. - @available(macOS 10.15, *) public func publisher(in writer: some DatabaseWriter) -> DatabasePublishers.DatabaseRegion { DatabasePublishers.DatabaseRegion(self, in: writer) } @@ -186,7 +184,6 @@ private class DatabaseRegionObserver: TransactionObserver { } #if canImport(Combine) -@available(macOS 10.15, *) extension DatabasePublishers { /// A publisher that tracks transactions that modify a database region. /// diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 36b131cea8..7609a3433f 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -151,7 +151,6 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { try reader.sync(block) } - @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -173,7 +172,6 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { // We can't provide this as a default implementation in // `DatabaseSnapshotReader`, because of // . - @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index 826c7d0f68..20398c5562 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -293,7 +293,6 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { } } - @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -353,7 +352,6 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { // We can't provide this as a default implementation in // `DatabaseSnapshotReader`, because of // . - @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index b1928b14ae..29d39d2ac3 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -131,7 +131,6 @@ public protocol DatabaseWriter: DatabaseReader { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. - @available(macOS 10.15, *) func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -217,7 +216,6 @@ public protocol DatabaseWriter: DatabaseReader { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. - @available(macOS 10.15, *) func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T @@ -649,7 +647,6 @@ extension DatabaseWriter { /// - throws: Any ``DatabaseError`` that happens while establishing the /// database access, or the error thrown by `updates`, or /// `CancellationError` if the task is cancelled. - @available(macOS 10.15, *) public func write( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -666,7 +663,6 @@ extension DatabaseWriter { /// Erase the database: delete all content, drop all tables, etc. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - @available(macOS 10.15, *) public func erase() async throws { try await writeWithoutTransaction { try $0.erase() } } @@ -677,7 +673,6 @@ extension DatabaseWriter { /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// Related SQLite documentation: - @available(macOS 10.15, *) public func vacuum() async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM") } } @@ -694,7 +689,6 @@ extension DatabaseWriter { /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database - @available(macOS 10.15, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -709,7 +703,6 @@ extension DatabaseWriter { /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database - @available(macOS 10.15, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -754,7 +747,6 @@ extension DatabaseWriter { /// /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which accesses the database. - @available(macOS 10.15, *) public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, updates: @escaping @Sendable (Database) throws -> Output @@ -818,7 +810,6 @@ extension DatabaseWriter { /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which writes in the database. /// - parameter value: A closure which reads from the database. - @available(macOS 10.15, *) public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, updates: @escaping @Sendable (Database) throws -> T, @@ -848,7 +839,6 @@ extension DatabaseWriter { } } -@available(macOS 10.15, *) extension DatabasePublishers { /// A publisher that writes into the database. /// @@ -867,7 +857,6 @@ extension DatabasePublishers { } } -@available(macOS 10.15, *) extension Publisher where Failure == Error { fileprivate func eraseToWritePublisher() -> DatabasePublishers.Write { .init(upstream: self.eraseToAnyPublisher()) @@ -911,7 +900,6 @@ extension AnyDatabaseWriter: DatabaseReader { try base.read(value) } - @available(macOS 10.15, *) public func read( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -929,7 +917,6 @@ extension AnyDatabaseWriter: DatabaseReader { try base.unsafeRead(value) } - @available(macOS 10.15, *) public func unsafeRead( _ value: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -964,7 +951,6 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.writeWithoutTransaction(updates) } - @available(macOS 10.15, *) public func writeWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -976,7 +962,6 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.barrierWriteWithoutTransaction(updates) } - @available(macOS 10.15, *) public func barrierWriteWithoutTransaction( _ updates: @escaping @Sendable (Database) throws -> T ) async throws -> T { diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index 6668360116..b794fa63ea 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -244,7 +244,6 @@ final class SerializedDatabase { } /// Asynchrously executes the block. - @available(macOS 10.15, *) func execute( _ block: @escaping @Sendable (Database) throws -> T ) async throws -> T { @@ -309,7 +308,6 @@ extension SerializedDatabase: @unchecked Sendable { } // MARK: - Task Cancellation Support -@available(macOS 10.15, *) enum DatabaseAccessCancellationState: @unchecked Sendable { // @unchecked Sendable because database is only accessed from its // dispatch queue. @@ -319,7 +317,6 @@ enum DatabaseAccessCancellationState: @unchecked Sendable { case expired } -@available(macOS 10.15, *) typealias CancellableDatabaseAccess = Mutex /// Supports Task cancellation in async database accesses. @@ -341,7 +338,6 @@ typealias CancellableDatabaseAccess = Mutex /// } /// } /// ``` -@available(macOS 10.15, *) extension CancellableDatabaseAccess: DatabaseCancellable { convenience init() { self.init(.notConnected) diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index f2b85776d6..f2792af315 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -139,21 +139,9 @@ public final class Statement { authorizer.reset() var sqliteStatement: SQLiteStatement? = nil - let code: CInt - // sqlite3_prepare_v3 was introduced in SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20 -#if GRDBCUSTOMSQLITE || GRDBCIPHER - code = sqlite3_prepare_v3( + let code = sqlite3_prepare_v3( database.sqliteConnection, statementStart, -1, prepFlags, &sqliteStatement, statementEnd) -#else - if #available(macOS 10.14, tvOS 12, *) { // SQLite 3.20+ - code = sqlite3_prepare_v3( - database.sqliteConnection, statementStart, -1, prepFlags, - &sqliteStatement, statementEnd) - } else { - code = sqlite3_prepare_v2(database.sqliteConnection, statementStart, -1, &sqliteStatement, statementEnd) - } -#endif guard code == SQLITE_OK else { throw DatabaseError( diff --git a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift index f24eaa1216..d2c2525a10 100644 --- a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift @@ -52,9 +52,7 @@ public struct JSONDumpFormat: Sendable { public static var defaultEncoder: JSONEncoder { // This encoder MUST NOT CHANGE, because some people rely on this format. let encoder = JSONEncoder() - if #available(macOS 10.15.0, *) { - encoder.outputFormatting = .withoutEscapingSlashes - } + encoder.outputFormatting = .withoutEscapingSlashes encoder.nonConformingFloatEncodingStrategy = .convertToString( positiveInfinity: "inf", negativeInfinity: "-inf", diff --git a/GRDB/FTS/FTS5.swift b/GRDB/FTS/FTS5.swift index e6dce2abbe..a87b184b1d 100644 --- a/GRDB/FTS/FTS5.swift +++ b/GRDB/FTS/FTS5.swift @@ -117,46 +117,6 @@ public struct FTS5 { /// /// Related SQLite documentation: public static func api(_ db: Database) -> UnsafePointer { - // Access to FTS5 is one of the rare SQLite api which was broken in - // SQLite 3.20.0+, for security reasons: - // - // Starting SQLite 3.20.0+, we need to use the new sqlite3_bind_pointer api. - // The previous way to access FTS5 does not work any longer. - // - // So let's see which SQLite version we are linked against: - - #if GRDBCUSTOMSQLITE || GRDBCIPHER - // GRDB is linked against SQLCipher or a custom SQLite build: SQLite 3.20.0 or more. - return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer) - #else - // GRDB is linked against the system SQLite. - if #available(macOS 10.14, tvOS 12, *) { // SQLite 3.20+ - return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer) - } else { - return api_v1(db) - } - #endif - } - - private static func api_v1(_ db: Database) -> UnsafePointer { - guard let data = try! Data.fetchOne(db, sql: "SELECT fts5()") else { - fatalError("FTS5 is not available") - } - return data.withUnsafeBytes { - $0.bindMemory(to: UnsafePointer.self).first! - } - } - - // Technique given by Jordan Rose: - // https://forums.swift.org/t/c-interoperability-combinations-of-library-and-os-versions/14029/4 - private static func api_v2( - _ db: Database, - // swiftlint:disable:next line_length - _ sqlite3_prepare_v3: @convention(c) (OpaquePointer?, UnsafePointer?, CInt, CUnsignedInt, UnsafeMutablePointer?, UnsafeMutablePointer?>?) -> CInt, - // swiftlint:disable:next line_length - _ sqlite3_bind_pointer: @convention(c) (OpaquePointer?, CInt, UnsafeMutableRawPointer?, UnsafePointer?, (@convention(c) (UnsafeMutableRawPointer?) -> Void)?) -> CInt) - -> UnsafePointer - { var statement: SQLiteStatement? = nil var api: UnsafePointer? = nil let type: StaticString = "fts5_api_ptr" diff --git a/GRDB/Fixits.swift b/GRDB/Fixits.swift index 453741c455..4851095638 100644 --- a/GRDB/Fixits.swift +++ b/GRDB/Fixits.swift @@ -119,7 +119,6 @@ extension PersistableRecord { public func performSave(_ db: Database) throws { preconditionFailure() } } -@available(macOS 10.15, *) extension QueryInterfaceRequest where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public func selectID() -> QueryInterfaceRequest { preconditionFailure() } @@ -144,13 +143,11 @@ extension SelectionRequest { @available(*, unavailable, renamed: "SQLExpression.AssociativeBinaryOperator") public typealias SQLAssociativeBinaryOperator = SQLExpression.AssociativeBinaryOperator -@available(macOS 10.15, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public func selectID() -> QueryInterfaceRequest { preconditionFailure() } } -@available(macOS 10.15, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") public static func selectID() -> QueryInterfaceRequest { preconditionFailure() } diff --git a/GRDB/JSON/SQLJSONExpressible.swift b/GRDB/JSON/SQLJSONExpressible.swift index 5bde4eefe7..e2c402283e 100644 --- a/GRDB/JSON/SQLJSONExpressible.swift +++ b/GRDB/JSON/SQLJSONExpressible.swift @@ -312,7 +312,7 @@ extension SQLJSONExpressible { /// Related SQL documentation: /// /// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public func jsonExtract(atPath path: some SQLExpressible) -> SQLExpression { Database.jsonExtract(self, atPath: path) } @@ -334,7 +334,7 @@ extension SQLJSONExpressible { /// Related SQL documentation: /// /// - parameter paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public func jsonExtract( atPaths paths: some Collection ) -> SQLExpression { @@ -385,7 +385,7 @@ extension SQLJSONExpressible { // /// ``` // /// // /// Related SQLite documentation: -// @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS +// @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS // public func jsonPatch( // with patch: some SQLExpressible) // -> ColumnAssignment @@ -408,7 +408,7 @@ extension SQLJSONExpressible { // /// // /// - Parameters: // /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). -// @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS +// @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS // public func jsonRemove(atPath path: some SQLExpressible) -> ColumnAssignment { // .init(columnName: name, value: Database.jsonRemove(self, atPath: path)) // } @@ -428,7 +428,7 @@ extension SQLJSONExpressible { // /// // /// - Parameters: // /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). -// @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS +// @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS // public func jsonRemove( // atPaths paths: some Collection // ) -> ColumnAssignment { diff --git a/GRDB/JSON/SQLJSONFunctions.swift b/GRDB/JSON/SQLJSONFunctions.swift index ee360aa6ef..7b3d3d16ef 100644 --- a/GRDB/JSON/SQLJSONFunctions.swift +++ b/GRDB/JSON/SQLJSONFunctions.swift @@ -431,7 +431,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func json(_ value: some SQLExpressible) -> SQLExpression { .function("JSON", [value.sqlExpression]) } @@ -446,7 +446,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonArray( _ values: some Collection ) -> SQLExpression { @@ -463,7 +463,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonArray( _ values: some Collection ) -> SQLExpression { @@ -481,7 +481,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonArrayLength(_ value: some SQLExpressible) -> SQLExpression { .function("JSON_ARRAY_LENGTH", [value.sqlExpression]) } @@ -501,7 +501,7 @@ extension Database { /// - Parameters: /// - value: A JSON array. /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonArrayLength( _ value: some SQLExpressible, atPath path: some SQLExpressible) @@ -524,7 +524,7 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonExtract(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { .function("JSON_EXTRACT", [value.sqlExpression, path.sqlExpression]) } @@ -543,7 +543,7 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonExtract( _ value: some SQLExpressible, atPaths paths: some Collection @@ -566,7 +566,7 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonInsert( _ value: some SQLExpressible, _ assignments: some Collection<(key: String, value: any SQLExpressible)> @@ -591,7 +591,7 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonReplace( _ value: some SQLExpressible, _ assignments: some Collection<(key: String, value: any SQLExpressible)> @@ -616,7 +616,7 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonSet( _ value: some SQLExpressible, _ assignments: some Collection<(key: String, value: any SQLExpressible)> @@ -649,7 +649,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonObject( _ elements: some Collection<(key: String, value: any SQLExpressible)> ) -> SQLExpression { @@ -668,7 +668,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonPatch( _ value: some SQLExpressible, with patch: some SQLExpressible) @@ -691,7 +691,7 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonRemove(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { .function("JSON_REMOVE", [value.sqlExpression, path.sqlExpression]) } @@ -710,7 +710,7 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonRemove( _ value: some SQLExpressible, atPaths paths: some Collection @@ -728,7 +728,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonType(_ value: some SQLExpressible) -> SQLExpression { .function("JSON_TYPE", [value.sqlExpression]) } @@ -747,7 +747,7 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonType(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { .function("JSON_TYPE", [value.sqlExpression, path.sqlExpression]) } @@ -762,7 +762,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonIsValid(_ value: some SQLExpressible) -> SQLExpression { .function("JSON_VALID", [value.sqlExpression]) } @@ -780,7 +780,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonQuote(_ value: some SQLExpressible) -> SQLExpression { .function("JSON_QUOTE", [value.sqlExpression.jsonBuilderExpression]) } @@ -798,7 +798,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonGroupArray( _ value: some SQLExpressible, filter: (any SQLSpecificExpressible)? = nil) @@ -828,7 +828,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonGroupObject( key: some SQLExpressible, value: some SQLExpressible, diff --git a/GRDB/Migration/DatabaseMigrator.swift b/GRDB/Migration/DatabaseMigrator.swift index 11c2ae92e4..d92635c294 100644 --- a/GRDB/Migration/DatabaseMigrator.swift +++ b/GRDB/Migration/DatabaseMigrator.swift @@ -496,7 +496,6 @@ extension DatabaseMigrator { /// - parameter writer: A DatabaseWriter. /// where migrations should apply. /// - parameter scheduler: A Combine Scheduler. - @available(macOS 10.15, *) public func migratePublisher( _ writer: some DatabaseWriter, receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main) @@ -514,7 +513,6 @@ extension DatabaseMigrator { } } -@available(macOS 10.15, *) extension DatabasePublishers { /// A publisher that migrates a database. /// diff --git a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift index ab6a1047ae..56451b4384 100644 --- a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift @@ -646,7 +646,6 @@ extension QueryInterfaceRequest { /// - parameter db: A database connection. /// - returns: A set of deleted ids. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - @available(macOS 10.15, *) // Identifiable public func deleteAndFetchIds(_ db: Database) throws -> Set where RowDecoder: TableRecord & Identifiable, diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index c265069f95..6059e56d38 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -631,7 +631,6 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { } } -@available(macOS 10.15, *) extension TableRequest where Self: FilteredRequest, Self: TypedRequest, diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift index 6203aaef54..58acaf7c41 100644 --- a/GRDB/QueryInterface/SQL/SQLExpression.swift +++ b/GRDB/QueryInterface/SQL/SQLExpression.swift @@ -2106,7 +2106,7 @@ extension SQLExpression { } } #else - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS /// Returns an expression suitable in JSON building contexts. var jsonBuilderExpression: SQLExpression { switch preferredJSONInterpretation { diff --git a/GRDB/QueryInterface/SQL/Table.swift b/GRDB/QueryInterface/SQL/Table.swift index cdaf32b85c..a96714bc15 100644 --- a/GRDB/QueryInterface/SQL/Table.swift +++ b/GRDB/QueryInterface/SQL/Table.swift @@ -722,7 +722,6 @@ extension Table { } } -@available(macOS 10.15, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { /// Returns a request filtered by primary key. /// @@ -1545,7 +1544,6 @@ extension Table { } } -@available(macOS 10.15, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible @@ -1686,7 +1684,6 @@ extension Table { } } -@available(macOS 10.15, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible diff --git a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift index fa9acff667..e86156f24e 100644 --- a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift @@ -604,7 +604,6 @@ extension TableRecord { } } -@available(macOS 10.15, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns a request filtered by primary key. /// diff --git a/GRDB/Record/FetchableRecord+TableRecord.swift b/GRDB/Record/FetchableRecord+TableRecord.swift index 18ea1eeb23..b755110640 100644 --- a/GRDB/Record/FetchableRecord+TableRecord.swift +++ b/GRDB/Record/FetchableRecord+TableRecord.swift @@ -216,7 +216,6 @@ extension FetchableRecord where Self: TableRecord { } } -@available(macOS 10.15, *) extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseValueConvertible { // MARK: Fetching by Single-Column Primary Key @@ -358,7 +357,6 @@ extension FetchableRecord where Self: TableRecord & Hashable { } } -@available(macOS 10.15, *) extension FetchableRecord where Self: TableRecord & Hashable & Identifiable, ID: DatabaseValueConvertible { /// Returns a set of records identified by their primary keys. /// diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index 46794444f1..295b13785f 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -319,7 +319,6 @@ extension TableRecord { } } -@available(macOS 10.15, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns whether a record exists for this primary key. /// @@ -454,7 +453,6 @@ extension TableRecord { } } -@available(macOS 10.15, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Deletes records identified by their primary keys, and returns the number /// of deleted records. @@ -774,7 +772,6 @@ extension TableRecord where Self: EncodableRecord { } } -@available(macOS 10.15, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns an error for a record that does not exist in the database. /// diff --git a/GRDB/Utils/OnDemandFuture.swift b/GRDB/Utils/OnDemandFuture.swift index e91d8af447..938750d4ab 100644 --- a/GRDB/Utils/OnDemandFuture.swift +++ b/GRDB/Utils/OnDemandFuture.swift @@ -17,7 +17,6 @@ import Foundation /// /// OnDemandFuture also adds Sendable requirements that avoid /// compiler warnings. -@available(macOS 10.15, *) struct OnDemandFuture: Publisher { typealias Promise = @Sendable (Result) -> Void typealias Output = Output @@ -36,7 +35,6 @@ struct OnDemandFuture: Publisher { } } -@available(macOS 10.15, *) private class OnDemandFutureSubscription: Subscription, @unchecked Sendable { // @unchecked because `state` is protected with `lock`. typealias Promise = @Sendable (Result) -> Void diff --git a/GRDB/Utils/ReceiveValuesOn.swift b/GRDB/Utils/ReceiveValuesOn.swift index 5a309e1f50..2df87fc46a 100644 --- a/GRDB/Utils/ReceiveValuesOn.swift +++ b/GRDB/Utils/ReceiveValuesOn.swift @@ -11,7 +11,6 @@ import Foundation /// This scheduling guarantee is used by GRDB in order to be able /// to make promises on the scheduling of database values without surprising /// the users as in . -@available(macOS 10.15, *) struct ReceiveValuesOn: Publisher { typealias Output = Upstream.Output typealias Failure = Upstream.Failure @@ -30,7 +29,6 @@ struct ReceiveValuesOn: Publisher { } } -@available(macOS 10.15, *) private class ReceiveValuesOnSubscription: Subscription, Subscriber where Upstream: Publisher, @@ -211,7 +209,6 @@ where } } -@available(macOS 10.15, *) extension Publisher { /// Specifies the scheduler on which to receive values from the publisher /// diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index 925a67a2c3..1550a619c3 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -292,7 +292,6 @@ public final class SharedValueObservation: @unchecked Sendabl /// print("fresh players: \(players)") /// } /// ``` - @available(macOS 10.15, *) public func publisher() -> DatabasePublishers.Value { DatabasePublishers.Value { onError, onChange in self.start(onError: onError, onChange: onChange) @@ -369,7 +368,6 @@ extension SharedValueObservation { /// print("Fresh players: \(players)") /// } /// ``` - @available(macOS 10.15, *) public func values(bufferingPolicy: AsyncValueObservation.BufferingPolicy = .unbounded) -> AsyncValueObservation { diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index 84d9eda005..8ce4b8abae 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -179,7 +179,6 @@ extension ValueObservation: Refinable { /// - parameter onChange: The closure to execute on receipt of a /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. - @available(macOS 10.15, *) @preconcurrency @MainActor public func start( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationMainActorScheduler = .mainActor, @@ -350,7 +349,6 @@ extension ValueObservation { /// fresh values are dispatched on the cooperative thread pool. /// - parameter bufferingPolicy: see the documntation /// of `AsyncThrowingStream`. - @available(macOS 10.15, *) public func values( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .task, @@ -386,7 +384,6 @@ extension ValueObservation { /// /// You build an `AsyncValueObservation` from ``ValueObservation`` or /// ``SharedValueObservation``. -@available(macOS 10.15, *) public struct AsyncValueObservation: AsyncSequence { public typealias BufferingPolicy = AsyncThrowingStream.Continuation.BufferingPolicy public typealias AsyncIterator = Iterator @@ -488,7 +485,6 @@ extension ValueObservation { /// - parameter scheduler: A ValueObservationScheduler. By default, fresh /// values are dispatched asynchronously on the main dispatch queue. /// - returns: A Combine publisher - @available(macOS 10.15, *) public func publisher( in reader: some DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main)) @@ -505,7 +501,6 @@ extension ValueObservation { } } -@available(macOS 10.15, *) extension DatabasePublishers { /// A publisher that publishes the values of a ``ValueObservation``. /// diff --git a/GRDB/ValueObservation/ValueObservationScheduler.swift b/GRDB/ValueObservation/ValueObservationScheduler.swift index 887ce09cf1..168c7c7062 100644 --- a/GRDB/ValueObservation/ValueObservationScheduler.swift +++ b/GRDB/ValueObservation/ValueObservationScheduler.swift @@ -182,7 +182,6 @@ extension ValueObservationMainActorScheduler where Self == ImmediateValueObserva // MARK: - TaskValueObservationScheduler /// A scheduler that notifies all values on the cooperative thread pool. -@available(macOS 10.15, *) public final class TaskValueObservationScheduler: ValueObservationScheduler { typealias Action = @Sendable () -> Void let continuation: AsyncStream.Continuation @@ -212,7 +211,6 @@ public final class TaskValueObservationScheduler: ValueObservationScheduler { } } -@available(macOS 10.15, *) extension ValueObservationScheduler where Self == TaskValueObservationScheduler { /// A scheduler that notifies all values from a new `Task`. public static var task: TaskValueObservationScheduler { @@ -229,7 +227,6 @@ extension ValueObservationScheduler where Self == TaskValueObservationScheduler // MARK: - DelayedMainActorValueObservationScheduler /// A scheduler that notifies all values on the cooperative thread pool. -@available(macOS 10.15, *) public final class DelayedMainActorValueObservationScheduler: ValueObservationMainActorScheduler { public func immediateInitialValue() -> Bool { false @@ -240,7 +237,6 @@ public final class DelayedMainActorValueObservationScheduler: ValueObservationMa } } -@available(macOS 10.15, *) extension ValueObservationScheduler where Self == DelayedMainActorValueObservationScheduler { /// A scheduler that notifies all values on the main actor. public static var mainActor: DelayedMainActorValueObservationScheduler { diff --git a/Package.swift b/Package.swift index 6b53e53ff4..8d645a1cdb 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ let package = Package( defaultLocalization: "en", // for tests platforms: [ .iOS(.v13), - .macOS(.v10_13), + .macOS(.v10_15), .tvOS(.v13), .watchOS(.v7), ], diff --git a/README.md b/README.md index 530e3d309c..4d5d4e24c4 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ **Latest release**: September 7, 2024 • [version 6.29.3](https://github.com/groue/GRDB.swift/tree/v6.29.3) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md) -**Requirements**: iOS 13.0+ / macOS 10.13+ / tvOS 13.0+ / watchOS 7.0+ • SQLite 3.19.3+ • Swift 6+ / Xcode 16+ +**Requirements**: iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 7.0+ • SQLite 3.20.0+ • Swift 6+ / Xcode 16+ **Contact**: diff --git a/SQLiteCustom/GRDBDeploymentTarget.xcconfig b/SQLiteCustom/GRDBDeploymentTarget.xcconfig index 369f90a510..b4c8e9ed72 100644 --- a/SQLiteCustom/GRDBDeploymentTarget.xcconfig +++ b/SQLiteCustom/GRDBDeploymentTarget.xcconfig @@ -1,5 +1,5 @@ IPHONEOS_DEPLOYMENT_TARGET = 13.0 -MACOSX_DEPLOYMENT_TARGET = 10.13 +MACOSX_DEPLOYMENT_TARGET = 10.15 TVOS_DEPLOYMENT_TARGET = 13.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES diff --git a/Support/GRDBDeploymentTarget.xcconfig b/Support/GRDBDeploymentTarget.xcconfig index a457235eed..76035ab53d 100644 --- a/Support/GRDBDeploymentTarget.xcconfig +++ b/Support/GRDBDeploymentTarget.xcconfig @@ -1,5 +1,5 @@ IPHONEOS_DEPLOYMENT_TARGET = 13.0 -MACOSX_DEPLOYMENT_TARGET = 10.13 +MACOSX_DEPLOYMENT_TARGET = 10.15 TVOS_DEPLOYMENT_TARGET = 13.0 WATCHOS_DEPLOYMENT_TARGET = 7.0 OTHER_SWIFT_FLAGS = $(inherited) -D SQLITE_ENABLE_FTS5 diff --git a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj index d4e67092f7..a872849dd6 100644 --- a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj @@ -1822,7 +1822,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; ONLY_ACTIVE_ARCH = YES; SWIFT_VERSION = 5.0; }; @@ -1856,7 +1856,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/Tests/CocoaPods/SQLCipher3/Podfile b/Tests/CocoaPods/SQLCipher3/Podfile index fbe38136b0..66346dd900 100644 --- a/Tests/CocoaPods/SQLCipher3/Podfile +++ b/Tests/CocoaPods/SQLCipher3/Podfile @@ -1,4 +1,4 @@ -platform :macos, '10.13' +platform :macos, '10.15' use_frameworks! def common @@ -19,7 +19,7 @@ post_install do |installer| target.build_configurations.each do |config| # Workaround for Xcode 14.3+ # https://github.com/CocoaPods/CocoaPods/issues/11839 - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.13' + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.15' config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '3' end end diff --git a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj index 1359a5a85c..64c03419a8 100644 --- a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj @@ -1828,7 +1828,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; ONLY_ACTIVE_ARCH = YES; SWIFT_VERSION = 5.0; }; @@ -1862,7 +1862,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/Tests/CocoaPods/SQLCipher4/Podfile b/Tests/CocoaPods/SQLCipher4/Podfile index 031d49b72e..d6e3762437 100644 --- a/Tests/CocoaPods/SQLCipher4/Podfile +++ b/Tests/CocoaPods/SQLCipher4/Podfile @@ -1,4 +1,4 @@ -platform :macos, '10.13' +platform :macos, '10.15' use_frameworks! def common @@ -19,7 +19,7 @@ post_install do |installer| target.build_configurations.each do |config| # Workaround for Xcode 14.3+ # https://github.com/CocoaPods/CocoaPods/issues/11839 - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.13' + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.15' config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '3' end end diff --git a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift index 7c8656f354..f37f2eb1ec 100644 --- a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift +++ b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the timeout to expire, or /// the recorded publisher to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Finished.swift b/Tests/CombineExpectations/PublisherExpectations/Finished.swift index 6c773f4f3d..c7b7ecf3dc 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Finished.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Finished.swift @@ -17,7 +17,6 @@ import XCTest // try wait(for: recorder.finished.inverted, timeout: 1) // } -@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Inverted.swift b/Tests/CombineExpectations/PublisherExpectations/Inverted.swift index 218402af60..755897823d 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Inverted.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Inverted.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation that fails if the base expectation is fulfilled. /// diff --git a/Tests/CombineExpectations/PublisherExpectations/Map.swift b/Tests/CombineExpectations/PublisherExpectations/Map.swift index 2f55cc60ec..87276c91de 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Map.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Map.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation that transforms the value of a base expectation. /// @@ -20,7 +19,6 @@ extension PublisherExpectations { } } -@available(macOS 10.15, *) extension PublisherExpectation { /// Returns a publisher expectation that transforms the value of the /// base expectation. diff --git a/Tests/CombineExpectations/PublisherExpectations/Next.swift b/Tests/CombineExpectations/PublisherExpectations/Next.swift index 8d511ccd8e..6a27fa7df7 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Next.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Next.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `count` elements, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/NextOne.swift b/Tests/CombineExpectations/PublisherExpectations/NextOne.swift index 6d4869a5c9..65ebe04b12 100644 --- a/Tests/CombineExpectations/PublisherExpectations/NextOne.swift +++ b/Tests/CombineExpectations/PublisherExpectations/NextOne.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// one element, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Prefix.swift b/Tests/CombineExpectations/PublisherExpectations/Prefix.swift index 3089272c9b..c9bae6cf56 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Prefix.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Prefix.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `maxLength` elements, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Recording.swift b/Tests/CombineExpectations/PublisherExpectations/Recording.swift index 59bd76c1bf..1368f42252 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Recording.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Recording.swift @@ -2,7 +2,6 @@ import Combine import XCTest -@available(macOS 10.15, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. diff --git a/Tests/CombineExpectations/Recorder.swift b/Tests/CombineExpectations/Recorder.swift index 153f328a57..bc61f110dd 100644 --- a/Tests/CombineExpectations/Recorder.swift +++ b/Tests/CombineExpectations/Recorder.swift @@ -13,7 +13,6 @@ import XCTest /// /// let elements = try wait(for: recorder.elements, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) -@available(macOS 10.15, *) public class Recorder: Subscriber { public typealias Input = Input public typealias Failure = Failure @@ -287,7 +286,6 @@ public class Recorder: Subscriber { // MARK: - Publisher Expectations -@available(macOS 10.15, *) extension PublisherExpectations { /// The type of the publisher expectation returned by `Recorder.completion`. public typealias Completion = Map, Subscribers.Completion> @@ -302,7 +300,6 @@ extension PublisherExpectations { public typealias Single = Map, Input> } -@available(macOS 10.15, *) extension Recorder { /// Returns a publisher expectation which waits for the timeout to expire, /// or the recorded publisher to complete. @@ -584,7 +581,6 @@ extension Recorder { // MARK: - Publisher + Recorder -@available(macOS 10.15, *) extension Publisher { /// Returns a subscribed Recorder. /// diff --git a/Tests/CustomSQLite/CustomSQLite.xcodeproj/project.pbxproj b/Tests/CustomSQLite/CustomSQLite.xcodeproj/project.pbxproj index 4d0fd10cde..315344000f 100644 --- a/Tests/CustomSQLite/CustomSQLite.xcodeproj/project.pbxproj +++ b/Tests/CustomSQLite/CustomSQLite.xcodeproj/project.pbxproj @@ -342,7 +342,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -397,7 +397,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift index 79defbd8ca..9d7f4a813f 100644 --- a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift @@ -22,10 +22,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisher() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -128,10 +124,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // frame #71: 0x00007fff72311cc9 libdyld.dylib`start + 1 // frame #72: 0x00007fff72311cc9 libdyld.dylib`start + 1 func testReadPublisherError() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(reader: some DatabaseReader) throws { let publisher = reader.readPublisher(value: { db in try Row.fetchAll(db, sql: "THIS IS NOT SQL") @@ -157,10 +149,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherIsAsynchronous() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -197,10 +185,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherDefaultScheduler() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -237,10 +221,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherCustomScheduler() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -278,10 +258,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherIsReadonly() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(reader: some DatabaseReader) throws { let publisher = reader.readPublisher(value: { db in try Player.createTable(db) diff --git a/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift index 10a3568b89..a83adfa02a 100644 --- a/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift @@ -20,10 +20,6 @@ private struct Player: Codable, FetchableRecord, PersistableRecord { class DatabaseRegionObservationPublisherTests : XCTestCase { func testChangesNotifications() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -61,10 +57,6 @@ class DatabaseRegionObservationPublisherTests : XCTestCase { // TODO: do the same, but asynchronously. If this is too hard, update the // public API so that users can easily do it. func testPrependInitialDatabaseSync() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer diff --git a/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift b/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift index d9534238cb..7d1b2d0a57 100644 --- a/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift @@ -22,10 +22,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisher() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -49,10 +45,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherValue() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -76,10 +68,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherError() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = writer.writePublisher(updates: { db in try db.execute(sql: "THIS IS NOT SQL") @@ -99,10 +87,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { } func testWritePublisherErrorRollbacksTransaction() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -132,10 +116,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherIsAsynchronous() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -168,10 +148,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherDefaultScheduler() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -206,10 +182,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherCustomScheduler() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -247,10 +219,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // TODO: Fix flaky test with both pool and on-disk queue: // - Expectation timeout func testWriteThenReadPublisher() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -274,10 +242,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWriteThenReadPublisherIsReadonly() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = writer .writePublisher( @@ -299,10 +263,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWriteThenReadPublisherWriteError() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = writer.writePublisher( updates: { db in try db.execute(sql: "THIS IS NOT SQL") }, @@ -322,10 +282,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { } func testWriteThenReadPublisherWriteErrorRollbacksTransaction() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -359,10 +315,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // TODO: Fix flaky test with both pool and on-disk queue: // - Expectation timeout func testWriteThenReadPublisherReadError() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = writer.writePublisher( updates: { _ in }, @@ -386,10 +338,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // Regression test against deadlocks created by concurrent completion // and cancellations triggered by .switchToLatest().prefix(1) func testDeadlockPrevention() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer diff --git a/Tests/GRDBCombineTests/Support.swift b/Tests/GRDBCombineTests/Support.swift index 678195909d..6d35dd7718 100644 --- a/Tests/GRDBCombineTests/Support.swift +++ b/Tests/GRDBCombineTests/Support.swift @@ -51,7 +51,6 @@ final class Test { } } -@available(macOS 10.15, *) final class AsyncTest { // Raise the repeatCount in order to help spotting flaky tests. private let repeatCount: Int @@ -100,7 +99,6 @@ final class AsyncTest { } } -@available(macOS 10.15, *) public func assertNoFailure( _ completion: Subscribers.Completion, file: StaticString = #file, @@ -111,7 +109,6 @@ public func assertNoFailure( } } -@available(macOS 10.15, *) public func assertFailure( _ completion: Subscribers.Completion, file: StaticString = #file, diff --git a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift index ae89d3f27b..0849a0085e 100644 --- a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift @@ -22,10 +22,6 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Default Scheduler func testDefaultSchedulerChangesNotifications() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -64,10 +60,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDefaultSchedulerFirstValueIsEmittedAsynchronously() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -97,10 +89,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDefaultSchedulerError() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = ValueObservation .trackingConstantRegion { try $0.execute(sql: "THIS IS NOT SQL") } @@ -123,10 +111,6 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Immediate Scheduler func testImmediateSchedulerChangesNotifications() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -165,10 +149,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testImmediateSchedulerEmitsFirstValueSynchronously() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -201,10 +181,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testImmediateSchedulerError() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = ValueObservation .trackingConstantRegion { try $0.execute(sql: "THIS IS NOT SQL") } @@ -226,7 +202,6 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Demand - @available(macOS 10.15, *) private class DemandSubscriber: Subscriber { private var subscription: Subscription? let subject = PassthroughSubject() @@ -257,10 +232,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandNoneReceivesNoElement() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -292,10 +263,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandOneReceivesOneElement() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -330,10 +297,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandOneDoesNotReceiveTwoElements() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -372,10 +335,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandTwoReceivesTwoElements() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -418,10 +377,6 @@ class ValueObservationPublisherTests : XCTestCase { /// Regression test for https://github.com/groue/GRDB.swift/issues/1194 func testIssue1194() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - struct Record: Codable, FetchableRecord, PersistableRecord { var id: Int64 } diff --git a/Tests/GRDBTests/AsyncSemaphore.swift b/Tests/GRDBTests/AsyncSemaphore.swift index cc6dd61097..97691ab67c 100644 --- a/Tests/GRDBTests/AsyncSemaphore.swift +++ b/Tests/GRDBTests/AsyncSemaphore.swift @@ -43,7 +43,6 @@ import Foundation /// /// - ``wait()`` /// - ``waitUnlessCancelled()`` -@available(macOS 10.15, *) public final class AsyncSemaphore: @unchecked Sendable { /// `Suspension` is the state of a task waiting for a signal. /// diff --git a/Tests/GRDBTests/DatabaseCursorTests.swift b/Tests/GRDBTests/DatabaseCursorTests.swift index 19e53f3700..6b43570b48 100644 --- a/Tests/GRDBTests/DatabaseCursorTests.swift +++ b/Tests/GRDBTests/DatabaseCursorTests.swift @@ -175,42 +175,40 @@ class DatabaseCursorTests: GRDBTestCase { // with raw C SQLite3 apis. The faulty line is the call to // sqlite3_set_authorizer during the statement iteration. -// if #available(OSX 10.14, *) { -// var connection: SQLiteConnection? = nil -// sqlite3_open_v2(":memory:", &connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_NOMUTEX, nil) -// sqlite3_extended_result_codes(connection, 1) +// var connection: SQLiteConnection? = nil +// sqlite3_open_v2(":memory:", &connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_NOMUTEX, nil) +// sqlite3_extended_result_codes(connection, 1) // -// sqlite3_exec(connection, """ -// CREATE TABLE user (username TEXT NOT NULL); -// CREATE TABLE flagUser (username TEXT NOT NULL); -// INSERT INTO flagUser (username) VALUES ('User1'); -// INSERT INTO flagUser (username) VALUES ('User2'); -// """, nil, nil, nil) +// sqlite3_exec(connection, """ +// CREATE TABLE user (username TEXT NOT NULL); +// CREATE TABLE flagUser (username TEXT NOT NULL); +// INSERT INTO flagUser (username) VALUES ('User1'); +// INSERT INTO flagUser (username) VALUES ('User2'); +// """, nil, nil, nil) // -// var statement: SQLiteStatement? = nil -// sqlite3_set_authorizer(connection, { (_, _, _, _, _, _) in SQLITE_OK }, nil) -// sqlite3_prepare_v3(connection, """ -// SELECT * FROM flagUser WHERE (SELECT COUNT(*) FROM user WHERE username = flagUser.username) = 0 -// """, -1, 0, &statement, nil) -// sqlite3_set_authorizer(connection, nil, nil) -// while true { -// let code = sqlite3_step(statement) -// if code == SQLITE_DONE { -// break -// } else if code == SQLITE_ROW { -// // part of the compilation of another statement, here -// // reduced to the strict minimum that reproduces -// // the error. -// sqlite3_set_authorizer(connection, nil, nil) -// } else { -// print(String(cString: sqlite3_errmsg(connection))) -// XCTFail("Error \(code)") -// break -// } +// var statement: SQLiteStatement? = nil +// sqlite3_set_authorizer(connection, { (_, _, _, _, _, _) in SQLITE_OK }, nil) +// sqlite3_prepare_v3(connection, """ +// SELECT * FROM flagUser WHERE (SELECT COUNT(*) FROM user WHERE username = flagUser.username) = 0 +// """, -1, 0, &statement, nil) +// sqlite3_set_authorizer(connection, nil, nil) +// while true { +// let code = sqlite3_step(statement) +// if code == SQLITE_DONE { +// break +// } else if code == SQLITE_ROW { +// // part of the compilation of another statement, here +// // reduced to the strict minimum that reproduces +// // the error. +// sqlite3_set_authorizer(connection, nil, nil) +// } else { +// print(String(cString: sqlite3_errmsg(connection))) +// XCTFail("Error \(code)") +// break // } -// sqlite3_finalize(statement) -// sqlite3_close_v2(connection) // } +// sqlite3_finalize(statement) +// sqlite3_close_v2(connection) } // This test passes if it compiles diff --git a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift index 58d33c7584..61f42d7515 100644 --- a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift @@ -25,7 +25,6 @@ private struct RecordWithData: EncodableRecord, Enco var data: Data } -@available(macOS 10.15, *) extension RecordWithData: Identifiable { var id: Data { data } } @@ -37,7 +36,6 @@ private struct RecordWithOptionalData: EncodableReco var data: Data? } -@available(macOS 10.15, *) extension RecordWithOptionalData: Identifiable { var id: Data? { data } } @@ -154,10 +152,6 @@ extension DatabaseDataEncodingStrategyTests { } func testFilterID() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .blob) } @@ -234,10 +228,6 @@ extension DatabaseDataEncodingStrategyTests { } func testDeleteID() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .blob) } diff --git a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift index 0f8b7629e1..90ab8c4102 100644 --- a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift @@ -52,7 +52,6 @@ private struct RecordWithDate: EncodableRecord, Enco var date: Date } -@available(macOS 10.15, *) extension RecordWithDate: Identifiable { var id: Date { date } } @@ -64,7 +63,6 @@ private struct RecordWithOptionalDate: EncodableReco var date: Date? } -@available(macOS 10.15, *) extension RecordWithOptionalDate: Identifiable { var id: Date? { date } } @@ -264,10 +262,6 @@ extension DatabaseDateEncodingStrategyTests { } func testFilterID() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .datetime) } @@ -344,10 +338,6 @@ extension DatabaseDateEncodingStrategyTests { } func testDeleteID() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .datetime) } diff --git a/Tests/GRDBTests/DatabaseDumpTests.swift b/Tests/GRDBTests/DatabaseDumpTests.swift index f05da9916e..29d59caa72 100644 --- a/Tests/GRDBTests/DatabaseDumpTests.swift +++ b/Tests/GRDBTests/DatabaseDumpTests.swift @@ -238,10 +238,6 @@ final class DatabaseDumpTests: GRDBTestCase { // MARK: - JSON func test_json_value_formatting() throws { - guard #available(macOS 10.15.0, *) else { - throw XCTSkip("Skip because this test relies on JSONEncoder.OutputFormatting.withoutEscapingSlashes") - } - try makeValuesDatabase().read { db in let stream = TestStream() try db.dumpSQL("SELECT * FROM value ORDER BY name", format: .json(), to: stream) diff --git a/Tests/GRDBTests/DatabaseMigratorTests.swift b/Tests/GRDBTests/DatabaseMigratorTests.swift index 700ad3381e..d6b118923b 100644 --- a/Tests/GRDBTests/DatabaseMigratorTests.swift +++ b/Tests/GRDBTests/DatabaseMigratorTests.swift @@ -41,10 +41,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testEmptyMigratorPublisher() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let migrator = DatabaseMigrator() let publisher = migrator.migratePublisher(writer) @@ -153,10 +149,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testNonEmptyMigratorPublisher() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { var migrator = DatabaseMigrator() migrator.registerMigration("createPersons") { db in @@ -209,10 +201,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testEmptyMigratorPublisherIsAsynchronous() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let migrator = DatabaseMigrator() let expectation = self.expectation(description: "") @@ -235,10 +223,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testNonEmptyMigratorPublisherIsAsynchronous() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { var migrator = DatabaseMigrator() migrator.registerMigration("first", migrate: { _ in }) @@ -262,10 +246,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testMigratorPublisherDefaultScheduler() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: Writer) { var migrator = DatabaseMigrator() migrator.registerMigration("first", migrate: { _ in }) @@ -291,10 +271,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testMigratorPublisherCustomScheduler() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: Writer) { var migrator = DatabaseMigrator() migrator.registerMigration("first", migrate: { _ in }) diff --git a/Tests/GRDBTests/DatabaseReaderTests.swift b/Tests/GRDBTests/DatabaseReaderTests.swift index d7ebdb9e1c..ab64b6e7ad 100644 --- a/Tests/GRDBTests/DatabaseReaderTests.swift +++ b/Tests/GRDBTests/DatabaseReaderTests.swift @@ -49,7 +49,6 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(macOS 10.15, *) func testAsyncAwait_ReadCanRead() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -91,7 +90,6 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(macOS 10.15, *) func testAsyncAwait_ReadPreventsDatabaseModification() async throws { func test(_ dbReader: some DatabaseReader) async throws { do { @@ -135,7 +133,6 @@ class DatabaseReaderTests : GRDBTestCase { #endif } - @available(macOS 10.15, *) func testAsyncAwait_UnsafeReadCanRead() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -347,7 +344,6 @@ class DatabaseReaderTests : GRDBTestCase { // MARK: - Task Cancellation - @available(macOS 10.15, *) func test_read_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -383,7 +379,6 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -420,7 +415,6 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_statement_execution_from_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -459,7 +453,6 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_cursor_iteration_from_read_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -502,7 +495,6 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_unsafeRead_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -538,7 +530,6 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -575,7 +566,6 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_statement_execution_from_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -614,7 +604,6 @@ class DatabaseReaderTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_cursor_iteration_from_unsafeRead_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbReader: some DatabaseReader) async throws { let semaphore = AsyncSemaphore(value: 0) diff --git a/Tests/GRDBTests/DatabaseRegionObservationTests.swift b/Tests/GRDBTests/DatabaseRegionObservationTests.swift index 49d76f9eba..3ffdfc39eb 100644 --- a/Tests/GRDBTests/DatabaseRegionObservationTests.swift +++ b/Tests/GRDBTests/DatabaseRegionObservationTests.swift @@ -8,10 +8,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: .fullDatabase) _ = observation.start(in: writer, onError: { _ in }, onChange: { _ in }) - - if #available(macOS 10.15, *) { - _ = observation.publisher(in: writer) - } + _ = observation.publisher(in: writer) } func testDatabaseRegionObservation_FullDatabase() throws { diff --git a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift index 3a3e26c2cd..3235f5db9e 100644 --- a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift +++ b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift @@ -229,7 +229,6 @@ final class DatabaseSnapshotPoolTests: GRDBTestCase { try XCTAssertEqual(dbPool.read(counter.value), 2) } - @available(macOS 10.15, *) func test_read_async() async throws { let dbPool = try makeDatabasePool() let counter = try Counter(dbPool: dbPool) // 0 diff --git a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift index 119953a1b6..14dce9d843 100644 --- a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift @@ -26,7 +26,6 @@ private struct RecordWithUUID: EncodableRecord, Enco var uuid: UUID } -@available(macOS 10.15, *) extension RecordWithUUID: Identifiable { var id: UUID { uuid } } @@ -39,7 +38,6 @@ private struct RecordWithOptionalUUID: EncodableReco var uuid: UUID? } -@available(macOS 10.15, *) extension RecordWithOptionalUUID: Identifiable { var id: UUID? { uuid } } @@ -190,10 +188,6 @@ extension DatabaseUUIDEncodingStrategyTests { } func testFilterID() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .blob) } let uuids = [ @@ -309,10 +303,6 @@ extension DatabaseUUIDEncodingStrategyTests { } func testDeleteID() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .blob) } let uuids = [ diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 5d4f9e9652..221f01063e 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -275,7 +275,6 @@ class DatabaseWriterTests : GRDBTestCase { try DatabaseQueue().backup(to: dbQueue) } - @available(macOS 10.15, *) func testAsyncAwait_write() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -295,7 +294,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(macOS 10.15, *) func testAsyncAwait_writeWithoutTransaction() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -318,7 +316,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(macOS 10.15, *) func testAsyncAwait_barrierWriteWithoutTransaction() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -341,7 +338,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(macOS 10.15, *) func testAsyncAwait_erase() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -359,7 +355,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(macOS 10.15, *) func testAsyncAwait_vacuum() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -406,7 +401,6 @@ class DatabaseWriterTests : GRDBTestCase { } /// A test related to - @available(macOS 10.15, *) func testAsyncWriteThenRead() async throws { /// An async read performed after an async write should see the write. func test(_ dbWriter: some DatabaseWriter) async throws { @@ -430,7 +424,6 @@ class DatabaseWriterTests : GRDBTestCase { // MARK: - Task Cancellation - @available(macOS 10.15, *) func test_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -461,7 +454,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -493,7 +485,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_statement_execution_from_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -527,7 +518,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_cursor_iteration_from_writeWithoutTransaction_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -565,7 +555,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_write_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -596,7 +585,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_write_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -628,7 +616,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_statement_execution_from_write_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -662,7 +649,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_cursor_iteration_from_write_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -700,7 +686,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -731,7 +716,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -763,7 +747,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_statement_execution_from_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) @@ -797,7 +780,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(AnyDatabaseWriter(makeDatabaseQueue())) } - @available(macOS 10.15, *) func test_cursor_iteration_from_barrierWriteWithoutTransaction_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { func test(_ dbWriter: some DatabaseWriter) async throws { let semaphore = AsyncSemaphore(value: 0) diff --git a/Tests/GRDBTests/JSONColumnTests.swift b/Tests/GRDBTests/JSONColumnTests.swift index b73c21128d..da37da8fca 100644 --- a/Tests/GRDBTests/JSONColumnTests.swift +++ b/Tests/GRDBTests/JSONColumnTests.swift @@ -9,7 +9,7 @@ final class JSONColumnTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -51,7 +51,7 @@ final class JSONColumnTests: GRDBTestCase { throw XCTSkip("JSON_EXTRACT is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON_EXTRACT is not available") } #endif diff --git a/Tests/GRDBTests/JSONExpressionsTests.swift b/Tests/GRDBTests/JSONExpressionsTests.swift index f0b4390927..465fc06923 100644 --- a/Tests/GRDBTests/JSONExpressionsTests.swift +++ b/Tests/GRDBTests/JSONExpressionsTests.swift @@ -9,7 +9,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -44,7 +44,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -105,7 +105,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -281,7 +281,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -316,7 +316,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -392,7 +392,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -435,7 +435,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -470,7 +470,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -525,7 +525,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -580,7 +580,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -635,7 +635,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -738,7 +738,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -821,7 +821,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -904,7 +904,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -947,7 +947,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -990,7 +990,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1025,7 +1025,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1060,7 +1060,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1103,7 +1103,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1138,7 +1138,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1185,7 +1185,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1216,7 +1216,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1282,7 +1282,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1309,7 +1309,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1397,7 +1397,7 @@ final class JSONExpressionsTests: GRDBTestCase { // throw XCTSkip("JSON support is not available") // } // #else -// guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { +// guard #available(iOS 16, tvOS 17, watchOS 9, *) else { // throw XCTSkip("JSON support is not available") // } // #endif diff --git a/Tests/GRDBTests/JoinSupportTests.swift b/Tests/GRDBTests/JoinSupportTests.swift index e67523fbf7..fadd8bf3eb 100644 --- a/Tests/GRDBTests/JoinSupportTests.swift +++ b/Tests/GRDBTests/JoinSupportTests.swift @@ -92,7 +92,6 @@ private struct FlatModel: FetchableRecord { self.t5count = row.scopes[Scopes.suffix]!["t5count"] } - @available(macOS 10.15, *) static func modernAll() -> some FetchRequest { all() } @@ -138,7 +137,6 @@ private struct CodableFlatModel: FetchableRecord, Codable { var t3: T3? var t5count: Int - @available(macOS 10.15, *) static func modernAll() -> some FetchRequest { all() } @@ -186,7 +184,6 @@ private struct CodableNestedModel: FetchableRecord, Codable { var t3: T3? var t5count: Int - @available(macOS 10.15, *) static func modernAll() -> some FetchRequest { all() } diff --git a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift index d3c598488c..11bd3e4288 100644 --- a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift @@ -41,7 +41,6 @@ private class MinimalNonOptionalPrimaryKeySingle: Record, Hashable { } } -@available(macOS 10.15, *) extension MinimalNonOptionalPrimaryKeySingle: Identifiable { } class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { @@ -473,20 +472,18 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, *) { - do { - let ids: [String] = [] - let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id, record2.id] - let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [String] = [] + let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id, record2.id] + let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -512,19 +509,17 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [String] = [] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id, record2.id] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - } + do { + let ids: [String] = [] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id, record2.id] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } } } @@ -550,19 +545,17 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [String] = [] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id, record2.id] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - } + do { + let ids: [String] = [] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id, record2.id] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } } } @@ -585,12 +578,10 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(macOS 10.15, *) { - do { - let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.fetchOne(db, id: record.id)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") - } + do { + let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.fetchOne(db, id: record.id)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } } } @@ -613,17 +604,15 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(macOS 10.15, *) { - do { - _ = try MinimalNonOptionalPrimaryKeySingle.find(db, key: "missing") - XCTFail("Expected RecordError") - } catch RecordError.recordNotFound(databaseTableName: "minimalSingles", key: ["id": "missing".databaseValue]) { } - - do { - let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.find(db, id: record.id) - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") - } + do { + _ = try MinimalNonOptionalPrimaryKeySingle.find(db, key: "missing") + XCTFail("Expected RecordError") + } catch RecordError.recordNotFound(databaseTableName: "minimalSingles", key: ["id": "missing".databaseValue]) { } + + do { + let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.find(db, id: record.id) + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } } } @@ -653,20 +642,18 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, *) { - do { - let ids: [String] = [] - let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id, record2.id] - let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [String] = [] + let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id, record2.id] + let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -692,19 +679,17 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [String] = [] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id, record2.id] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - } + do { + let ids: [String] = [] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id, record2.id] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } } } @@ -730,19 +715,17 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [String] = [] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id, record2.id] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - } + do { + let ids: [String] = [] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id, record2.id] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } } } @@ -765,12 +748,10 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(macOS 10.15, *) { - do { - let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.filter(id: record.id).fetchOne(db)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") - } + do { + let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.filter(id: record.id).fetchOne(db)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } } } diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift index 9b4dc91874..91f38ad018 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift @@ -45,7 +45,6 @@ class MinimalRowID : Record, Hashable { } } -@available(macOS 10.15, *) extension MinimalRowID: Identifiable { } class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { @@ -507,20 +506,18 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let cursor = try MinimalRowID.fetchCursor(db, ids: ids) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id!, record2.id!] - let cursor = try MinimalRowID.fetchCursor(db, ids: ids) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [Int64] = [] + let cursor = try MinimalRowID.fetchCursor(db, ids: ids) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id!, record2.id!] + let cursor = try MinimalRowID.fetchCursor(db, ids: ids) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -546,19 +543,17 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -584,19 +579,17 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -619,15 +612,13 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(macOS 10.15, *) { - do { - let fetchedRecord = try MinimalRowID.fetchOne(db, id: record.id!)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") - } - do { - try XCTAssertNil(MinimalRowID.fetchOne(db, id: nil)) - } + do { + let fetchedRecord = try MinimalRowID.fetchOne(db, id: record.id!)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") + } + do { + try XCTAssertNil(MinimalRowID.fetchOne(db, id: nil)) } } } @@ -650,20 +641,18 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(macOS 10.15, *) { - do { - _ = try MinimalRowID.find(db, id: -1) - XCTFail("Expected RecordError") - } catch RecordError.recordNotFound(databaseTableName: "minimalRowIDs", key: ["id": (-1).databaseValue]) { } - - do { - let fetchedRecord = try MinimalRowID.find(db, id: record.id!) - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") - } - do { - try XCTAssertNil(MinimalRowID.fetchOne(db, id: nil)) - } + do { + _ = try MinimalRowID.find(db, id: -1) + XCTFail("Expected RecordError") + } catch RecordError.recordNotFound(databaseTableName: "minimalRowIDs", key: ["id": (-1).databaseValue]) { } + + do { + let fetchedRecord = try MinimalRowID.find(db, id: record.id!) + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") + } + do { + try XCTAssertNil(MinimalRowID.fetchOne(db, id: nil)) } } } @@ -693,20 +682,18 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id!, record2.id!] - let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [Int64] = [] + let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id!, record2.id!] + let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -732,19 +719,17 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -770,19 +755,17 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -805,15 +788,13 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(macOS 10.15, *) { - do { - let fetchedRecord = try MinimalRowID.filter(id: record.id!).fetchOne(db)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") - } - do { - try XCTAssertNil(MinimalRowID.filter(id: nil).fetchOne(db)) - } + do { + let fetchedRecord = try MinimalRowID.filter(id: record.id!).fetchOne(db)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") + } + do { + try XCTAssertNil(MinimalRowID.filter(id: nil).fetchOne(db)) } } } diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift index 28e0a59131..cd2b838296 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift @@ -39,7 +39,6 @@ class MinimalSingle: Record, Hashable { } } -@available(macOS 10.15, *) extension MinimalSingle: Identifiable { /// Test non-optional ID type var id: String { UUID! } @@ -531,20 +530,18 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, *) { - do { - let UUIDs: [String] = [] - let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) - try XCTAssertNil(cursor.next()) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let UUIDs: [String] = [] + let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) + try XCTAssertNil(cursor.next()) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -572,19 +569,17 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(macOS 10.15, *) { - do { - let UUIDs: [String] = [] - let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - } + do { + let UUIDs: [String] = [] + let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } } } @@ -612,19 +607,17 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(macOS 10.15, *) { - do { - let UUIDs: [String] = [] - let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - } + do { + let UUIDs: [String] = [] + let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } } } @@ -648,12 +641,10 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(macOS 10.15, *) { - do { - let fetchedRecord = try MinimalSingle.fetchOne(db, id: record.UUID!)! - XCTAssertTrue(fetchedRecord.UUID == record.UUID) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") - } + do { + let fetchedRecord = try MinimalSingle.fetchOne(db, id: record.UUID!)! + XCTAssertTrue(fetchedRecord.UUID == record.UUID) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } } } @@ -677,17 +668,15 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(macOS 10.15, *) { - do { - _ = try MinimalSingle.find(db, id: "missing") - XCTFail("Expected RecordError") - } catch RecordError.recordNotFound(databaseTableName: "minimalSingles", key: ["UUID": "missing".databaseValue]) { } - - do { - let fetchedRecord = try MinimalSingle.find(db, id: record.UUID!) - XCTAssertTrue(fetchedRecord.UUID == record.UUID) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") - } + do { + _ = try MinimalSingle.find(db, id: "missing") + XCTFail("Expected RecordError") + } catch RecordError.recordNotFound(databaseTableName: "minimalSingles", key: ["UUID": "missing".databaseValue]) { } + + do { + let fetchedRecord = try MinimalSingle.find(db, id: record.UUID!) + XCTAssertTrue(fetchedRecord.UUID == record.UUID) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } } } @@ -719,20 +708,18 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, *) { - do { - let UUIDs: [String] = [] - let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) - try XCTAssertNil(cursor.next()) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let UUIDs: [String] = [] + let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) + try XCTAssertNil(cursor.next()) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -760,19 +747,17 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(macOS 10.15, *) { - do { - let UUIDs: [String] = [] - let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - } + do { + let UUIDs: [String] = [] + let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } } } @@ -800,19 +785,17 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(macOS 10.15, *) { - do { - let UUIDs: [String] = [] - let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - } + do { + let UUIDs: [String] = [] + let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } } } @@ -836,12 +819,10 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(macOS 10.15, *) { - do { - let fetchedRecord = try MinimalSingle.filter(id: record.UUID!).fetchOne(db)! - XCTAssertTrue(fetchedRecord.UUID == record.UUID) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") - } + do { + let fetchedRecord = try MinimalSingle.filter(id: record.UUID!).fetchOne(db)! + XCTAssertTrue(fetchedRecord.UUID == record.UUID) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } } } diff --git a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift index b16e2fc3f4..ca99bceb34 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift @@ -77,7 +77,6 @@ private class Person : Record, Hashable { } } -@available(macOS 10.15, *) extension Person: Identifiable { } class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { @@ -599,20 +598,18 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let cursor = try Person.fetchCursor(db, ids: ids) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id!, record2.id!] - let cursor = try Person.fetchCursor(db, ids: ids) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [Int64] = [] + let cursor = try Person.fetchCursor(db, ids: ids) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id!, record2.id!] + let cursor = try Person.fetchCursor(db, ids: ids) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -638,19 +635,17 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try Person.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try Person.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try Person.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try Person.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -676,19 +671,17 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try Person.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try Person.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try Person.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try Person.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -714,18 +707,16 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(macOS 10.15, *) { - do { - let fetchedRecord = try Person.fetchOne(db, id: record.id!)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertTrue(fetchedRecord.name == record.name) - XCTAssertTrue(fetchedRecord.age == record.age) - XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. - XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") - } - do { - try XCTAssertNil(Person.fetchOne(db, id: nil)) - } + do { + let fetchedRecord = try Person.fetchOne(db, id: record.id!)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertTrue(fetchedRecord.age == record.age) + XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") + } + do { + try XCTAssertNil(Person.fetchOne(db, id: nil)) } } } @@ -751,23 +742,21 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(macOS 10.15, *) { - do { - _ = try Person.find(db, id: -1) - XCTFail("Expected RecordError") - } catch RecordError.recordNotFound(databaseTableName: "persons", key: ["rowid": (-1).databaseValue]) { } - - do { - let fetchedRecord = try Person.find(db, id: record.id!) - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertTrue(fetchedRecord.name == record.name) - XCTAssertTrue(fetchedRecord.age == record.age) - XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. - XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") - } - do { - try XCTAssertNil(Person.fetchOne(db, id: nil)) - } + do { + _ = try Person.find(db, id: -1) + XCTFail("Expected RecordError") + } catch RecordError.recordNotFound(databaseTableName: "persons", key: ["rowid": (-1).databaseValue]) { } + + do { + let fetchedRecord = try Person.find(db, id: record.id!) + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertTrue(fetchedRecord.age == record.age) + XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") + } + do { + try XCTAssertNil(Person.fetchOne(db, id: nil)) } } } @@ -797,20 +786,18 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let cursor = try Person.filter(ids: ids).fetchCursor(db) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id!, record2.id!] - let cursor = try Person.filter(ids: ids).fetchCursor(db) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [Int64] = [] + let cursor = try Person.filter(ids: ids).fetchCursor(db) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id!, record2.id!] + let cursor = try Person.filter(ids: ids).fetchCursor(db) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -836,19 +823,17 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -874,19 +859,17 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(macOS 10.15, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -912,18 +895,16 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(macOS 10.15, *) { - do { - let fetchedRecord = try Person.filter(id: record.id!).fetchOne(db)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertTrue(fetchedRecord.name == record.name) - XCTAssertTrue(fetchedRecord.age == record.age) - XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. - XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") - } - do { - try XCTAssertNil(Person.filter(id: nil).fetchOne(db)) - } + do { + let fetchedRecord = try Person.filter(id: record.id!).fetchOne(db)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertTrue(fetchedRecord.age == record.age) + XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") + } + do { + try XCTAssertNil(Person.filter(id: nil).fetchOne(db)) } } } diff --git a/Tests/GRDBTests/SharedValueObservationTests.swift b/Tests/GRDBTests/SharedValueObservationTests.swift index f794c5906e..4a8d629a56 100644 --- a/Tests/GRDBTests/SharedValueObservationTests.swift +++ b/Tests/GRDBTests/SharedValueObservationTests.swift @@ -120,10 +120,6 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_immediate_publisher() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -397,10 +393,6 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_async_publisher() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -525,7 +517,6 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), []) } - @available(macOS 10.15, *) func test_task_observationLifetime() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -622,10 +613,6 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_task_publisher() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -656,7 +643,6 @@ class SharedValueObservationTests: GRDBTestCase { } #endif - @available(macOS 10.15, *) func test_task_whileObserved() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -753,10 +739,6 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_error_recovery_observationLifetime() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -811,10 +793,6 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_error_recovery_whileObserved() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Combine is not available") - } - let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -867,7 +845,6 @@ class SharedValueObservationTests: GRDBTestCase { } #endif - @available(macOS 10.15, *) func testAsyncAwait_mainQueue() async throws { let dbQueue = try makeDatabaseQueue() try await dbQueue.write { db in @@ -887,7 +864,6 @@ class SharedValueObservationTests: GRDBTestCase { } } - @available(macOS 10.15, *) func testAsyncAwait_task() async throws { let dbQueue = try makeDatabaseQueue() try await dbQueue.write { db in diff --git a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift index 4021373d54..e803c37335 100644 --- a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift @@ -357,10 +357,6 @@ class TableRecordQueryInterfaceRequestTests: GRDBTestCase { } func testExistsIdentifiable() throws { - guard #available(macOS 10.15, *) else { - throw XCTSkip("Identifiable is not available") - } - let dbQueue = try makeDatabaseQueue() try dbQueue.inTransaction { db in struct Player: TableRecord, Identifiable { diff --git a/Tests/GRDBTests/TableRecordDeleteTests.swift b/Tests/GRDBTests/TableRecordDeleteTests.swift index 7dedbab24a..50dfb561fd 100644 --- a/Tests/GRDBTests/TableRecordDeleteTests.swift +++ b/Tests/GRDBTests/TableRecordDeleteTests.swift @@ -6,7 +6,6 @@ private struct Hacker : TableRecord { var id: Int64? // Optional } -@available(macOS 10.15, *) extension Hacker: Identifiable { } private struct Person : Codable, PersistableRecord, FetchableRecord, Hashable { @@ -16,7 +15,6 @@ private struct Person : Codable, PersistableRecord, FetchableRecord, Hashable { var email: String } -@available(macOS 10.15, *) extension Person: Identifiable { } private struct Citizenship : TableRecord { @@ -46,23 +44,23 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertTrue(deleted) XCTAssertEqual(try Hacker.fetchCount(db), 0) - if #available(macOS 10.15, *) { + try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [1, "Arthur"]) + try XCTAssertFalse(Hacker.deleteOne(db, id: nil)) + deleted = try Hacker.deleteOne(db, id: 1) + XCTAssertTrue(deleted) + XCTAssertEqual(try Hacker.fetchCount(db), 0) + + do { try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [1, "Arthur"]) - try XCTAssertFalse(Hacker.deleteOne(db, id: nil)) - deleted = try Hacker.deleteOne(db, id: 1) - XCTAssertTrue(deleted) - XCTAssertEqual(try Hacker.fetchCount(db), 0) + try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [2, "Barbara"]) + try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [3, "Craig"]) + let deletedCount = try Hacker.deleteAll(db, keys: [2, 3, 4]) + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"hackers\" WHERE \"rowid\" IN (2, 3, 4)") + XCTAssertEqual(deletedCount, 2) + XCTAssertEqual(try Hacker.fetchCount(db), 1) } - try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [1, "Arthur"]) - try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [2, "Barbara"]) - try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [3, "Craig"]) - let deletedCount = try Hacker.deleteAll(db, keys: [2, 3, 4]) - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"hackers\" WHERE \"rowid\" IN (2, 3, 4)") - XCTAssertEqual(deletedCount, 2) - XCTAssertEqual(try Hacker.fetchCount(db), 1) - - if #available(macOS 10.15, *) { + do { try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [2, "Barbara"]) try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [3, "Craig"]) let deletedCount = try Hacker.deleteAll(db, ids: [2, 3, 4]) @@ -85,22 +83,22 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertTrue(deleted) XCTAssertEqual(try Person.fetchCount(db), 0) - if #available(macOS 10.15, *) { + try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [1, "Arthur", "arthur@example.com"]) + deleted = try Person.deleteOne(db, id: 1) + XCTAssertTrue(deleted) + XCTAssertEqual(try Person.fetchCount(db), 0) + + do { try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [1, "Arthur", "arthur@example.com"]) - deleted = try Person.deleteOne(db, id: 1) - XCTAssertTrue(deleted) - XCTAssertEqual(try Person.fetchCount(db), 0) + try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [2, "Barbara", "barbara@example.com"]) + try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [3, "Craig", "craig@example.com"]) + let deletedCount = try Person.deleteAll(db, keys: [2, 3, 4]) + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (2, 3, 4)") + XCTAssertEqual(deletedCount, 2) + XCTAssertEqual(try Person.fetchCount(db), 1) } - try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [1, "Arthur", "arthur@example.com"]) - try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [2, "Barbara", "barbara@example.com"]) - try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [3, "Craig", "craig@example.com"]) - let deletedCount = try Person.deleteAll(db, keys: [2, 3, 4]) - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (2, 3, 4)") - XCTAssertEqual(deletedCount, 2) - XCTAssertEqual(try Person.fetchCount(db), 1) - - if #available(macOS 10.15, *) { + do { try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [2, "Barbara", "barbara@example.com"]) try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [3, "Craig", "craig@example.com"]) let deletedCount = try Person.deleteAll(db, ids: [2, 3, 4]) @@ -189,15 +187,13 @@ class TableRecordDeleteTests: GRDBTestCase { try Person.filter(keys: [1, 2]).deleteAll(db) XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2)") - - if #available(macOS 10.15, *) { - try Person.filter(id: 1).deleteAll(db) - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1") - - try Person.filter(ids: [1, 2]).deleteAll(db) - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2)") - } - + + try Person.filter(id: 1).deleteAll(db) + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1") + + try Person.filter(ids: [1, 2]).deleteAll(db) + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2)") + try Person.filter(sql: "id = 1").deleteAll(db) XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE id = 1") @@ -279,13 +275,11 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2) RETURNING *") #if GRDBCUSTOMSQLITE || GRDBCIPHER - if #available(macOS 10.15, *) { - _ = try Person.filter(id: 1).deleteAndFetchCursor(db).next() - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1 RETURNING *") - - _ = try Person.filter(ids: [1, 2]).deleteAndFetchCursor(db).next() - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2) RETURNING *") - } + _ = try Person.filter(id: 1).deleteAndFetchCursor(db).next() + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1 RETURNING *") + + _ = try Person.filter(ids: [1, 2]).deleteAndFetchCursor(db).next() + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2) RETURNING *") #else _ = try Person.filter(id: 1).deleteAndFetchCursor(db).next() XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1 RETURNING *") @@ -364,7 +358,6 @@ class TableRecordDeleteTests: GRDBTestCase { } } - @available(macOS 10.15, *) // Identifiable func testRequestDeleteAndFetchIds() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { diff --git a/Tests/GRDBTests/TableRecordUpdateTests.swift b/Tests/GRDBTests/TableRecordUpdateTests.swift index 787c1fb47f..44982695a0 100644 --- a/Tests/GRDBTests/TableRecordUpdateTests.swift +++ b/Tests/GRDBTests/TableRecordUpdateTests.swift @@ -17,7 +17,6 @@ private struct Player: Codable, PersistableRecord, FetchableRecord, Hashable { } } -@available(macOS 10.15, *) extension Player: Identifiable { } private enum Columns: String, ColumnExpression { @@ -56,18 +55,16 @@ class TableRecordUpdateTests: GRDBTestCase { UPDATE "player" SET "score" = 0 WHERE "id" IN (1, 2) """) - if #available(macOS 10.15, *) { - try Player.filter(id: 1).updateAll(db, assignment) - XCTAssertEqual(self.lastSQLQuery, """ + try Player.filter(id: 1).updateAll(db, assignment) + XCTAssertEqual(self.lastSQLQuery, """ UPDATE "player" SET "score" = 0 WHERE "id" = 1 """) - - try Player.filter(ids: [1, 2]).updateAll(db, assignment) - XCTAssertEqual(self.lastSQLQuery, """ + + try Player.filter(ids: [1, 2]).updateAll(db, assignment) + XCTAssertEqual(self.lastSQLQuery, """ UPDATE "player" SET "score" = 0 WHERE "id" IN (1, 2) """) - } - + try Player.filter(sql: "id = 1").updateAll(db, assignment) XCTAssertEqual(self.lastSQLQuery, """ UPDATE "player" SET "score" = 0 WHERE id = 1 diff --git a/Tests/GRDBTests/TableTests.swift b/Tests/GRDBTests/TableTests.swift index 34f732b5f7..780fab6eed 100644 --- a/Tests/GRDBTests/TableTests.swift +++ b/Tests/GRDBTests/TableTests.swift @@ -117,7 +117,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, *) { + do { struct Player: Identifiable { var id: Int64 } let t = Table("player") @@ -129,7 +129,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, *) { + do { struct Player: Identifiable { var id: Int64? } let t = Table("player") @@ -806,7 +806,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, *) { + do { // Non-optional ID struct Country: Identifiable { var id: String } @@ -821,7 +821,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, *) { + do { // Optional ID struct Country: Identifiable { var id: String? } @@ -920,7 +920,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, *) { + do { // Non-optional ID struct Country: Identifiable { var id: String } @@ -930,7 +930,7 @@ class TableTests: GRDBTestCase { """) } - if #available(macOS 10.15, *) { + do { // Optional ID struct Country: Identifiable { var id: String? } diff --git a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift index 0e3ae4432f..390df4af0a 100644 --- a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift +++ b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift @@ -105,7 +105,6 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { } } - @available(macOS 10.15, *) func testTupleObservation() throws { // Here we just test that user can destructure an observed tuple. // I'm completely paranoid about tuple destructuring - I can't wrap my @@ -120,7 +119,6 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { onChange: { (int: Int, string: String) in }) // <- destructure } - @available(macOS 10.15, *) func testVaryingRegionTrackingImmediateScheduling() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index c72b90c5ea..ba59108d1e 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -5,7 +5,6 @@ import Dispatch class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(macOS 10.15, *) func testStartFromAnyDatabaseReader(reader: any DatabaseReader) { _ = ValueObservation .trackingConstantRegion { _ in } @@ -14,7 +13,6 @@ class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(macOS 10.15, *) func testStartFromAnyDatabaseWriter(writer: any DatabaseWriter) { _ = ValueObservation .trackingConstantRegion { _ in } @@ -23,7 +21,6 @@ class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(macOS 10.15, *) func testValuesFromAnyDatabaseWriter(writer: any DatabaseWriter) { func observe( fetch: @escaping @Sendable (Database) throws -> T @@ -55,7 +52,6 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(macOS 10.15, *) func testErrorCompletesTheObservation() throws { struct TestError: Error { } @@ -105,7 +101,6 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(macOS 10.15, *) func testViewOptimization() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { @@ -143,7 +138,6 @@ class ValueObservationTests: GRDBTestCase { } } - @available(macOS 10.15, *) func testPragmaTableOptimization() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { @@ -179,7 +173,6 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Constant Explicit Region - @available(macOS 10.15, *) func testTrackingExplicitRegion() throws { class TestStream: TextOutputStream { private var stringsMutex: Mutex<[String]> = Mutex([]) @@ -620,7 +613,6 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Cancellation - @available(macOS 10.15, *) func testCancellableLifetime() throws { // We need something to change let dbQueue = try makeDatabaseQueue() @@ -666,7 +658,6 @@ class ValueObservationTests: GRDBTestCase { XCTAssertEqual(changesCountMutex.load(), 2) } - @available(macOS 10.15, *) func testCancellableExplicitCancellation() throws { // We need something to change let dbQueue = try makeDatabaseQueue() @@ -804,7 +795,6 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(macOS 10.15, *) func testIssue1550() throws { func test(_ writer: some DatabaseWriter) throws { try writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } @@ -852,7 +842,6 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - @available(macOS 10.15, *) func testIssue1209() throws { func test(_ dbWriter: some DatabaseWriter) throws { try dbWriter.write { @@ -903,7 +892,6 @@ class ValueObservationTests: GRDBTestCase { } // MARK: - Main Actor - @available(macOS 10.15, *) @MainActor func test_mainActor_observation() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in @@ -939,7 +927,6 @@ class ValueObservationTests: GRDBTestCase { // MARK: - Async Await - @available(macOS 10.15, *) func testAsyncAwait_values_prefix() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -977,7 +964,6 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(macOS 10.15, *) func testAsyncAwait_values_break() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -1019,7 +1005,6 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(macOS 10.15, *) func testAsyncAwait_values_cancelled() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -1201,7 +1186,6 @@ class ValueObservationTests: GRDBTestCase { } // Regression test for - @available(macOS 10.15, *) func testIssue1362() throws { func test(_ writer: some DatabaseWriter) throws { try writer.write { try $0.execute(sql: "CREATE TABLE s(id INTEGER PRIMARY KEY AUTOINCREMENT)") } @@ -1292,7 +1276,6 @@ class ValueObservationTests: GRDBTestCase { } // Regression test for - @available(macOS 10.15, *) func testIssue1383_async() throws { do { let dbPool = try makeDatabasePool(filename: "test") diff --git a/Tests/Performance/GRDBProfiling/GRDBProfiling.xcodeproj/project.pbxproj b/Tests/Performance/GRDBProfiling/GRDBProfiling.xcodeproj/project.pbxproj index 2851c7774d..73c21f9549 100644 --- a/Tests/Performance/GRDBProfiling/GRDBProfiling.xcodeproj/project.pbxproj +++ b/Tests/Performance/GRDBProfiling/GRDBProfiling.xcodeproj/project.pbxproj @@ -364,7 +364,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -416,7 +416,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_VERSION = 5.0; diff --git a/Tests/SPM/PlainPackage/Package.swift b/Tests/SPM/PlainPackage/Package.swift index 578a011bc3..6b69969994 100644 --- a/Tests/SPM/PlainPackage/Package.swift +++ b/Tests/SPM/PlainPackage/Package.swift @@ -5,6 +5,12 @@ import PackageDescription let package = Package( name: "SPM", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v7), + ], dependencies: [ .package(name: "GRDB", path: "../../.."), ], diff --git a/Tests/SPM/PlainProject/Plain.xcodeproj/project.pbxproj b/Tests/SPM/PlainProject/Plain.xcodeproj/project.pbxproj index 00bcf692ef..46a8734ad3 100644 --- a/Tests/SPM/PlainProject/Plain.xcodeproj/project.pbxproj +++ b/Tests/SPM/PlainProject/Plain.xcodeproj/project.pbxproj @@ -187,7 +187,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -241,7 +241,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; From 900180dafdd315d58959ce518a9782298ef32735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 16:42:37 +0200 Subject: [PATCH 116/160] [BREAKING] Prefer any DatabaseReader and DatabaseWriter --- GRDB/Core/DatabaseReader.swift | 4 ++-- GRDB/Core/DatabaseRegionObservation.swift | 8 ++++---- GRDB/Core/DatabaseWriter.swift | 2 +- GRDB/Documentation.docc/Extension/ValueObservation.md | 4 ++-- GRDB/Migration/DatabaseMigrator.swift | 8 ++++---- GRDB/ValueObservation/SharedValueObservation.swift | 2 +- GRDB/ValueObservation/ValueObservation.swift | 8 ++++---- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 61c8d59e0b..f8a0794098 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -472,7 +472,7 @@ extension DatabaseReader { /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or the /// error thrown by `progress`. public func backup( - to writer: some DatabaseWriter, + to writer: any DatabaseWriter, pagesPerStep: CInt = -1, progress: ((DatabaseBackupProgress) throws -> Void)? = nil) throws @@ -628,7 +628,7 @@ public final class AnyDatabaseReader { /// Creates a new database reader that wraps and forwards operations /// to `base`. - public init(_ base: some DatabaseReader) { + public init(_ base: any DatabaseReader) { self.base = base } } diff --git a/GRDB/Core/DatabaseRegionObservation.swift b/GRDB/Core/DatabaseRegionObservation.swift index c0e89e4b38..8d9cd97b03 100644 --- a/GRDB/Core/DatabaseRegionObservation.swift +++ b/GRDB/Core/DatabaseRegionObservation.swift @@ -84,7 +84,7 @@ extension DatabaseRegionObservation { /// modified the observed region. /// - returns: A DatabaseCancellable that can stop the observation. public func start( - in writer: some DatabaseWriter, + in writer: any DatabaseWriter, onError: @escaping @Sendable (Error) -> Void, onChange: @escaping @Sendable (Database) -> Void) -> AnyDatabaseCancellable @@ -139,7 +139,7 @@ extension DatabaseRegionObservation { /// /// Do not reschedule the publisher with `receive(on:options:)` or any /// `Publisher` method that schedules publisher elements. - public func publisher(in writer: some DatabaseWriter) -> DatabasePublishers.DatabaseRegion { + public func publisher(in writer: any DatabaseWriter) -> DatabasePublishers.DatabaseRegion { DatabasePublishers.DatabaseRegion(self, in: writer) } } @@ -195,7 +195,7 @@ extension DatabasePublishers { let writer: any DatabaseWriter let observation: DatabaseRegionObservation - init(_ observation: DatabaseRegionObservation, in writer: some DatabaseWriter) { + init(_ observation: DatabaseRegionObservation, in writer: any DatabaseWriter) { self.writer = writer self.observation = observation } @@ -247,7 +247,7 @@ extension DatabasePublishers { private var lock = NSRecursiveLock() // Allow re-entrancy init( - writer: some DatabaseWriter, + writer: any DatabaseWriter, observation: DatabaseRegionObservation, downstream: Downstream) { diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index 29d39d2ac3..f60e6a790b 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -873,7 +873,7 @@ public final class AnyDatabaseWriter { /// Creates a new database reader that wraps and forwards operations /// to `base`. - public init(_ base: some DatabaseWriter) { + public init(_ base: any DatabaseWriter) { self.base = base } } diff --git a/GRDB/Documentation.docc/Extension/ValueObservation.md b/GRDB/Documentation.docc/Extension/ValueObservation.md index f2535e93e6..7358a2ac2d 100644 --- a/GRDB/Documentation.docc/Extension/ValueObservation.md +++ b/GRDB/Documentation.docc/Extension/ValueObservation.md @@ -311,9 +311,9 @@ When needed, you can help GRDB optimize observations and reduce database content ### Accessing Observed Values +- ``start(in:scheduling:onError:onChange:)-t62r`` +- ``start(in:scheduling:onError:onChange:)-4mqbs`` - ``publisher(in:scheduling:)`` -- ``start(in:scheduling:onError:onChange:)-10vwf`` -- ``start(in:scheduling:onError:onChange:)-7z197`` - ``values(in:scheduling:bufferingPolicy:)`` - ``DatabaseCancellable`` - ``ValueObservationScheduler`` diff --git a/GRDB/Migration/DatabaseMigrator.swift b/GRDB/Migration/DatabaseMigrator.swift index d92635c294..739945a270 100644 --- a/GRDB/Migration/DatabaseMigrator.swift +++ b/GRDB/Migration/DatabaseMigrator.swift @@ -230,7 +230,7 @@ public struct DatabaseMigrator: Sendable { /// /// - parameter writer: A DatabaseWriter. /// - throws: The error thrown by the first failed migration. - public func migrate(_ writer: some DatabaseWriter) throws { + public func migrate(_ writer: any DatabaseWriter) throws { guard let lastMigration = _migrations.last else { return } @@ -249,7 +249,7 @@ public struct DatabaseMigrator: Sendable { /// - parameter writer: A DatabaseWriter. /// - parameter targetIdentifier: A migration identifier. /// - throws: The error thrown by the first failed migration. - public func migrate(_ writer: some DatabaseWriter, upTo targetIdentifier: String) throws { + public func migrate(_ writer: any DatabaseWriter, upTo targetIdentifier: String) throws { try writer.barrierWriteWithoutTransaction { db in try migrate(db, upTo: targetIdentifier) } @@ -263,7 +263,7 @@ public struct DatabaseMigrator: Sendable { /// database, or the failure that prevented the migrations /// from succeeding. public func asyncMigrate( - _ writer: some DatabaseWriter, + _ writer: any DatabaseWriter, completion: @escaping @Sendable (Result) -> Void) { writer.asyncBarrierWriteWithoutTransaction { dbResult in @@ -497,7 +497,7 @@ extension DatabaseMigrator { /// where migrations should apply. /// - parameter scheduler: A Combine Scheduler. public func migratePublisher( - _ writer: some DatabaseWriter, + _ writer: any DatabaseWriter, receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main) -> DatabasePublishers.Migrate { diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index 1550a619c3..8e7b8c26b9 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -117,7 +117,7 @@ extension ValueObservation { /// - parameter extent: The extent of the shared database observation. /// - returns: A `SharedValueObservation` public func shared( - in reader: some DatabaseReader, + in reader: any DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main), extent: SharedValueObservationExtent = .whileObserved) -> SharedValueObservation diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index 8ce4b8abae..79eea431db 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -120,7 +120,7 @@ extension ValueObservation: Refinable { /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. @preconcurrency public func start( - in reader: some DatabaseReader, + in reader: any DatabaseReader, scheduling scheduler: some ValueObservationScheduler, onError: @escaping @Sendable (Error) -> Void, onChange: @escaping @Sendable (Reducer.Value) -> Void) @@ -180,7 +180,7 @@ extension ValueObservation: Refinable { /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. @preconcurrency @MainActor public func start( - in reader: some DatabaseReader, + in reader: any DatabaseReader, scheduling scheduler: some ValueObservationMainActorScheduler = .mainActor, onError: @escaping @MainActor (Error) -> Void, onChange: @escaping @MainActor (Reducer.Value) -> Void) @@ -350,7 +350,7 @@ extension ValueObservation { /// - parameter bufferingPolicy: see the documntation /// of `AsyncThrowingStream`. public func values( - in reader: some DatabaseReader, + in reader: any DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .task, bufferingPolicy: AsyncValueObservation.BufferingPolicy = .unbounded) -> AsyncValueObservation @@ -486,7 +486,7 @@ extension ValueObservation { /// values are dispatched asynchronously on the main dispatch queue. /// - returns: A Combine publisher public func publisher( - in reader: some DatabaseReader, + in reader: any DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main)) -> DatabasePublishers.Value where Reducer: ValueReducer From 173f363bdce0ba676ab07ef43d1a1a1225a5fddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 17:21:59 +0200 Subject: [PATCH 117/160] TableRecord.databaseSelection should be declared as a computed property --- Documentation/SQLInterpolation.md | 4 ++- GRDB/Documentation.docc/DatabaseSchema.md | 6 ++-- GRDB/Record/TableRecord.swift | 28 +++++++++++++++---- README.md | 19 +++++++------ TODO.md | 4 ++- ...sociationBelongsToSQLDerivationTests.swift | 4 +-- .../AssociationHasOneSQLDerivationTests.swift | 4 +-- ...ationHasOneThroughSQLDerivationTests.swift | 4 +-- Tests/GRDBTests/ColumnExpressionTests.swift | 16 ++++++++--- Tests/GRDBTests/FTS3RecordTests.swift | 2 +- Tests/GRDBTests/FTS4RecordTests.swift | 2 +- Tests/GRDBTests/FTS5RecordTests.swift | 2 +- Tests/GRDBTests/JSONColumnTests.swift | 2 +- Tests/GRDBTests/JoinSupportTests.swift | 2 +- Tests/GRDBTests/SQLLiteralTests.swift | 4 ++- Tests/GRDBTests/TableRecordDeleteTests.swift | 2 +- Tests/GRDBTests/TableRecordTests.swift | 6 ++-- .../FetchRecordOptimizedTests.swift | 26 +++++++++-------- 18 files changed, 87 insertions(+), 50 deletions(-) diff --git a/Documentation/SQLInterpolation.md b/Documentation/SQLInterpolation.md index 2af59d2fb4..38666a8b9a 100644 --- a/Documentation/SQLInterpolation.md +++ b/Documentation/SQLInterpolation.md @@ -395,7 +395,9 @@ This chapter lists all kinds of supported interpolations. struct AltPlayer: TableRecord { static let databaseTableName = "player" - static let databaseSelection: [any SQLSelectable] = [Column("id"), Column("name")] + static var databaseSelection: [any SQLSelectable] { + [Column("id"), Column("name")] + } } // SELECT player.id, player.name FROM player diff --git a/GRDB/Documentation.docc/DatabaseSchema.md b/GRDB/Documentation.docc/DatabaseSchema.md index abfd53021b..b81073adc0 100644 --- a/GRDB/Documentation.docc/DatabaseSchema.md +++ b/GRDB/Documentation.docc/DatabaseSchema.md @@ -170,9 +170,9 @@ struct Player: Codable { extension Player: FetchableRecord, MutablePersistableRecord { // Required because the primary key // is the hidden rowid column. - static let databaseSelection: [any SQLSelectable] = [ - AllColumns(), - Column.rowID] + static var databaseSelection: [any SQLSelectable] { + [AllColumns(), Column.rowID] + } // Update id upon successful insertion mutating func didInsert(_ inserted: InsertionSuccess) { diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index 295b13785f..2f29ec8fbd 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -134,15 +134,16 @@ public protocol TableRecord { /// /// ```swift /// struct Player: TableRecord { - /// static let databaseSelection: [any SQLSelectable] = [AllColumns()] + /// static var databaseSelection: [any SQLSelectable] { + /// [AllColumns()] + /// } /// } /// /// struct PartialPlayer: TableRecord { /// static let databaseTableName = "player" - /// static let databaseSelection: [any SQLSelectable] = [ - /// Column("id"), - /// Column("name"), - /// ] + /// static var databaseSelection: [any SQLSelectable] { + /// [Column("id"), Column("name")] + /// } /// } /// /// // SELECT * FROM player @@ -156,6 +157,19 @@ public protocol TableRecord { /// > explicitly declared as `[any SQLSelectable]`. If it is not, the /// > Swift compiler may silently miss the protocol requirement, /// > resulting in sticky `SELECT *` requests. + /// + /// > Important: Make sure the property is declared as a computed + /// > property (`static var`), instead of a stored property + /// > (`static let`). Computed properties avoid a compiler diagnostic + /// > with stored properties: + /// > + /// > ```swift + /// > // static property 'databaseSelection' is not + /// > // concurrency-safe because non-'Sendable' type + /// > // '[any SQLSelectable]' may have shared + /// > // mutable state. + /// > static let databaseSelection: [any SQLSelectable] = [AllColumns()] + /// > ``` static var databaseSelection: [any SQLSelectable] { get } } @@ -243,7 +257,9 @@ extension TableRecord { /// /// struct PartialPlayer: TableRecord { /// static let databaseTableName = "player" - /// static let databaseSelection = [Column("id"), Column("name")] + /// static var databaseSelection: [any SQLSelectable] { + /// [Column("id"), Column("name")] + /// } /// } /// /// try dbQueue.write { db in diff --git a/README.md b/README.md index 4d5d4e24c4..5928ab542a 100644 --- a/README.md +++ b/README.md @@ -3303,12 +3303,15 @@ extension Place: TableRecord { } /// Arrange the selected columns and lock their order - static let databaseSelection: [any SQLSelectable] = [ - Columns.id, - Columns.title, - Columns.favorite, - Columns.latitude, - Columns.longitude] + static var databaseSelection: [any SQLSelectable] { + [ + Columns.id, + Columns.title, + Columns.favorite, + Columns.latitude, + Columns.longitude, + ] + } } // Fetching methods @@ -3996,12 +3999,12 @@ The default selection for a record type is controlled by the `databaseSelection` ```swift struct RestrictedPlayer : TableRecord { static let databaseTableName = "player" - static let databaseSelection: [any SQLSelectable] = [Column("id"), Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("id"), Column("name")] } } struct ExtendedPlayer : TableRecord { static let databaseTableName = "player" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } } // SELECT id, name FROM player diff --git a/TODO.md b/TODO.md index 09cb7ee9fc..69dea72a2a 100644 --- a/TODO.md +++ b/TODO.md @@ -121,7 +121,7 @@ - [ ] GRDB7: Not Sendable: databasepublishers/databaseregion, migrate, read, value, write - [X] GRDB7: Sendable closures for writePublisher - [X] GRDB7: Sendable closures for readPublisher -- [ ] GRDB7: Not Sendable: fts5customtokenizer, fts5tokenizer, fts5wrappertokenizer +- [-] GRDB7: Not Sendable: fts5customtokenizer, fts5tokenizer, fts5wrappertokenizer - [X] GRDB7: Sendable: DatabasePromise (05899228, 5a2c15b8) - [X] GRDB7: Sendable: TableAlias (f2b0b186) - [X] GRDB7: Sendable: SQLRelation (9545bf70) @@ -143,6 +143,8 @@ - [X] GRDB7: doc (c0838cf9) - [X] GRDB7/BREAKING: PersistenceContainer is Sendable (50eefa8c) - [ ] GRDB7: TableRecord.databaseSelection must be declared as a computed property (24d232aa) + - [X] Doc + - [ ] Migration Guide - [X] GRDB7: Sendable: Association (b06aaee4) - [ ] GRDB7/Tests: Sendable: ValueObservationRecorder (2947b3d7) - [ ] GRDB7: ValueObservation.print cautiously uses its stream argument (5f8b39b7) diff --git a/Tests/GRDBTests/AssociationBelongsToSQLDerivationTests.swift b/Tests/GRDBTests/AssociationBelongsToSQLDerivationTests.swift index b51ec961ec..829ff6c025 100644 --- a/Tests/GRDBTests/AssociationBelongsToSQLDerivationTests.swift +++ b/Tests/GRDBTests/AssociationBelongsToSQLDerivationTests.swift @@ -16,12 +16,12 @@ private struct B : TableRecord { private struct RestrictedB : TableRecord { static let databaseTableName = "b" - static let databaseSelection: [any SQLSelectable] = [Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("name")] } } private struct ExtendedB : TableRecord { static let databaseTableName = "b" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } } /// Test SQL generation diff --git a/Tests/GRDBTests/AssociationHasOneSQLDerivationTests.swift b/Tests/GRDBTests/AssociationHasOneSQLDerivationTests.swift index 932167d5eb..32bd7f4658 100644 --- a/Tests/GRDBTests/AssociationHasOneSQLDerivationTests.swift +++ b/Tests/GRDBTests/AssociationHasOneSQLDerivationTests.swift @@ -16,12 +16,12 @@ private struct B : TableRecord { private struct RestrictedB : TableRecord { static let databaseTableName = "b" - static let databaseSelection: [any SQLSelectable] = [Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("name")] } } private struct ExtendedB : TableRecord { static let databaseTableName = "b" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } } /// Test SQL generation diff --git a/Tests/GRDBTests/AssociationHasOneThroughSQLDerivationTests.swift b/Tests/GRDBTests/AssociationHasOneThroughSQLDerivationTests.swift index ce9a335e02..47dff47104 100644 --- a/Tests/GRDBTests/AssociationHasOneThroughSQLDerivationTests.swift +++ b/Tests/GRDBTests/AssociationHasOneThroughSQLDerivationTests.swift @@ -19,12 +19,12 @@ private struct C: TableRecord { private struct RestrictedC : TableRecord { static let databaseTableName = "c" - static let databaseSelection: [any SQLSelectable] = [Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("name")] } } private struct ExtendedC : TableRecord { static let databaseTableName = "c" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } } /// Test SQL generation diff --git a/Tests/GRDBTests/ColumnExpressionTests.swift b/Tests/GRDBTests/ColumnExpressionTests.swift index b716a07002..537b43f4fd 100644 --- a/Tests/GRDBTests/ColumnExpressionTests.swift +++ b/Tests/GRDBTests/ColumnExpressionTests.swift @@ -20,7 +20,9 @@ class ColumnExpressionTests: GRDBTestCase { } // Test databaseSelection - static let databaseSelection: [any SQLSelectable] = [Columns.id, Columns.name, Columns.score] + static var databaseSelection: [any SQLSelectable] { + [Columns.id, Columns.name, Columns.score] + } init(row: Row) { // Test row subscript @@ -80,7 +82,9 @@ class ColumnExpressionTests: GRDBTestCase { } // Test databaseSelection - static let databaseSelection: [any SQLSelectable] = [Columns.id, Columns.name, Columns.score] + static var databaseSelection: [any SQLSelectable] { + [Columns.id, Columns.name, Columns.score] + } init(row: Row) { // Test row subscript @@ -148,7 +152,9 @@ class ColumnExpressionTests: GRDBTestCase { } // Test databaseSelection - static let databaseSelection: [any SQLSelectable] = [Columns.id, Columns.name, Columns.score] + static var databaseSelection: [any SQLSelectable] { + [Columns.id, Columns.name, Columns.score] + } static var testRequest: QueryInterfaceRequest { // Test expression derivation @@ -196,7 +202,9 @@ class ColumnExpressionTests: GRDBTestCase { } // Test databaseSelection - static let databaseSelection: [any SQLSelectable] = [CodingKeys.id, CodingKeys.name, CodingKeys.score] + static var databaseSelection: [any SQLSelectable] { + [CodingKeys.id, CodingKeys.name, CodingKeys.score] + } static var testRequest: QueryInterfaceRequest { // Test expression derivation diff --git a/Tests/GRDBTests/FTS3RecordTests.swift b/Tests/GRDBTests/FTS3RecordTests.swift index c1c5168686..0369fc959c 100644 --- a/Tests/GRDBTests/FTS3RecordTests.swift +++ b/Tests/GRDBTests/FTS3RecordTests.swift @@ -19,7 +19,7 @@ extension Book : FetchableRecord { extension Book : MutablePersistableRecord { static let databaseTableName = "books" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } func encode(to container: inout PersistenceContainer) { container[.rowID] = id diff --git a/Tests/GRDBTests/FTS4RecordTests.swift b/Tests/GRDBTests/FTS4RecordTests.swift index 1c81bc6a49..c197dfad93 100644 --- a/Tests/GRDBTests/FTS4RecordTests.swift +++ b/Tests/GRDBTests/FTS4RecordTests.swift @@ -19,7 +19,7 @@ extension Book : FetchableRecord { extension Book : MutablePersistableRecord { static let databaseTableName = "books" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } func encode(to container: inout PersistenceContainer) { container[.rowID] = id diff --git a/Tests/GRDBTests/FTS5RecordTests.swift b/Tests/GRDBTests/FTS5RecordTests.swift index 6656b2ef92..0a47ce2ce8 100644 --- a/Tests/GRDBTests/FTS5RecordTests.swift +++ b/Tests/GRDBTests/FTS5RecordTests.swift @@ -20,7 +20,7 @@ extension Book : FetchableRecord { extension Book : MutablePersistableRecord { static let databaseTableName = "books" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } func encode(to container: inout PersistenceContainer) { container[.rowID] = id diff --git a/Tests/GRDBTests/JSONColumnTests.swift b/Tests/GRDBTests/JSONColumnTests.swift index da37da8fca..e544fd148f 100644 --- a/Tests/GRDBTests/JSONColumnTests.swift +++ b/Tests/GRDBTests/JSONColumnTests.swift @@ -28,7 +28,7 @@ final class JSONColumnTests: GRDBTestCase { static let info = JSONColumn(CodingKeys.info) } - static let databaseSelection: [any SQLSelectable] = [Columns.id, Columns.info] + static var databaseSelection: [any SQLSelectable] { [Columns.id, Columns.info] } } let dbQueue = try makeDatabaseQueue() diff --git a/Tests/GRDBTests/JoinSupportTests.swift b/Tests/GRDBTests/JoinSupportTests.swift index fadd8bf3eb..81ec2c3ab7 100644 --- a/Tests/GRDBTests/JoinSupportTests.swift +++ b/Tests/GRDBTests/JoinSupportTests.swift @@ -50,7 +50,7 @@ private struct T2: Codable, FetchableRecord, TableRecord { private struct T3: Codable, FetchableRecord, TableRecord { static let databaseTableName = "t3" - static let databaseSelection: [any SQLSelectable] = [Column("t1id"), Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("t1id"), Column("name")] } var t1id: Int64 var name: String } diff --git a/Tests/GRDBTests/SQLLiteralTests.swift b/Tests/GRDBTests/SQLLiteralTests.swift index e5ef6272b1..d49de427aa 100644 --- a/Tests/GRDBTests/SQLLiteralTests.swift +++ b/Tests/GRDBTests/SQLLiteralTests.swift @@ -309,7 +309,9 @@ extension SQLLiteralTests { try makeDatabaseQueue().inDatabase { db in struct Player: TableRecord { } struct AltPlayer: TableRecord { - static let databaseSelection: [any SQLSelectable] = [Column("id"), Column("name")] + static var databaseSelection: [any SQLSelectable] { + [Column("id"), Column("name")] + } } do { let query: SQL = """ diff --git a/Tests/GRDBTests/TableRecordDeleteTests.swift b/Tests/GRDBTests/TableRecordDeleteTests.swift index 50dfb561fd..758d314ed8 100644 --- a/Tests/GRDBTests/TableRecordDeleteTests.swift +++ b/Tests/GRDBTests/TableRecordDeleteTests.swift @@ -461,7 +461,7 @@ class TableRecordDeleteTests: GRDBTestCase { struct Team: MutablePersistableRecord, FetchableRecord { // Test RETURNING - static let databaseSelection: [any SQLSelectable] = [Column("id"), Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("id"), Column("name")] } static let players = hasMany(Player.self) func encode(to container: inout PersistenceContainer) { preconditionFailure("should not be called") } init(row: Row) { preconditionFailure("should not be called") } diff --git a/Tests/GRDBTests/TableRecordTests.swift b/Tests/GRDBTests/TableRecordTests.swift index f59fcd1c5e..e1c6536352 100644 --- a/Tests/GRDBTests/TableRecordTests.swift +++ b/Tests/GRDBTests/TableRecordTests.swift @@ -103,7 +103,7 @@ class TableRecordTests: GRDBTestCase { func testExtendedDatabaseSelection() throws { struct Record: TableRecord { static let databaseTableName = "t1" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in @@ -116,7 +116,9 @@ class TableRecordTests: GRDBTestCase { func testRestrictedDatabaseSelection() throws { struct Record: TableRecord { static let databaseTableName = "t1" - static let databaseSelection: [any SQLSelectable] = [Column("a"), Column("b")] + static var databaseSelection: [any SQLSelectable] { + [Column("a"), Column("b")] + } } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in diff --git a/Tests/Performance/GRDBPerformance/FetchRecordOptimizedTests.swift b/Tests/Performance/GRDBPerformance/FetchRecordOptimizedTests.swift index 3195e3003b..8cdfd53948 100644 --- a/Tests/Performance/GRDBPerformance/FetchRecordOptimizedTests.swift +++ b/Tests/Performance/GRDBPerformance/FetchRecordOptimizedTests.swift @@ -29,18 +29,20 @@ private struct Item: Codable, FetchableRecord, PersistableRecord { i9 = row[9] } - static let databaseSelection: [any SQLSelectable] = [ - Column("i0"), - Column("i1"), - Column("i2"), - Column("i3"), - Column("i4"), - Column("i5"), - Column("i6"), - Column("i7"), - Column("i8"), - Column("i9"), - ] + static var databaseSelection: [any SQLSelectable] { + [ + Column("i0"), + Column("i1"), + Column("i2"), + Column("i3"), + Column("i4"), + Column("i5"), + Column("i6"), + Column("i7"), + Column("i8"), + Column("i9"), + ] + } } /// Here we test the extraction of a plain Swift struct From e4e3da3a8ece38d9c1dfdd4607626dfc62319f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 17:22:33 +0200 Subject: [PATCH 118/160] TODO --- TODO.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 69dea72a2a..2664285025 100644 --- a/TODO.md +++ b/TODO.md @@ -160,8 +160,8 @@ - [X] GRDB7: Sendable: DatabaseRegionObservation (b4ff52fb) - [-] GRDB7: DispatchQueue.asyncSending (7b075e6b) - [X] GRDB7: Replace sequences with collection (e.g. https://github.com/tidal-music/tidal-sdk-ios/pull/39) -- [ ] GRDB7: Replace `some` DatabaseReader/Writer with `any` where possible, in order to avoid issues with accessing DatabaseContext from GRDBQuery (if the problem exists in Xcode 16) -- [ ] GRDB7: bump to iOS 13, macOS 10.15, tvOS 13 (for ValueObservation support for MainActor) +- [X] GRDB7: Replace `some` DatabaseReader/Writer with `any` where possible, in order to avoid issues with accessing DatabaseContext from GRDBQuery (if the problem exists in Xcode 16) +- [X] GRDB7: bump to iOS 13, macOS 10.15, tvOS 13 (for ValueObservation support for MainActor) - [?] GRDB7: Change ValueObservation callback argument so that it could expose snapshots? https://github.com/groue/GRDB.swift/discussions/1523#discussioncomment-9092500 From 22d43b67baa727ca75353965115ebbacaeea09c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Sep 2024 21:18:47 +0200 Subject: [PATCH 119/160] New demo app --- .../GRDBDemo.xcodeproj/project.pbxproj | 485 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../GRDBDemo/Database/AppDatabase.swift | 197 +++++++ .../GRDBDemo/Database/Models/Player.swift | 108 ++++ .../GRDBDemo/Database/Persistence.swift | 61 +++ .../GRDBDemo/GRDBDemo/GRDBDemoApp.swift | 29 ++ .../DemoApps2/GRDBDemo/GRDBDemo/Info.plist | 11 + .../Preview Assets.xcassets/Contents.json | 6 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 37 ++ .../Icon-Dark-1024\303\2271024.png" | Bin 0 -> 457655 bytes .../Icon-Light-1024\303\2271024.png" | Bin 0 -> 288380 bytes .../Resources/Assets.xcassets/Contents.json | 6 + .../LaunchScreen.imageset/Contents.json | 22 + .../LaunchScreen.imageset/LaunchIcon.pdf | Bin 0 -> 217656 bytes .../LaunchScreen.imageset/LaunchIcon~Dark.pdf | Bin 0 -> 943900 bytes .../GRDBDemo/Resources/Localizable.xcstrings | 83 +++ .../GRDBDemo/Views/PlayerCreationSheet.swift | 32 ++ .../GRDBDemo/Views/PlayerEditionView.swift | 41 ++ .../GRDBDemo/Views/PlayerFormView.swift | 52 ++ .../GRDBDemo/Views/PlayerListModel.swift | 72 +++ .../GRDBDemo/Views/PlayerListView.swift | 68 +++ .../GRDBDemo/Views/PlayerNavigationView.swift | 171 ++++++ .../GRDBDemoTests/AppDatabaseTests.swift | 55 ++ 24 files changed, 1554 insertions(+) create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo.xcodeproj/project.pbxproj create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/AppDatabase.swift create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/Models/Player.swift create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/Persistence.swift create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/GRDBDemoApp.swift create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Info.plist create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 "Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Dark-1024\303\2271024.png" create mode 100644 "Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Light-1024\303\2271024.png" create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/Contents.json create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/Contents.json create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/LaunchIcon.pdf create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/LaunchIcon~Dark.pdf create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Localizable.xcstrings create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Views/PlayerCreationSheet.swift create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Views/PlayerEditionView.swift create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Views/PlayerFormView.swift create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Views/PlayerListModel.swift create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Views/PlayerListView.swift create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemo/Views/PlayerNavigationView.swift create mode 100644 Documentation/DemoApps2/GRDBDemo/GRDBDemoTests/AppDatabaseTests.swift diff --git a/Documentation/DemoApps2/GRDBDemo/GRDBDemo.xcodeproj/project.pbxproj b/Documentation/DemoApps2/GRDBDemo/GRDBDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..e0f7ee8615 --- /dev/null +++ b/Documentation/DemoApps2/GRDBDemo/GRDBDemo.xcodeproj/project.pbxproj @@ -0,0 +1,485 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 56CFC6772C9F1E1B000B5023 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56CFC6762C9F1E1B000B5023 /* GRDB */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 56CFC6552C9F1DCA000B5023 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 56CFC63C2C9F1DC9000B5023 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 56CFC6432C9F1DC9000B5023; + remoteInfo = GRDBDemo; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 56CFC6442C9F1DC9000B5023 /* GRDBDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 56CFC6542C9F1DCA000B5023 /* GRDBDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 56CFC6AA2C9F3ADB000B5023 /* Exceptions for "GRDBDemo" folder in "GRDBDemo" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 56CFC6432C9F1DC9000B5023 /* GRDBDemo */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 56CFC6462C9F1DC9000B5023 /* GRDBDemo */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 56CFC6AA2C9F3ADB000B5023 /* Exceptions for "GRDBDemo" folder in "GRDBDemo" target */, + ); + path = GRDBDemo; + sourceTree = ""; + }; + 56CFC6572C9F1DCA000B5023 /* GRDBDemoTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = GRDBDemoTests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 56CFC6412C9F1DC9000B5023 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 56CFC6772C9F1E1B000B5023 /* GRDB in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 56CFC6512C9F1DCA000B5023 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 56CFC63B2C9F1DC9000B5023 = { + isa = PBXGroup; + children = ( + 56CFC6462C9F1DC9000B5023 /* GRDBDemo */, + 56CFC6572C9F1DCA000B5023 /* GRDBDemoTests */, + 56CFC6452C9F1DC9000B5023 /* Products */, + ); + sourceTree = ""; + }; + 56CFC6452C9F1DC9000B5023 /* Products */ = { + isa = PBXGroup; + children = ( + 56CFC6442C9F1DC9000B5023 /* GRDBDemo.app */, + 56CFC6542C9F1DCA000B5023 /* GRDBDemoTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 56CFC6432C9F1DC9000B5023 /* GRDBDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 56CFC6682C9F1DCB000B5023 /* Build configuration list for PBXNativeTarget "GRDBDemo" */; + buildPhases = ( + 56CFC6402C9F1DC9000B5023 /* Sources */, + 56CFC6412C9F1DC9000B5023 /* Frameworks */, + 56CFC6422C9F1DC9000B5023 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 56CFC6462C9F1DC9000B5023 /* GRDBDemo */, + ); + name = GRDBDemo; + packageProductDependencies = ( + 56CFC6762C9F1E1B000B5023 /* GRDB */, + ); + productName = GRDBDemo; + productReference = 56CFC6442C9F1DC9000B5023 /* GRDBDemo.app */; + productType = "com.apple.product-type.application"; + }; + 56CFC6532C9F1DCA000B5023 /* GRDBDemoTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 56CFC66B2C9F1DCB000B5023 /* Build configuration list for PBXNativeTarget "GRDBDemoTests" */; + buildPhases = ( + 56CFC6502C9F1DCA000B5023 /* Sources */, + 56CFC6512C9F1DCA000B5023 /* Frameworks */, + 56CFC6522C9F1DCA000B5023 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 56CFC6562C9F1DCA000B5023 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 56CFC6572C9F1DCA000B5023 /* GRDBDemoTests */, + ); + name = GRDBDemoTests; + packageProductDependencies = ( + ); + productName = GRDBDemoTests; + productReference = 56CFC6542C9F1DCA000B5023 /* GRDBDemoTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 56CFC63C2C9F1DC9000B5023 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + 56CFC6432C9F1DC9000B5023 = { + CreatedOnToolsVersion = 16.0; + }; + 56CFC6532C9F1DCA000B5023 = { + CreatedOnToolsVersion = 16.0; + TestTargetID = 56CFC6432C9F1DC9000B5023; + }; + }; + }; + buildConfigurationList = 56CFC63F2C9F1DC9000B5023 /* Build configuration list for PBXProject "GRDBDemo" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 56CFC63B2C9F1DC9000B5023; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 56CFC6752C9F1E1B000B5023 /* XCLocalSwiftPackageReference "../../../../GRDB.swift" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 56CFC6452C9F1DC9000B5023 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 56CFC6432C9F1DC9000B5023 /* GRDBDemo */, + 56CFC6532C9F1DCA000B5023 /* GRDBDemoTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 56CFC6422C9F1DC9000B5023 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 56CFC6522C9F1DCA000B5023 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 56CFC6402C9F1DC9000B5023 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 56CFC6502C9F1DCA000B5023 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 56CFC6562C9F1DCA000B5023 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 56CFC6432C9F1DC9000B5023 /* GRDBDemo */; + targetProxy = 56CFC6552C9F1DCA000B5023 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 56CFC6662C9F1DCB000B5023 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 56CFC6672C9F1DCB000B5023 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 56CFC6692C9F1DCB000B5023 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"GRDBDemo/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = GRDBDemo/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "GRDB Demo"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 56CFC66A2C9F1DCB000B5023 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"GRDBDemo/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = GRDBDemo/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "GRDB Demo"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 56CFC66C2C9F1DCB000B5023 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GRDBDemo"; + }; + name = Debug; + }; + 56CFC66D2C9F1DCB000B5023 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GRDBDemo"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 56CFC63F2C9F1DC9000B5023 /* Build configuration list for PBXProject "GRDBDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 56CFC6662C9F1DCB000B5023 /* Debug */, + 56CFC6672C9F1DCB000B5023 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 56CFC6682C9F1DCB000B5023 /* Build configuration list for PBXNativeTarget "GRDBDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 56CFC6692C9F1DCB000B5023 /* Debug */, + 56CFC66A2C9F1DCB000B5023 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 56CFC66B2C9F1DCB000B5023 /* Build configuration list for PBXNativeTarget "GRDBDemoTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 56CFC66C2C9F1DCB000B5023 /* Debug */, + 56CFC66D2C9F1DCB000B5023 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 56CFC6752C9F1E1B000B5023 /* XCLocalSwiftPackageReference "../../../../GRDB.swift" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../../GRDB.swift; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 56CFC6762C9F1E1B000B5023 /* GRDB */ = { + isa = XCSwiftPackageProductDependency; + productName = GRDB; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 56CFC63C2C9F1DC9000B5023 /* Project object */; +} diff --git a/Documentation/DemoApps2/GRDBDemo/GRDBDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Documentation/DemoApps2/GRDBDemo/GRDBDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/Documentation/DemoApps2/GRDBDemo/GRDBDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/AppDatabase.swift b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/AppDatabase.swift new file mode 100644 index 0000000000..a561262b04 --- /dev/null +++ b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/AppDatabase.swift @@ -0,0 +1,197 @@ +import Foundation +import GRDB +import os.log + +/// A repository of players. +/// +/// You create a `AppDatabase` with a +/// [connection to an SQLite database](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections), +/// created with a configuration returned from +/// ``makeConfiguration(_:)``. +/// +/// For example: +/// +/// ```swift +/// // Create an in-memory AppDatabase +/// let config = AppDatabase.makeConfiguration() +/// let dbQueue = try DatabaseQueue(configuration: config) +/// let appDatabase = try AppDatabase(dbQueue) +/// ``` +final class AppDatabase: Sendable { + /// Creates a `AppDatabase`, and makes sure the database schema + /// is ready. + /// + /// - important: Create the `DatabaseWriter` with a configuration + /// returned by ``makeConfiguration(_:)``. + init(_ dbWriter: any GRDB.DatabaseWriter) throws { + self.dbWriter = dbWriter + try migrator.migrate(dbWriter) + } + + /// Provides access to the database. + /// + /// Application can use a `DatabasePool`, while SwiftUI previews and tests + /// can use a fast in-memory `DatabaseQueue`. + /// + /// See + private let dbWriter: any DatabaseWriter + + /// The DatabaseMigrator that defines the database schema. + /// + /// See + private var migrator: DatabaseMigrator { + var migrator = DatabaseMigrator() + +#if DEBUG + // Speed up development by nuking the database when migrations change + // See + migrator.eraseDatabaseOnSchemaChange = true +#endif + + migrator.registerMigration("v1") { db in + // Create a table + // See + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + t.column("name", .text).notNull() + t.column("score", .integer).notNull() + } + } + + // Migrations for future application versions will be inserted here: + // migrator.registerMigration(...) { db in + // ... + // } + + return migrator + } +} + +// MARK: - Database Configuration + +extension AppDatabase { + private static let sqlLogger = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SQL") + + /// Returns a database configuration suited for `AppDatabase`. + /// + /// SQL statements are logged if the `SQL_TRACE` environment variable + /// is set. + /// + /// - parameter base: A base configuration. + static func makeConfiguration(_ base: Configuration = Configuration()) -> Configuration { + var config = base + + // An opportunity to add required custom SQL functions or + // collations, if needed: + // config.prepareDatabase { db in + // db.add(function: ...) + // } + + // Log SQL statements if the `SQL_TRACE` environment variable is set. + // See + if ProcessInfo.processInfo.environment["SQL_TRACE"] != nil { + config.prepareDatabase { db in + db.trace { + // It's ok to log statements publicly. Sensitive + // information (statement arguments) are not logged + // unless config.publicStatementArguments is set + // (see below). + os_log("%{public}@", log: sqlLogger, type: .debug, String(describing: $0)) + } + } + } + +#if DEBUG + // Protect sensitive information by enabling verbose debugging in + // DEBUG builds only. + // See + config.publicStatementArguments = true +#endif + + return config + } +} + +// MARK: - Database Access: Writes +// The write methods execute invariant-preserving database transactions. +// In this demo repository, they are pretty simple. + +extension AppDatabase { + /// Saves (inserts or updates) a player. When the method returns, the + /// player is present in the database, and its id is not nil. + func savePlayer(_ player: inout Player) throws { + try dbWriter.write { db in + try player.save(db) + } + } + + /// Delete the specified players + func deletePlayers(ids: [Int64]) throws { + try dbWriter.write { db in + _ = try Player.deleteAll(db, keys: ids) + } + } + + /// Delete all players + func deleteAllPlayers() throws { + try dbWriter.write { db in + _ = try Player.deleteAll(db) + } + } + + /// Refresh all players (by performing some random changes, for demo purpose). + func refreshPlayers() async throws { + try await dbWriter.write { [self] db in + if try Player.all().isEmpty(db) { + // When database is empty, insert new random players + try createRandomPlayers(db) + } else { + // Insert a player + if Bool.random() { + _ = try Player.makeRandom().inserted(db) // insert but ignore inserted id + } + + // Delete a random player + if Bool.random() { + try Player.order(sql: "RANDOM()").limit(1).deleteAll(db) + } + + // Update some players + for var player in try Player.fetchAll(db) where Bool.random() { + try player.updateChanges(db) { + $0.score = Player.randomScore() + } + } + } + } + } + + /// Create random players if the database is empty. + func createRandomPlayersIfEmpty() throws { + try dbWriter.write { db in + if try Player.all().isEmpty(db) { + try createRandomPlayers(db) + } + } + } + + /// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`. + private func createRandomPlayers(_ db: Database) throws { + for _ in 0..<8 { + _ = try Player.makeRandom().inserted(db) // insert but ignore inserted id + } + } +} + +// MARK: - Database Access: Reads + +// This demo app does not provide any specific reading method, and instead +// gives an unrestricted read-only access to the rest of the application. +// In your app, you are free to choose another path, and define focused +// reading methods. +extension AppDatabase { + /// Provides a read-only access to the database. + var reader: any GRDB.DatabaseReader { + dbWriter + } +} diff --git a/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/Models/Player.swift b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/Models/Player.swift new file mode 100644 index 0000000000..0c63186f95 --- /dev/null +++ b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/Models/Player.swift @@ -0,0 +1,108 @@ +import GRDB + +/// The Player struct. +/// +/// Identifiable conformance supports SwiftUI list animations, and type-safe +/// GRDB primary key methods. +/// Equatable conformance supports tests. +struct Player: Equatable { + /// The player id. + /// + /// Int64 is the recommended type for auto-incremented database ids. + /// Use nil for players that are not inserted yet in the database. + var id: Int64? + var name: String + var score: Int +} + +extension Player { + private static let names = [ + "Arthur", "Anita", "Barbara", "Bernard", "Craig", "Chiara", "David", + "Dean", "Éric", "Elena", "Fatima", "Frederik", "Gilbert", "Georgette", + "Henriette", "Hassan", "Ignacio", "Irene", "Julie", "Jack", "Karl", + "Kristel", "Louis", "Liz", "Masashi", "Mary", "Noam", "Nicole", + "Ophelie", "Oleg", "Pascal", "Patricia", "Quentin", "Quinn", "Raoul", + "Rachel", "Stephan", "Susie", "Tristan", "Tatiana", "Ursule", "Urbain", + "Victor", "Violette", "Wilfried", "Wilhelmina", "Yvon", "Yann", + "Zazie", "Zoé"] + + /// Creates a new player with empty name and zero score + static func new() -> Player { + Player(id: nil, name: "", score: 0) + } + + /// Creates a new player with random name and random score + static func makeRandom() -> Player { + Player(id: nil, name: randomName(), score: randomScore()) + } + + /// Returns a random name + static func randomName() -> String { + names.randomElement()! + } + + /// Returns a random score + static func randomScore() -> Int { + 10 * Int.random(in: 0...100) + } +} + +// MARK: - Database + +/// Make Player a Codable Record. +/// +/// See +extension Player: Codable, FetchableRecord, MutablePersistableRecord { + // Define database columns from CodingKeys + enum Columns { + static let name = Column(CodingKeys.name) + static let score = Column(CodingKeys.score) + } + + /// Updates a player id after it has been inserted in the database. + mutating func didInsert(_ inserted: InsertionSuccess) { + id = inserted.rowID + } +} + +// Convenience access to player columns in this file +private typealias Columns = Player.Columns + +// MARK: - Player Database Requests + +/// Define some player requests used by the application. +/// +/// See +extension DerivableRequest { + /// A request of players ordered by name. + /// + /// For example: + /// + /// let players: [Player] = try dbWriter.read { db in + /// try Player.all().orderedByName().fetchAll(db) + /// } + func orderedByName() -> Self { + // Sort by name in a localized case insensitive fashion + // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison + order(Columns.name.collating(.localizedCaseInsensitiveCompare)) + } + + /// A request of players ordered by score. + /// + /// For example: + /// + /// let players: [Player] = try dbWriter.read { db in + /// try Player.all().orderedByScore().fetchAll(db) + /// } + /// let bestPlayer: Player? = try dbWriter.read { db in + /// try Player.all().orderedByScore().fetchOne(db) + /// } + func orderedByScore() -> Self { + // Sort by descending score, and then by name, in a + // localized case insensitive fashion + // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison + order( + Columns.score.desc, + Columns.name.collating(.localizedCaseInsensitiveCompare)) + } +} diff --git a/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/Persistence.swift b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/Persistence.swift new file mode 100644 index 0000000000..94ab0ae79d --- /dev/null +++ b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Database/Persistence.swift @@ -0,0 +1,61 @@ +import Foundation +import GRDB + +extension AppDatabase { + /// The database for the application + static let shared = makeShared() + + private static func makeShared() -> AppDatabase { + do { + // Apply recommendations from + // + + // Create the "Application Support/Database" directory if needed + let fileManager = FileManager.default + let appSupportURL = try fileManager.url( + for: .applicationSupportDirectory, in: .userDomainMask, + appropriateFor: nil, create: true) + let directoryURL = appSupportURL.appendingPathComponent("Database", isDirectory: true) + try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true) + + // Open or create the database + let databaseURL = directoryURL.appendingPathComponent("db.sqlite") + let config = AppDatabase.makeConfiguration() + let dbPool = try DatabasePool(path: databaseURL.path, configuration: config) + + // Create the AppDatabase + let appDatabase = try AppDatabase(dbPool) + + // Populate the database if it is empty, for better demo purpose. + try appDatabase.createRandomPlayersIfEmpty() + + return appDatabase + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. + // + // Typical reasons for an error here include: + // * The parent directory cannot be created, or disallows writing. + // * The database is not accessible, due to permissions or data protection when the device is locked. + // * The device is out of space. + // * The database could not be migrated to its latest schema version. + // Check the error message to determine what the actual problem was. + fatalError("Unresolved error \(error)") + } + } + + /// Creates an empty database for SwiftUI previews + static func empty() -> AppDatabase { + // Connect to an in-memory database + // See https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections + let dbQueue = try! DatabaseQueue(configuration: AppDatabase.makeConfiguration()) + return try! AppDatabase(dbQueue) + } + + /// Creates a database full of random players for SwiftUI previews + static func random() -> AppDatabase { + let appDatabase = empty() + try! appDatabase.createRandomPlayersIfEmpty() + return appDatabase + } +} diff --git a/Documentation/DemoApps2/GRDBDemo/GRDBDemo/GRDBDemoApp.swift b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/GRDBDemoApp.swift new file mode 100644 index 0000000000..e9c5dc94cc --- /dev/null +++ b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/GRDBDemoApp.swift @@ -0,0 +1,29 @@ +import SwiftUI + +@main +struct GRDBDemoApp: App { + var body: some Scene { + WindowGroup { + PlayerNavigationView().appDatabase(.shared) + } + } +} + +// MARK: - Give SwiftUI access to the database + +private struct AppDatabaseKey: EnvironmentKey { + static var defaultValue: AppDatabase { .empty() } +} + +extension EnvironmentValues { + var appDatabase: AppDatabase { + get { self[AppDatabaseKey.self] } + set { self[AppDatabaseKey.self] = newValue } + } +} + +extension View { + func appDatabase(_ appDatabase: AppDatabase) -> some View { + self.environment(\.appDatabase, appDatabase) + } +} diff --git a/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Info.plist b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Info.plist new file mode 100644 index 0000000000..0427f524a1 --- /dev/null +++ b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Info.plist @@ -0,0 +1,11 @@ + + + + + UILaunchScreen + + UIImageName + LaunchScreen + + + diff --git a/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Preview Content/Preview Assets.xcassets/Contents.json b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..3395dad77a --- /dev/null +++ b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,37 @@ +{ + "images" : [ + { + "filename" : "Icon-Light-1024×1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-Dark-1024×1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Dark-1024\303\2271024.png" "b/Documentation/DemoApps2/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Dark-1024\303\2271024.png" new file mode 100644 index 0000000000000000000000000000000000000000..a3aca30da863e338caca01153112afffe1b6cac5 GIT binary patch literal 457655 zcmeFZ=T}p0^9G72A}C@5M4Al?Eh^HR4NCHxv8hTGa5CViA zAoS1!1PBn4Ktht^`#k>6`4`TI=R?+>y|dO{+54WE>zZrk&f7=EdIJ1s__?^a1Rg%P zXUfHO;E#EL>(IeJ2e^c6Bp27dAosg>A9>!@y9@FH`9JY>aCE-s9^mWc5cWusi|a~! z&_|R(%cE1RCIzN_>Tixph~Id7cW{6|*w0P&lPhl2Vsu=;bwoPJ>L}h8*SC*6F4S)Zr9ITrM zM@DBo$#*IH`Av?W&8jC+`l(YUZ>)^!gO81x*e3TVBnM}4KY_FChO#@-&aeoBw_`+w zr{muydcWtJl9s&gc6By?x&e!MiBrhd$Vzm|*hzXi!0++8=Am4!#x{gE`JHo7W#q_I z_K;u=PnP|r^nG z?Uj!;0VJ4U5wpYFn=F*#?b`zw8@sb#HS=r2(D+#BfzxNWd{zvV%=A{i(iZ)jn0(h# zxL=d6lshaop9=xQk1T;__8gCFNN@)yQjiyV*3Lv_N64~xMXq`@JUMt9`)3_gou58* zF*4$k`C}jA+86Eq&m!&nb9Uy>hl`8*3-|wBIl%R0|Nm$I@5>FL_RCycx40hOyKNS_ zZxwa8#Cg<;P~D4~)Y3BP{&4fez|vdEL#MR3_*#!9a{0eY;rFvW_8~@KF}{Ys^M_Zi zui?iD;fsijN5!8Hn+oOr9QkS*+jKzmBiG@3#{gQlPjEksy!q+=fE$9C6Gq*hn@S*a zw1%z0*#XSpUN@I;7gzyvy>hr3jc2T($ShhdaZ1DJ=Am0$+{a#U9XQLi??3au_wb(+ z{1*)WWrF`Q;(u`PA1waI3I5|o{}IFgSn+?X`2UZr*yI>@!Q}^8KluLraiJ^6RsATN z%k3-`LngR4#iCxtkVI8PxI0UcJdd;uDWf~XnIqMLVo2|XPmsE1?btC)JOwf`5V)=> z;6IdM`s%Ie-+Hk|xT%1(&AnRO)V3zL36`a0IF4DVr8Tj{)5q_! z10$ifwV7>WzZoZrwd)-`1m~-qJXfGvm`{0$J_?}Yuk0nTmgT@50BRU1buIg{JTKVq z8KVK^yJ0m>mdAw?liWk}v-Q6C)m>$RGRhxTxS zb}E@u@Z}0cl-C#`Y`Ma^aZvId@VUuiZ#6H)b*yD`rHOaDMO9vEgBA56JU`hlMsVjw zrd4N@wZEutlf3;U70%r4m4`0O@_uf@pB=vej5>N><2zNMRCvddK2&WT1 zG&ybbp#Bx!YmZA}sqj%YsR!Ck}^61Ge-1tfA zRz&C~1hv@&OMz(>N^Sfx_ZkO*ZNOrH3DJRpr8;v*uesK4?dp?6Jf9 zrP6}wIyYG(p4!!hacchu+9LOF0n6O~ub2_PKmD6oT$1evk3z+;%xRAp`R=aHENekF zo({^!pWJObx!KIzdJF#Am{LWqswV#goD#;-CinDRk3${9Q&)6QAI?Qlj+U+M-Es?W^iG`e%IOEF}O#SGCe?#=vv37kf@&E62pOh*k&t@~j zFKmQ^i-%g+q}9A(7!IA~G_>MSjrQ`c`(@&aytReVEa+%EDAFgDRqbgG%f{3pzWV zvs+<=Y=GYToM#RtDLxHZ&x&2vo>N`h;+p6CAX&$lcm-`Iz{*|~m7Ec~#y;BUw(+>e z!uQ{BPQDot`99M?-TPDIzX)u5AmSw4jnLh=C$?JMH6E?Cjoc&BL-vYDA~TD(hVDvz z6s&-R9;OB}8JS$z>S+MVzK}h@F4;X9z+ncn*Le1UuRlXE*4)*p15`gl%d(*dv^C9kGXZM544;e3rS#I)do*zqgy4nfE&`_X&~F8K874Ro{S4`H&<4& zXe{!Ea|viHlL`V_`x@GKYc#fbVp?Xewv8k)um_TM<*3=>Oio2+boFTc{#BlcfW}s! z=xM;Qwtp6F2u?C^iL$AgUrUp##lk{ z7y4!&d2}o_0E}BMCoQkmvfMdeL=+Xys%je?zG0Wb_M88@rs71I^`(uQ_x{GIGC1Yd@1@ z{H`5gUw}S?9H8k1had{#q*9Aj(HeGsRxme6my=TC>Km;KfUxpf?N|MuL zV~z3lVP18MTNiLjmXevp7ND}FwR4=On(fH#d-m>FO^gD4=Y-PNoF5rKL~Z~*XaH5D6PW4wsAUL+?!2E~B5g=7J00&p>tb z!PMm}-kwy$UR-o#7RMoN9Pb&q0{9_6C%-n%P}|S3!Fgb&M15BYThE)KN_2Vuc`8X0 zZV^V}#%5Ri$iL2IcF}h>Qx7qu1RsSRWP-hz1P>{Uf_O`WpqI${b3~XQds||URIIIR z7c%si1V}Hg=4F$-*%h*JBOxnmtew#n2lXr}#b9ezHoz!HQPZ+M+69?P>7}MzDCew5Eyqp0B=n#_@`{HXGE3mY%Jk_PhkI6VlgV(_Mjh zsB58))+Cf;B}L$T)B@@h4qLsdExS6};`g*_WvGgw;dr>wi)^@SiDOJvn_Nb3&RilN zSN)5lfAF1}xZ1xFV{+)$v%~vjUs>vKa&i4wWP0qXRAw-_jsP>mGlq^cOQgo_GPNt~ z$7&%VA@uF|<<$yLotk=2P_ZcQjHx1V%Co|ve0oMX8Yjd6{0?cto*a$KnfDAcWP&c?n@eG;Yk zvT5$Yv=oZDjN80G&{{KrkC%m-*B=iQ{JH$PzYmn*kb%7v=2WEB@A9JVxGXvhf0vbP z7g7rqr^ksEqrB>9{!Hx4DhrPx%d#~dE}M;Q`B=%eA^qw*^>U!~Doe|u_uQ5JT*s@{ z`=LX>pwaK%`hVPK3w{Gsrf$fy+N+C3zBK;)G6duPagq|+gjY@b7XHL`6fF7a6#yt^ zTe*`MqJBjqWbVGO78KSG%b1?m%FP{Lhq=`aX`_AVdCOnRp2_Ung-gVzZcdTpXI+vlzMWZyxnK+t>J9=jbm!z6gWNCIBp$wXLY(3VdchbGYal zEJCOWP?FxZh&x=QT3MP`qxk1S4j3H z8;Ar!NWC)jHS{uP6mO@j#bn49>S)`!>L5Y%XffvFXqV?F!8s-YmHl?$E96eG@q8v) z{5MJ>Zo>L-b(-E@%;~Gh=tV>MABl#d=AW!3Xob$*LItbRTRVq)v@@@ESS(81itz+k zpLr5b9vtqo8Fd@58)bW+pA^0!kUqG`IR3Ji5Y`DiveP^nUfpyz0mgY&ks8DY2ENU+ z98dBp{JlA|tmDMRma2aZ3lGOxHiiUZPXAjJY&<>Qjtq=5SDn2zJ#cYjIDu4PlC4Ge zkeyyD!e}5%SI9~h?y#t#7nZw5&!g=>&^BwtguYR4`JAZ=+*w;t^2)1v`DAzz%Wo)z zqCT76?9OQz9%*qZlq8^B_WZS7#xRdHJ+s8NXYoH=lz&%eYo_=p#iOp3pj+9H5!dZI zzrtOKIMUFR5r(WydrJ;&)z@L_M>a(rSKTxQ_chWq)BHto!^nBK#F zbF?6MpOQ0PCmZU&$wcn~w)FzbcMu}kA_FC-yzBN}Stvv^?c00+K^*ZJaIE3HhZno{ z-VNz#RE!9Xx?3o@SZTk~dAV!*xt`iJk-J|$ zhk6|{i`p8wcY*(a!aiGz zv-hb|FWoNa6J)6m7YQpXBobs4jQT8}%SluU1A{6>3O4($Nktb+3CU#Eh8saj_4@4@ z=!%+*`rE(h+vO!yr6QC5KD*p})Rx%OG!^96WxZ4wsb(iN7jN0D)SV1THRxBT7wv)}he_Q)>fK|FsZt~i_J4s5D zZaMkd;!mY~?5YKf&Y89Gh9#u>PQ3e&)_u*-Gvrt{sPhs;#zc^)R#7^Rxg)Rg*vA@1 zU(4yTS)=nUwZ7<`^6Rue7J7w!&!G+isT;DZD#*^H#qM=&pG2Z}H#;JJ{v`qDONxBL zWY)4#+B}E$W{D>71-1wEAau1j7ga>YXIS^r zVbqnu7GJY%csB(}bzU>R0o-i7vyJ@4*tQGZ<;E zr^2^p1&>yp(|Xa~ocTb-Ss{=5HN;%vJbS+jJYknJ;(=oCRFpGIStyjCPT+W{4pReZ zXV*?(d#s<rgTlfx#G&&4<$EfcJ-!RR$QXh;hEPN1YPGWu*L>4@v|$RMcp zqb;Ac_wh(9sxQ$6R zit=muNwtnDlu^ZG0R(GyThu~d9J$EcbL2>a0Nel^5l1pjz{aOKzfh z&ys`R6YlGuwO;sX6_cnppR#$^oWzKa(6L!Fl(HowwJYq6q$bAIwtkVpv=yiPJ}uEh z89PnDGVi@NnH!u6!m}VD0TCpvuE7mJ+vkx7Vy~LG@7OSa&jj}XWxSl+(ZgJIfz%h0 z^5oPH5~7K6KB;Eg#gePNAgSbrBeuN|ruVa+Ims1z9%UIWts17 zk&Z&3v87d3Zj&7Y4^)22hQ3l(5To)|cG{>hi6T$7_o%hsoQC5RI`3;wR!7J60?r)< zUWo43_D_swN2|m+w*|pEL+nmyMUIh#!6%`c5#_jUU=5s6fjavogl|dj&>kgOoN;z` zH*f?fCy2Hy(fnK#u7vB|gR0Z%y%{RK_%)dIp)^xj*gF?iWQj}QqIJoNanYb?x%$Z5S2?;%3eFlTe>{{oECdE z%3JqMvr-}8672W3_>Fej&gKih+nZX|SJ37c@%!}w#zIDq&$~*wX}1B7i60JG)_@Ce zzr5%Y&jW8mh8AQF6_KdyXap-$&=|+&UBd+F?7SpOF-KIk3=ZjTyzOTfRIeH%LpQ5O z$!~X#YYYKN$s~LM4T6q{zao|h^2C8?yQwV{z9N|ToKS2X z-~xKNPJ^QOWL^8_fT_XI;`LZf+kjg|wZlXAlNA!)S}*6?PqwHjF;ANpD~hEie*?Jr zaGTsXkS@3!kdDZL-D&(}0ACmnCoCU((lP@5utNE6#n4{>hyWn+z7LVz1%lj)u-Lryawv`HApW&LPw|BmlvzF8l0fJT$!Yk|ecP8?2 z%dMB{ukBbJTp`gnj8)}^pEcD1^jf{{)_zhLZQr3-j{sOr zM#3CZUyg%!7Mr#y?A`<66UQn}Z`r;N`tIQ~*Ip1syRASqLWHIERJKsP2EN1NCNt+k!Q)~)z&MG7V`tYi`Q8qlpHF$(4%kr**x-9lpTlKC zw}_$VreE&+cIY3ijQ7}2fH|++h1X3vcc;BoiU1nW?kS)>nal~2eT^PkL1N8c>G~Bt z?{l$Vb?!0Gc2C=iY8Oy`*Db%OkRNWC;Go%t`njWjzjz*NqZ;F`d88u80XjIOS~!on zpLI)LR+KtWPL+WuA)}*4;GeEf&h`f>_Deoo95(dfOd@+)YStg_KA(3QBgW{=ho9T* zW@*vBY}$iIAoPkUaBuvDWE2sk=^2!s+d6ao(lec=O@tsM8s#{bJ)%n$3jTJ1?Q3i| zjeiy6Z-qCmcWR#9!$h(Cn8qA~tbp$ASy!Mth|+iiN%UQh`x)eIEy-_QlqQ?yZ5GxnLx^LjyX+*akNspWWhKay|iuH9WVx)fa zA5t@Ieh)OA%2lj(L(DVhj^2wL1%5De~&y~H=7p*(Yco~ z+qXKI3`s8&t<-d7Pns_-oH=(*VnLFFi+GOc-CnHwv@CK6);uM;LCmfe+26CMvY2CI z_`}MP>ak#fvX1IqIF?1cQlGi(h-0rvnSj`7)hUr%fsz-J{-(`S1qR>a4=50(&YolqxKE-Vfpq-AC{zhInelHLaPqE6E4d8R1S zf%7alQLTX2Af5gA>;{6)7NRg*n4v=mdc&JBH6+FS@hcMijZ=n#y3QT}t3$ zu>*Ci%@Pu~1SGXPTK4RXQBGeCpQ}~_U^6`~!YYvHEb{nkif_(^WIu_Y0FG95pV+Pi zx;UXU5aEP8Jv+X()|DcV$K#sH zf{}yvIZ?rj#B)=IQAIY4W5KPsR+N2E7^Fl>klNEWDV1mX zomePCx{Up%c2@$d(osLm#7t4OdTpS_KJFq9renz9oXl3Z&lo6Wv+)+?{p0#BSQr7j zrojThr<8D1ODPkaX0kHQ9l1y^vnx@j22FYtVGYIu2W}`N)P%Z(uQwG<61u+H!Ggk+ zz;PP(#Jc<4puOx=J!AEJ-AgQnA+dH}uj)Vm(2qRYC(?XZ{=+V2zt0Z6DE3xcsq0z$ zUuGx`OR~K3G4}a*>xVof!>r($%Lu5}UiFX35W)+lMw7j#q!PZ8vm<`8_TGn`y|djd zbIi*xt}97d>Q|aj7c{Cw0(6E4rkz`+${Lt#94>pbo?F`ka4@4->iB6d1-GA1DMkFZ z+La~t5YX;DFfOi<){<`U;BO)g;AY8g+V&jbjuVZjF2vXCHI~r5X^&|1qK{B0pecXq z?W{Fl4E5vX6rRUJZ|o1<&lyRi$dJ`bD=mnvgK_2$lG`WKq00($mRc65)tf%vPv3AW zC0vi%P`fhniK!BA;BBgw>@)A$R(u5&`X$ePYrtE-ST5`oBQgDX7xzelZk>0X^Am02 zW`VerB&1Ssn|G6?ANSlmn>BU2`?~4Is~Y zB}QNJvW2VP`$Rw#$~DDL2H&6Pf2DjWN2p5loWINk5{9LB@= zaV}c5O5+TiMf?3}M{4Ll5d60z`1HgZq=&)PfPXMDn8-*gC-y8y%to(yf9R#(=*-Wx zr+Wz}I7X-esus&1G)fwlUkM;Y_R3pw3J-d|O;G`Ch5JO$JU;$Z)Epotg`X9^OxZzP zTcTCoerh~qkkPFqmolDGm~=il3MwiP9{E!M<)(6}=uL4*|6DV#;9 zUaspJ%kKNmB}aNi&6iK(p`UniCIP&KfL{WPdRhxBeELsEx100YGp^UzTXPB1n#23p z?!r06=%l+b7|#NQm~dZeUmnS_(X@+Hxd$2dp6``$yQ-%;6if%*wraK$$agVrQWjnO!=iM*nWX#d;xRYuWqFwe+)6 zwn(+jVJ*e6D~8eq&Bba0CPkqEnfW>4$2Ia*>@?NDxaQKuzRqQ>YcYM z`FJlCcW9PvZ6&rla`cmVmpSczUIA*T4a3Tn>(0UL@;U-NnoN_3id`2Al&j-*tVx1L zwe;SvWylyWZSO2RS*#Tf9M`;Ahhj?lCx9zqtecbRaw=f(8bd;l9>*FEK|Ko!ant6B z1%>xkk14=AM;Zr1Y>0o@UcK=r`@6zeF+H8=GOQ3KV8OW_C)>DJw=A0lj$wImj%RX# zrozOZRB_btK|Qn952S{>o5y~8M}G&}qy`k`c%K;#x|kR~feWgztHuZQ|7pPvjW-0HX4s#Y}knJO?6Mkl%-CUtRID}cZ}VH z=u0LyCG~!HIF;7jb+8J*15r>Fp462#`2Lmh0Osp$!>2)=(d%rfNy6!kfREt;7PYIJ z3uVT@jsn53i_>%JmJ2`wA1QT5nCY^b496{Z&EyblaO(KRj0xN?e0oq+@$tYgciXRy z!70;8i6ldsQZdyXGu2(e5k#oKCjp+Rf3?di+-nB9d`ssx+>ZBUZ)$+M*>H^_I(*PW zlIhLPw<-)RhYx{Qx7jZyog?>5f}~*))n0D!f4a-!G-xUm2?rww0t9UQkJXtlx{sq}On^<#i;b zk2Ji{x`HMhjXq+ojGpaCLAK0}hcDRecAy~wi(galEyqyKcFOYXJC>xia9rBLn%$!M z`)qqfHk$RTUT_4-fMLQbDY4Yi3ypLXz*=GpJ$1e1a{CTEw3o6faZJZ?luaadKYg^7 zQEwkJBsEZsS}9u|qD^xK#E_}1OB_v!HUIeZaJK^7$YSYI{0hU(Q#xfd{Sgr@UYm1@ zu`s&o>d6ZD{M*@nT)>LHmi>Le9-m5f{~*SVzXFWE4S($Q06xOG(#<7wxfwGRC}B`3 z{WRf`51!qnyVNy&5Zvf$eFURgKKx^F2uxvi+^>4rcp3A_*Zg$|0_x|< z9w=E=jBo))3a~m|-9blXUT?W!(;4VXoMNeWiQcP~qRgfzdAMnDJdQmTUbj5VLK_xf~v)T{`$Ayt(kLZF!@vNAYydY z(V&U{+Yo?IMbE@}aI&piw_d)HOtu#?xI4L^TPX76Zp7SO#bu?*343h=)VrFr0)kgr z@A<(~){v9c=WE0t1iX(O7wkz7ukqPEBBK>^JC{%Kfr5b8N{h_JobMKzW{2M8_{aGb zRzq7Wq!b^_Tcqk`8*KSVrBNR*C0ER*4|03AA$iY)9KZiwU~sor_eq7s)AkYkX&st{ zE=^9)61#JwYBB6xG%@kh;z;0~?ttL^A~oh?ziYO(jv>n5fCM=tLZu}SETFQUO(#%R zze|;bI-mB*et` z_!rk6Wh&^vBKZ$G^;-i1xbd--;PPQJ~Pakkldrx^_4OEt;|yb`?hbWy!Fgo z_P>iqvh>fQ5W64Y4`hZ^&N|RsHgdIGjS1T=O{AEtZNY-x=r~6kZ_d170 zO|fTNy5`2hJMAXh;&%t&PWeOYCa(>czfu9lxLlulIAi)UIm4%1>g1Ary-uE8V0tQ& zTrK6zC|zzRv0DS5JL|yzoji%iy%E7sr+9!G zN7Kx&md{?}Z@nU86O}CF_St-~w+r>OJOZ2FM><^Gw=M4*)6%23*;VnavhM2hj8LBVk5boRNogL1{VyD*w6>(qxp5s1AHal1CQ}J*~IX_WIy( zGpXPsHStVGyySD2aQ}61tg710p^8sU(H(V*sR2t?7OAe(_yTE1(WuKBW1N^Bc`V>9Xyl;*5 zlM9vfsaW`Otq0|0tC#*qk&`yTs)+VLiX**^!Feptj2YKW=_dty7*;*NW@AqeEc(MC zY|;au<%xD-K!Km`D{D=4+H-~aGUiMsl%R_=CRPT#46qrgzL$i>hW#AR7N`Ku@6I7P z)BdUdPN``I9aI!_C10NO6#LX}FX7^PMfvUKz&s1eqAU+(C&!t!JBzWn6Lvj`Ls23mT>U)eraiV> zC-KglTKwex%1~$C>xB`Kl|G@xsQfonTYVd7?PgHG8A1Nq*^ZH#**`pPP9=A-H4ycb zZm%yhLDC#;53F9W#XnxQkOCWB5CvE-OlI`VVm#nWc{`ud0ksCa*qxTyEY#)#-H5(c zk7IblNwpY-?w3q5i@ksws^^T*D1u&=w7WZw1u?>n6L4>IgzB@&K@emy5$6fq`mp?( zv?{{PfoWY~?poB0?!NVGZ*`O@7T$d&mH9HXhC=yVH?Xw67*;#spHCf&S)8jB+NEmF zI$4_WY;YVB=8ll<%IPeNEawVQc*W4p%e6nU4y#=`<*8id>Pp~{HWt2CJMVC6H-~hL zZ*XKD??j!Jfft(|mG@mZj#zlsfMM#_1!VjE{B7V=SKGl>yL0?}Tqk_J^s;Rj|APhT zUVY0y^5@QAYH@t@$LBexZci8=ZPuoG&jxJz3eTt+X`TyleojqHrWgR1eFp$XAcSnf zb?Ey1{+9b~PSJh~!%6|CHi;ees~Ky=Tm+NG?%3|-%;pIw9{9Ie_T#eebaL!vR?q14 z_dC<_)rEk@*^yc)6{3!p?s&h9zjCDcbwr{}%^gqs7rI{&++7AD$da2q=ds9V8lk@y zikq{$3rhHreue?r6@IcKcm7oK>B5RFa{e7%Uj5sOQi-q6SXj@vBW69n+VeQNncvnJ zbDP+!jpws&7+8hM=fz#@YG;!mZg8dHXQC#a-PLMK&OTClKi-w86(kA+Zx?$1R;i5+ z(0y{K^8NelT?@mwgyL|>!^i+lE?tE&NSeVjnbccq<=QkUT6~IaYF4%_33oe#l2R}G zM9fRaRT)$*wFe!qox@CRvQ!G!6EK)c)`%1&l0}W-v~0mkiPu-dJnd^u2*^`Oj21?+ z9ej6tEP3)6VG~fBddfbP8TMc?=NmfhLRE+Du+pUEVqcE4L}@d$$fr$i?K<*Oc{|M5 z=Ip|GtNEL~fahixsX;Lnq;5IK-cmEah7eclIBfnX_7cT6SvhzEk@Ec-eQxoq|D^5p zj)92JBHk5rFn%^^%x&GJ_L?oFHnoQQ;|a6iS^!6S=xBg6GAo!V1hr5o(%KA8^ z70*^BqTfzxzjpR13v}WyCH56O8h7k;a0_H^XMfqV{?t1d@Lpf$l6qCfP4v(os>hD| z;|w#!heM4)e4PW7?uu0*105XR;SXpamWM*9|_Odjvd_bwkYMdz@qhP%UiH zGZ!_~I||SThuJ=TylU1@;`zwmE>mKyloD<(ZvOQfzl+?**W3n(eBW=k46>I&INMI) zN#W1f(}Xp}buaJNS6Aon}Q;ojQe?ymufWlACDZ$}!6lC#9?EU$t~ zmXhnXKG*)R6dj2!oP688x;N4o?uL68*DFGhc*WwuNKtMQ+@ZdE?%wb;PpSfz<_*?h zHZoAXwX5H=b{vCt$l9R`funbI+#8q3(I+~RFO{vaUV9=_&@^Y0Y#&HlVs~zKqN-Ax z)6`1V6Qq(3(A(B^Fg#(Xa-dic2O@?#Twob#?RM;h2@IN<77gV*-n=ta$#~#8>T+VF zWH&LQG7N&fLDg^kaNqA~n_tV+Z_$3CmAa$w_!rDFfmhx;qTY8*w`xRgdZHoXHu;)* z%rgZck4(!rRJFm@ROeN!l5~|Y5ztne{P8vP{$#uK%?0nl@(o-4dzHlPlq`lMiSd?d+w*WVH*mOeB3u>7nUD}PBy&b2a2;T( zr`|w+=&zmz@oVu`Ub3hB-(O-bFAr{VU<{zWR>R~pm(-hc|J!yr{qXiJAh+AG(0d1_ z%IwMZK^S)2I+;99;Kz@@DG$%%ZfyXs%><TYC<0{pCg8HlJ1zr zBA0aclqH^5o|lohsk4=$Ee{k5k5(e2dwKL7k+CmyXAY%}o0i(&u*~zLIV;i}f1WAO z`B@7JkvzlOW|BDo3crD~K1iYZnx7}hCfJ^?Q?1t8M<35E9+lcqM{rWi2 z+8UzAKc0|+iQ~PhyJ@jGe$yhq4>`pOGp(8`=D@Ua!xr;XW#JPr+v#LOR-PKh29MN9cRl_RjE4#g5ElH zhsEAnKFjAF*38c8*@}k{0b=1imt~H^9cy)NBH#hRqiI?tOI&3G3#~xd_3Lz%S zqdrXg8%5*fbUxwP1tB>jSxh?kSL9LJ^6pvYoyl3SZ^ zcK0wS#cs{rp??!l2AUS`ypt~20-%Hm`1y<06r{VcZOBLhM8XZEoYmG0^Mvg7>$E45>@O;{fx@@WxrVW2$k2qSD_fF7KZ?bzVQNP+RpCB>2&G;n%CW~*GO=U1 zAn>zrGRD}LuI&cbesqKPxTkkUt=&AMdvSvqs`A2d@n1(UxdlAxek^q40E{}%aK^D~ z;od==r0YaHVNLy`aqhb@0e z2h0lc3`MAK^@{`y=vK}vrGr%cy*9ni6rLBwzjxsJwBK_FHqF949FPr1&;4e~H4vw8~bL6^|k8%Sh9O8~biWQNX8LK0T+>h~ha02^;5zu$|_ z&ioy1Yu=F6k@f8r5j!2!Y!rSxn{pUAJ7klQKJl~m7*<`-UkxvR$tT<3`~4;Q@0VZP z4X(E4=$NRSnn0&wc2{cKe&(7PE1Hah=5=0SpB1~q>b*yHD*In`eB;Srdd`Y_Vc~DQ ztRr;{&KBQ2S^qUW@1Ci)Ts(CuiFsT^>8hmxID1%?I{T*qc>L=rS%Dl!$5c6+lR3tj z)it0WYf}w`&+hoqv9pUXsrud>h-mi7x=%#Y;_e^FA5ep;;{J_t;6+db`5<-W762`Fms=KIHM|3ikvKs=v>0R?cXsUxDl!QSS#VEFT6(>{bnE>nx1K`U zgc>${=Gjfa_zG#l4d%Poj#X)pM^7VAu8qfaBtL(~lr5imCm|)?=r10$kh{i#Y|1CQ zkPlvR+ZDh!?k$L#YuA1Q@~}N=0d*j)GWe=xBZ;S=UWooJom0q>L6hM9ZQpO}B-CZw z(oJkAWub2dVch{+IPVKY1t~>sfm~Qd1gJh$4I6zBrV^9%!WQ;!4Wv4P5 z7v|hFX7NN2Rt%iiO1f$R2ul;u;adb>#W+~9$CNzK7b#lp$(3W@m;+7*0b=jX^UMtK z>qnbI%0V6BM?`Y3Y`r}oqqinM`#G|!vVN1NHG?F-?J0vg!$fz*V?LG?ZBN|KOH!1w zQ}#b3MmaiC}%rp)~2cFi0s=Js_CTgdt~Wvqu4vC4ynFr)w_nTk7{o3afwk zpMtM>{m@SGx6AkQox<{t?ZhOIh#5B7<-G5PRAh54UYYMB>VA@Un)&t|DWq9`=2GX9 zOtTs1Cs*;VY7!IxdwyzI2zY(a6p&Gid;XE@vR6w<$9r2NHM zx&EoIb|J#Ve6i&Qrdr6trf&MVkJ6`!mxu2T?ODu@>cM^m!!mbb6@SJ+&xBE{xl$fJX?q zh``Fg>RiB4s$$3tz-;w<8G4AlD|B-d54;YYwA2o$W2a%C%evTq@C3CYQdIMgAlpj;NPElH>g9hKhJT@g?;~^*%@)^9Nb!`_qP26e-v_!j^o@$P=uc#SHJNmF7F7O1cig--S3J}UR8TS} z&a#a@^W9r<$wuo2O_K8?T+JRQTU3E(EOuXV(2OB{>ejA0wobV{~HL7xXc?-l)alRZsRBtcQB%Q(vOyo zKrIuCRO_|%g>*#^zm<7m*7s9a?Icg~q8xc%4%Qm|}gv?w}AEa(yK*!`LwuxheCRyn|Yld5g-(?@YuOSjJRZtiBcciX zshn!#rl#$m+ftL&Te(SbSy84_YhLCN_nNLvlx!^6f(|BL9slQ zdX6dI4F6SroRDO80(?9Ds4|Ys6r;+73q{MrY@QzNG#LbDmE?8JjzbE&JMVV9Ec)!x z${3rvqv=-R@YO1X$gbSqs*+?bj&P0xC0X03UJ>i<6=_LM_e_bfGifk*W>A{JZ#sEK z$A7^3w+ZXW6J=hEV0{?G*>$@+nn^YMoL`(1C@178o!%Of*yp)vSLou$K^Ef4#^#~I*%iZ z0qN3wb*bV}Z!t$=U;<$ohFzHDW{DI#?C5+}ZpYr4zB^gi)XY_*l5?`)qFlS}fyqJ1 z3GDgw?L{X34l&tdD$$2}{Bj}X%$BBpaboc}CcW%|P7m_VZtZ8^eizAV!#(R{rf@ZL zwaj;A-44GfDzo~;jepGCrou15%%j%!Nu&0{)U$ek6@GR)|DLJ&VGZV=d6!ty_i<*O z#Dwi@^IZZBMlHxVLfkX!;a=GXZCmU7vfnzJL-Zqf7N* zov--PAa?eP@2fw%^)}|uenCFDc_<_Kg^*?eP$z6X+q#w_Xl`znd5q_dins2EWSvjZ zhiah*IaNY~sej}xh1_%^Mh!alD!f}-6 zz7|~_5#WwH*RRS2HAhNB?Ca4YuZG3i)7PUw-@U++YrR~ThF(@m0;B5QdGV1xhNtCzuT$T% zR^;$Up!9S-<@ypGM6EsQuiRDeLh(10gygZazPC_R`=iP?j^DgslfU-x7e=&t z%fz!9S2q!q{>zRJI~7bM8Xq0J!5+|l?lF0)AvKfr9$ZVGgw?>Dk}Mom>U&Q0rID;Y zj99t>0{n_j4YM6v5ahE5!Rh$n=JCCyl^e1-6P!Ko4I}JL$IC(?s|&U6buEM5rtbIB z$92(_mwqS4hX zJ^giw?U_^WVL##8mPcArX8OG#aN0!>V5#35aDvz`hfSY?_~ zsulH2Wt0=OaSTHPV4pK|wLMV~w#PAhAQ5JPIF0kcnX?59b4+eaeEVI7W*fO?L_LR^ za5|qe&YK9q8H;R{WEYju#}a4uU+}|pPMy3#+&#Lx$4*RkJX%E>J<_)Kv?aRewb@PS z_0#}6%SP2V_sNaMVapPvG80Vm{;_kBbja%$qOpHuvaf^_yJ3`m#LXhZ#1Vd{R>Q`` z;+0|nv9!0Jxf%vCN+Ji2-`=*~-R;Oj8a!?iv=)W;QBz_4jn_f|s|m2G-EV~0G?zsy z-HPhH_FK0<;s2$YC$Hjw(^)~O2`9OlEBY?Lz}bVE%fr&qubJP5BB|bgA1I_?`Djp5 zT5MT^Qk+@?2|x*1?-U#abwmcdINvsDrScL|PKn7cl&AOE3^D?5`k617d}_v_GI<{# zh;b}a8p9{;&nLb+vwa;N=HIF{wi#O&wV~6|l-B1ZG~OGJmRUhHn00N3r@vy z=JZo1^OiaitY?U%VH^7ftxKL|5jsX!G&(O!hZ(&6`(WFi5lBg(+2JS+hLh%B&Ngdm z9A0;tP5_oXw>;;mPBSs-F10$rH&e+P4`ZAVX;Z%`8*pT_ZuB1zv0m3Vl+SKrleNPQR?wvc6{+*RGDRJOy z*_H22)cO7~MYH=y+uB5@i#kW{6J8hRbn#oGO(S-ci4MAASt_RTv)AuNUOVGUB?m>> zT~+PdBXvP=LfB}Hgsjut$DPrT)WO7%+E}aH4nFc9*>27gsn_Dr00R-f%VPPtp-Y{h zW-XdJDgT^+k;CE$fZW=)$orxb?!!O8>J`C|RO8b&r@${b4GS#<`l7Ku9v_ZFif!`) zM*{2$mT!-?3MCI~sW5veW7acNctYZ%eVwG9YJ0c_47L<63MfWJA5ETL& zn~m=JKih3xbC~2@^kTh?Oz1spKNv0GlR4rwv&Iene!O3-7Yk`-5}u~B4ew@kYn*dAM@KZM z97=|Gl;YXIQZEvzE=}Jze&>9YQ-rnl2}P;EcZrt^Z?EFboLH=SU3Qudk0=`Th6~m| z|ALg*$owj?Y^ni5ahzGevGc=$DM=wSfd|DW)Vj_x*V*uAN8zH2(u=1nsdagWH}Ksq zt@=k{CBSIb;NkbzJ-+UU*_V0w3Ov=rJv^eIzA4-bjrwWF#x<3xJ5XGiNiy_bsSd5+ zPAISOiJRJhH*?4lq+&we&2yQ?o<@1?R|sCO1|GTbQ@jH&>Za9#0Njy`HHowL#s-@! zM9-C&{}fiRmJGO-Xq-~t#-jXK)$v01q&f(pW{nEy-+JJX08=^cor4dzpNIPC1!_MPqh*ZGi;)o9_fU8 zS-|fWU*KgZOL_TW1RYGi+Pc5wP4+K`9k}?-(UA9zBo=xRgJR{^xoWUCwv@nkgZ~Ea zznb9F>+wqQyeB(5etiV0hun>ne(;I3ajT`G)LWSh%Yk0SD~^E@j9h(&Oa4|0BNDZD ze59FqpX<2DyYTCeX_K+QYC3|2jL=~L+i6*AB^=zQPue$g=u@`$5B7>YTp%=Vq-DAwyND)UUWmzoN7*0q?V|j}$np){4V);2d za>0p-cA>e2L>du~?_kDhx>5D@GvA++qNEsUyFMu};iK=_^21iH`V`O;J@%D4R{T~t zUx5R*X0Mg|wk``K-j|Wz{96A~+$7dU_f82N=3J(KNC%I*nv*~I)L!+ly>*y=-?l_? z)R$0LfN0rEo$$I?e8>v0)*Pe50oOuFAx2j(4k4+99qt#~W`oCiv zh7pPo5~E51Jx%d1@Q#LgP#$x?Q6%6zQo)L6e!V$fXQ)M$1=7t#1c_Mo*puLf^`=-( zsj7-jt5cKYBs+u@RI*Y%ho?8oG4gQ0@T%myE=T!i>+Z&l3uGf*Hf`q5+&@?--HGrZ zU;K{!A@03W&7^s1fxX2Y%)Wyb+LYn2jWo{(b0_&ibCk;z8hxUft;Peve$*Gdwf^R=na7UXSp<6jB-hiIU<3#LD+^~hxA_*{ zQWab9)t0qJcunMsCQ={$$i*&$8=gM@0G=N_q)wU8D?n5F#j0A*cpLzbTyjQLC8i%i zXEek|zaf;_SLSgN3}D8-cF)}FZg~ais!uI6R~@cp-DwMGJd|>rQ9EEvPnzF=y)&RW z3m6)2HY-U1iGJ^PK3+)*ggJ!_g6i0 zpKyxY_N&kSLQtrjDsSusJ2>4+I)IJB_y5M`8iWWO!Cj3)r>{zSg4$d%q;4Sw1V>*= zuy_z*{FJYC+lpL0eRN+8tYt@F&CWLv4pjk&h#Pz3Wr$)KJy>A+mVe+hvQpwXk=8Pf&Q>wAhj}*R4yl}ePdL+IeBE1K?O{Qoen!eYQ$Pb%EXotE@ zslw~nA%d;iqX!c(SEp{$pXnQGj#8R6&K~<5<%LSTYsBI)1~wwGvSLH~q|d`#?;>8` zM&t?O%tyI!dTEdj@1nbW+^iS`n9gMclQ%z+-}2f@17l}iprTa0@vYXcOX)|}lOJ7P zmv=ZbCVj$8JBaZ7iGEtkjIq!cIHlK($4Hd`?iQvyno&7 zz#M}${p>3RQBt?->^JZ_JW*TDm9qItn(Eeb<(%ne)B0g&Q?N^@7j4Yt{Vcp8ZEc}> z-r|8}py@l_CqR=g@_^*GQTINEy1}>@6j`{3GD>oA==K6Q`l4slXMNbjOQfgo_w&Lu z;N>it@yfX6W_<>SV8Mq78i=To9+~@0&Y+-wH*t7k?jbk*Z98=}(w-zwn?K*c7b!X6 z-s*P{*t_)fC~T+PPfM{PDY#}z;NMAA%8g=K&_1hsSbE|4{iW8!1OhJT|9{LEBUtU% z+kgW23lEmUp13yXqT41)4>cuOI@%!7|AOL`uDvd1O==v<_i3EsuZUm{ozEFK?FO8` z`wen>^<#?-bn7N;GE%h3fl`cp{?!O6Y|M2(Axzp|Iw!f&sn1 zQ=kCxr3(Hq3(&B22b;)5##FI0OkfMl9hOcM2|$FOoPlN%D&4w%`;Rf-$9V& zTqj;T#=_*1=h!@_nhhfO3h*VlC8R=zo@9)yeNT_h&; zBx`xwe^&*%c^1c4?wmj%qmr^-XYFTV7A-7}GON-L9V z=h^fx?6)?OFJY58Zx5B^ub4M|nt`Xe(w_T1FOx@JOr-aWptu=+$9|C(zW!?ZJ%B5E z-De@KQ45co2-Gn{pxK^Y_su2pfb3;tG5cW z1&$dGe2CV6z_y#de;4$)dB%6d|NAG!_YdN|D~ughmr<`G_n6C165NvKq>Pc&BG13X z*nDd8f_`~8p}F-+-O9^&J>Z`h85|oBMbnAfKHUAM;x%}dEs|PaJvr~8LJN}hsmnWA z=GVF0kjF5QwP0bzU-cY-m6xp>!z*&6v64G~j8oI(K0=QDbK#%lX1Aodl5(}$@w1-{ zue=ONx+_tksXZig6LHXgcD<;S$_+fr_V}*8A*;AK!-d13e|nx; z*4y~9$d?;XV>esqv;JmZjI@#=mVf(tA*#Pz=_fqNxZjHB*H13y62Ajk>AT>&{GjH8 z2?!AbOV^UT_M>OiV*V*5s$^>DQCw4T`%heREhZ}(*+@1r+c>2ryE>QDg!|jg-940w zeYJg|N@zmSh9>fT!J>P{McBsGupq%1$I;Rfi&G*wxb@DvKva)cx2Ykzsvm2~YxB6I z!L6)Rt%X{2F`VH+s`0aAqBrm4H~m6=oD5&Qmn94EO;#xSZ?dg-T+ukR&u`EoBkb?A zHDAi@jJTtUAgpiE?>cE|rETGwJ>g3fiyaLGFgkr{VZoF_>Dsnbz5e_e)Hkv<6jR}w#SK8FGkUI7qh@}PEaiG zy-#L})^d7}e&=tsF(c%2yQ`q9aiTX_x%52Hha}MMw6JcgrQ-baW%o%JGC1n@5{0XC z(*#(@JaEI}d%uX~XP3`OCAbVF@n>kuMO~A6oo*8$=S^`}%3Kp3M@GNu_ey#n>WAMV z?IuSJcE>sWuC-zFYYkwA@jsI+1z*fr>PG}^)^$Ju0|T{I2#4pTxI-t1;q=N#HnKwZ ziz{rzsNt1}o#{c=Cv9<7XsTJ+d*IModcsWgkWkXhlm6G=1NOdI$d<4AIwPZy|7f`B z3q@loU?JAu2@oCpKf^ntgh@yEW7Jt51R|-$>DH8q%apQCvMW!hoigB_dx0Y#?>S;@ zMyJ~&!`+@T3>vp{-Hso+Ed*yMIcg1C%kY+2AFPc+6LkjZJNj*^v|e}JCp_wocYTE? zix}|(h{5-UPN;fXmB*}7xBBisH~EU&t7kwt#-1nmcUO;L!&?fh@vuHLy}-He<}hn9 zh?rxFOzMZi8hWHuv$^I}yEwy|@+fcgOul~1X!^K4)5*zj&Y{qx87;3RGqn6pzx=lx z@_Ayv0C9haX=KkX;nua$J+I__+TE1_q|5L|(v$Xu`gfBt;=!q-z{cf<-lM62geBPaHaCx4SRRl(<%aE`(Om5(?V^jM#YD3@3U z@>h3lZ_h&>;qYt9|6BLR_y+4(c-@UqbEYS@&1I<)<`&p`$#XmRz&u4>HcPP@7Ebt+ z>xFuc^*CFXL8QB~HU+>`?~C(>zJZDD$CO?z385aN;%~3da)W4y`ltt-)%?A^-lFA} ztAG3CJui3M^YN2QJSd^=H~ZLQBBT#0SACqK8TQ_6GMh1q>R$@m$!-YCiKz{Y{DsR1 zoDK;Q(sv(*<~R*_fzZSaH1NZrb*fx`SRZ}rHzQ8zVgrflM$SA}mE|rxhR-fYeT#)U zN}+nieciINq+^ssOr*LDE}pRS@S^Tjwbc)Kc!}q&A=;Vti)v9ORdo_J2%L$qri9;4 zxjzm$h!i-pXx*84z>xztV3}Jl9RdaaRKK|GMY#>s%kUOdL^Ozld8deyrl*@@1aJ62 z0|Gx>@EpHSYooa(!(9H{RQ^7*`XtA{wV0^zHNPCbGF|?Guk#Czoj_Ke)a$*}`}ScxE6n$%}q8Tw08YJ1nQ zh-`G?L?v)@{d98C5{U3`j>(g_q0>~)vA-;EDJEDPtwH%}@J1Bd(q%1U^X%Q^Z}s0j z^D)fZC`@b;0qtEX&&BTOfK8KFwqBxo*?G!)Cl+~-nHJzNk9k2Di_TSi)+-JbNa1J6I&EX6(66iOP z|FV=z{8hBghph6yG5eKDrVo%lK)Ss`<174@(~EOmKgq2nC*T=eRZ;#$i*B32Piy_I zD`B|jt5YId6DV_=O>6cLEsNTBju#^JKv?Y>PSm&CP7>ikdb_4_K?R3no8+!E@wiB{ zCp@PW6y!q}hx{FXpBFVu`EB!qH}_ES^;a^L@{dKvy*4Y=vQOs1*y)rH$#qA@vk3E zv635mSf|3c6_pTuHNG<|xNuh2h`RDAzP=>&H%?fAyESZabM$KUjX|SZY>JPn#N(<4 z!mM$~i&8lA2ZZxT=|N*}PD%|IE3+h)78KF+8cEFY%nJ;r5#J6I4E%8$=zJ0=! z11F>JKARg})t3ILt>Vo5C{h5Y7bOm{P@u=}W25gDPx^HSSg-fTl|?qA`@(=MRE`io zovSlr7?#Ech2O4}ZHqrMr+9@3h8Wv}MRPeL9*uF-s^f2cgg0ZVErGVJoXuWY5-rrI zLgu6|x~b)`c(N(LQSpR#?hx1hQFD1`8oI->wsk)cpgG`zYQ7uV41bhr`Ti%>UOXYW z(Yc$kd)L_$)h4N#bt`%ib_>0x@`&6GO9-$4_-PdcRMro4-rwGQDEdm4dSRF~qn@n8 zng9J`^5+e&NVM8KVThi!EhFs1as9T#9a&c_1~|$RH1m)Gq2gxbKGlIaKa%6k&xG2= z(qZQYD`BnGpH0Z$A3wz-8|2po<8_HboXPK${9`}4h0FMGcACquo&Cv;1N_{3R`~j) zf8xM+Jnfd&k}|8{4S?|PUE2Di*@2Z2K3E3zc~a{`Xv?nwolXz#A-$tq#HF3rEKeQ>0K4TUX2^9PY z{>}7?>~H5l5MlpfuhaJdbyC5Ov%xc)g4?t6g%gvdt-Gb#i~AArMG?V8X%xz1`ZC}U z>63M73OcO4KUlma)kCU<<*Sg^&MLBmxkDan?5x)_FHv*zK=hI}XpJu_JOkp8JK~vq zJ#guN(gDn~{=n@sl1mb=2xnjDIpRx3qMABQPSkQ={)~W=+0VBm@swFv*|rsNvVadq zjvef3P8h7CO?t!>N^}~ubH7=anv1zR(K%7<@pju- za4xno-V%`ED0BU;>&uaUo8(a}W8?W7HbVQR^H~la{_s@^-czp5Ljbvx4!hsTE#s za9oZz(lFhrEJ2JbPC|S##Z?pB9AJ{7uIGDf9WX3QfemKGV z4b+!i=SLjaXQDwvUt=Q#XM7&02EQ#7)Z}HhJj-BDi(}9sN(>Luh>G-3m`+hUiGgF` zVsO)USa*cwjw~PC@T!ExdYKB8lYT(}F+-v#V@htUpr<@1&D!D7=S@i~-%^HzSp?$8 zZT6b|m4-h!&Oi0!WHpCD4fpw8sW$& zwdIE4Rq0m#MzH}U(EdsmAn?0${o*+ak5XX+6LID1`3aoG(_sUTt#rEH z!5;a(OgIRXSW|9Qvs)vylFDyT*adnyex>U<+nC#C*`WV9GWgX(|HFJMuoUxs3n8ZI z)ptOSg~(?Nfx|QQ7+BQGl#D~F!5V4p$`3+)zt0AR+@*WY z#+J5}>F9I9{Gz;OtF$ITerxXcDmCT0JvC-KCI~3hr02^2yC>Ayd{;^ru42Y+haNLv zD&}>YV=>$jvjzx1HL>}kks#amgMg=_S~K#=$ES`!;n8QgSu&r_N@en7NlydG+Tg@r z_h``R80WqvI;~0tu8L8@(X`D=F6@7)`1GPT?bdqrfzcim`k&Ro!_~ei{)3XfYFsRp z2$eh#lT6QFqV!!C<}Im><`>8ifZQoPf4G&GG%9-x!cYU*cm#k z2-XuGzNsb=-6EXe@;iRgC(cI#1_Gm$0_jBWh{r&a$Cbqa9iIdM_A~5NfsMt?0ep#*%M=nz;%^L=PtRM*P z)z2Ys?3DTu?17b+ZFR3wZ=V6?`wwxi;?jExF`LAD*8Z0^;0+AD#^<~|1Ht#0hZwNq zhh?Ah5|=ytQ1nSj=hZ5EY~v{=)}@^+dw&WZw)7J$uFGSvcFD{+MlxO!A0k2S&72Hi zY42|XhHevMRokz#!7cRaka|#2kGp=`hN$lGWkS?x$Xgu2wJfx%)|abFUIscah43gv zH8Z}=Jy}Tpm#D53QeR*5yktz8h)=j&Z82IqVl|itx2sfyUTzhl+mcRgPT;0@4!qp7 z8g@Yxg} zN3c7~7d%5hf=Ww3oaP-Pu9#2+@Yz{Y=XVqOKI7`rlbxW3E$1ciR=Q=1BTaMwA&}XA zk`Obh-0g_MI-hJ=_$%^eli=_x&zEZjIvQsK9h)Y=J1eXZ0&9fp~{%ptBH@On<^2Ya?I)& zpks@W@njuBBQIIDb9-!Zb|enspkIYm)P>{w!GzHEH=1Sq}@4(Mq~5b3bpo z=;YQgX1Lh83G=6^^D_Q@zSs)hT~l!!wMzG@O7|Gda0cQ9Q$0L0h0L{CcF`&Noe%Ch z1T3H19XSMfXEFMr9rj9x#zwZbPY;Yzy!W{aLG_PcMtnO?<-{#xN$45GBwRgLuDw^n zU>>r81uW~>)4>nx!^BT9zRXm&n zdu$SaBZ8URjiY}d{NN9(Y4S>gKKOE9V>Hk z(XUOLo?)vQYGb8SyLmmxfF*u{E#s3!`aKFe)$1Zqrv4L>?9OhHIjYWHtzKK()^d19 zS0d=!oLF!5`#c7vEX?k1yL+Z*b0xYlHr#__QgV$KGr+={G(b-fh23ea39s7AuUFw1 zk)j9yOYW3mjAj3LD%-{94lRXLm6fqAcHG?!eA$a?nd`rlsJ#B9QXtFKXo9HbAqzHDl2k()C8_Z)5x@NWnf)#)5XFvz-o-z4gC>xjn_8I?Z84pCf@1s!s zp;VzkU_$FxeCd_gIg>8R7YvGWjkcvC*-5wFAb{GF@=vLux%+KO8Rft}+S zfb|mb;q2Kz8eIGfSVj}I;`FgfDxo#r+p|%+mS*G#sBS&P#Jq$gR~wF-o)axuxp&0O zYOz`mFT-y#f&U}OLX#BI8pEn<^{BPa=icU^mXXP}O2=>oSR{n7?Sa|# zuG{JN>@e=SSFgeSnDBe8@IC|B2_h^GReze;(@CphV`*iXOz+#fI-aHu-mOr_6pIrR zDjzkY15{pB*Oa!ENXUGrOyZN)3+VGzZi*97Zh0`il8@Tcu+^U5dxO_A7|Xh< z9o$(4(}plb12o1t_~h!Ox|OXg8CK0Ysg_ncK~A%V;Y=0@bz^TXa<1HYQkGk6O5;`S z&Fds~tesJR!`KhEMRCw8T8{z2*yD{os^S_)^=AM#ovnb+z(!F##EAzLH-Y! znhla{WZrajN>!M>W4Jm{cDdI>yZAkb9t?^hP@bsis4|ze7klZ#EYoFRig8j>K6c!n zx8Vdnp^g4AOqz_i#G_xIo9ihFZ*K0qjJW#n32lI6O7__g^0MTWVpYx9q{&6oKPSJJ zV&l)82@&-xP@>BHQFiuA5X-t%RQ4l(+oP%C7$KE!b@h0DmI5C1%}6|w*Q~N_sw%M+ zWZO9Rp|9ZY5*cNj`O=o>WriiJO>pvhv%G#jEBT7a0X%G_7_1qqSmwkJA9a(Vo1oEr zLcsY?5Vvk6g?G3Q?if1iY|g9b*{+sN8?`v^3*DR)x2+E|ygoAZzat}gIJ?%8?@~4^ zM$JNHB$5lB!o_w9_nOSOtoQf3#^w$J?wSiwBa8pw_+JmsvI?D1xf>WdyuG=i}ZG(7BNs}gR`T{0} z*INu3OUS0tJmCY%G7URE>HE&UQ&-MXd-kJ3 zz_*J3@E6CbCH)7UIcTrT_&DXPWLa|g`slN7C<$RB(wV>42k0}U%cOyClcHxWD996a z28^bd(QF#fmq0Y6m|c)h(_3e&@OcVS`N&Diy5;zv^6?$U;W0`G!$V|Hm5|0RrWHbY zUl6oDfpIY6CiVZ4$4qd`IqXLq-lSSSw#(PO;WrD7UZ={(eAmCu9p5;BF^WFPy0m+V zjs&FqL#;aLCgS}vcn;iAY5$vL%iC2K$g0${a#n5qZKWvX{N;E-gX(yHBk#Q_)6U3u z_*_@`?=B$eGBMY?y5I=hp_}dWqC%xfTNP&HqG2#oq1Rcj`MxGE9|zB; z0o%|#Qbnl*{FmW(x_O&BPv1qWl{l1{^WC z5ZF2xIm%4L(`?zy#~~EeC?=beaLE2lcphT~P=)@rqF@~N%rIA2dC%vHcuv&5NNyIO@lsu;^2l{JGuX`F*ngz%| zIH--{tSO4k{=~z{7iPP$C+56`Uw)z}TMoc^j{U&KcV~iMd5hlGxjr#uTGPxJex=ZQ z{4e~Gt%jfX1K^pRq@+Fo#=7#=bUF(joZNM@i)dZWrFV*8Sac`&Dwb6YxE(-gKOIlgJ+V#g z3UGZEJJ9hX=kk&2;z+5OmMC*+h$-)$PHkM@rP zBH&^d3RAd1ewgZWff2t`gyp<6HB*$&hR21Qx=a0+B9extKv~xYxk+lp6U(R8+xv`yYde)xwq< z(PLay#*2AmFf-n5hx>l0%0NT!m8Y=_*H<8K=esA)lkiZ{wFOx(k0pIvmi<#RRC;qM zr>``_hPWb(d$a#I1P}0%et5;HblgTKijm`}0*7fxR#7A9PgjbCVYR+b(6P)1h$h0_4+_OQ|zjy7eIp{KfTVXijZ^@|kRK|dCfye!q;ZDK?0?Ke)cj7%hEk%Vh*oGJ&l=Jt-=~Qk~^tZOw zP_YuM7}LWn>c2PAicolrccc1VgJ-PzA}7ao1HhDOcJ-Xpx72?3%Ku%rqx|4lnE>)- zhKnG1P7$BkFV#<9AC%|T^d^R`4e4D!$!F*Yustk`^plXDsGE*RPUgfG<09~g3+XE#J`{y=PBCxV!C7z)zGN4m6!yt@xi!&yH&{XleX>WyrmSbs}+sIlXe*45J9|P z@y`$m3K;_rGwM0ehFHw>say9U`F37usz^lh8@L!R{JyC!zN!%mn@i@I<~`F(?a8DU z$*1iLX7VbCz^${rA<4q;(o;fL5>rk4{ql8At%bUkeP41nWC{jrDq|+bDVT=rqbYu`z*tubc zT$51^`&cT0n~VG2bK<#`?EWJFr7_(ULlE??#I?vm)BIA;E}Fqg@sYe0YUH`?Todf? z`v|fR@paeVg?MlZNGZ66Dqx2<)rEkY;=m9sY~rj+|D&_EOj*~Pv_}b~f??o49X)0^ z9f|w@?*e$tCk?SA8%u70KgzuWf3TNNd!v`XDSr!mTXy=<>hontD7PU>?j0#@4PucF z<9LAZ^dPlRo)?)8uTU?vV*DFQz$3jK%tnd;f0L`ba#}t~jU;L}dvS5sc`f*K5tE*J?B= zVU)N0cwFC8zA6ldlG?Jq0%M)|gipxYv&G{PbDj>ix;4ufI)Ys1I!C^gqU2|Xg6Shl zW@wU4a%9lIWhsuXJnD_#z2HaSoA{__P;4PgzRjB%#ShHhMbyOP*`Y!jGI;BI&&8~c zDt|}*v zs7_PkwF0nq^A5)I!mbSoe#VfEjdnZW&$(SAVr9_pa7dEM%R4tzcT#bv93R?)^P5Avfy0yfA=#c)WQ=OJPqKujsLe#Q3> z?<{IRgs_xsNU_G;IaB+svXVrCv~bq^TI(R`;iwQmrrJ5PbNUmF$5DOQ>K}rKqd#r~ z0PZaE3Ev-um7F5JlUKe5lrG1jZ;>5^b7>IFuv967QcE{O)DN2&F{j9-81lA$J4f}` zUUhl+MAg?nhklv)c<)fR8sAgA*Npj{VwgW+fiW`cj1=7MuT+zbB$qwH%|jIiTVb^& zF?KQDkzDYz-=5JV2wREQp)0D$sJf9TKqdA|1ew!Vv!|vP*9f8iBA#vcpztwI>%AdH zpO}l)^O5Tvx;N-U%#qBc=s>2nW!AP8jnse>f_jR;eN33l%rm%>>7ibvQwbRAw7Z^OuIW}xfmK^SgCpmnh}9g>{n6)&Gg^mj`cKPnRT*etg~WUV+HCqu|n}&8J?Cffr>}2k8r!DWXdv zzGTUaPh5MZtDl=CVPM4+fYqoChQ=Y4j`L)yXKq6jEKidqjK{(vh+XohJg<7 z>MZO$8KXqdKR!ZK<+R40VoxD4y*py5Er)(BMpet!mYud!=ZGr26RTh9mD!45UBA8h zcHKolOkw<^c6#SKP$HL%X?3}f&#wmBHeDl><~%^ao6v)QzgDkE{{w|eMPW&<1DZyx z+CLcRpCs3PR(Tolbj7hfTbS~MOeFWI{}AzA#QW@kVcVQ_YAxdvPv#&yV>f1rt&n%> z;r@|xgN?QfBLH`2$Wn(E&PpUU^wb#pXFm4(BmL+t?IL8YNZ0FstbKZH`_%k?^)DH$ z1~mdoy|F*wYuM%uP4oS6#bnwX{Ky|VT>FX~UE=*?0=!dxa+n5iJSOJhVX-Rr$Q+%< z%op5}1)vp zVdgF~i!2ozSWAmYZdy8<4%c$=A25Z{W#iykh$^$%kpMqDifIjW|9?x@d;jf{#38Mx z81RPSD)|w~z8=YPZpBfR{R==JF!?Plc<~NB^-e~J-dAnG2GU!!cRBhyhQG`HOa1u~ zbecZN3iUqQ3>rJmEa!9IWV(rhWYbp%&Geg4Y}4>w_tS(+6)jQ8MnF#}-<@LrVPxGA z0_gX)0j9J;4wTO+oCEocu8#2IliJBAubBEtPL;xQ@B$qKWHXw{*}yYRvfM$8?-65? z2OU~DPwZlbytpye1wVI?%75MiKMfdKe!MkLHY!+GlUX;jjwOS#7#-;B^1DL<(pBG> z%9}9@dpjC70x^oKGaes|3UnC0ZtMl#yy%yAfM9vvr?$`y!K*zaJ$OzciL51Lr(kO6 z3IfoCIr(sjBkyVyKh-BBMEonasqZq(?WG&5b!x{-)F$-3=0FvTjGZC8MQ#HWzf<($8L) zRG)=RNi*F&|MfxSrQ35c47v%%XJ3~A~ilo%HobYy|vt7WchMh_G2Kv1k01Xz{ za5*iZMpP2UEv#*w3vA~rBS_KnE#rjL|5QV+$wh@Hxc%%)^qk}k40gy-B$RY3!of%N zRyd_g7WrdtGsPnDLP_uZ?K55nP07YQw&;FAe3kDCFj)1otE$Yz=^hNl)~?bTJ+|cT z3WDHg;t$qrVoCq2J7N-i64~{rO4%gd2@-U5k8{9z=DfF=tjjcv%8ZAtep(72X*M@T zk&PGAq*^r{%^2LdwN3ZXyBh5)y}_jNWT?scw1OyEY~U|Lf;|e)IGS z8InE6%t0j&wsWJUWxt^mOx8rHeDHO5E56Z&%`>ad*alYFI%=MA#M%q72(}3m5v|CG z=Iqw7{lvSXFbmoQ-eAoh*P7Gg-AS1Z0pl6$?+G|0N%W6-lT+Mh9(P|Id+4cgK%`^J zZWZKbz1}I6A}2wwt(88+Yrr;temq&t?a2<{?wCdZBbli!)eL&tml5>8cF_b070k4V zo?cOxD-T7$uTb$V-py4w#NoWU(Fo`#SG^c*u3ZBQh>vJO&tWK52xFv?u?0SHp!I1e<+YC(NE?>U#R)8mh2T9*to4(tDXYr)@kAN&sdi?dx+y- z2ATiRTRcOZtC}yjyki-vTjBNDiJ=MXL+T@2Ljt%TBEQfyxR=r#@{{+tl z=S->0Ks_5n)zt7jkIfXL2K|TBLE-y@JNJz^jBZOOO*-C_h?@FI$ ze+r{_2%J3jQ!;WVgHVj`cV)2;a54Bm(eH-O?^%vYqlZp-u4$TP60AzhP@g=fGsKHX zJACha^$aF+WrM~)*&J09q`aGlJ^G5FaVKDn)oO5`7}oh|2tPwB;}*Cm<-E~nTl^w^#; z_@zoc?JkRFhzd<-e~lU9^5-3sZyr0r0Pwn`=3xNZq504!Z<>_>RQ&!T zDrVK|4nKKzv>jyG-r0S<`~y<^{*ADE2K>JLf9{w}QhW#SZZ`uuGU)%K=`H-4{=fJC zQ6gOu(xHH)h;$4D0i{H`1*989dP9+Jk%kSWq(MMBq`SLDkIn%bjPcv+{rUd>gxzk> zo%1-?bwAf(LJHzaoV63mhTKO-X+QYzn{Kvdb9LHC(EE{o+{B%D>Pvh>O~&d$jksDv z;l`3SC%fhFZie;i<7IAePH14ygS<^X_byAHX6|efe0$9!bsR(Tzu=`9;%U9a92Tgb z;yB7df#J|*>zqehT!pU6x)dxhz!kIQJL8dTpQXJN8!2vW3~ep^1gdjKJ{Ca6mBUK2 zbT##NyV=ra_6K&g`K-zNQlY7?<+sQ}`-<~m!KSvsgpwrL)D5$^Up(1n6=Fba@r*@W7RKJ@1^dix8Lm z=nnleVh-e2Z7N{_BzrzDaow;}w&b%uoE4BEjW|Y!wG;k6EQ0v&T#aFtP1%q>?K_5N z3mtjZCs~#h93XZs1zp8-kfh)|@sL)H^%!voiOJ1Ra>d>)5sF3(T<}dX~fU#(NEr3@3yoLk0HagWi;s1)h{E! zZ$!tB=K}_+FGh|^UrY9=y;V*^Q# zL5~d=0x`3u^UHX`@XQtyrv8*^6CYjo_aezrC>*0!2F2|Jt)BX&I~sD=9jeb;1JtbN~?gMQCAcfaOX8di`Dw}g#bPSWu2NAJn5ZAqIjuH5%PACyJ9uF$gi z=2$|BpfN|zo(tmA$q7<74wIQYp?Q~q%~!r!+OjC-Hm0?sqU#+Ag6B`@$?fCYgdTpf zI^5JQBE%}SX1h~X(*KG1p?~oF`7yZyn3Q)2U@&5q?VsT7f%gR>jxpg$1JCF!+cona z|6(SOl~C22V;uJCbQ-2)cAH_dQBlyM4KpCfZ&2xYN*g-bFTDC`;)Ry;OYa>h z8IUx!c9Z90ZT!OC6Y=d0d)z)bBu!oh;e~1q_UrX=Z0)wh9YrOX$<-)Nhk^f66wKsu z+L}F(E3>*JHl;6r9gy-M`-}uYh|eu3#j=Fpk1u1^x^Aqp4bC9ny|_;!r1J}Tiv6Clr>SJ<=aP^8cn>Y)K2R1M{$k{Bvhe^<(c2q> zUVR{K^K6B}U;F=_c_x!&xSdCxL*n#$Lz<#&Lf$Zt7I-wv7(7k2;BCMzW4J9U&kUg_ z6nUSer9AUmhLEFwD9mFT9PoUrBisAeqJiyenAKOR_&@X>R#`CFXkNgihm`Y6F+pb9 z$dq|2ErHf3>8TTkkMPVFF&l&mgW10?m=nSJala|QGuxaAjqmCy+!V!awrZG<(!eYF zQYN!wQ5l46z3wC(*yNu6L*~3Z4$f-~`>aN+^Bnv2s6@6rdeIPKfj!RbOVxMve{(k5 zOP2oTb3)C3mHDqcLtCy|Pg+qbL0Xr(Muu*orM=6Pu4i`%65G*{fffV^D=;0?_|`|@ zP6zpvoqxELKmYlHJya%msQ#|K=pKC`4@ZcwT_(*+OVtS_F(K41-SNK9P_UWS`=;$D zjPiw`g2wlC)SD!fOwz(A-unC1`F!}4PEa=g8SzF0co-0}amaAN)y|4t%L7=sv5s?k z?9X2W3Wsan^}r~sX6~aGEEIOACe2aBiQCf@3}Veq%y%I})N`jQ3rWs@_8)Ur*FU4^ z=Ql0gUl6~0@3NqNj?E+kw3Mk@g| zv>tAm_hG-Ccpiw1+zsOB=I6KX53~et86j=#1QWz>s-ioJ_~OX>NAi892ix&}bqySJ3b z7*x||@3Wg8;9h9xq2faAm)QMo17H|`ef^E?&>{F~xz9olOUNvx^Un@`zJ`3@X z=J_38C`^aaWsL0#UF+1#99t@o-i!A=!xdCVTy~ z=7DUv6Lz5lp zAF;iYch6vM0^VSZZ_iF&jQ%~I+vF}=dvL1tyniB8nJ_l2-$1~y>xt?~1h@3C1&+3Z@Oc&AQ*5Dgq7i(A@*ng6uY^9(5JcHor=Z^Sq6qN@i!o>NeGO_Q;<+ zPdZ8?*tt>b(S@=`wBCJ`p;$^T?L0cgSq>?HDTz0Vg1yB4wX{Lkif9(oU*c%1rcZYY zeR3$0ntj-3SbC|U3~0K6PZ0BXBRL<_teV=D*y{ptUOBA~x(*<5U3%M2IYl&EIM12x z4CPB6SjlyS5x7KD-Z;a4G!uBjDSHZy0==84v*h3-l$Icc_c3FZ;5YLQqQyw>IBs(9 zitY42a9K-L`8w23UeOvAoo9b4Eo)wsjNR|&dgTf6F3rKdRbE)mlW4C=c@|+|O8M4{ zfh;5p&kf>+^Kx#T2==%mT!#un)BZ80=E?rH0-K87*aE8zoT7LjPcxUL|Mkm2SK#Ig zgPAzTu!sGPcBHDO!f^LHepPtVg4)ot6d_3cH%-3^kzMM|x8GYS-k$tczCNryI^dn? zUJFS2V1DB;%e%UpupsJ@!Q7FVF}W#VInyzcQJhlgGsBla(dJf&*(eJV_nv%KN(%AI zM&U85_egfbpZ+D*UGXXRwd?T)I+wq+v-8zc^C!nMFW(c#1GgENqMkK2TN=C|{o3`}zUdU4!qFQF?12u_w)PKc>qJ$hNbg8I+&R;Zt5{%e^y%uX< zx~`LzL>6Y!WChk;(m7t&tsL8R`?ydNs84oNsSKMJswZuy#pGkD5j=d`1VsRVVw zYY=&a7VxDt9Z9wJ|7yz2-WlRcTf0fka{vIx5<>2M|*Rj zO-L8g?%*?u;Lf*RXD`8iqS}~29f27=B2vfT(7x7ZAy(}&;Ej4*yLjZkBQ{xPd(7a^ zs{Lx~XUb?qrSp59%J*OQTsK4|vM<6$)NZ6aUMu{);Xn=GrD?}^`F(RPFq9|NkYW2* zeG_c}(sizeQ@lSTw40D>#fgKKL6Ql(Wcm4?HPV)4eim7Us!5O56$P7UD=;y@#Vac&?0kmK5v zgj=)F9uL$RQ3sUF%UF~SH)+)j7%&w}wZf=JX)@u@t>wkF{f&+mK+04ot#j`jqvXX7 z>Nr6}^!fYOmmK5rv5E4wf>k+u3UjJFPE@Wftn+z2z)ePO6-LQ-lTCKPtbSw+&Iv>= zIYe7W=NGJnCMTv<0&^8$w@BUb@y{3*!$7e7q$SzsH*I3$5F&7!Rt!7G$_;3`Jwx$KkthCmZXJ5 zfb?jxiwy(x73H{uW6_Nf;!Tl=^C&Y@*!Hv%k;mp{nlQG`V6Y{t_2g^{is1sUA*|TK z-{XBuX8}u{M6ijdH(y+#??QCUGKJ2&`ausVy)SNmMr9_aM%0l<^tH)8DZeqK%b@>C zBv8V(MfbtcN(C8%ClXIcxJ~?dYz)yYk{eBBKASD~Bba-{6C`hHL^C(X@SO7HRFw4Q zU-S-Qf-&3t3Xo!VQKYpIt(Dk@N%9p2rA>CneEon{8Ujf^tImpKm@+)HjTTpM-EU_C zE=a7IvixjPkECwWu5%caQ*g|;JQv9TKj<_CO!58VCvV|yw#&|cD^1>A-Tg$eN4xckx1NQ#b;u3}t`b(#-Of zjhOvF=JFP~9A>DR6Vkc|zF=-yGcVFjw#=;ZF~A{vv7`EDcA2i-0Kc7TJffp}wmRfpbGK2MZ)k$P$R_JNaMY74PT6(dlI^vg?a}AUw=`+K{-v53B`r*= zYZO=~D|1gOVrNz0zUE;5Ks@z%8c5_(!sB0skyK91aD+5v53I=aOt!rs;(6#)>sAJ@ z>~vFJLhN5rp32wOVVPg1foC#OR||ymNPZbGITng4f<9PN`oIt<30bwZu#5r{NX`8^fzz$Pj6XUFC$JZp%?>bN0Vu#DHAK}sW#9k z52ulqJN!XGv6N4Z2Ib~(up;1uyZrd|^r+Zi_57uKWVV*a)u*;Pq z;QcoE5HSEn@=J4EuH?Yh{X6E*%Ok#%AZY%wzN0MoDw4-BnHG!_Nhp5C9^%%U0S%O& z7BM31K9jYoeSX|v_3hGD&<3^j+JUw6h#%sBn}m$i=kb70>>xl}k*3sJOswC$UC;&xDlP$uxm}HGSM0w9{wF z?QvUE)|1bF7njSJ(8zq1Xb*`-_?~gqdPzMqAXyp;(~R49KOE8aJ*QTuaT!UF+msTz zfZy3}9Y#fu=NQ|Uz7q*sUw*N@eUl<7Iq7KGp4|TDy~95q;F%C4I}mf^JQ{5brZph5 zG;Ifkb~f<1Z_GWE2hmtE`zhTA0(8;R5-K*GNi6~m5}yNNvZ)Mb8jWvl9EUDNRw`t~ z67yR)5sPz+zkL8M=MgzWvwB8M4s^OVwv}?p4ZhKaKcI^;8gIUx-u{a>Sz7bR^I-N1 zE`v0WHQ7B1qBSU{ip&_TSfZYtQ_^2^*r^F8NK8rE)&st`m(ErG@QihItC6E$C}dYB z?BOD!fYE`JowLs*{qWx%H{s3>-*=Ao3$h%w{q9UtPlo0vgYrJTOHxdu)s6zn+5|m5poiC-K=GaB*;6&QCLM$?h&%!3CLV2&8tPEzLB1LYOOJlT>NUTi?)1Ik!0&v zb|S%=<84*IDy^NnYC%^z?WizgZl+@w6f{n9|LsHO=$q{##SGdk6|0JQbpM|KebmF; za<(SnbNb?{)+;=|y2uYBOrTqRwy+;`VF;_fu}5h0{fY*RVMwTY#56&O=y*P@^Gte7 zb_HXFgJYzO(QFfLDzPPXcHwB-lKB8@^3g_du;1L_OjBY0o58>D-WPnZWV_2$I`pmr zBJkUJ)_RHVwQ|EPxt5nbivx20G=emB#F^*4-!pqx@BB^ud*C3GhVay5q)QQDF&ev$ z)KZHj^_hqF@dWEQw`q;N4w>IQJ?L#-zdCs1c~IvF-V?&#PHy%S6;0)cvcp5L=1X$t z^U(IoeV>Mzh=xBb;eL%@wUf@$-1FxMbD`T_n?-xr_BoOkAvL7NgJ__Clqz=QCF#Qb z7sFt9SDRmXAt#fREt;w87oP}w{XIgs=0x%_`a8c#>Z~3%B+JqQX@6-V+GI~>jKo%O#TK%6N(UL8_=DVGiyrFau z#dZ%qUOc*9+qq`^Ya{=uMDCWbAn;1Sw+(;if!THMGVl6MT;?frgp0}M`;tMFhi zVWU!Z>k9yeN?lxUJ7(R0NT5UQ ztjQbi6;>;96n_N2q&P+5_$vBa`*Xy2*Kd}hszuNjwzGZDmv~DFE9TNH9tVM z31ZhzBT3LceGan@RnyuB(aA3lr|h?N4a4HD;tUwok|t!hR*sexO=Wl` z4IaGdod(eXk|1YVT6Idt`3X_l*Sg~$KB#X%>FO~bj!`*qw8gtWr-aGA>m+z6tgblZ zPFf~u^&vRUlG%vSg-fAz>`}yfzwsC{H+=3?(Y`Rg6G!hIl!#GASbKpX^&RuXuaVx} zCpi*5<#Vat3@zpC)u!Y6FnOhH*+-Q{%KjGVYv-?xTT*H#!)pFGhcTD$1jdcNKG&W5 zY8bqU-d?PJH|N46Q?;t}_4oJ0_SSZiy4qy^i5TcB$2!?^y_{E-;~EvN_aD!k^PR*e z*Vc*nKsPU3yj~utyQ)WV=?;h|yr3-paIK7Tbso%joGrou?YkaK0cr~kMwuVcFJee2wAdQEO8AA(b`1LHoJB9S1dokeM)6z&j@*pORt=-_B z{VIBGkKf|Hu80DaIG5daGjxyKGXxLv&}+?%<*EZ-meP%pE}i!ce8R1IUwH@Uj8&_{ z{vks0FhfQlz*bAQi~AeSnDX!=BZ?J}jTYLkr?$tw*D>@PF&N#)89NwfWI@ zH@M2RSV+wi#OHg#s#5aq?8>QAe1Ry^LlDETfaViks${#kx+u9y-`?RmuXoA*B^CLh zMJTHNwnUeSKyA|AH)^UAZ)M0eIvfPn7iasTMYT+G#$2}_>0JYpw$_WSFk9JQ^4HiO z%tdduRaMvC;5%tMlP#(%cx?^}U|b{#OKZgd=3Vp?hn|NMk0q}k%z8cl+-6`LX)P{2 z7v9zese_w|;(x3!eFB$N}&WHfO7pV@Bsj&MKc zm{xCsQ1RLQMztv{mT8;5blUTQcl+jwgv9$5m#JjW(3sZasYXklKRbS&!rz`Y4>zy1 z!6w}VeQLCE;fx4x4a1DXbgeR;dl|NX>%I2d{q~@M-IWumyH(+vBjoLpR%O6U>0dOS zX$a1E?@a@k3rW1d6D)V+yS(2$kMWlYV<6w6`PJrdV84|#kc+&qH z7HJyS^Syhx9~(YH#Y&qcXlJPlBIwRSnk5i{Pim0LfHs~r5i|*oI3GwyT(|;G$okvc zHs4Ga#o=gPcv`~{Z}LDlL%(wDb!)0b^*p=z^mP)jG5tveZoi3K>7v?pwbOQ+*epTP zO%qRto~5yJ%A6QK%$6nsXZ8g5YX0FnjnRTpBM&KWJ;6H4hU%l#R)R9oRgu9;Bd7Ow z%ferKPHqg#M~8|{ZhTP>-$5e>#W2<1Q!3?aM>mp|svF;qYcKXuMz zA*0I@FWq^v)x&HQK4_R3m8c3*68WlDP*;()MS(M@^P%T6XhEu;@? zh}7hkY2tDA(tL}$W~N}p5@PlV@ge_vT&Z9mSL;J+=Ny-}tj#Fz2FGAdcZ(*}flzeY zZukS6BPmZ8w?y%Bo0WdY)4Oqshx{S@u&3V8>Ct~XFqq;{{kpvdfV<(Rt&Yh6l;iT- z_@2hP`vau+{pMT_Xd@0Qfd9}&f185dO@R>zqI--mEPqcC|5EHV(zYYK&c@6M`X!%t zI}{eUIZ~H*-RF%NkYY<0cr6Wp$O88|K6LIt7lUrz+V2fz4BKR}Q6{Cl!cil)pl3E7 zlaezGQHy-b8c+QV70dQuJuz=1_)bI^_4Fya%G740l&X+VK2waY^4v-MOhWL6a4ROW9brN%CJ5STejN%`kH<~q`5?w~f=9TsF$NNTvn7y2>pP(ubR%WwKN-ZqW>um~rki?Y;R+y5WF_$cS0&e) zo%9=rJuVc@ZWI|boO{<({=2ipd$*;)@%%POas#)u^2@$uMbWN?^W@*s^M!YHZ*VFM zotEQMS(rc+f3(;6Nt6ROXhnj^-%NT*=mj=% z+u^$4TpzY=WJ1)Qa59&QE@rGo;&)`hu1@#tEwFL*iHDPk2xqJnD|ES2#qvMF@}Q&C z;&U{xp8$Nn=_}88J#ZgI0rHPr)0~ib8KJ`HG)Tb~@Qzu$=UEB)fKKei-T)smOn|t; zTQ?OrM*Qy;AT5dt;v(x|7<#Z(R*+4^UqAH$T}5#>J_QHO+Cx^)#+wl<0N5Et`z<2V zuzerl47}6@Y?lQmq-&(L;FLuWGu?&I&~w#kkzVUPC9S-8x0U@;VX`0;dsLnBx_4Y> zB+5eNtvCL23jke*%kkZuktyihAjDu0;3UUOqkTW_9|g5j?oRW`QLvl0tVv8Gb5qd+ zT~gof)Xx!j{~P6162(n&mi5sTdH$_EsN~BcrS`YDoyF3$Itax-Ymy4%pQ>LLJvGs* z4uYwZcHnE?Ox5_HYP`X7`a*QwQ;M{IQ%zM*bYC&|1f!2g#vqpI-DF@KsxEfCMwj7Y zS?NXymt(x~>U{8v2Xv9*=;hM{YQNJ&n2)y4$~6`AM$fO&OR8k*rSNRb)@GNd-x@$lVz^`D_;$@MI7`9c++JOeA6)3 zY9gh*^4wZyun>>zk#4filL~9j4wLgt-Pam6Imn35j(UDq206@APufn!Q5Bj2-wdho zSy*hR@L6~A?CuoQ2uiJ3j0zmbeCUV;i!RFFpbyoZ)?XZKd2KroZ%Fdh_)u%&kMgQR@C`u!>}BwiWd| z!Ea^H2E3=b8;Ao5KIT4DP9c6^^>HL)lNBR_TpV>S(gw-}!mqRCFJiDtLNE+X2WRQ~ zKiH%NEuHAwmIghic;9osF_8sL$Qt);LTAgbHtg@1Nthnr{>$t10>hbsaM>RVYDz1_ zeQ~Cq#Pe(qEzm}nHX`F2@1U=pU>Xv+^R;yluodK*1q90&~T zS-)XP-rWBt7@L0-wAEs{q53JLp<7xy%I0RDpN#o+e(iVQF!2I2;MLtkTBHBTfV+7h zz?~|0@?|GG!AB6!9N(%yI(*Wo;bG!V=C%iQ zDs4ohKMP7fEGX{{O*_}=PagMd5RN5dKh(G^!-0C-Xj*vWt2nh7Rh$wX(v0M@$&kL< z3`vfRWK`(%hwSYkO<_E#Kcg=EN2xmv7Z%*)7mxJq&gh>|Att=Myzm0&o9tah6p*!e zZ8vbT6sgEQg6Y40DWaq0cyAR7+7g4#nF#u_fumxzog8&isRkA4@2m4F{`8+Puy{(2 zaoCUJ+v09(F`pgo!!MuT(m#94O1NlVgbK#+_Q5%S#&Y$r7CGyE*k*8|VIuG23K8jH zad|a9CWqBkYbe@C=8|JS`DoScfBo{0U>JE6a`6SbRJsIVZvKD0_ILekTG9rES266q z&D1h3p}rO3x^@(0vyu%T8FYq`&W+4bbfje5GEw^FWlL;bo~nO6k0EWq?|FE?o%QjI zOY7#zM<-fkwz8V%ILp%NHac%@BI)n<5a#Ykcv0nD7VJ}#=VtF-9r9C0+8yTpeY;R1 z(fjw31~av-y!Ah;b%b{XRasw7<_+UMbg*M|ZuMx*XBC&@ zRdhl4mQi;t+8Q`tgc64`=LN0Zn&`eTJFTW-wmEVhxGEb-kQCE;tno`R3zS>BveDQL zl^td7HWF8FWV(;`38-)%?)<5-xa;Gx^e?5(i)`F8);C=2_0V6Ca)%(>#R2-a*hXzR zel6_h?BJ!oO;