Skip to content

Commit

Permalink
Merge branch 'development'
Browse files Browse the repository at this point in the history
  • Loading branch information
groue committed Oct 6, 2024
2 parents dc03b8a + 5b4a86b commit bccf312
Show file tree
Hide file tree
Showing 18 changed files with 238 additions and 12 deletions.
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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.

Expand Down
8 changes: 4 additions & 4 deletions Documentation/GRDB7MigrationGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion GRDB.swift.podspec
Original file line number Diff line number Diff line change
@@ -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.'
Expand Down
3 changes: 3 additions & 0 deletions GRDB/Core/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import SQLCipher
import SQLite3
#endif

#if !canImport(Darwin)
@preconcurrency
#endif
import Dispatch
import Foundation

Expand Down
49 changes: 49 additions & 0 deletions GRDB/Core/Row.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ import Foundation
/// - ``subscript(_:)-3tp8o``
/// - ``subscript(_:)-4k8od``
/// - ``subscript(_:)-9rbo7``
/// - ``coalesce(_:)-359k7``
/// - ``coalesce(_:)-6nbah``
/// - ``withUnsafeData(named:_:)``
/// - ``dataNoCopy(named:)``
///
Expand Down Expand Up @@ -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<T: DatabaseValueConvertible>(
_ columns: some Collection<String>
) -> 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<T: DatabaseValueConvertible>(
_ columns: some Collection<any ColumnExpression>
) -> T? {
return coalesce(columns.lazy.map { $0.name })
}
}

extension Row {
Expand Down
3 changes: 3 additions & 0 deletions GRDB/Core/SchedulingWatchdog.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#if !canImport(Darwin)
@preconcurrency
#endif
import Dispatch

/// SchedulingWatchdog makes sure that databases connections are used on correct
Expand Down
15 changes: 15 additions & 0 deletions GRDB/Documentation.docc/JSON.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
Expand Down
3 changes: 3 additions & 0 deletions GRDB/Fixits.swift
Original file line number Diff line number Diff line change
@@ -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.")
Expand All @@ -15,3 +16,5 @@ extension DatabasePool {
@available(*, unavailable, message: "concurrentRead has been removed. Use `asyncConcurrentRead` instead.")
public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> { preconditionFailure() }
}

// swiftlint:enable all
1 change: 1 addition & 0 deletions GRDB/QueryInterface/SQL/SQLExpression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2168,6 +2168,7 @@ extension SQLExpressible where Self == Column {
/// - ``average(_:filter:)``
/// - ``capitalized``
/// - ``cast(_:as:)-1dmu3``
/// - ``coalesce(_:)``
/// - ``count(_:)``
/// - ``count(distinct:)``
/// - ``dateTime(_:_:)``
Expand Down
26 changes: 26 additions & 0 deletions GRDB/QueryInterface/SQL/SQLFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<any SQLSpecificExpressible>) -> 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:
Expand Down
3 changes: 3 additions & 0 deletions GRDB/Utils/Utils.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#if !canImport(Darwin)
@preconcurrency import Dispatch
#endif
import Foundation

// MARK: - Public
Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<a href="https://github.com/groue/GRDB.swift/actions/workflows/CI.yml"><img alt="CI Status" src="https://github.com/groue/GRDB.swift/actions/workflows/CI.yml/badge.svg?branch=master"></a>
</p>

**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+ &bull; SQLite 3.20.0+ &bull; Swift 6+ / Xcode 16+

Expand Down Expand Up @@ -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:**
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Support/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>7.0.0-beta.2</string>
<string>7.0.0-beta.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
16 changes: 16 additions & 0 deletions Tests/GRDBTests/QueryInterfaceExpressionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
25 changes: 25 additions & 0 deletions Tests/GRDBTests/RowCopiedFromStatementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>()) 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],
])
}
}
}
21 changes: 21 additions & 0 deletions Tests/GRDBTests/RowFromDictionaryLiteralTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>()) 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],
])
}
}
21 changes: 21 additions & 0 deletions Tests/GRDBTests/RowFromDictionaryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,25 @@ class RowFromDictionaryTests : RowTestCase {
let debugVariants: Set<String> = ["[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<String>()) 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],
])
}
}
25 changes: 25 additions & 0 deletions Tests/GRDBTests/RowFromStatementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>()) 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],
])
}
}
}

0 comments on commit bccf312

Please sign in to comment.