Skip to content

Commit

Permalink
Merge pull request #1515 from groue/dev/cast
Browse files Browse the repository at this point in the history
Support for the CAST SQLite function
  • Loading branch information
groue committed Mar 23, 2024
2 parents a49fd85 + 155a16b commit 0434567
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Documentation/AssociationsBasics.md
Expand Up @@ -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:

<details>
<summary>SQL</summary>
Expand Down
27 changes: 27 additions & 0 deletions GRDB/Core/Database.swift
Expand Up @@ -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 {
Expand Down Expand Up @@ -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`
Expand Down
17 changes: 13 additions & 4 deletions GRDB/QueryInterface/Request/Association/AssociationAggregate.swift
Expand Up @@ -835,7 +835,7 @@ extension AssociationAggregate {
}
}

// MARK: - IFNULL(...)
// MARK: - Functions

extension AssociationAggregate {
/// The `IFNULL` SQL function.
Expand All @@ -854,16 +854,25 @@ extension AssociationAggregate {
}
}

// MARK: - ABS(...)

/// The `ABS` SQL function.
public func abs<RowDecoder>(_ aggregate: AssociationAggregate<RowDecoder>)
-> AssociationAggregate<RowDecoder>
{
aggregate.map(abs)
}

// MARK: - LENGTH(...)
/// The `CAST` SQL function.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_expr.html#castexpr>
public func cast<RowDecoder>(
_ aggregate: AssociationAggregate<RowDecoder>,
as storageClass: Database.StorageClass)
-> AssociationAggregate<RowDecoder>
{
aggregate
.map { cast($0, as: storageClass) }
.with { $0.key = aggregate.key } // Preserve key
}

/// The `LENGTH` SQL function.
public func length<RowDecoder>(_ aggregate: AssociationAggregate<RowDecoder>)
Expand Down
21 changes: 21 additions & 0 deletions GRDB/QueryInterface/SQL/SQLExpression.swift
Expand Up @@ -90,6 +90,11 @@ public struct SQLExpression {
/// A literal SQL expression
case literal(SQL)

/// The `CAST(expr AS storage-class)` expression.
///
/// See <https://www.sqlite.org/lang_expr.html#castexpr>.
indirect case cast(SQLExpression, Database.StorageClass)

/// The `BETWEEN` and `NOT BETWEEN` operators.
///
/// <expression> BETWEEN <lowerBound> AND <upperBound>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1092,6 +1100,13 @@ extension SQLExpression {
self.init(impl: .isEmpty(expression, isNegated: isNegated))
}

/// The `CAST(expr AS storage-class)` expression.
///
/// See <https://www.sqlite.org/lang_expr.html#castexpr>.
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.
Expand Down Expand Up @@ -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)) \
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions GRDB/QueryInterface/SQL/SQLFunctions.swift
Expand Up @@ -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: <https://www.sqlite.org/lang_expr.html#castexpr>
public func cast(_ expression: some SQLSpecificExpressible, as storageClass: Database.StorageClass) -> SQLExpression {
.cast(expression.sqlExpression, as: storageClass)
}

/// The `COUNT` SQL function.
///
/// For example:
Expand Down
11 changes: 11 additions & 0 deletions README.md
Expand Up @@ -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:
Expand Down
24 changes: 24 additions & 0 deletions Tests/GRDBTests/AssociationAggregateTests.swift
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion Tests/GRDBTests/QueryInterfaceExpressionsTests.swift
Expand Up @@ -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()

Expand Down
6 changes: 3 additions & 3 deletions Tests/GRDBTests/QueryInterfaceExtensibilityTests.swift
@@ -1,7 +1,7 @@
import XCTest
import GRDB

private func cast<T: SQLExpressible>(_ value: T, as type: Database.ColumnType) -> SQLExpression {
private func myCast<T: SQLExpressible>(_ value: T, as type: Database.ColumnType) -> SQLExpression {
SQL("CAST(\(value) AS \(sql: type.rawValue))").sqlExpression
}

Expand All @@ -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:
Expand All @@ -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")
}
Expand Down
4 changes: 4 additions & 0 deletions Tests/GRDBTests/SQLExpressionIsConstantTests.swift
Expand Up @@ -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)
Expand Down

0 comments on commit 0434567

Please sign in to comment.