From 4232fa687d6cdc25ef701b4598c03ffbe9290f8b Mon Sep 17 00:00:00 2001 From: Simon Jarbrant Date: Fri, 8 Mar 2024 14:28:36 +0100 Subject: [PATCH 01/25] Conform Database.ColumnType to Sendable --- GRDB/Core/Database.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 0e06a8acc2..d09c529403 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1910,7 +1910,7 @@ extension Database { /// /// For more information, see /// [Datatypes In SQLite](https://www.sqlite.org/datatype3.html). - public struct ColumnType: RawRepresentable, Hashable { + public struct ColumnType: RawRepresentable, Hashable, Sendable { /// The SQL for the column type (`"TEXT"`, `"BLOB"`, etc.) public let rawValue: String From 9593297cf0c93e9ddac6c292ae185fb93f7c8052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 14:54:07 +0100 Subject: [PATCH 02/25] CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecb83362cd..88c9924c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,10 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: --- +## Next Release + +- **New**: [#1503](https://github.com/groue/GRDB.swift/pull/1503) by [@simba909](https://github.com/simba909): Conform Database.ColumnType to Sendable + ## 6.25.0 Released February 25, 2024 From 3b096b38e73864e41d1e73c8feba6ad1edaa52f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 13:40:18 +0100 Subject: [PATCH 03/25] Failing test for #1500 --- Tests/GRDBTests/ValueObservationTests.swift | 30 ++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index a0d04fb5ba..f5c1e49088 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -1268,5 +1268,33 @@ class ValueObservationTests: GRDBTestCase { onChange: { _ in }) } - + + // Regression test for + func testIssue1500() throws { + let pool = try makeDatabasePool() + + try pool.read { db in + _ = try db.tableExists("t") + } + + try pool.write { db in + try db.create(table: "t") { t in + t.column("a") + } + } + + _ = ValueObservation + .trackingConstantRegion { db in + try db.tableExists("t") + } + .start( + in: pool, + scheduling: .immediate, + onError: { error in + XCTFail("Unexpected error \(error)") + }, + onChange: { value in + XCTAssertEqual(value, true) + }) + } } From d54d0abc07608168b79773cc02290f98893026cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 13:40:45 +0100 Subject: [PATCH 04/25] Fix WALSnapshotTransaction so that it clears its schema cache if needed --- GRDB/Core/WALSnapshotTransaction.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/GRDB/Core/WALSnapshotTransaction.swift b/GRDB/Core/WALSnapshotTransaction.swift index 32eb3afca1..5f56f1d2fc 100644 --- a/GRDB/Core/WALSnapshotTransaction.swift +++ b/GRDB/Core/WALSnapshotTransaction.swift @@ -46,7 +46,9 @@ class WALSnapshotTransaction { // Open a long-lived transaction, and enter snapshot isolation self.walSnapshot = try reader.sync(allowingLongLivedTransaction: true) { db in try db.beginTransaction(.deferred) - try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1") + // This also acquires snapshot isolation because checking + // database schema performs a read access. + try db.clearSchemaCacheIfNeeded() return try WALSnapshot(db) } self.reader = reader From 62e7ea1ef2e3eaee33c8d18d695acf81c63f7a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 17:55:15 +0100 Subject: [PATCH 05/25] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88c9924c30..f1c639fc89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,6 +126,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: ## Next Release - **New**: [#1503](https://github.com/groue/GRDB.swift/pull/1503) by [@simba909](https://github.com/simba909): Conform Database.ColumnType to Sendable +- **Fixed**: [#1508](https://github.com/groue/GRDB.swift/pull/1508) by [@groue](https://github.com/groue): Fix ValueObservation mishandling of database schema modification ## 6.25.0 From 4ffad63f1b47bde9208a3321f4648562e399e781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 16:49:52 +0100 Subject: [PATCH 06/25] Fix Swift 5.10 warning on WALSnapshot --- GRDB/Core/WALSnapshot.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/GRDB/Core/WALSnapshot.swift b/GRDB/Core/WALSnapshot.swift index 279f280c5b..4d50c67e08 100644 --- a/GRDB/Core/WALSnapshot.swift +++ b/GRDB/Core/WALSnapshot.swift @@ -21,7 +21,9 @@ /// Yes, this is an awfully complex logic. /// /// See . -final class WALSnapshot: Sendable { +final class WALSnapshot: @unchecked Sendable { + // @unchecked because sqlite3_snapshot has no threading requirements. + // let sqliteSnapshot: UnsafeMutablePointer init(_ db: Database) throws { From 43ced9b2f0730ed84e813c8067f3e2bee0b2cad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 16:50:05 +0100 Subject: [PATCH 07/25] Trivial Sendable conformances Either added, either made explicitly unavailable. --- GRDB/Core/Configuration.swift | 2 +- GRDB/Core/Cursor.swift | 40 +++++++++++++++++++ GRDB/Core/Database+Schema.swift | 12 +++--- GRDB/Core/Database+Statements.swift | 5 +++ GRDB/Core/Database.swift | 29 ++++++++++---- GRDB/Core/DatabaseBackupProgress.swift | 2 +- GRDB/Core/DatabaseRegion.swift | 2 +- GRDB/Core/DatabaseValueConvertible.swift | 5 +++ GRDB/Core/FetchRequest.swift | 5 +++ GRDB/Core/Row.swift | 13 +++++- GRDB/Core/RowAdapter.swift | 8 ++-- GRDB/Core/Statement.swift | 5 +++ GRDB/Core/StatementColumnConvertible.swift | 5 +++ .../Foundation/DatabaseDateComponents.swift | 4 +- GRDB/Core/TransactionObserver.swift | 11 +++-- GRDB/Dump/Database+Dump.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/FTS3.swift | 7 +++- GRDB/FTS/FTS3Pattern.swift | 2 +- GRDB/FTS/FTS3TokenizerDescriptor.swift | 2 +- GRDB/FTS/FTS4.swift | 10 +++++ GRDB/FTS/FTS5.swift | 12 +++++- GRDB/FTS/FTS5Pattern.swift | 2 +- GRDB/FTS/FTS5Tokenizer.swift | 2 +- GRDB/FTS/FTS5TokenizerDescriptor.swift | 2 +- GRDB/FTS/FTS5WrapperTokenizer.swift | 2 +- GRDB/JSON/JSONColumn.swift | 2 +- GRDB/Migration/DatabaseMigrator.swift | 2 +- GRDB/QueryInterface/ForeignKey.swift | 2 +- GRDB/QueryInterface/SQL/Column.swift | 2 +- GRDB/QueryInterface/SQL/SQLExpression.swift | 2 +- GRDB/QueryInterface/SQL/SQLFunctions.swift | 2 +- GRDB/QueryInterface/SQL/SQLSelection.swift | 2 +- GRDB/QueryInterface/SQL/Table.swift | 2 +- .../Schema/ColumnDefinition.swift | 7 +++- .../Schema/Database+SchemaDefinition.swift | 2 +- .../Schema/ForeignKeyDefinition.swift | 5 +++ .../Schema/IndexDefinition.swift | 2 +- .../Schema/TableAlteration.swift | 5 +++ .../Schema/TableDefinition.swift | 7 +++- GRDB/Record/FetchableRecord+Decodable.swift | 1 + GRDB/Record/FetchableRecord.swift | 5 +++ GRDB/Record/MutablePersistableRecord.swift | 2 +- GRDB/Utils/Inflections.swift | 2 +- .../SharedValueObservation.swift | 2 +- .../ValueObservationScheduler.swift | 2 +- 50 files changed, 203 insertions(+), 56 deletions(-) diff --git a/GRDB/Core/Configuration.swift b/GRDB/Core/Configuration.swift index 780ce452a4..6b261902b6 100644 --- a/GRDB/Core/Configuration.swift +++ b/GRDB/Core/Configuration.swift @@ -294,7 +294,7 @@ public struct Configuration { /// connection is opened. /// /// Related SQLite documentation: - public enum JournalModeConfiguration { + public enum JournalModeConfiguration: Sendable { /// The default setup has ``DatabaseQueue`` perform no specific /// configuration of the journal mode, and ``DatabasePool`` /// configure the database for the WAL mode (just like the diff --git a/GRDB/Core/Cursor.swift b/GRDB/Core/Cursor.swift index 58834bb859..b8ee021552 100644 --- a/GRDB/Core/Cursor.swift +++ b/GRDB/Core/Cursor.swift @@ -733,6 +733,11 @@ public final class AnyCursor: Cursor { } } +// Explicit non-conformance to Sendable: a type-erased cursor can't be more +// sendable than non-sendable cursors (such as `DatabaseCursor`). +@available(*, unavailable) +extension AnyCursor: Sendable { } + /// A `Cursor` that consumes and drops n elements from an underlying `Base` /// cursor before possibly returning the first available element. public final class DropFirstCursor { @@ -747,6 +752,11 @@ public final class DropFirstCursor { } } +// Explicit non-conformance to Sendable: `DropFirstCursor` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension DropFirstCursor: Sendable { } + extension DropFirstCursor: Cursor { public func next() throws -> Base.Element? { while dropped < limit { @@ -773,6 +783,11 @@ public final class DropWhileCursor { } } +// Explicit non-conformance to Sendable: `DropWhileCursor` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension DropWhileCursor: Sendable { } + extension DropWhileCursor: Cursor { public func next() throws -> Base.Element? { if predicateHasFailed { @@ -818,6 +833,11 @@ public final class EnumeratedCursor { } } +// Explicit non-conformance to Sendable: `EnumeratedCursor` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension EnumeratedCursor: Sendable { } + extension EnumeratedCursor: Cursor { public func next() throws -> (Int, Base.Element)? { guard let element = try base.next() else { return nil } @@ -875,6 +895,11 @@ public final class FlattenCursor where Base.Element: Cursor { } } +// Explicit non-conformance to Sendable: `EnumeratedCursor` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension FlattenCursor: Sendable { } + extension FlattenCursor: Cursor { public func next() throws -> Base.Element.Element? { while true { @@ -901,6 +926,11 @@ public final class MapCursor { } } +// Explicit non-conformance to Sendable: There is no known reason for making +// it thread-safe (`transform` a Sendable closure). +@available(*, unavailable) +extension MapCursor: Sendable { } + extension MapCursor: Cursor { public func next() throws -> Element? { guard let element = try base.next() else { return nil } @@ -927,6 +957,11 @@ public final class PrefixCursor { } } +// Explicit non-conformance to Sendable: `PrefixCursor` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension PrefixCursor: Sendable { } + extension PrefixCursor: Cursor { public func next() throws -> Base.Element? { if taken >= maxLength { return nil } @@ -954,6 +989,11 @@ public final class PrefixWhileCursor { } } +// Explicit non-conformance to Sendable: `PrefixCursor` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension PrefixWhileCursor: Sendable { } + extension PrefixWhileCursor: Cursor { public func next() throws -> Base.Element? { if !predicateHasFailed, let nextElement = try base.next() { diff --git a/GRDB/Core/Database+Schema.swift b/GRDB/Core/Database+Schema.swift index f636f91698..878cbe72bf 100644 --- a/GRDB/Core/Database+Schema.swift +++ b/GRDB/Core/Database+Schema.swift @@ -997,7 +997,7 @@ extension Database { /// /// - [pragma `table_info`](https://www.sqlite.org/pragma.html#pragma_table_info) /// - [pragma `table_xinfo`](https://www.sqlite.org/pragma.html#pragma_table_xinfo) -public struct ColumnInfo: FetchableRecord { +public struct ColumnInfo: FetchableRecord, Sendable { let cid: Int let hidden: Int? @@ -1083,9 +1083,9 @@ public struct ColumnInfo: FetchableRecord { /// /// - [pragma `index_list`](https://www.sqlite.org/pragma.html#pragma_index_list) /// - [pragma `index_info`](https://www.sqlite.org/pragma.html#pragma_index_info) -public struct IndexInfo { +public struct IndexInfo: Sendable{ /// The origin of an index. - public struct Origin: RawRepresentable, Equatable, DatabaseValueConvertible { + public struct Origin: RawRepresentable, Equatable, DatabaseValueConvertible, Sendable { public var rawValue: String public init(rawValue: String) { @@ -1158,7 +1158,7 @@ public struct IndexInfo { /// ``` /// /// Related SQLite documentation: -public struct ForeignKeyViolation { +public struct ForeignKeyViolation: Sendable { /// The name of the table that contains the foreign key. public var originTable: String @@ -1307,7 +1307,7 @@ extension ForeignKeyViolation: CustomStringConvertible { /// pk.rowIDColumn // nil /// pk.isRowID // false /// ``` -public struct PrimaryKeyInfo { +public struct PrimaryKeyInfo: Sendable { private enum Impl { /// The hidden rowID. case hiddenRowID @@ -1433,7 +1433,7 @@ public struct PrimaryKeyInfo { /// `Database` method. /// /// Related SQLite documentation: [pragma `foreign_key_list`](https://www.sqlite.org/pragma.html#pragma_foreign_key_list). -public struct ForeignKeyInfo { +public struct ForeignKeyInfo: Sendable { /// The first column in the output of the `foreign_key_list` pragma. public var id: Int diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index 48d4ad74e0..061c5b3646 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -361,6 +361,11 @@ public class SQLStatementCursor { } } +// Explicit non-conformance to Sendable: database cursors must be used from +// a serialized database access dispatch queue. +@available(*, unavailable) +extension SQLStatementCursor: Sendable { } + extension SQLStatementCursor: Cursor { public func next() throws -> Statement? { guard offset < cString.count - 1 /* trailing \0 */ else { diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index d09c529403..ae705dbe1c 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1729,6 +1729,11 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib } } +// Explicit non-conformance to Sendable: `Database` must be used from a +// serialized database access dispatch queue (see `SerializedDatabase`). +@available(*, unavailable) +extension Database: Sendable { } + #if SQLITE_HAS_CODEC extension Database { @@ -1854,7 +1859,7 @@ extension Database { /// The available checkpoint modes. /// /// Related SQLite documentation: - public enum CheckpointMode: CInt { + public enum CheckpointMode: CInt, Sendable { /// The `SQLITE_CHECKPOINT_PASSIVE` mode. case passive = 0 @@ -1873,7 +1878,7 @@ extension Database { /// Related SQLite documentation: /// - /// - - public struct CollationName: RawRepresentable, Hashable { + public struct CollationName: RawRepresentable, Hashable, Sendable { public let rawValue: String /// Creates a collation name. @@ -1962,7 +1967,7 @@ extension Database { /// An SQLite conflict resolution. /// /// Related SQLite documentation: - public enum ConflictResolution: String { + public enum ConflictResolution: String, Sendable { /// The `ROLLBACK` conflict resolution. case rollback = "ROLLBACK" @@ -1982,7 +1987,7 @@ extension Database { /// A foreign key action. /// /// Related SQLite documentation: - public enum ForeignKeyAction: String { + public enum ForeignKeyAction: String, Sendable { /// The `CASCADE` foreign key action. case cascade = "CASCADE" @@ -2005,7 +2010,7 @@ extension Database { /// ``Database/trace(options:_:)`` method. /// /// Related SQLite documentation: - public struct TracingOptions: OptionSet { + public struct TracingOptions: OptionSet, Sendable { /// The raw trace event code. public let rawValue: CInt @@ -2138,7 +2143,7 @@ extension Database { /// /// Related SQLite documentation: . @frozen - public enum TransactionCompletion { + public enum TransactionCompletion: Sendable { case commit case rollback } @@ -2146,7 +2151,7 @@ extension Database { /// A transaction kind. /// /// Related SQLite documentation: . - public enum TransactionKind: String { + public enum TransactionKind: String, Sendable { /// The `DEFERRED` transaction kind. case deferred = "DEFERRED" @@ -2179,3 +2184,13 @@ extension Database { } } } + +// Explicit non-conformance to Sendable: a trace event contains transient +// information. +@available(*, unavailable) +extension Database.TraceEvent: Sendable { } + +// Explicit non-conformance to Sendable: a trace event contains transient +// information. +@available(*, unavailable) +extension Database.TraceEvent.Statement: Sendable { } diff --git a/GRDB/Core/DatabaseBackupProgress.swift b/GRDB/Core/DatabaseBackupProgress.swift index 3119dfa5b6..52a4a56d5e 100644 --- a/GRDB/Core/DatabaseBackupProgress.swift +++ b/GRDB/Core/DatabaseBackupProgress.swift @@ -1,7 +1,7 @@ /// Describe the progress of a database backup. /// /// Related SQLite documentation: -public struct DatabaseBackupProgress { +public struct DatabaseBackupProgress: Sendable { /// The number of pages still to be backed up. /// /// It is the result of the `sqlite3_backup_remaining` function. diff --git a/GRDB/Core/DatabaseRegion.swift b/GRDB/Core/DatabaseRegion.swift index 0099ddb028..fddeb77d37 100644 --- a/GRDB/Core/DatabaseRegion.swift +++ b/GRDB/Core/DatabaseRegion.swift @@ -40,7 +40,7 @@ /// /// - ``isModified(byEventsOfKind:)`` /// - ``isModified(by:)`` -public struct DatabaseRegion { +public struct DatabaseRegion: Sendable { private let tableRegions: [CaseInsensitiveIdentifier: TableRegion]? private init(tableRegions: [CaseInsensitiveIdentifier: TableRegion]?) { diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index 47f54c5af2..df9b1b6939 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -222,6 +222,11 @@ public final class DatabaseValueCursor: Databas } } +// Explicit non-conformance to Sendable: database cursors must be used from +// a serialized database access dispatch queue. +@available(*, unavailable) +extension DatabaseValueCursor: Sendable { } + /// DatabaseValueConvertible comes with built-in methods that allow to fetch /// cursors, arrays, or single values: /// diff --git a/GRDB/Core/FetchRequest.swift b/GRDB/Core/FetchRequest.swift index 87ff4b3404..43115cab70 100644 --- a/GRDB/Core/FetchRequest.swift +++ b/GRDB/Core/FetchRequest.swift @@ -152,6 +152,11 @@ public struct PreparedRequest { } } +// Explicit non-conformance to Sendable: `PreparedRequest` contains +// a statement. +@available(*, unavailable) +extension PreparedRequest: Sendable { } + extension PreparedRequest: Refinable { } // MARK: - AdaptedFetchRequest diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 4c3698f7ef..e11f35d1ff 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -245,6 +245,12 @@ public final class Row { } } +// Explicit non-conformance to Sendable: a row contains transient +// information. TODO GRDB7: split non sendable statement rows from sendable +// copied rows. +@available(*, unavailable) +extension Row: Sendable { } + extension Row { // MARK: - Columns @@ -1377,6 +1383,11 @@ public final class RowCursor: DatabaseCursor { public func _element(sqliteStatement: SQLiteStatement) -> Row { _row } } +// Explicit non-conformance to Sendable: database cursors must be used from +// a serialized database access dispatch queue. +@available(*, unavailable) +extension RowCursor: Sendable { } + extension Row { // MARK: - Fetching From Prepared Statement @@ -2059,7 +2070,7 @@ typealias RowIndex = Row.Index extension Row { /// An index to a (column, value) pair in a ``Row``. - public struct Index { + public struct Index: Sendable { let index: Int init(_ index: Int) { self.index = index } } diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index a17062460f..a077e58152 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -378,7 +378,7 @@ extension RowAdapter { /// /// This limit adapter may turn out useful in some narrow use cases. You'll /// be happy to find it when you need it. -public struct EmptyRowAdapter: RowAdapter { +public struct EmptyRowAdapter: RowAdapter, Sendable { /// Creates an `EmptyRowAdapter`. public init() { } @@ -403,7 +403,7 @@ public struct EmptyRowAdapter: RowAdapter { /// /// Note that columns that are not present in the dictionary are not present /// in the resulting adapted row. -public struct ColumnMapping: RowAdapter { +public struct ColumnMapping: RowAdapter, Sendable { /// A dictionary from mapped column names to column names in a base row. let mapping: [String: String] @@ -444,7 +444,7 @@ public struct ColumnMapping: RowAdapter { /// // [c:2, d: 3] /// try Row.fetchOne(db, sql: sql, adapter: adapter)! /// ``` -public struct SuffixRowAdapter: RowAdapter { +public struct SuffixRowAdapter: RowAdapter, Sendable { /// The suffix index let index: Int @@ -473,7 +473,7 @@ public struct SuffixRowAdapter: RowAdapter { /// // [b:1 c:2] /// try Row.fetchOne(db, sql: sql, adapter: adapter) /// ``` -public struct RangeRowAdapter: RowAdapter { +public struct RangeRowAdapter: RowAdapter, Sendable { /// The range let range: CountableRange diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index 5bec60a162..e1a90a1082 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -623,6 +623,11 @@ public final class Statement { } } +// Explicit non-conformance to Sendable: statements must be used from +// a serialized database access dispatch queue. +@available(*, unavailable) +extension Statement: Sendable { } + extension Statement: CustomStringConvertible { public var description: String { SchedulingWatchdog.allows(database) ? sql : "Statement" diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index 3b18bcb9af..cb081ec63d 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -241,6 +241,11 @@ where Value: DatabaseValueConvertible & StatementColumnConvertible } } +// Explicit non-conformance to Sendable: database cursors must be used from +// a serialized database access dispatch queue. +@available(*, unavailable) +extension FastDatabaseValueCursor: Sendable { } + /// Types that adopt both DatabaseValueConvertible and /// StatementColumnConvertible can be efficiently initialized from /// database values. diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index 954edb350b..d4ebba478a 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -1,10 +1,10 @@ import Foundation /// A database value that holds date components. -public struct DatabaseDateComponents { +public struct DatabaseDateComponents: Sendable { /// The SQLite formats for date components. - public enum Format: String { + public enum Format: String, Sendable { /// The format "yyyy-MM-dd". case YMD = "yyyy-MM-dd" diff --git a/GRDB/Core/TransactionObserver.swift b/GRDB/Core/TransactionObserver.swift index 4ce21468b1..d45b3b1206 100644 --- a/GRDB/Core/TransactionObserver.swift +++ b/GRDB/Core/TransactionObserver.swift @@ -135,7 +135,7 @@ extension Database { } /// The extent of the observation performed by a ``TransactionObserver``. - public enum TransactionObservationExtent { + public enum TransactionObservationExtent: Sendable { /// Observation lasts until observer is deallocated. case observerLifetime /// Observation lasts until the next transaction. @@ -1056,7 +1056,7 @@ struct StatementObservation { /// See the ``TransactionObserver/observes(eventsOfKind:)`` method in the /// ``TransactionObserver`` protocol for more information. @frozen -public enum DatabaseEventKind { +public enum DatabaseEventKind: Sendable { /// The insertion of a row in a database table. case insert(tableName: String) @@ -1109,7 +1109,7 @@ protocol DatabaseEventProtocol { /// ``TransactionObserver`` protocol for more information. public struct DatabaseEvent { /// An event kind. - public enum Kind: CInt { + public enum Kind: CInt, Sendable { /// An insertion event case insert = 18 // SQLITE_INSERT @@ -1173,6 +1173,11 @@ public struct DatabaseEvent { } } +// Explicit non-conformance to Sendable: this type can't be made Sendable +// until GRDB7 where we can distinguish between a transient event and its copy. +@available(*, unavailable) +extension DatabaseEvent: Sendable { } + extension DatabaseEvent: DatabaseEventProtocol { func send(to observer: TransactionObservation) { observer.databaseDidChange(with: self) diff --git a/GRDB/Dump/Database+Dump.swift b/GRDB/Dump/Database+Dump.swift index e98716edc3..33edfda640 100644 --- a/GRDB/Dump/Database+Dump.swift +++ b/GRDB/Dump/Database+Dump.swift @@ -308,7 +308,7 @@ extension Database { } /// Options for printing table names. -public enum DumpTableHeaderOptions { +public enum DumpTableHeaderOptions: Sendable { /// Table names are only printed when several tables are printed. case automatic diff --git a/GRDB/Dump/DumpFormats/DebugDumpFormat.swift b/GRDB/Dump/DumpFormats/DebugDumpFormat.swift index 58b8f97482..02f2f11304 100644 --- a/GRDB/Dump/DumpFormats/DebugDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/DebugDumpFormat.swift @@ -17,7 +17,7 @@ import Foundation /// // Craig|200 /// try db.dumpRequest(Player.all(), format: .debug()) /// ``` -public struct DebugDumpFormat { +public struct DebugDumpFormat: Sendable { /// A boolean value indicating if column labels are printed as the first /// line of output. public var header: Bool diff --git a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift index 0e039863bf..7e3bc61d5b 100644 --- a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift @@ -27,7 +27,7 @@ import Foundation /// encoder.outputFormatting = .prettyPrinted /// try db.dumpRequest(Player.all(), format: .json(encoder)) /// ``` -public struct JSONDumpFormat { +public struct JSONDumpFormat: Sendable { /// The default `JSONEncoder` for database values. /// /// It is configured so that blob values (`Data`) are encoded in the diff --git a/GRDB/Dump/DumpFormats/LineDumpFormat.swift b/GRDB/Dump/DumpFormats/LineDumpFormat.swift index debc28956c..a0c9f9a1fd 100644 --- a/GRDB/Dump/DumpFormats/LineDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/LineDumpFormat.swift @@ -13,7 +13,7 @@ import Foundation /// // score = 1000 /// try db.dumpRequest(Player.all(), format: .line()) /// ``` -public struct LineDumpFormat { +public struct LineDumpFormat: Sendable { /// The string to print for NULL values. public var nullValue: String diff --git a/GRDB/Dump/DumpFormats/ListDumpFormat.swift b/GRDB/Dump/DumpFormats/ListDumpFormat.swift index 4062095415..c256af4c82 100644 --- a/GRDB/Dump/DumpFormats/ListDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/ListDumpFormat.swift @@ -14,7 +14,7 @@ import Foundation /// // Craig|200 /// try db.dumpRequest(Player.all(), format: .list()) /// ``` -public struct ListDumpFormat { +public struct ListDumpFormat: Sendable { /// A boolean value indicating if column labels are printed as the first /// line of output. public var header: Bool diff --git a/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift b/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift index 2fd8587a99..3c6ae5c17a 100644 --- a/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift @@ -9,7 +9,7 @@ /// // 'Craig',200 /// try db.dumpRequest(Player.all(), format: .quote()) /// ``` -public struct QuoteDumpFormat { +public struct QuoteDumpFormat: Sendable { /// A boolean value indicating if column labels are printed as the first /// line of output. public var header: Bool diff --git a/GRDB/FTS/FTS3.swift b/GRDB/FTS/FTS3.swift index cb308cd069..80e677ae18 100644 --- a/GRDB/FTS/FTS3.swift +++ b/GRDB/FTS/FTS3.swift @@ -29,7 +29,7 @@ /// - ``tokenize(_:withTokenizer:)`` public struct FTS3 { /// Options for Latin script characters. - public enum Diacritics { + public enum Diacritics: Sendable { /// Do not remove diacritics from Latin script characters. This option /// matches the `remove_diacritics=0` tokenizer argument. /// @@ -183,3 +183,8 @@ public final class FTS3TableDefinition { columns.append(name) } } + +// Explicit non-conformance to Sendable: `FTS3TableDefinition` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension FTS3TableDefinition: Sendable { } diff --git a/GRDB/FTS/FTS3Pattern.swift b/GRDB/FTS/FTS3Pattern.swift index 9862566f2e..af6e82170d 100644 --- a/GRDB/FTS/FTS3Pattern.swift +++ b/GRDB/FTS/FTS3Pattern.swift @@ -16,7 +16,7 @@ /// - ``init(matchingAllTokensIn:)`` /// - ``init(matchingAnyTokenIn:)`` /// - ``init(matchingPhrase:)`` -public struct FTS3Pattern { +public struct FTS3Pattern: Sendable { /// The raw pattern string. /// /// It is guaranteed to be a valid FTS3/4 pattern. diff --git a/GRDB/FTS/FTS3TokenizerDescriptor.swift b/GRDB/FTS/FTS3TokenizerDescriptor.swift index 378af557c1..14b2a3e81e 100644 --- a/GRDB/FTS/FTS3TokenizerDescriptor.swift +++ b/GRDB/FTS/FTS3TokenizerDescriptor.swift @@ -20,7 +20,7 @@ /// - ``simple`` /// - ``unicode61(diacritics:separators:tokenCharacters:)`` /// - ``FTS3/Diacritics`` -public struct FTS3TokenizerDescriptor { +public struct FTS3TokenizerDescriptor: Sendable { let name: String let arguments: [String] diff --git a/GRDB/FTS/FTS4.swift b/GRDB/FTS/FTS4.swift index 36e5e7c748..ef9a54f5e3 100644 --- a/GRDB/FTS/FTS4.swift +++ b/GRDB/FTS/FTS4.swift @@ -313,6 +313,11 @@ public final class FTS4TableDefinition { } } +// Explicit non-conformance to Sendable: `FTS4TableDefinition` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension FTS4TableDefinition: Sendable { } + /// Describes a column in an ``FTS4`` virtual table. /// /// You get instances of `FTS4ColumnDefinition` when you create an ``FTS4`` @@ -377,6 +382,11 @@ public final class FTS4ColumnDefinition { } } +// Explicit non-conformance to Sendable: `FTS4ColumnDefinition` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension FTS4ColumnDefinition: Sendable { } + extension Database { /// Deletes the synchronization triggers for a synchronized FTS4 table. /// diff --git a/GRDB/FTS/FTS5.swift b/GRDB/FTS/FTS5.swift index 43daaf5dda..a18da5a3f0 100644 --- a/GRDB/FTS/FTS5.swift +++ b/GRDB/FTS/FTS5.swift @@ -44,7 +44,7 @@ public struct FTS5 { /// tokenizer argument. /// /// Related SQLite documentation: - public enum Diacritics { + public enum Diacritics: Sendable { /// Do not remove diacritics from Latin script characters. This /// option matches the raw "remove_diacritics=0" tokenizer argument. case keep @@ -492,6 +492,11 @@ public final class FTS5TableDefinition { } } +// Explicit non-conformance to Sendable: `FTS5TableDefinition` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension FTS5TableDefinition: Sendable { } + /// Describes a column in an ``FTS5`` virtual table. /// /// You get instances of `FTS5ColumnDefinition` when you create an ``FTS5`` @@ -534,6 +539,11 @@ public final class FTS5ColumnDefinition { } } +// Explicit non-conformance to Sendable: `FTS5ColumnDefinition` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension FTS5ColumnDefinition: Sendable { } + extension Column { /// The ``FTS5`` rank column. public static let rank = Column("rank") diff --git a/GRDB/FTS/FTS5Pattern.swift b/GRDB/FTS/FTS5Pattern.swift index c5e62a10eb..045a409a1b 100644 --- a/GRDB/FTS/FTS5Pattern.swift +++ b/GRDB/FTS/FTS5Pattern.swift @@ -16,7 +16,7 @@ /// - ``init(matchingAnyTokenIn:)`` /// - ``init(matchingPhrase:)`` /// - ``init(matchingPrefixPhrase:)`` -public struct FTS5Pattern { +public struct FTS5Pattern: Sendable { /// The raw pattern string. /// diff --git a/GRDB/FTS/FTS5Tokenizer.swift b/GRDB/FTS/FTS5Tokenizer.swift index 02db061538..8609a5ad72 100644 --- a/GRDB/FTS/FTS5Tokenizer.swift +++ b/GRDB/FTS/FTS5Tokenizer.swift @@ -16,7 +16,7 @@ public typealias FTS5TokenCallback = @convention(c) ( /// The reason why FTS5 is requesting tokenization. /// /// See the `FTS5_TOKENIZE_*` constants in . -public struct FTS5Tokenization: OptionSet { +public struct FTS5Tokenization: OptionSet, Sendable { public let rawValue: CInt public init(rawValue: CInt) { diff --git a/GRDB/FTS/FTS5TokenizerDescriptor.swift b/GRDB/FTS/FTS5TokenizerDescriptor.swift index b12a8acaaf..9750aa76fb 100644 --- a/GRDB/FTS/FTS5TokenizerDescriptor.swift +++ b/GRDB/FTS/FTS5TokenizerDescriptor.swift @@ -24,7 +24,7 @@ /// ### Instantiating Tokenizers /// /// - ``Database/makeTokenizer(_:)`` -public struct FTS5TokenizerDescriptor { +public struct FTS5TokenizerDescriptor: Sendable { /// The tokenizer components. /// /// For example: diff --git a/GRDB/FTS/FTS5WrapperTokenizer.swift b/GRDB/FTS/FTS5WrapperTokenizer.swift index 8b9de904aa..16ac1e1f96 100644 --- a/GRDB/FTS/FTS5WrapperTokenizer.swift +++ b/GRDB/FTS/FTS5WrapperTokenizer.swift @@ -4,7 +4,7 @@ import Foundation /// Flags that tell SQLite how to register a token. /// /// See the `FTS5_TOKEN_*` constants in . -public struct FTS5TokenFlags: OptionSet { +public struct FTS5TokenFlags: OptionSet, Sendable { public let rawValue: CInt public init(rawValue: CInt) { diff --git a/GRDB/JSON/JSONColumn.swift b/GRDB/JSON/JSONColumn.swift index 6ca345dd74..8f778d2b51 100644 --- a/GRDB/JSON/JSONColumn.swift +++ b/GRDB/JSON/JSONColumn.swift @@ -73,7 +73,7 @@ /// > .fetchAll(db) /// > } /// > ``` -public struct JSONColumn: ColumnExpression, SQLJSONExpressible { +public struct JSONColumn: ColumnExpression, SQLJSONExpressible, Sendable { public var name: String /// Creates a `JSONColumn` given its name. diff --git a/GRDB/Migration/DatabaseMigrator.swift b/GRDB/Migration/DatabaseMigrator.swift index 88b178c7ce..f4de96c69e 100644 --- a/GRDB/Migration/DatabaseMigrator.swift +++ b/GRDB/Migration/DatabaseMigrator.swift @@ -42,7 +42,7 @@ import Foundation /// - ``hasCompletedMigrations(_:)`` public struct DatabaseMigrator { /// Controls how a migration handle foreign keys constraints. - public enum ForeignKeyChecks { + public enum ForeignKeyChecks: Sendable { /// The migration runs with disabled foreign keys. /// /// Foreign keys are checked right before changes are committed on disk, diff --git a/GRDB/QueryInterface/ForeignKey.swift b/GRDB/QueryInterface/ForeignKey.swift index f89ee964ce..fbe27461d6 100644 --- a/GRDB/QueryInterface/ForeignKey.swift +++ b/GRDB/QueryInterface/ForeignKey.swift @@ -80,7 +80,7 @@ /// using: Book.translatorForeignKey) /// } /// ``` -public struct ForeignKey: Equatable { +public struct ForeignKey: Equatable, Sendable { var originColumns: [String] var destinationColumns: [String]? diff --git a/GRDB/QueryInterface/SQL/Column.swift b/GRDB/QueryInterface/SQL/Column.swift index a81c07d0a1..535166789e 100644 --- a/GRDB/QueryInterface/SQL/Column.swift +++ b/GRDB/QueryInterface/SQL/Column.swift @@ -92,7 +92,7 @@ extension ColumnExpression where Self == Column { /// /// - ``init(_:)-5grmu`` /// - ``init(_:)-7xc4z`` -public struct Column { +public struct Column: Sendable { /// The hidden rowID column. public static let rowID = Column("rowid") diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift index 3613bdb16c..c612827048 100644 --- a/GRDB/QueryInterface/SQL/SQLExpression.swift +++ b/GRDB/QueryInterface/SQL/SQLExpression.swift @@ -315,7 +315,7 @@ public struct SQLExpression { /// 1000.databaseValue] /// let request = Player.select(values.joined(operator: .add)) /// ``` - public struct AssociativeBinaryOperator: Hashable { + public struct AssociativeBinaryOperator: Hashable, Sendable { /// The SQL operator let sql: String diff --git a/GRDB/QueryInterface/SQL/SQLFunctions.swift b/GRDB/QueryInterface/SQL/SQLFunctions.swift index a6938dab5b..bb1253907b 100644 --- a/GRDB/QueryInterface/SQL/SQLFunctions.swift +++ b/GRDB/QueryInterface/SQL/SQLFunctions.swift @@ -408,7 +408,7 @@ extension SQLSpecificExpressible { /// A date modifier for SQLite date functions. /// /// Related SQLite documentation: -public enum SQLDateModifier: SQLSpecificExpressible { +public enum SQLDateModifier: SQLSpecificExpressible, Sendable { /// Adds the specified amount of seconds case second(Double) diff --git a/GRDB/QueryInterface/SQL/SQLSelection.swift b/GRDB/QueryInterface/SQL/SQLSelection.swift index 351b483775..c4ff88045b 100644 --- a/GRDB/QueryInterface/SQL/SQLSelection.swift +++ b/GRDB/QueryInterface/SQL/SQLSelection.swift @@ -324,7 +324,7 @@ extension SQLSelection: SQLSelectable { /// let players = try Player.select(AllColumns()).fetchAll(db) /// } /// ``` -public struct AllColumns { +public struct AllColumns: Sendable { /// The `*` selection. public init() { } } diff --git a/GRDB/QueryInterface/SQL/Table.swift b/GRDB/QueryInterface/SQL/Table.swift index 0bad29a8e4..c6de851c74 100644 --- a/GRDB/QueryInterface/SQL/Table.swift +++ b/GRDB/QueryInterface/SQL/Table.swift @@ -138,7 +138,7 @@ /// ### Database Observation Support /// /// - ``databaseRegion(_:)`` -public struct Table { +public struct Table: Sendable { /// The table name. public var tableName: String diff --git a/GRDB/QueryInterface/Schema/ColumnDefinition.swift b/GRDB/QueryInterface/Schema/ColumnDefinition.swift index 08b6119456..88cf0103a3 100644 --- a/GRDB/QueryInterface/Schema/ColumnDefinition.swift +++ b/GRDB/QueryInterface/Schema/ColumnDefinition.swift @@ -76,7 +76,7 @@ public final class ColumnDefinition { /// The kind of a generated column. /// /// Related SQLite documentation: - public enum GeneratedColumnQualification { + public enum GeneratedColumnQualification: Sendable { /// A `VIRTUAL` generated column. case virtual /// A `STORED` generated column. @@ -565,3 +565,8 @@ public final class ColumnDefinition { } } } + +// Explicit non-conformance to Sendable: `ColumnDefinition` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension ColumnDefinition: Sendable { } diff --git a/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift b/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift index eb62d7e888..10b95ebc3a 100644 --- a/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift +++ b/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift @@ -681,7 +681,7 @@ extension Database { } /// View creation options -public struct ViewOptions: OptionSet { +public struct ViewOptions: OptionSet, Sendable { public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } diff --git a/GRDB/QueryInterface/Schema/ForeignKeyDefinition.swift b/GRDB/QueryInterface/Schema/ForeignKeyDefinition.swift index 7da337ec6f..e7754a0527 100644 --- a/GRDB/QueryInterface/Schema/ForeignKeyDefinition.swift +++ b/GRDB/QueryInterface/Schema/ForeignKeyDefinition.swift @@ -104,3 +104,8 @@ public final class ForeignKeyDefinition { throw DatabaseError.noSuchTable(name) } } + +// Explicit non-conformance to Sendable: `ForeignKeyDefinition` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension ForeignKeyDefinition: Sendable { } diff --git a/GRDB/QueryInterface/Schema/IndexDefinition.swift b/GRDB/QueryInterface/Schema/IndexDefinition.swift index 616d8d8868..567c4beffd 100644 --- a/GRDB/QueryInterface/Schema/IndexDefinition.swift +++ b/GRDB/QueryInterface/Schema/IndexDefinition.swift @@ -7,7 +7,7 @@ struct IndexDefinition { } /// Index creation options -public struct IndexOptions: OptionSet { +public struct IndexOptions: OptionSet, Sendable { public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } diff --git a/GRDB/QueryInterface/Schema/TableAlteration.swift b/GRDB/QueryInterface/Schema/TableAlteration.swift index a22b41991c..c731e7ea54 100644 --- a/GRDB/QueryInterface/Schema/TableAlteration.swift +++ b/GRDB/QueryInterface/Schema/TableAlteration.swift @@ -161,3 +161,8 @@ public final class TableAlteration { alterations.append(.drop(name)) } } + +// Explicit non-conformance to Sendable: `TableAlteration` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension TableAlteration: Sendable { } diff --git a/GRDB/QueryInterface/Schema/TableDefinition.swift b/GRDB/QueryInterface/Schema/TableDefinition.swift index 40958d1c41..235c54bf88 100644 --- a/GRDB/QueryInterface/Schema/TableDefinition.swift +++ b/GRDB/QueryInterface/Schema/TableDefinition.swift @@ -1,5 +1,5 @@ /// Table creation options. -public struct TableOptions: OptionSet { +public struct TableOptions: OptionSet, Sendable { public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } @@ -753,3 +753,8 @@ public final class TableDefinition { literalConstraints.append(literal) } } + +// Explicit non-conformance to Sendable: `TableDefinition` is a mutable +// class and there is no known reason for making it thread-safe. +@available(*, unavailable) +extension TableDefinition: Sendable { } diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index e1d7db7156..563c0f877b 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -7,6 +7,7 @@ extension FetchableRecord where Self: Decodable { } } +// TODO GRDB7: make it a final class, and Sendable. /// An object that decodes fetchable records from database rows. /// /// The example below shows how to decode an instance of a simple `Player` diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index 9a5c5679b3..609279560f 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -854,6 +854,11 @@ public final class RecordCursor: DatabaseCursor { } } +// Explicit non-conformance to Sendable: database cursors must be used from +// a serialized database access dispatch queue. +@available(*, unavailable) +extension RecordCursor: Sendable { } + // MARK: - DatabaseDataDecodingStrategy /// `DatabaseDataDecodingStrategy` specifies how `FetchableRecord` types that diff --git a/GRDB/Record/MutablePersistableRecord.swift b/GRDB/Record/MutablePersistableRecord.swift index c9932e1c67..d26cb1cbcc 100644 --- a/GRDB/Record/MutablePersistableRecord.swift +++ b/GRDB/Record/MutablePersistableRecord.swift @@ -348,7 +348,7 @@ extension MutablePersistableRecord { /// See `MutablePersistableRecord.persistenceConflictPolicy`. /// /// See -public struct PersistenceConflictPolicy { +public struct PersistenceConflictPolicy: Sendable { /// The conflict resolution algorithm for insertions public let conflictResolutionForInsert: Database.ConflictResolution diff --git a/GRDB/Utils/Inflections.swift b/GRDB/Utils/Inflections.swift index 860b4d0ba8..9c845c2c6b 100644 --- a/GRDB/Utils/Inflections.swift +++ b/GRDB/Utils/Inflections.swift @@ -31,7 +31,7 @@ extension String { /// A type that controls GRDB string inflections. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) -public struct Inflections { +public struct Inflections: Sendable { private var pluralizeRules: [(NSRegularExpression, String)] = [] private var singularizeRules: [(NSRegularExpression, String)] = [] private var uncountablesRegularExpressions: [String: NSRegularExpression] = [:] diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index 123c44c28c..0a0e742ae3 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -1,7 +1,7 @@ import Foundation /// The extent of the shared subscription to a ``SharedValueObservation``. -public enum SharedValueObservationExtent { +public enum SharedValueObservationExtent: Sendable { /// The ``SharedValueObservation`` starts a single database observation, /// which stops when the `SharedValueObservation` is deallocated and all /// subscriptions terminated. diff --git a/GRDB/ValueObservation/ValueObservationScheduler.swift b/GRDB/ValueObservation/ValueObservationScheduler.swift index 6ca3447cf9..ad3b5f246f 100644 --- a/GRDB/ValueObservation/ValueObservationScheduler.swift +++ b/GRDB/ValueObservation/ValueObservationScheduler.swift @@ -76,7 +76,7 @@ extension ValueObservationScheduler where Self == AsyncValueObservationScheduler /// A scheduler that notifies all values on the main `DispatchQueue`. The /// first value is immediately notified when the `ValueObservation` /// is started. -public struct ImmediateValueObservationScheduler: ValueObservationScheduler { +public struct ImmediateValueObservationScheduler: ValueObservationScheduler, Sendable { public init() { } public func immediateInitialValue() -> Bool { From 77da5f4a30218b53a1e9ad4c15b5bd5fc2ca5122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 19:18:30 +0100 Subject: [PATCH 08/25] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c639fc89..113f8b633a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,6 +126,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: ## Next Release - **New**: [#1503](https://github.com/groue/GRDB.swift/pull/1503) by [@simba909](https://github.com/simba909): Conform Database.ColumnType to Sendable +- **New**: [#1510](https://github.com/groue/GRDB.swift/pull/1510) by [@groue](https://github.com/groue): Add Sendable conformances and unavailabilities - **Fixed**: [#1508](https://github.com/groue/GRDB.swift/pull/1508) by [@groue](https://github.com/groue): Fix ValueObservation mishandling of database schema modification ## 6.25.0 From c3e7736534937a4502bf734d203e5a811d3f2ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 19:35:03 +0100 Subject: [PATCH 09/25] Dump schema --- GRDB/Core/Database.swift | 1 + GRDB/Core/DatabaseReader.swift | 1 + GRDB/Dump/Database+Dump.swift | 76 +++++++++--- GRDB/Dump/DatabaseReader+dump.swift | 41 +++++- Tests/GRDBTests/DatabaseDumpTests.swift | 158 ++++++++++++++++++++++++ 5 files changed, 255 insertions(+), 22 deletions(-) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index ae705dbe1c..c460a2e6fe 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -70,6 +70,7 @@ let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_ /// /// - ``dumpContent(format:to:)`` /// - ``dumpRequest(_:format:to:)`` +/// - ``dumpSchema(to:)`` /// - ``dumpSQL(_:format:to:)`` /// - ``dumpTables(_:format:tableHeader:stableOrder:to:)`` /// - ``DumpFormat`` diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index a07abf9ec4..5fac0df44e 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -37,6 +37,7 @@ import Dispatch /// /// - ``dumpContent(format:to:)`` /// - ``dumpRequest(_:format:to:)`` +/// - ``dumpSchema(to:)`` /// - ``dumpSQL(_:format:to:)`` /// - ``dumpTables(_:format:tableHeader:stableOrder:to:)`` /// - ``DumpFormat`` diff --git a/GRDB/Dump/Database+Dump.swift b/GRDB/Dump/Database+Dump.swift index 33edfda640..d4abfb653a 100644 --- a/GRDB/Dump/Database+Dump.swift +++ b/GRDB/Dump/Database+Dump.swift @@ -12,7 +12,7 @@ extension Database { /// // Prints /// // 1|Arthur|500 /// // 2|Barbara|1000 - /// db.dumpSQL("SELECT * FROM player ORDER BY id") + /// try db.dumpSQL("SELECT * FROM player ORDER BY id") /// } /// ``` /// @@ -40,7 +40,7 @@ extension Database { /// // Prints /// // 1|Arthur|500 /// // 2|Barbara|1000 - /// db.dumpRequest(Player.orderByPrimaryKey()) + /// try db.dumpRequest(Player.orderByPrimaryKey()) /// } /// ``` /// @@ -72,7 +72,7 @@ extension Database { /// // team /// // 1|Red /// // 2|Blue - /// db.dumpTables(["player", "team"]) + /// try db.dumpTables(["player", "team"]) /// } /// ``` /// @@ -111,7 +111,7 @@ extension Database { /// /// ```swift /// try dbQueue.read { db in - /// db.dumpContent() + /// try db.dumpContent() /// } /// ``` /// @@ -145,6 +145,40 @@ extension Database { var dumpStream = DumpStream(stream) try _dumpContent(format: format, to: &dumpStream) } + + /// Prints the schema of the database. + /// + /// For example: + /// + /// ```swift + /// try dbQueue.read { db in + /// try db.dumpSchema() + /// } + /// ``` + /// + /// This prints the database schema. For example: + /// + /// ``` + /// sqlite_master + /// CREATE TABLE player (id INTEGER PRIMARY KEY, name TEXT, score INTEGER) + /// ``` + /// + /// > Note: Internal SQLite and GRDB schema objects are not recorded + /// > (those with a name that starts with "sqlite_" or "grdb_"). + /// > + /// > [Shadow tables](https://www.sqlite.org/vtab.html#xshadowname) are + /// > not recorded, starting SQLite 3.37+. + /// + /// - Parameters: + /// - stream: A stream for text output, which directs output to the + /// console by default. + public func dumpSchema( + to stream: (any TextOutputStream)? = nil) + throws + { + var dumpStream = DumpStream(stream) + try _dumpSchema(to: &dumpStream) + } } // MARK: - @@ -241,6 +275,26 @@ extension Database { format: some DumpFormat, to stream: inout DumpStream) throws + { + try _dumpSchema(to: &stream) + stream.margin() + + let tables = try String + .fetchAll(self, sql: """ + SELECT name + FROM sqlite_master + WHERE type = 'table' + ORDER BY name COLLATE NOCASE + """) + .filter { + try !ignoresObject(named: $0) + } + try _dumpTables(tables, format: format, tableHeader: .always, stableOrder: true, to: &stream) + } + + func _dumpSchema( + to stream: inout DumpStream) + throws { stream.writeln("sqlite_master") let sqlRows = try Row.fetchAll(self, sql: """ @@ -260,20 +314,6 @@ extension Database { } stream.writeln(row[0]) } - - let tables = try String - .fetchAll(self, sql: """ - SELECT name - FROM sqlite_master - WHERE type = 'table' - ORDER BY name COLLATE NOCASE - """) - .filter { - try !ignoresObject(named: $0) - } - if tables.isEmpty { return } - stream.write("\n") - try _dumpTables(tables, format: format, tableHeader: .always, stableOrder: true, to: &stream) } private func ignoresObject(named name: String) throws -> Bool { diff --git a/GRDB/Dump/DatabaseReader+dump.swift b/GRDB/Dump/DatabaseReader+dump.swift index cd3649caee..55e829c89d 100644 --- a/GRDB/Dump/DatabaseReader+dump.swift +++ b/GRDB/Dump/DatabaseReader+dump.swift @@ -7,7 +7,7 @@ extension DatabaseReader { /// // Prints /// // 1|Arthur|500 /// // 2|Barbara|1000 - /// dbQueue.dumpSQL("SELECT * FROM player ORDER BY id") + /// try dbQueue.dumpSQL("SELECT * FROM player ORDER BY id") /// ``` /// /// - Parameters: @@ -34,7 +34,7 @@ extension DatabaseReader { /// // Prints /// // 1|Arthur|500 /// // 2|Barbara|1000 - /// dbQueue.dumpRequest(Player.orderByPrimaryKey()) + /// try dbQueue.dumpRequest(Player.orderByPrimaryKey()) /// ``` /// /// - Parameters: @@ -65,7 +65,7 @@ extension DatabaseReader { /// // team /// // 1|Red /// // 2|Blue - /// dbQueue.dumpTables(["player", "team"]) + /// try dbQueue.dumpTables(["player", "team"]) /// ``` /// /// - Parameters: @@ -103,7 +103,7 @@ extension DatabaseReader { /// For example: /// /// ```swift - /// dbQueue.dumpContent() + /// try dbQueue.dumpContent() /// ``` /// /// This prints the database schema as well as the content of all @@ -137,4 +137,37 @@ extension DatabaseReader { try db.dumpContent(format: format, to: stream) } } + + /// Prints the schema of the database. + /// + /// For example: + /// + /// ```swift + /// try dbQueue.dumpSchema() + /// ``` + /// + /// This prints the database schema. For example: + /// + /// ``` + /// sqlite_master + /// CREATE TABLE player (id INTEGER PRIMARY KEY, name TEXT, score INTEGER) + /// ``` + /// + /// > Note: Internal SQLite and GRDB schema objects are not recorded + /// > (those with a name that starts with "sqlite_" or "grdb_"). + /// > + /// > [Shadow tables](https://www.sqlite.org/vtab.html#xshadowname) are + /// > not recorded, starting SQLite 3.37+. + /// + /// - Parameters: + /// - stream: A stream for text output, which directs output to the + /// console by default. + public func dumpSchema( + to stream: (any TextOutputStream)? = nil) + throws + { + try unsafeReentrantRead { db in + try db.dumpSchema(to: stream) + } + } } diff --git a/Tests/GRDBTests/DatabaseDumpTests.swift b/Tests/GRDBTests/DatabaseDumpTests.swift index 3dc49a9fc8..c0551a0e88 100644 --- a/Tests/GRDBTests/DatabaseDumpTests.swift +++ b/Tests/GRDBTests/DatabaseDumpTests.swift @@ -1196,6 +1196,164 @@ final class DatabaseDumpTests: GRDBTestCase { } } + // MARK: - Database schema dump + + func test_dumpSchema() throws { + try makeRugbyDatabase().read { db in + let stream = TestStream() + try db.dumpSchema(to: stream) + XCTAssertEqual(stream.output, """ + sqlite_master + CREATE TABLE "player" ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "teamId" TEXT REFERENCES "team"("id"), "name" TEXT NOT NULL); + CREATE INDEX "player_on_teamId" ON "player"("teamId"); + CREATE TABLE "team" ("id" TEXT PRIMARY KEY NOT NULL, "name" TEXT NOT NULL, "color" TEXT NOT NULL); + + """) + } + } + + func test_dumpSchema_empty_database() throws { + try makeDatabaseQueue().read { db in + let stream = TestStream() + try db.dumpSchema(to: stream) + XCTAssertEqual(stream.output, """ + sqlite_master + + """) + } + } + + func test_dumpSchema_empty_tables() throws { + try makeDatabaseQueue().write { db in + try db.execute(literal: """ + CREATE TABLE blue(name); + CREATE TABLE red(name); + CREATE TABLE yellow(name); + INSERT INTO red VALUES ('vermillon') + """) + let stream = TestStream() + try db.dumpSchema(to: stream) + XCTAssertEqual(stream.output, """ + sqlite_master + CREATE TABLE blue(name); + CREATE TABLE red(name); + CREATE TABLE yellow(name); + + """) + } + } + + func test_dumpSchema_sqlite_master_ordering() throws { + try makeDatabaseQueue().write { db in + try db.execute(literal: """ + CREATE TABLE blue(name); + CREATE TABLE RED(name); + CREATE TABLE yellow(name); + CREATE INDEX index_blue1 ON blue(name); + CREATE INDEX INDEX_blue2 ON blue(name); + CREATE INDEX indexRed1 ON RED(name); + CREATE INDEX INDEXRed2 ON RED(name); + CREATE VIEW colors1 AS SELECT name FROM blue; + CREATE VIEW COLORS2 AS SELECT name FROM blue UNION SELECT name FROM yellow; + CREATE TRIGGER update_blue UPDATE OF name ON blue + BEGIN + DELETE FROM RED; + END; + CREATE TRIGGER update_RED UPDATE OF name ON RED + BEGIN + DELETE FROM yellow; + END; + """) + let stream = TestStream() + try db.dumpSchema(to: stream) + XCTAssertEqual(stream.output, """ + sqlite_master + CREATE TABLE blue(name); + CREATE INDEX index_blue1 ON blue(name); + CREATE INDEX INDEX_blue2 ON blue(name); + CREATE TRIGGER update_blue UPDATE OF name ON blue + BEGIN + DELETE FROM RED; + END; + CREATE VIEW colors1 AS SELECT name FROM blue; + CREATE VIEW COLORS2 AS SELECT name FROM blue UNION SELECT name FROM yellow; + CREATE TABLE RED(name); + CREATE INDEX indexRed1 ON RED(name); + CREATE INDEX INDEXRed2 ON RED(name); + CREATE TRIGGER update_RED UPDATE OF name ON RED + BEGIN + DELETE FROM yellow; + END; + CREATE TABLE yellow(name); + + """) + } + } + + func test_dumpSchema_ignores_shadow_tables() throws { + guard sqlite3_libversion_number() >= 3037000 else { + throw XCTSkip("Can't detect shadow tables") + } + + try makeDatabaseQueue().write { db in + try db.create(table: "document") { t in + t.autoIncrementedPrimaryKey("id") + t.column("body") + } + + try db.execute(sql: "INSERT INTO document VALUES (1, 'Hello world!')") + + try db.create(virtualTable: "document_ft", using: FTS4()) { t in + t.synchronize(withTable: "document") + t.column("body") + } + + let stream = TestStream() + try db.dumpSchema(to: stream) + print(stream.output) + XCTAssertEqual(stream.output, """ + sqlite_master + CREATE TABLE "document" ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "body"); + CREATE TRIGGER "__document_ft_ai" AFTER INSERT ON "document" BEGIN + INSERT INTO "document_ft"("docid", "body") VALUES(new."id", new."body"); + END; + CREATE TRIGGER "__document_ft_au" AFTER UPDATE ON "document" BEGIN + INSERT INTO "document_ft"("docid", "body") VALUES(new."id", new."body"); + END; + CREATE TRIGGER "__document_ft_bd" BEFORE DELETE ON "document" BEGIN + DELETE FROM "document_ft" WHERE docid=old."id"; + END; + CREATE TRIGGER "__document_ft_bu" BEFORE UPDATE ON "document" BEGIN + DELETE FROM "document_ft" WHERE docid=old."id"; + END; + CREATE VIRTUAL TABLE "document_ft" USING fts4(body, content="document"); + + """) + } + } + + func test_dumpSchema_ignores_GRDB_internal_tables() throws { + let dbQueue = try makeDatabaseQueue() + var migrator = DatabaseMigrator() + migrator.registerMigration("v1") { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + } + } + try migrator.migrate(dbQueue) + + try dbQueue.read { db in + let stream = TestStream() + try db.dumpSchema(to: stream) + print(stream.output) + XCTAssertEqual(stream.output, """ + sqlite_master + CREATE TABLE "player" ("id" INTEGER PRIMARY KEY AUTOINCREMENT); + + """) + } + } + // MARK: - Database content dump func test_dumpContent() throws { From f274e464cdad234a98c0d94ac95c48237f7012c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 19:40:15 +0100 Subject: [PATCH 10/25] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 113f8b633a..0f5b0c22d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: - **New**: [#1503](https://github.com/groue/GRDB.swift/pull/1503) by [@simba909](https://github.com/simba909): Conform Database.ColumnType to Sendable - **New**: [#1510](https://github.com/groue/GRDB.swift/pull/1510) by [@groue](https://github.com/groue): Add Sendable conformances and unavailabilities +- **New**: [#1511](https://github.com/groue/GRDB.swift/pull/1511) by [@groue](https://github.com/groue): Database schema dump - **Fixed**: [#1508](https://github.com/groue/GRDB.swift/pull/1508) by [@groue](https://github.com/groue): Fix ValueObservation mishandling of database schema modification ## 6.25.0 From c52db6095a6a6b701d7527f9e939178f5431b472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 22:15:08 +0100 Subject: [PATCH 11/25] Stop testing test_CocoaPodsLint Because it does no longer work due to https://github.com/CocoaPods/CocoaPods/issues/11839 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4d5da809e7..819e8dcae7 100644 --- a/Makefile +++ b/Makefile @@ -76,7 +76,7 @@ test_framework_GRDB: test_framework_GRDBOSX test_framework_GRDBiOS test_framewor test_framework_GRDBCustom: test_framework_GRDBCustomSQLiteOSX test_framework_GRDBCustomSQLiteiOS test_framework_SQLCipher: test_framework_SQLCipher3 test_framework_SQLCipher3Encrypted test_framework_SQLCipher4 test_framework_SQLCipher4Encrypted test_archive: test_universal_xcframework -test_install: test_install_manual test_install_SPM test_install_customSQLite test_install_GRDB_CocoaPods test_CocoaPodsLint +test_install: test_install_manual test_install_SPM test_install_customSQLite test_install_GRDB_CocoaPods test_CocoaPodsLint: test_CocoaPodsLint_GRDB test_demo_apps: test_GRDBDemoiOS test_GRDBCombineDemo test_GRDBAsyncDemo From 4e0ab251064a12ef9f76f01de775b12eefc6d1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 17 Mar 2024 22:40:03 +0100 Subject: [PATCH 12/25] Fix flaky tests due to Recorder incorrect critical section --- Tests/CombineExpectations/Recorder.swift | 174 ++++++++++++----------- 1 file changed, 93 insertions(+), 81 deletions(-) diff --git a/Tests/CombineExpectations/Recorder.swift b/Tests/CombineExpectations/Recorder.swift index 48e981b274..0d7031aaf1 100644 --- a/Tests/CombineExpectations/Recorder.swift +++ b/Tests/CombineExpectations/Recorder.swift @@ -106,53 +106,59 @@ public class Recorder: Subscriber { /// the expectation. For example, the Prefix expectation uses true, but /// the NextOne expectation uses false. func fulfillOnInput(_ expectation: XCTestExpectation, includingConsumed: Bool) { - synchronized { - preconditionCanFulfillExpectation() + lock.lock() + + preconditionCanFulfillExpectation() + + let expectedFulfillmentCount = expectation.expectedFulfillmentCount + + switch state { + case .waitingForSubscription: + let exp = RecorderExpectation.onInput(expectation, remainingCount: expectedFulfillmentCount) + state = .waitingForSubscription(exp) + lock.unlock() - let expectedFulfillmentCount = expectation.expectedFulfillmentCount + case let .subscribed(subscription, _, elements): + let maxFulfillmentCount = includingConsumed + ? elements.count + : elements.count - consumedCount + let fulfillmentCount = min(expectedFulfillmentCount, maxFulfillmentCount) - switch state { - case .waitingForSubscription: - let exp = RecorderExpectation.onInput(expectation, remainingCount: expectedFulfillmentCount) - state = .waitingForSubscription(exp) - - case let .subscribed(subscription, _, elements): - let maxFulfillmentCount = includingConsumed - ? elements.count - : elements.count - consumedCount - let fulfillmentCount = min(expectedFulfillmentCount, maxFulfillmentCount) - expectation.fulfill(count: fulfillmentCount) - - let remainingCount = expectedFulfillmentCount - fulfillmentCount - if remainingCount > 0 { - let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount) - state = .subscribed(subscription, exp, elements) - } - - case .completed: - expectation.fulfill(count: expectedFulfillmentCount) + let remainingCount = expectedFulfillmentCount - fulfillmentCount + if remainingCount > 0 { + let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount) + state = .subscribed(subscription, exp, elements) } + lock.unlock() + expectation.fulfill(count: fulfillmentCount) + + case .completed: + lock.unlock() + expectation.fulfill(count: expectedFulfillmentCount) } } /// Registers the expectation so that it gets fulfilled when /// publisher completes. func fulfillOnCompletion(_ expectation: XCTestExpectation) { - synchronized { - preconditionCanFulfillExpectation() + lock.lock() + + preconditionCanFulfillExpectation() + + switch state { + case .waitingForSubscription: + let exp = RecorderExpectation.onCompletion(expectation) + state = .waitingForSubscription(exp) + lock.unlock() - switch state { - case .waitingForSubscription: - let exp = RecorderExpectation.onCompletion(expectation) - state = .waitingForSubscription(exp) - - case let .subscribed(subscription, _, elements): - let exp = RecorderExpectation.onCompletion(expectation) - state = .subscribed(subscription, exp, elements) - - case .completed: - expectation.fulfill() - } + case let .subscribed(subscription, _, elements): + let exp = RecorderExpectation.onCompletion(expectation) + state = .subscribed(subscription, exp, elements) + lock.unlock() + + case .completed: + lock.unlock() + expectation.fulfill() } } @@ -171,7 +177,7 @@ public class Recorder: Subscriber { _ completion: Subscribers.Completion?, _ remainingElements: ArraySlice, _ consume: (_ count: Int) -> ()) throws -> T) - rethrows -> T + rethrows -> T { try synchronized { let (elements, completion) = state.elementsAndCompletion @@ -217,58 +223,64 @@ public class Recorder: Subscriber { } public func receive(_ input: Input) -> Subscribers.Demand { - return synchronized { - switch state { - case let .subscribed(subscription, exp, elements): - var elements = elements - elements.append(input) - - if case let .onInput(expectation, remainingCount: remainingCount) = exp { - assert(remainingCount > 0) - expectation.fulfill() - if remainingCount > 1 { - let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount - 1) - state = .subscribed(subscription, exp, elements) - } else { - state = .subscribed(subscription, nil, elements) - } - } else { + lock.lock() + + switch state { + case let .subscribed(subscription, exp, elements): + var elements = elements + elements.append(input) + + if case let .onInput(expectation, remainingCount: remainingCount) = exp { + assert(remainingCount > 0) + expectation.fulfill() + if remainingCount > 1 { + let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount - 1) state = .subscribed(subscription, exp, elements) + } else { + state = .subscribed(subscription, nil, elements) } - - return .unlimited - - case .waitingForSubscription: - XCTFail("Publisher recorder got unexpected input before subscription: \(String(reflecting: input))") - return .none - - case .completed: - XCTFail("Publisher recorder got unexpected input after completion: \(String(reflecting: input))") - return .none + } else { + state = .subscribed(subscription, exp, elements) } + + lock.unlock() + return .unlimited + + case .waitingForSubscription: + lock.unlock() + XCTFail("Publisher recorder got unexpected input before subscription: \(String(reflecting: input))") + return .none + + case .completed: + lock.unlock() + XCTFail("Publisher recorder got unexpected input after completion: \(String(reflecting: input))") + return .none } } public func receive(completion: Subscribers.Completion) { - synchronized { - switch state { - case let .subscribed(_, exp, elements): - if let exp { - switch exp { - case let .onCompletion(expectation): - expectation.fulfill() - case let .onInput(expectation, remainingCount: remainingCount): - expectation.fulfill(count: remainingCount) - } + lock.lock() + + switch state { + case let .subscribed(_, exp, elements): + if let exp { + switch exp { + case let .onCompletion(expectation): + expectation.fulfill() + case let .onInput(expectation, remainingCount: remainingCount): + expectation.fulfill(count: remainingCount) } - state = .completed(elements, completion) - - case .waitingForSubscription: - XCTFail("Publisher recorder got unexpected completion before subscription: \(String(describing: completion))") - - case .completed: - XCTFail("Publisher recorder got unexpected completion after completion: \(String(describing: completion))") } + state = .completed(elements, completion) + lock.unlock() + + case .waitingForSubscription: + lock.unlock() + XCTFail("Publisher recorder got unexpected completion before subscription: \(String(describing: completion))") + + case .completed: + lock.unlock() + XCTFail("Publisher recorder got unexpected completion after completion: \(String(describing: completion))") } } } From 5413dcfeeb4680a2a1361ceefee9e4a405310428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Rou=C3=A9?= Date: Tue, 19 Mar 2024 12:15:47 +0100 Subject: [PATCH 13/25] Remove `jsonErrorPosition` unless GRDBCUSTOMSQLITE || GRDBCIPHER For some reason, if #available(iOS 9999, macOS 9999, tvOS 9999, watchOS 9999, *) has stopped being able to skip the test (WTF?) --- GRDB/Documentation.docc/JSON.md | 1 - GRDB/JSON/SQLJSONFunctions.swift | 15 --------------- Tests/GRDBTests/JSONExpressionsTests.swift | 8 +++----- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/GRDB/Documentation.docc/JSON.md b/GRDB/Documentation.docc/JSON.md index 581561b67d..34fdba1257 100644 --- a/GRDB/Documentation.docc/JSON.md +++ b/GRDB/Documentation.docc/JSON.md @@ -147,5 +147,4 @@ The `->` and `->>` SQL operators are available on the ``SQLJSONExpressible`` pro ### Validate JSON values at the SQL level -- ``Database/jsonErrorPosition(_:)`` - ``Database/jsonIsValid(_:)`` diff --git a/GRDB/JSON/SQLJSONFunctions.swift b/GRDB/JSON/SQLJSONFunctions.swift index 0972c2509c..85122f2895 100644 --- a/GRDB/JSON/SQLJSONFunctions.swift +++ b/GRDB/JSON/SQLJSONFunctions.swift @@ -521,21 +521,6 @@ extension Database { .function("JSON_ARRAY_LENGTH", [value.sqlExpression, path.sqlExpression]) } - /// The `JSON_ERROR_POSITION` SQL function. - /// - /// For example: - /// - /// ```swift - /// // JSON_ERROR_POSITION(info) - /// Database.jsonErrorPosition(Column("info")) - /// ``` - /// - /// Related SQLite documentation: - @available(iOS 9999, macOS 9999, tvOS 9999, watchOS 9999, *) - public static func jsonErrorPosition(_ value: some SQLExpressible) -> SQLExpression { - .function("JSON_ERROR_POSITION", [value.sqlExpression]) - } - /// The `JSON_EXTRACT` SQL function. /// /// For example: diff --git a/Tests/GRDBTests/JSONExpressionsTests.swift b/Tests/GRDBTests/JSONExpressionsTests.swift index 666cdde8b1..f0b4390927 100644 --- a/Tests/GRDBTests/JSONExpressionsTests.swift +++ b/Tests/GRDBTests/JSONExpressionsTests.swift @@ -358,11 +358,6 @@ final class JSONExpressionsTests: GRDBTestCase { guard sqlite3_libversion_number() >= 3042000 else { throw XCTSkip("JSON_ERROR_JSON is not available") } -#else - guard #available(iOS 9999, macOS 9999, tvOS 9999, watchOS 9999, *) else { - throw XCTSkip("JSON_ERROR_JSON is not available") - } -#endif try makeDatabaseQueue().inDatabase { db in try db.create(table: "player") { t in @@ -385,6 +380,9 @@ final class JSONExpressionsTests: GRDBTestCase { SELECT JSON_ERROR_POSITION("info") FROM "player" """) } +#else + throw XCTSkip("JSON_ERROR_JSON is not available") +#endif } func test_Database_jsonExtract_atPath() throws { From ac8c4aea63ae17f651555c3fc195d4911e1924d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 20 Mar 2024 19:24:09 +0100 Subject: [PATCH 14/25] Report correct error when decoding NULL into Date or Data Fixes #1512 --- GRDB/Record/FetchableRecord+Decodable.swift | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 563c0f877b..98fc2b9ce3 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -591,9 +591,19 @@ extension DatabaseDataDecodingStrategy { fileprivate func decode(fromRow row: Row, atUncheckedIndex index: Int) throws -> Data { if let sqliteStatement = row.sqliteStatement { + let statementIndex = CInt(index) + + if sqlite3_column_type(sqliteStatement, statementIndex) == SQLITE_NULL { + throw RowDecodingError.valueMismatch( + Data.self, + sqliteStatement: sqliteStatement, + index: statementIndex, + context: RowDecodingContext(row: row, key: .columnIndex(index))) + } + return try decode( fromStatement: sqliteStatement, - atUncheckedIndex: CInt(index), + atUncheckedIndex: statementIndex, context: RowDecodingContext(row: row, key: .columnIndex(index))) } else { return try decode( @@ -690,6 +700,16 @@ extension DatabaseDateDecodingStrategy { fileprivate func decode(fromRow row: Row, atUncheckedIndex index: Int) throws -> Date { if let sqliteStatement = row.sqliteStatement { + let statementIndex = CInt(index) + + if sqlite3_column_type(sqliteStatement, statementIndex) == SQLITE_NULL { + throw RowDecodingError.valueMismatch( + Date.self, + sqliteStatement: sqliteStatement, + index: statementIndex, + context: RowDecodingContext(row: row, key: .columnIndex(index))) + } + return try decode( fromStatement: sqliteStatement, atUncheckedIndex: CInt(index), From a49fd8562743ca78f10de126b54a951165762cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 21 Mar 2024 14:09:26 +0100 Subject: [PATCH 15/25] Regression tests for #1512 --- .../FetchableRecordDecodableTests.swift | 122 +++++++++++++++++- 1 file changed, 119 insertions(+), 3 deletions(-) diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index 2e2a3321d9..d0cf06b464 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -263,14 +263,130 @@ extension FetchableRecordDecodableTests { extension FetchableRecordDecodableTests { + func testStructWithData() throws { + struct StructWithData : FetchableRecord, Decodable { + let data: Data + } + + let dbQueue = try makeDatabaseQueue() + + do { + let data = "foo".data(using: .utf8) + + do { + let value = try StructWithData(row: ["data": data]) + XCTAssertEqual(value.data, data) + } + + do { + let value = try dbQueue.read { + try StructWithData.fetchOne($0, sql: "SELECT ? AS data", arguments: [data])! + } + XCTAssertEqual(value.data, data) + } + } + do { + do { + _ = try StructWithData(row: ["data": nil]) + XCTFail("Expected Error") + } catch let error as RowDecodingError { + switch error { + case .valueMismatch: + XCTAssertEqual(error.description, """ + could not decode Data from database value NULL - \ + column: "data", \ + column index: 0, \ + row: [data:NULL] + """) + default: + XCTFail("Unexpected Error") + } + } + + do { + try dbQueue.read { + _ = try StructWithData.fetchOne($0, sql: "SELECT NULL AS data") + } + XCTFail("Expected Error") + } catch let error as RowDecodingError { + switch error { + case .valueMismatch: + XCTAssertEqual(error.description, """ + could not decode Data from database value NULL - \ + column: "data", \ + column index: 0, \ + row: [data:NULL], \ + sql: `SELECT NULL AS data`, \ + arguments: [] + """) + default: + XCTFail("Unexpected Error") + } + } + } + } + func testStructWithDate() throws { struct StructWithDate : FetchableRecord, Decodable { let date: Date } - let date = Date() - let value = try StructWithDate(row: ["date": date]) - XCTAssert(abs(value.date.timeIntervalSince(date)) < 0.001) + let dbQueue = try makeDatabaseQueue() + + do { + let date = Date() + + do { + let value = try StructWithDate(row: ["date": date]) + XCTAssert(abs(value.date.timeIntervalSince(date)) < 0.001) + } + + do { + let value = try dbQueue.read { + try StructWithDate.fetchOne($0, sql: "SELECT ? AS date", arguments: [date])! + } + XCTAssert(abs(value.date.timeIntervalSince(date)) < 0.001) + } + } + do { + do { + _ = try StructWithDate(row: ["date": nil]) + XCTFail("Expected Error") + } catch let error as RowDecodingError { + switch error { + case .valueMismatch: + XCTAssertEqual(error.description, """ + could not decode Date from database value NULL - \ + column: "date", \ + column index: 0, \ + row: [date:NULL] + """) + default: + XCTFail("Unexpected Error") + } + } + + do { + try dbQueue.read { + _ = try StructWithDate.fetchOne($0, sql: "SELECT NULL AS date") + } + XCTFail("Expected Error") + } catch let error as RowDecodingError { + switch error { + case .valueMismatch: + XCTAssertEqual(error.description, """ + could not decode Date from database value NULL - \ + column: "date", \ + column index: 0, \ + row: [date:NULL], \ + sql: `SELECT NULL AS date`, \ + arguments: [] + """) + default: + XCTFail("Unexpected Error") + } + } + } } func testStructWithURL() throws { From fab4638fc9d491e6dab1ef25d39cbfaa6b6a19f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 23 Mar 2024 10:46:52 +0100 Subject: [PATCH 16/25] Database.StorageClass --- GRDB/Core/Database.swift | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index c460a2e6fe..68941ec2ae 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -115,6 +115,7 @@ let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_ /// - ``trace(options:_:)`` /// - ``CheckpointMode`` /// - ``DatabaseBackupProgress`` +/// - ``StorageClass`` /// - ``TraceEvent`` /// - ``TracingOptions`` public final class Database: CustomStringConvertible, CustomDebugStringConvertible { @@ -2005,6 +2006,32 @@ extension Database { /// An error log function that takes an error code and message. public typealias LogErrorFunction = (_ resultCode: ResultCode, _ message: String) -> Void + /// An SQLite storage class. + /// + /// For more information, see + /// [Datatypes In SQLite](https://www.sqlite.org/datatype3.html). + public struct StorageClass: RawRepresentable, Hashable, Sendable { + /// The SQL for the storage class (`"INTEGER"`, `"REAL"`, etc.) + public let rawValue: String + + /// Creates an SQL storage class. + public init(rawValue: String) { + self.rawValue = rawValue + } + + /// The `INTEGER` storage class. + public static let integer = StorageClass(rawValue: "INTEGER") + + /// The `REAL` storage class. + public static let real = StorageClass(rawValue: "REAL") + + /// The `TEXT` storage class. + public static let text = StorageClass(rawValue: "TEXT") + + /// The `BLOB` storage class. + public static let blob = StorageClass(rawValue: "BLOB") + } + /// An option for the SQLite tracing feature. /// /// You use `TracingOptions` with the `Database` From 79d82337b2f1b95b1c305f7639170298d2950f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 23 Mar 2024 10:47:13 +0100 Subject: [PATCH 17/25] CAST expression --- GRDB/QueryInterface/SQL/SQLExpression.swift | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift index c612827048..ad772c8dff 100644 --- a/GRDB/QueryInterface/SQL/SQLExpression.swift +++ b/GRDB/QueryInterface/SQL/SQLExpression.swift @@ -90,6 +90,11 @@ public struct SQLExpression { /// A literal SQL expression case literal(SQL) + /// The `CAST(expr AS storage-class)` expression. + /// + /// See . + indirect case cast(SQLExpression, Database.StorageClass) + /// The `BETWEEN` and `NOT BETWEEN` operators. /// /// BETWEEN AND @@ -224,6 +229,9 @@ public struct SQLExpression { case let .literal(sqlLiteral): return .literal(sqlLiteral.qualified(with: alias)) + case let .cast(expression, storageClass): + return .cast(expression.qualified(with: alias), storageClass) + case let .between( expression: expression, lowerBound: lowerBound, @@ -1092,6 +1100,13 @@ extension SQLExpression { self.init(impl: .isEmpty(expression, isNegated: isNegated)) } + /// The `CAST(expr AS storage-class)` expression. + /// + /// See . + static func cast(_ expression: SQLExpression, as storageClass: Database.StorageClass) -> Self { + self.init(impl: .cast(expression, storageClass)) + } + // MARK: Deferred // TODO: replace with something that can work for WITHOUT ROWID table with a multi-columns primary key. @@ -1269,6 +1284,9 @@ extension SQLExpression { } return resultSQL + case let .cast(expression, storageClass): + return try "CAST(\(expression.sql(context, wrappedInParenthesis: false)) AS \(storageClass.rawValue))" + case let .between(expression: expression, lowerBound: lowerBound, upperBound: upperBound, isNegated: isNegated): var resultSQL = try """ \(expression.sql(context, wrappedInParenthesis: true)) \ @@ -1822,6 +1840,9 @@ extension SQLExpression { let .associativeBinary(_, expressions): return expressions.allSatisfy(\.isConstantInRequest) + case let .cast(expression, _): + return expression.isConstantInRequest + case let .between(expression: expression, lowerBound: lowerBound, upperBound: upperBound, isNegated: _): return expression.isConstantInRequest && lowerBound.isConstantInRequest From 87fdd9a66fe015e432d5b1f6117735de159a7a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 23 Mar 2024 10:47:19 +0100 Subject: [PATCH 18/25] cast function --- .../Association/AssociationAggregate.swift | 17 +++++++++++++---- GRDB/QueryInterface/SQL/SQLFunctions.swift | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/GRDB/QueryInterface/Request/Association/AssociationAggregate.swift b/GRDB/QueryInterface/Request/Association/AssociationAggregate.swift index cb6ecbcc46..028cfcc42d 100644 --- a/GRDB/QueryInterface/Request/Association/AssociationAggregate.swift +++ b/GRDB/QueryInterface/Request/Association/AssociationAggregate.swift @@ -835,7 +835,7 @@ extension AssociationAggregate { } } -// MARK: - IFNULL(...) +// MARK: - Functions extension AssociationAggregate { /// The `IFNULL` SQL function. @@ -854,8 +854,6 @@ extension AssociationAggregate { } } -// MARK: - ABS(...) - /// The `ABS` SQL function. public func abs(_ aggregate: AssociationAggregate) -> AssociationAggregate @@ -863,7 +861,18 @@ public func abs(_ aggregate: AssociationAggregate) aggregate.map(abs) } -// MARK: - LENGTH(...) +/// The `CAST` SQL function. +/// +/// Related SQLite documentation: +public func cast( + _ aggregate: AssociationAggregate, + as storageClass: Database.StorageClass) +-> AssociationAggregate +{ + aggregate + .map { cast($0, as: storageClass) } + .with { $0.key = aggregate.key } // Preserve key +} /// The `LENGTH` SQL function. public func length(_ aggregate: AssociationAggregate) diff --git a/GRDB/QueryInterface/SQL/SQLFunctions.swift b/GRDB/QueryInterface/SQL/SQLFunctions.swift index bb1253907b..08a1a39ea8 100644 --- a/GRDB/QueryInterface/SQL/SQLFunctions.swift +++ b/GRDB/QueryInterface/SQL/SQLFunctions.swift @@ -57,6 +57,20 @@ public func average(_ value: some SQLSpecificExpressible) -> SQLExpression { } #endif +/// The `CAST` SQL function. +/// +/// For example: +/// +/// ```swift +/// // CAST(value AS REAL) +/// cast(Column("value"), as: .real) +/// ``` +/// +/// Related SQLite documentation: +public func cast(_ expression: some SQLSpecificExpressible, as storageClass: Database.StorageClass) -> SQLExpression { + .cast(expression.sqlExpression, as: storageClass) +} + /// The `COUNT` SQL function. /// /// For example: From 1f4d5ac61ac4c567a704ecfb03a6795b9683cd03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 23 Mar 2024 10:47:49 +0100 Subject: [PATCH 19/25] Fix test ambiguity --- Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift b/Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift index 0c54705fba..ac708cf355 100644 --- a/Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift +++ b/Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift @@ -1,7 +1,7 @@ import XCTest import GRDB -private func cast(_ value: T, as type: Database.ColumnType) -> SQLExpression { +private func myCast(_ value: T, as type: Database.ColumnType) -> SQLExpression { SQL("CAST(\(value) AS \(sql: type.rawValue))").sqlExpression } @@ -19,7 +19,7 @@ class QueryInterfaceExtensibilityTests: GRDBTestCase { try db.execute(sql: "INSERT INTO records (text) VALUES (?)", arguments: ["foo"]) do { - let request = Record.select(cast(Column("text"), as: .blob)) + let request = Record.select(myCast(Column("text"), as: .blob)) let dbValue = try DatabaseValue.fetchOne(db, request)! switch dbValue.storage { case .blob: @@ -30,7 +30,7 @@ class QueryInterfaceExtensibilityTests: GRDBTestCase { XCTAssertEqual(self.lastSQLQuery, "SELECT CAST(\"text\" AS BLOB) FROM \"records\" LIMIT 1") } do { - let request = Record.select(cast(Column("text"), as: .blob) && true) + let request = Record.select(myCast(Column("text"), as: .blob) && true) _ = try DatabaseValue.fetchOne(db, request)! XCTAssertEqual(self.lastSQLQuery, "SELECT (CAST(\"text\" AS BLOB)) AND 1 FROM \"records\" LIMIT 1") } From 32642d566b32152465a9039ef56fc091ab7ca641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 23 Mar 2024 10:47:57 +0100 Subject: [PATCH 20/25] Test cast function --- .../GRDBTests/AssociationAggregateTests.swift | 24 +++++++++++++++++++ .../QueryInterfaceExpressionsTests.swift | 10 +++++++- .../SQLExpressionIsConstantTests.swift | 4 ++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Tests/GRDBTests/AssociationAggregateTests.swift b/Tests/GRDBTests/AssociationAggregateTests.swift index a29ece144b..a8875291ae 100644 --- a/Tests/GRDBTests/AssociationAggregateTests.swift +++ b/Tests/GRDBTests/AssociationAggregateTests.swift @@ -1511,6 +1511,30 @@ class AssociationAggregateTests: GRDBTestCase { } } + func testCast() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + do { + let request = Team.annotated(with: cast(Team.players.count, as: .real)) + try assertEqualSQL(db, request, """ + SELECT "team".*, CAST(COUNT(DISTINCT "player"."id") AS REAL) AS "playerCount" \ + FROM "team" \ + LEFT JOIN "player" ON "player"."teamId" = "team"."id" \ + GROUP BY "team"."id" + """) + } + do { + let request = Team.annotated(with: cast(Team.players.count, as: .real).forKey("foo")) + try assertEqualSQL(db, request, """ + SELECT "team".*, CAST(COUNT(DISTINCT "player"."id") AS REAL) AS "foo" \ + FROM "team" \ + LEFT JOIN "player" ON "player"."teamId" = "team"."id" \ + GROUP BY "team"."id" + """) + } + } + } + func testLength() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in diff --git a/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift b/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift index 67c33c4d9b..e2e50cb240 100644 --- a/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift +++ b/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift @@ -1526,7 +1526,15 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { sql(dbQueue, tableRequest.select(average(Col.age / 2, filter: Col.age > 0))), "SELECT AVG(\"age\" / 2) FILTER (WHERE \"age\" > 0) FROM \"readers\"") } - + + func testCastExpression() throws { + let dbQueue = try makeDatabaseQueue() + + XCTAssertEqual( + sql(dbQueue, tableRequest.select(cast(Col.name, as: .blob))), + "SELECT CAST(\"name\" AS BLOB) FROM \"readers\"") + } + func testLengthExpression() throws { let dbQueue = try makeDatabaseQueue() diff --git a/Tests/GRDBTests/SQLExpressionIsConstantTests.swift b/Tests/GRDBTests/SQLExpressionIsConstantTests.swift index 9c2f87f955..c4ed35c0b4 100644 --- a/Tests/GRDBTests/SQLExpressionIsConstantTests.swift +++ b/Tests/GRDBTests/SQLExpressionIsConstantTests.swift @@ -274,6 +274,10 @@ class SQLExpressionIsConstantTests: GRDBTestCase { XCTAssertFalse((Column("a") - 2.databaseValue).isConstantInRequest) XCTAssertFalse((1.databaseValue - Column("a")).isConstantInRequest) + // CAST + XCTAssertTrue(cast(1.databaseValue, as: .real).isConstantInRequest) + XCTAssertFalse(cast(Column("a"), as: .real).isConstantInRequest) + // SQLExpressionCollate XCTAssertTrue("foo".databaseValue.collating(.binary).isConstantInRequest) XCTAssertFalse(Column("a").collating(.binary).isConstantInRequest) From 2ad1f37d2b7f24dd7d62a3ebc47a69ea6af489f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 23 Mar 2024 10:48:20 +0100 Subject: [PATCH 21/25] Documentation for cast function --- Documentation/AssociationsBasics.md | 2 +- README.md | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Documentation/AssociationsBasics.md b/Documentation/AssociationsBasics.md index b517dea444..50e241674f 100644 --- a/Documentation/AssociationsBasics.md +++ b/Documentation/AssociationsBasics.md @@ -2661,7 +2661,7 @@ Aggregates can be modified and combined with Swift operators: let request = Team.annotated(with: Team.players.min(Column("score")) ?? 0) ``` -- SQL functions `ABS` and `LENGTH` are available as the `abs` and `length` Swift functions: +- SQL functions `ABS`, `CAST`, and `LENGTH` are available as the `abs`, `cast`, and `length` Swift functions:
SQL diff --git a/README.md b/README.md index 6f28662b0a..2acc668b6d 100644 --- a/README.md +++ b/README.md @@ -4291,6 +4291,17 @@ GRDB comes with a Swift version of many SQLite [built-in functions](https://sqli For more information about the functions `dateTime` and `julianDay`, see [Date And Time Functions](https://www.sqlite.org/lang_datefunc.html). +- `CAST` + + Use the `cast` Swift function: + + ```swift + // SELECT (CAST(wins AS REAL) / games) AS successRate FROM player + Player.select((cast(winsColumn, as: .real) / gamesColumn).forKey("successRate")) + ``` + + See [CAST expressions](https://www.sqlite.org/lang_expr.html#castexpr) for more information about SQLite conversions. + - `IFNULL` Use the Swift `??` operator: From 155a16bef8a435722dfca17a0d9b7e855dc99690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 23 Mar 2024 10:52:28 +0100 Subject: [PATCH 22/25] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f5b0c22d3..c7fa623004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: - **New**: [#1503](https://github.com/groue/GRDB.swift/pull/1503) by [@simba909](https://github.com/simba909): Conform Database.ColumnType to Sendable - **New**: [#1510](https://github.com/groue/GRDB.swift/pull/1510) by [@groue](https://github.com/groue): Add Sendable conformances and unavailabilities - **New**: [#1511](https://github.com/groue/GRDB.swift/pull/1511) by [@groue](https://github.com/groue): Database schema dump +- **New**: [#1515](https://github.com/groue/GRDB.swift/pull/1515) by [@groue](https://github.com/groue): Support for the CAST SQLite function - **Fixed**: [#1508](https://github.com/groue/GRDB.swift/pull/1508) by [@groue](https://github.com/groue): Fix ValueObservation mishandling of database schema modification ## 6.25.0 From 2af60cb3243b0125b548e6210d11d29bdd09d49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 23 Mar 2024 11:46:44 +0100 Subject: [PATCH 23/25] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7fa623004..01472ae76f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,6 +130,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: - **New**: [#1511](https://github.com/groue/GRDB.swift/pull/1511) by [@groue](https://github.com/groue): Database schema dump - **New**: [#1515](https://github.com/groue/GRDB.swift/pull/1515) by [@groue](https://github.com/groue): Support for the CAST SQLite function - **Fixed**: [#1508](https://github.com/groue/GRDB.swift/pull/1508) by [@groue](https://github.com/groue): Fix ValueObservation mishandling of database schema modification +- **Fixed**: [#1512](https://github.com/groue/GRDB.swift/issues/1512): Decoding errors are now correctly reported when decoding NULL into a non-optional property of type `Data` or `Date`. ## 6.25.0 From d804b5ed9cd962f7db2bcc4f4d23e41d1273d3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 23 Mar 2024 13:39:04 +0100 Subject: [PATCH 24/25] Bump performance dependencies --- .../GRDBPerformance.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Tests/Performance/GRDBPerformance/GRDBPerformance.xcodeproj/project.pbxproj b/Tests/Performance/GRDBPerformance/GRDBPerformance.xcodeproj/project.pbxproj index d15100f227..18c7372e0d 100755 --- a/Tests/Performance/GRDBPerformance/GRDBPerformance.xcodeproj/project.pbxproj +++ b/Tests/Performance/GRDBPerformance/GRDBPerformance.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ diff --git a/Tests/Performance/GRDBPerformance/GRDBPerformance.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tests/Performance/GRDBPerformance/GRDBPerformance.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4bea37b569..65281ac4e5 100644 --- a/Tests/Performance/GRDBPerformance/GRDBPerformance.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Tests/Performance/GRDBPerformance/GRDBPerformance.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "8d182a38fc35b0d50c198e0cbd39dc7fa5922eae5336ff3b0c73c2d51bcba752", "pins" : [ { "identity" : "fmdb", "kind" : "remoteSourceControl", "location" : "https://github.com/ccgus/fmdb.git", "state" : { - "revision" : "61e51fde7f7aab6554f30ab061cc588b28a97d04", - "version" : "2.7.7" + "revision" : "47a2fa12a242b5a2fe13b916c22f2212e426055c", + "version" : "2.7.9" } }, { @@ -14,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-core.git", "state" : { - "revision" : "f0045b71f9d3e9a6d82c711828f14222811fa6e9", - "version" : "13.9.4" + "revision" : "374dd672af357732dccc135fecc905406fec3223", + "version" : "14.4.1" } }, { @@ -23,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-swift.git", "state" : { - "revision" : "00e098a61acdf5e8cee4e2a6292c9ec6762ba1b4", - "version" : "10.38.3" + "revision" : "e0c2fbb442979fbf1e4be80e01d142f310a9c762", + "version" : "10.49.1" } }, { @@ -32,10 +33,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stephencelis/SQLite.swift.git", "state" : { - "revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb", - "version" : "0.14.1" + "revision" : "e78ae0220e17525a15ac68c697a155eb7a672a8e", + "version" : "0.15.0" } } ], - "version" : 2 + "version" : 3 } From bb8323b91d8f754dc9fd00556d335e8d147d77e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 23 Mar 2024 13:36:04 +0100 Subject: [PATCH 25/25] v6.26.0 --- CHANGELOG.md | 5 ++++- GRDB.swift.podspec | 2 +- README.md | 2 +- Support/Info.plist | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01472ae76f..80961ff99f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: #### 6.x Releases +- `6.26.x` Releases - [6.26.0](#6260) - `6.25.x` Releases - [6.25.0](#6250) - `6.24.x` Releases - [6.24.0](#6240) - [6.24.1](#6241) - [6.24.2](#6242) - `6.23.x` Releases - [6.23.0](#6230) @@ -123,7 +124,9 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: --- -## Next Release +## 6.26.0 + +Released March 23, 2024 - **New**: [#1503](https://github.com/groue/GRDB.swift/pull/1503) by [@simba909](https://github.com/simba909): Conform Database.ColumnType to Sendable - **New**: [#1510](https://github.com/groue/GRDB.swift/pull/1510) by [@groue](https://github.com/groue): Add Sendable conformances and unavailabilities diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index 7f3e57fb2e..1ba9efb231 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GRDB.swift' - s.version = '6.25.0' + s.version = '6.26.0' s.license = { :type => 'MIT', :file => 'LICENSE' } s.summary = 'A toolkit for SQLite databases, with a focus on application development.' diff --git a/README.md b/README.md index 2acc668b6d..ef6253e692 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ CI Status

-**Latest release**: February 25, 2024 • [version 6.25.0](https://github.com/groue/GRDB.swift/tree/v6.25.0) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md) +**Latest release**: March 23, 2024 • [version 6.26.0](https://github.com/groue/GRDB.swift/tree/v6.26.0) • [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+ diff --git a/Support/Info.plist b/Support/Info.plist index 5848e181f9..0f840a6865 100644 --- a/Support/Info.plist +++ b/Support/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 6.25.0 + 6.26.0 CFBundleSignature ???? CFBundleVersion