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 @@
-**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