diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6dd540058e..878a36eef4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
#### 7.x Releases
-- `7.0.0` Betas - [7.0.0-beta](#700-beta) - [7.0.0-beta.2](#700-beta2)
+- `7.0.0` Betas - [7.0.0-beta](#700-beta) - [7.0.0-beta.2](#700-beta2) - [7.0.0-beta.3](#700-beta3)
#### 6.x Releases
@@ -131,11 +131,19 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
---
+## 7.0.0-beta.3
+
+Released October 6, 2024
+
+- **Fix**: use #if directives to conditionally @preconcurrency import the Dispatch module to enable building the package on linux by [@tayloraswift](https://github.com/tayloraswift) in [#1644](https://github.com/groue/GRDB.swift/pull/1644)
+- **New**: Add coalesce free function and Row method by [@philmitchell](https://github.com/philmitchell) in [#1645](https://github.com/groue/GRDB.swift/pull/1645)
+- **Documentation Update**: Add `DatabaseValueConvertible` tip for JSON columns by [@bok-](https://github.com/bok-) in [#1649](https://github.com/groue/GRDB.swift/pull/1649)
+
## 7.0.0-beta.2
Released September 29, 2024
-- **Fix** Update .spi.yml by [@finestructure](https://github.com/finestructure) in [#1643](https://github.com/groue/GRDB.swift/pull/1643)
+- **Fix**: Update .spi.yml by [@finestructure](https://github.com/finestructure) in [#1643](https://github.com/groue/GRDB.swift/pull/1643)
## 7.0.0-beta
@@ -145,7 +153,7 @@ Released September 29, 2024
[Migrating From GRDB 6 to GRDB 7](Documentation/GRDB7MigrationGuide.md) describes in detail how to bump the GRDB version in your application.
-The new [Swift Concurrency and GRDB](https://swiftpackageindex.com/groue/grdb.swift/v7.0.0-beta.2/documentation/grdb/swiftconcurrency) guide explains how to best integrate GRDB and Swift Concurrency.
+The new [Swift Concurrency and GRDB](https://swiftpackageindex.com/groue/grdb.swift/v7.0.0-beta.3/documentation/grdb/swiftconcurrency) guide explains how to best integrate GRDB and Swift Concurrency.
The [demo app](Documentation/DemoApps/) was rewritten from scratch in a brand new Xcode 16 project.
diff --git a/Documentation/GRDB7MigrationGuide.md b/Documentation/GRDB7MigrationGuide.md
index 81eb576f70..a13d442f01 100644
--- a/Documentation/GRDB7MigrationGuide.md
+++ b/Documentation/GRDB7MigrationGuide.md
@@ -228,7 +228,7 @@ Do not miss [Swift Concurrency and GRDB], for more recommendations regarding non
- The async sequence returned by [`ValueObservation.values`](https://swiftpackageindex.com/groue/grdb.swiftdocumentation/grdb/valueobservation/values(in:scheduling:bufferingpolicy:)) now iterates on the cooperative thread pool by default. Use .mainActor as the scheduler if you need the previous behavior.
[Migrating to Swift 6]: https://www.swift.org/migration/documentation/migrationguide
-[Sharing a Database]: https://swiftpackageindex.com/groue/grdb.swift/v7.0.0-beta.2/documentation/grdb/databasesharing
-[Transaction Kinds]: https://swiftpackageindex.com/groue/grdb.swift/v7.0.0-beta.2/documentation/grdb/transactions#Transaction-Kinds
-[Swift Concurrency and GRDB]: https://swiftpackageindex.com/groue/grdb.swift/v7.0.0-beta.2/documentation/grdb/swiftconcurrency
-[Record]: https://swiftpackageindex.com/groue/grdb.swift/v7.0.0-beta.2/documentation/grdb/record
+[Sharing a Database]: https://swiftpackageindex.com/groue/grdb.swift/v7.0.0-beta.3/documentation/grdb/databasesharing
+[Transaction Kinds]: https://swiftpackageindex.com/groue/grdb.swift/v7.0.0-beta.3/documentation/grdb/transactions#Transaction-Kinds
+[Swift Concurrency and GRDB]: https://swiftpackageindex.com/groue/grdb.swift/v7.0.0-beta.3/documentation/grdb/swiftconcurrency
+[Record]: https://swiftpackageindex.com/groue/grdb.swift/v7.0.0-beta.3/documentation/grdb/record
diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec
index e5fb0391e6..a56a6a97a0 100644
--- a/GRDB.swift.podspec
+++ b/GRDB.swift.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'GRDB.swift'
- s.version = '7.0.0-beta.2'
+ s.version = '7.0.0-beta.3'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.summary = 'A toolkit for SQLite databases, with a focus on application development.'
diff --git a/GRDB/Core/Configuration.swift b/GRDB/Core/Configuration.swift
index 059bbb86f9..9e4e165010 100644
--- a/GRDB/Core/Configuration.swift
+++ b/GRDB/Core/Configuration.swift
@@ -7,6 +7,9 @@ import SQLCipher
import SQLite3
#endif
+#if !canImport(Darwin)
+@preconcurrency
+#endif
import Dispatch
import Foundation
diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift
index f1e49e5f67..8d39236bdc 100644
--- a/GRDB/Core/Row.swift
+++ b/GRDB/Core/Row.swift
@@ -60,6 +60,8 @@ import Foundation
/// - ``subscript(_:)-3tp8o``
/// - ``subscript(_:)-4k8od``
/// - ``subscript(_:)-9rbo7``
+/// - ``coalesce(_:)-359k7``
+/// - ``coalesce(_:)-6nbah``
/// - ``withUnsafeData(named:_:)``
/// - ``dataNoCopy(named:)``
///
@@ -671,6 +673,53 @@ extension Row {
public func dataNoCopy(_ column: some ColumnExpression) -> Data? {
dataNoCopy(named: column.name)
}
+
+ /// Returns the first non-null value, if any. Identical to SQL `COALESCE` function.
+ ///
+ /// For example:
+ ///
+ /// ```swift
+ /// let name: String? = row.coalesce(["nickname", "name"])
+ /// ```
+ ///
+ /// Prefer `coalesce` to nil-coalescing row values, which does not
+ /// return the expected value:
+ ///
+ /// ```swift
+ /// // INCORRECT
+ /// let name: String? = row["nickname"] ?? row["name"]
+ /// ```
+ public func coalesce(
+ _ columns: some Collection
+ ) -> T? {
+ for column in columns {
+ if let value = self[column] as T? {
+ return value
+ }
+ }
+ return nil
+ }
+
+ /// Returns the first non-null value, if any. Identical to SQL `COALESCE` function.
+ ///
+ /// For example:
+ ///
+ /// ```swift
+ /// let name: String? = row.coalesce([Column("nickname"), Column("name")])
+ /// ```
+ ///
+ /// Prefer `coalesce` to nil-coalescing row values, which does not
+ /// return the expected value:
+ ///
+ /// ```swift
+ /// // INCORRECT
+ /// let name: String? = row[Column("nickname")] ?? row[Column("name")]
+ /// ```
+ public func coalesce(
+ _ columns: some Collection
+ ) -> T? {
+ return coalesce(columns.lazy.map { $0.name })
+ }
}
extension Row {
diff --git a/GRDB/Core/SchedulingWatchdog.swift b/GRDB/Core/SchedulingWatchdog.swift
index 3c2ba702b0..dfbdfdace0 100644
--- a/GRDB/Core/SchedulingWatchdog.swift
+++ b/GRDB/Core/SchedulingWatchdog.swift
@@ -1,3 +1,6 @@
+#if !canImport(Darwin)
+@preconcurrency
+#endif
import Dispatch
/// SchedulingWatchdog makes sure that databases connections are used on correct
diff --git a/GRDB/Documentation.docc/JSON.md b/GRDB/Documentation.docc/JSON.md
index ad4acad302..5e3dcd6b89 100644
--- a/GRDB/Documentation.docc/JSON.md
+++ b/GRDB/Documentation.docc/JSON.md
@@ -98,6 +98,21 @@ extension Team: FetchableRecord, PersistableRecord {
}
```
+> Tip: Conform your `Codable` property to `DatabaseValueConvertible` if you want to be able to filter on specific values of it:
+>
+> ```swift
+> struct Address: Codable { ... }
+> extension Address: DatabaseValueConvertible {}
+>
+> // SELECT * FROM player
+> // WHERE address = '{"street": "...", "city": "...", "country": "..."}'
+> let players = try Player
+> .filter(JSONColumn("address") == Address(...))
+> .fetchAll(db)
+> ```
+>
+> Take care that SQLite will compare strings, not JSON objects: white-space and key ordering matter. For this comparison to succeed, make sure that the database contains values that are formatted exactly like a serialized `Address`.
+
## Manipulate JSON values at the database level
[SQLite JSON functions and operators](https://www.sqlite.org/json1.html) are available starting iOS 16+, macOS 10.15+, tvOS 17+, and watchOS 9+.
diff --git a/GRDB/Fixits.swift b/GRDB/Fixits.swift
index 1092b1164c..174a5ce7e2 100644
--- a/GRDB/Fixits.swift
+++ b/GRDB/Fixits.swift
@@ -1,4 +1,5 @@
// Fixits for changes introduced by GRDB 7.0.0
+// swiftlint:disable all
extension Configuration {
@available(*, unavailable, message: "The default transaction kind is now automatically managed.")
@@ -15,3 +16,5 @@ extension DatabasePool {
@available(*, unavailable, message: "concurrentRead has been removed. Use `asyncConcurrentRead` instead.")
public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { preconditionFailure() }
}
+
+// swiftlint:enable all
diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift
index 58acaf7c41..9f26b9983d 100644
--- a/GRDB/QueryInterface/SQL/SQLExpression.swift
+++ b/GRDB/QueryInterface/SQL/SQLExpression.swift
@@ -2168,6 +2168,7 @@ extension SQLExpressible where Self == Column {
/// - ``average(_:filter:)``
/// - ``capitalized``
/// - ``cast(_:as:)-1dmu3``
+/// - ``coalesce(_:)``
/// - ``count(_:)``
/// - ``count(distinct:)``
/// - ``dateTime(_:_:)``
diff --git a/GRDB/QueryInterface/SQL/SQLFunctions.swift b/GRDB/QueryInterface/SQL/SQLFunctions.swift
index fab42b8e42..8f3ccfea48 100644
--- a/GRDB/QueryInterface/SQL/SQLFunctions.swift
+++ b/GRDB/QueryInterface/SQL/SQLFunctions.swift
@@ -71,6 +71,32 @@ public func cast(_ expression: some SQLSpecificExpressible, as storageClass: Dat
.cast(expression.sqlExpression, as: storageClass)
}
+/// The `COALESCE` SQL function.
+///
+/// For example:
+///
+/// ```swift
+/// // COALESCE(value1, value2, ...)
+/// coalesce([Column("value1"), Column("value2"), ...])
+/// ```
+///
+/// Unlike the SQL function, `coalesce` accepts any number of arguments.
+/// When `values` is empty, the result is `NULL`. When `values` contains a
+/// single value, the result is this value. `COALESCE` is used from
+/// two values upwards.
+public func coalesce(_ values: some Collection) -> SQLExpression {
+ // SQLite COALESCE wants at least two arguments.
+ // There is no reason to apply the same limitation.
+ guard let value = values.first else {
+ return .null
+ }
+ if values.count > 1 {
+ return .function("COALESCE", values.map { $0.sqlExpression })
+ } else {
+ return value.sqlExpression
+ }
+}
+
/// The `COUNT` SQL function.
///
/// For example:
diff --git a/GRDB/Utils/Utils.swift b/GRDB/Utils/Utils.swift
index 561c3b91f1..d690b9eecb 100644
--- a/GRDB/Utils/Utils.swift
+++ b/GRDB/Utils/Utils.swift
@@ -1,3 +1,6 @@
+#if !canImport(Darwin)
+@preconcurrency import Dispatch
+#endif
import Foundation
// MARK: - Public
diff --git a/README.md b/README.md
index ff9bcc9654..b4cf208814 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
-**Latest release**: September 29, 2024 • [version 7.0.0-beta.2](https://github.com/groue/GRDB.swift/tree/v7.0.0-beta.2) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 6 to GRDB 7](Documentation/GRDB7MigrationGuide.md)
+**Latest release**: October 6, 2024 • [version 7.0.0-beta.3](https://github.com/groue/GRDB.swift/tree/v7.0.0-beta.3) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 6 to GRDB 7](Documentation/GRDB7MigrationGuide.md)
**Requirements**: iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 7.0+ • SQLite 3.20.0+ • Swift 6+ / Xcode 16+
@@ -841,6 +841,13 @@ row[...] as Int?
> if let int = row[...] as Int? { ... } // GOOD
> ```
+> **Warning**: avoid nil-coalescing row values, and prefer the `coalesce` method instead:
+>
+> ```swift
+> let name: String? = row["nickname"] ?? row["name"] // BAD - doesn't work
+> let name: String? = row.coalesce(["nickname", "name"]) // GOOD
+> ```
+
Generally speaking, you can extract the type you need, provided it can be converted from the underlying SQLite value:
- **Successful conversions include:**
@@ -3936,9 +3943,9 @@ GRDB comes with a Swift version of many SQLite [built-in operators](https://sqli
GRDB comes with a Swift version of many SQLite [built-in functions](https://sqlite.org/lang_corefunc.html), listed below. But not all: see [Embedding SQL in Query Interface Requests] for a way to add support for missing SQL functions.
-- `ABS`, `AVG`, `COUNT`, `DATETIME`, `JULIANDAY`, `LENGTH`, `MAX`, `MIN`, `SUM`, `TOTAL`:
+- `ABS`, `AVG`, `COALESCE`, `COUNT`, `DATETIME`, `JULIANDAY`, `LENGTH`, `MAX`, `MIN`, `SUM`, `TOTAL`:
- Those are based on the `abs`, `average`, `count`, `dateTime`, `julianDay`, `length`, `max`, `min`, `sum` and `total` Swift functions:
+ Those are based on the `abs`, `average`, `coalesce`, `count`, `dateTime`, `julianDay`, `length`, `max`, `min`, `sum`, and `total` Swift functions:
```swift
// SELECT MIN(score), MAX(score) FROM player
diff --git a/Support/Info.plist b/Support/Info.plist
index e1632ae7f2..0f9a7872c8 100644
--- a/Support/Info.plist
+++ b/Support/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
FMWK
CFBundleShortVersionString
- 7.0.0-beta.2
+ 7.0.0-beta.3
CFBundleSignature
????
CFBundleVersion
diff --git a/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift b/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift
index a4248160fc..f2a6d4b59f 100644
--- a/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift
+++ b/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift
@@ -1535,6 +1535,22 @@ class QueryInterfaceExpressionsTests: GRDBTestCase {
"SELECT CAST(\"name\" AS BLOB) FROM \"readers\"")
}
+ func testCoalesceExpression() throws {
+ let dbQueue = try makeDatabaseQueue()
+
+ XCTAssertEqual(
+ sql(dbQueue, tableRequest.select(coalesce([]))),
+ "SELECT NULL FROM \"readers\"")
+
+ XCTAssertEqual(
+ sql(dbQueue, tableRequest.select(coalesce([Col.name]))),
+ "SELECT \"name\" FROM \"readers\"")
+
+ XCTAssertEqual(
+ sql(dbQueue, tableRequest.select(coalesce([Col.name, Col.age]))),
+ "SELECT COALESCE(\"name\", \"age\") FROM \"readers\"")
+ }
+
func testLengthExpression() throws {
let dbQueue = try makeDatabaseQueue()
diff --git a/Tests/GRDBTests/RowCopiedFromStatementTests.swift b/Tests/GRDBTests/RowCopiedFromStatementTests.swift
index 8bc077c5b3..e672d67d58 100644
--- a/Tests/GRDBTests/RowCopiedFromStatementTests.swift
+++ b/Tests/GRDBTests/RowCopiedFromStatementTests.swift
@@ -287,4 +287,29 @@ class RowCopiedFromStatementTests: RowTestCase {
XCTAssertEqual(row.debugDescription, "[null:NULL int:1 double:1.1 string:\"foo\" data:Data(6 bytes)]")
}
}
+
+ func testCoalesce() throws {
+ let dbQueue = try makeDatabaseQueue()
+ try dbQueue.inDatabase { db in
+ let values = try Row
+ .fetchAll(db, sql: """
+ SELECT 'Artie' AS nickname, 'Arthur' AS name
+ UNION ALL SELECT NULL, 'Jacob'
+ UNION ALL SELECT NULL, NULL
+ """)
+ .map { row in
+ [
+ row.coalesce(Array()) as String?,
+ row.coalesce(["nickname"]) as String?,
+ row.coalesce(["nickname", "name"]) as String?,
+ row.coalesce([Column("nickname"), Column("name")]) as String?,
+ ]
+ }
+ XCTAssertEqual(values, [
+ [nil, "Artie", "Artie", "Artie"],
+ [nil, nil, "Jacob", "Jacob"],
+ [nil, nil, nil, nil],
+ ])
+ }
+ }
}
diff --git a/Tests/GRDBTests/RowFromDictionaryLiteralTests.swift b/Tests/GRDBTests/RowFromDictionaryLiteralTests.swift
index f840c437a0..2554e6e204 100644
--- a/Tests/GRDBTests/RowFromDictionaryLiteralTests.swift
+++ b/Tests/GRDBTests/RowFromDictionaryLiteralTests.swift
@@ -232,4 +232,25 @@ class RowFromDictionaryLiteralTests : RowTestCase {
XCTAssertEqual(row.description, "[a:0 b:1 c:2]")
XCTAssertEqual(row.debugDescription, "[a:0 b:1 c:2]")
}
+
+ func testCoalesce() throws {
+ let rows: [Row] = [
+ ["nickname": "Artie", "name": "Arthur"],
+ ["nickname": nil, "name": "Jacob"],
+ ["nickname": nil, "name": nil],
+ ]
+ let values = rows.map { row in
+ [
+ row.coalesce(Array()) as String?,
+ row.coalesce(["nickname"]) as String?,
+ row.coalesce(["nickname", "name"]) as String?,
+ row.coalesce([Column("nickname"), Column("name")]) as String?,
+ ]
+ }
+ XCTAssertEqual(values, [
+ [nil, "Artie", "Artie", "Artie"],
+ [nil, nil, "Jacob", "Jacob"],
+ [nil, nil, nil, nil],
+ ])
+ }
}
diff --git a/Tests/GRDBTests/RowFromDictionaryTests.swift b/Tests/GRDBTests/RowFromDictionaryTests.swift
index 08ddca00fd..5625295e4e 100644
--- a/Tests/GRDBTests/RowFromDictionaryTests.swift
+++ b/Tests/GRDBTests/RowFromDictionaryTests.swift
@@ -224,4 +224,25 @@ class RowFromDictionaryTests : RowTestCase {
let debugVariants: Set = ["[a:0 b:\"foo\"]", "[b:\"foo\" a:0]"]
XCTAssert(debugVariants.contains(row.debugDescription))
}
+
+ func testCoalesce() throws {
+ let rows = [
+ Row(["nickname": "Artie", "name": "Arthur"]),
+ Row(["nickname": nil, "name": "Jacob"]),
+ Row(["nickname": nil, "name": nil]),
+ ]
+ let values = rows.map { row in
+ [
+ row.coalesce(Array()) as String?,
+ row.coalesce(["nickname"]) as String?,
+ row.coalesce(["nickname", "name"]) as String?,
+ row.coalesce([Column("nickname"), Column("name")]) as String?,
+ ]
+ }
+ XCTAssertEqual(values, [
+ [nil, "Artie", "Artie", "Artie"],
+ [nil, nil, "Jacob", "Jacob"],
+ [nil, nil, nil, nil],
+ ])
+ }
}
diff --git a/Tests/GRDBTests/RowFromStatementTests.swift b/Tests/GRDBTests/RowFromStatementTests.swift
index c42624a2b9..85f17ff24b 100644
--- a/Tests/GRDBTests/RowFromStatementTests.swift
+++ b/Tests/GRDBTests/RowFromStatementTests.swift
@@ -380,4 +380,29 @@ class RowFromStatementTests : RowTestCase {
XCTAssertTrue(rowFetched)
}
}
+
+ func testCoalesce() throws {
+ let dbQueue = try makeDatabaseQueue()
+ try dbQueue.inDatabase { db in
+ let values = try Array(Row
+ .fetchCursor(db, sql: """
+ SELECT 'Artie' AS nickname, 'Arthur' AS name
+ UNION ALL SELECT NULL, 'Jacob'
+ UNION ALL SELECT NULL, NULL
+ """)
+ .map { row in
+ [
+ row.coalesce(Array()) as String?,
+ row.coalesce(["nickname"]) as String?,
+ row.coalesce(["nickname", "name"]) as String?,
+ row.coalesce([Column("nickname"), Column("name")]) as String?,
+ ]
+ })
+ XCTAssertEqual(values, [
+ [nil, "Artie", "Artie", "Artie"],
+ [nil, nil, "Jacob", "Jacob"],
+ [nil, nil, nil, nil],
+ ])
+ }
+ }
}