Skip to content

Commit

Permalink
Add coalesce free function and Row method
Browse files Browse the repository at this point in the history
  • Loading branch information
Phil Mitchell committed Sep 30, 2024
1 parent dc03b8a commit d7754fa
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 2 deletions.
39 changes: 39 additions & 0 deletions GRDB/Core/Row.swift
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,45 @@ 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 myInt: Int? = row.coalesce(["int_A", "int_B"])
/// ```
///
/// Use of `coalesce` is essential, as nil-coalescing row values does not work:
///
/// ```swift
/// let myInt: Int? = row["int_A"] ?? row["int_B"] // Won't work
/// ```
public func coalesce<T: DatabaseValueConvertible>(_ columns: [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 myInt: Int? = row.coalesce([Column("int_A"), Column("int_B")])
/// ```
///
/// Use of `coalesce` is essential, as nil-coalescing row values does not work:
///
/// ```swift
/// let myInt: Int? = row[Column("int_A")] ?? row[Column("int_B")] // Won't work
/// ```
public func coalesce<T: DatabaseValueConvertible>(_ columns: [any ColumnExpression]) -> T? {
return coalesce(columns.lazy.map { $0.name })
}
}

extension Row {
Expand Down
12 changes: 12 additions & 0 deletions GRDB/QueryInterface/SQL/SQLFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ 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"), ...])
/// ```
public func coalesce(_ values: [some SQLSpecificExpressible]) -> SQLExpression {
.function("COALESCE", values.map { $0.sqlExpression })
}

/// The `COUNT` SQL function.
///
/// For example:
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,13 @@ row[...] as Int?
> if let int = row[...] as Int? { ... } // GOOD
> ```

> **Warning**: avoid nil-coalescing row values:
>
> ```swift
> let myInt: Int? = row["int_A"] ?? row["int_B"] // BAD - doesn't work
> let myInt: Int? = row.coalesce(["int_A", "int_B"]) // 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`, `COUNT`, `DATETIME`, `JULIANDAY`, `LENGTH`, `MAX`, `MIN`, `SUM`, `TOTAL`, `COALESCE`:

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`, `count`, `dateTime`, `julianDay`, `length`, `max`, `min`, `sum`, `total`, and `coalesce` Swift functions:

```swift
// SELECT MIN(score), MAX(score) FROM player
Expand Down
67 changes: 67 additions & 0 deletions Tests/GRDBTests/SimpleFunctionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import XCTest
import GRDB

private struct Player: Codable, FetchableRecord, PersistableRecord {
var id: Int64
var name: String?
var nickname: String?
var score: Int
}

class SimpleFunctionTests: GRDBTestCase {

override func setup(_ dbWriter: some DatabaseWriter) throws {
try dbWriter.write { db in
try db.create(table: "player") { t in
t.primaryKey("id", .integer)
t.column("name", .text)
t.column("nickname", .text)
t.column("score", .integer)
}

try Player(id: 1, name: "Arthur", nickname: "Artie", score: 100).insert(db)
try Player(id: 2, name: "Jacob", nickname: nil, score: 200).insert(db)
try Player(id: 3, name: nil, nickname: nil, score: 200).insert(db)
}
}

func testCoalesce() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.read { db in
do {
let request = Player.annotated(with: coalesce([Column("nickname"), Column("name")]))
try assertEqualSQL(db, request, """
SELECT *, COALESCE("nickname", "name") \
FROM "player"
""")
}
do {
let request = Player.annotated(with: coalesce([Column("nickname"), Column("name")]).forKey("foo"))
try assertEqualSQL(db, request, """
SELECT *, COALESCE("nickname", "name") AS "foo" \
FROM "player"
""")
}
}
}

func testRowCoalesce() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let request = Player.all()
let rows = try Row.fetchAll(db, request)
var row = rows[0]
XCTAssertEqual(row.coalesce(["nickname", "name"]), "Artie")
XCTAssertEqual(row.coalesce([Column("nickname"), Column("name")]), "Artie")
row = rows[1]
XCTAssertEqual(row.coalesce(["nickname", "name"]), "Jacob")
XCTAssertEqual(row.coalesce([Column("nickname"), Column("name")]), "Jacob")
row = rows[2]
var result: String? = row.coalesce(["nickname", "name"])
XCTAssertNil(result)
result = row.coalesce([Column("nickname"), Column("name")])
XCTAssertNil(result)
}
}

}

0 comments on commit d7754fa

Please sign in to comment.