384 lines
14 KiB
Markdown
384 lines
14 KiB
Markdown
Migrating From GRDB 5 to GRDB 6
|
|
===============================
|
|
|
|
**This guide aims at helping you upgrading your applications from GRDB 5 to GRDB 6.**
|
|
|
|
- [Preparing the Migration to GRDB 6](#preparing-the-migration-to-grdb-6)
|
|
- [New requirements](#new-requirements)
|
|
- [Primary Associated Types](#primary-associated-types)
|
|
- [Record Changes](#record-changes)
|
|
- [Other Changes](#other-changes)
|
|
|
|
|
|
## Preparing the Migration to GRDB 6
|
|
|
|
If you haven't made it yet, upgrade to the [latest GRDB 5 release](https://github.com/groue/GRDB.swift/tags) first, and fix any deprecation warning prior to the GRDB 6 upgrade.
|
|
|
|
You can then upgrade to GRDB 6. Due to the breaking changes, it is possible that your application code no longer compiles. Follow the fix-its that suggest simple syntactic changes. Other modifications that you need to apply are described below.
|
|
|
|
## New requirements
|
|
|
|
GRDB requirements have been bumped:
|
|
|
|
- **Swift 5.7+** (was Swift 5.3+)
|
|
- **Xcode 14.0+** (was Xcode 12.0+)
|
|
- iOS 11.0+ (unchanged)
|
|
- **macOS 10.13+** (was macOS 10.10+)
|
|
- **tvOS 11.0+** (was tvOS 11.0+)
|
|
- **watchOS 4.0+** (was watchOS 2.0+)
|
|
- **SQLite 3.19.3+** (was SQLite 3.8.5+)
|
|
|
|
## Primary Associated Types
|
|
|
|
Request protocols now come with a primary associated type, enabled by [SE-0346](https://github.com/apple/swift-evolution/blob/main/proposals/0346-light-weight-same-type-syntax.md). This is a great opportunity to simplify your extensions:
|
|
|
|
```diff
|
|
-extension DerivableRequest where RowDecoder == Book {
|
|
+extension DerivableRequest<Book> {
|
|
/// Order books by title, in a localized case-insensitive fashion
|
|
func orderByTitle() -> Self {
|
|
order(Column("title").collating(.localizedCaseInsensitiveCompare))
|
|
}
|
|
}
|
|
```
|
|
|
|
Your extensions to `QueryInterfaceRequest` can be streamlined as well, thanks to [SE-0361](https://github.com/apple/swift-evolution/blob/main/proposals/0361-bound-generic-extensions.md):
|
|
|
|
```diff
|
|
-extension QueryInterfaceRequest where RowDecoder == Player {
|
|
+extension QueryInterfaceRequest<Player> {
|
|
func selectID() -> QueryInterfaceRequest<Int64> {
|
|
selectPrimaryKey()
|
|
}
|
|
}
|
|
```
|
|
|
|
The `Cursor` protocol has also gained a primary associated type (the type of its elements).
|
|
|
|
## Record Changes
|
|
|
|
The record protocols have been refactored. We tried to keep the amount of modifications to your existing code as small as possible, but some changes could not be avoided.
|
|
|
|
- **The `FetchableRecord.init(row:)` initializer can now throw errors.**
|
|
|
|
```diff
|
|
-let player = Player(row: row)
|
|
+let player = try Player(row: row)
|
|
```
|
|
|
|
Decodable records that derive their `FetchableRecord` implementation from the standard `Decodable` protocol now throw errors when they find unexpected database values (they used to crash in GRDB 5).
|
|
|
|
If you subclass the `Record` type, you have to update your override of `init(row:)`:
|
|
|
|
```diff
|
|
class Player: Record {
|
|
- required init(row: Row) {
|
|
+ required init(row: Row) throws {
|
|
self.id = row["id"]
|
|
self.name = row["name"]
|
|
- super.init(row: row)
|
|
+ try super.init(row: row)
|
|
}
|
|
}
|
|
```
|
|
|
|
In record types that do not derive their `FetchableRecord.init(row:)` implementation from the standard `Decodable` protocol, you are responsible for throwing decoding errors, as in the sample code below:
|
|
|
|
<details>
|
|
<summary>Handling untrusted input</summary>
|
|
|
|
For example:
|
|
|
|
```swift
|
|
struct LogEntry: FetchableRecord {
|
|
var date: Date
|
|
|
|
init(row: Row) throws {
|
|
let dbValue: DatabaseValue = row["date"]
|
|
if dbValue.isNull {
|
|
// Handle NULL
|
|
throw ...
|
|
} else if let date = Date.fromDatabaseValue(dbValue) {
|
|
self.date = date
|
|
} else {
|
|
// Handle invalid date
|
|
throw ...
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
- **The `EncodableRecord.encode(to:)` method can now throw errors.**
|
|
|
|
Encodable records that derive their `EncodableRecord` implementation from the standard `Encodable` protocol now throw errors when they can't be encoded into database values (they used to crash in GRDB 5).
|
|
|
|
If you subclass the `Record` type, you have to update your override of `encode(to:)`:
|
|
|
|
```diff
|
|
class Player: Record {
|
|
- override func encode(to container: inout PersistenceContainer) {
|
|
+ override func encode(to container: inout PersistenceContainer) throws {
|
|
container["id"] = id
|
|
container["name"] = name
|
|
}
|
|
}
|
|
```
|
|
|
|
This change has an impact on a few other apis, that can now throw errors as well:
|
|
|
|
```diff
|
|
-let dictionary = player.databaseDictionary
|
|
-let changes = newPlayer.databaseChanges(from: oldPlayer)
|
|
-let changes = player.databaseChanges // Record class only
|
|
+let dictionary = try player.databaseDictionary
|
|
+let changes = try newPlayer.databaseChanges(from: oldPlayer)
|
|
+let changes = try player.databaseChanges // Record class only
|
|
```
|
|
|
|
- **The signature of the `didInsert` method has changed**.
|
|
|
|
You have to update all the `didInsert` methods in your application:
|
|
|
|
```diff
|
|
struct Player: MutablePersistableRecord {
|
|
var id: Int64?
|
|
|
|
// Update auto-incremented id upon successful insertion
|
|
- mutating func didInsert(with rowID: Int64, for column: String?) {
|
|
- id = rowID
|
|
+ mutating func didInsert(_ inserted: InsertionSuccess) {
|
|
+ id = inserted.rowID
|
|
}
|
|
}
|
|
```
|
|
|
|
If you subclass the `Record` class, you have to call `super` at some point of your implementation:
|
|
|
|
```diff
|
|
class Player: Record {
|
|
var id: Int64?
|
|
|
|
// Update auto-incremented id upon successful insertion
|
|
- override func didInsert(with rowID: Int64, for column: String?) {
|
|
- id = rowID
|
|
+ override func didInsert(_ inserted: InsertionSuccess) {
|
|
+ super.didInsert(inserted)
|
|
+ id = inserted.rowID
|
|
}
|
|
}
|
|
```
|
|
|
|
- **PersistableRecord types now customize persistence methods with "persistence callbacks"**.
|
|
|
|
It is no longer possible to override persistence methods such as `insert` or `update`. Customizing the persistence methods is now possible with callbacks such as `willSave`, `willInsert`, or `didDelete` (see [persistence callbacks] for the full list of callbacks).
|
|
|
|
**You have to remove the methods below from your own code base**:
|
|
|
|
```swift
|
|
// GRDB 6: remove those methods from your code
|
|
func insert(_ db: Database) throws
|
|
func didInsert(with rowID: Int64, for column: String?)
|
|
func update(_ db: Database, columns: Set<String>) throws
|
|
func save(_ db: Database) throws
|
|
func delete(_ db: Database) throws -> Bool
|
|
func exists(_ db: Database) throws -> Bool
|
|
```
|
|
|
|
- `insert(_:)`: customization is now made with [persistence callbacks].
|
|
- `didInsert(with:for:)`: this method was renamed `didInsert(_:)` (see previous bullet point).
|
|
- `update(_:columns:)`: customization is now made with [persistence callbacks].
|
|
- `save(_:)`: customization is now made with [persistence callbacks].
|
|
- `delete(_:)`: customization is now made with [persistence callbacks].
|
|
- `exists(_:)`: this method is no longer customizable.
|
|
|
|
To help you update your applications with persistence callbacks, let's look at two examples.
|
|
|
|
First, check the updated [Single-Row Tables](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/singlerowtables) guide, if your application defines a "singleton record".
|
|
|
|
Next, let's consider a record that performs some validation before insertion and updates. In GRDB 5, this would look like:
|
|
|
|
```swift
|
|
// GRDB 5
|
|
struct Link: PersistableRecord {
|
|
var url: URL
|
|
|
|
func insert(_ db: Database) throws {
|
|
try validate()
|
|
try performInsert(db)
|
|
}
|
|
|
|
func update(_ db: Database, columns: Set<String>) throws {
|
|
try validate()
|
|
try performUpdate(db, columns: columns)
|
|
}
|
|
|
|
func validate() throws {
|
|
if url.host == nil {
|
|
throw ValidationError("url must be absolute.")
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
With GRDB 6, record validation can be implemented with the `willSave` callback:
|
|
|
|
```swift
|
|
// GRDB 6
|
|
struct Link: PersistableRecord {
|
|
var url: URL
|
|
|
|
func willSave(_ db: Database) throws {
|
|
if url.host == nil {
|
|
throw ValidationError("url must be absolute.")
|
|
}
|
|
}
|
|
}
|
|
|
|
try link.insert(db) // Calls the willSave callback
|
|
try link.update(db) // Calls the willSave callback
|
|
try link.save(db) // Calls the willSave callback
|
|
try link.upsert(db) // Calls the willSave callback
|
|
```
|
|
|
|
If you subclass the `Record` class, you have to call `super` at some point of your implementation:
|
|
|
|
```swift
|
|
// GRDB 6
|
|
class Link: Record {
|
|
var url: URL
|
|
|
|
override func willSave(_ db: Database) throws {
|
|
try super.willSave(db)
|
|
if url.host == nil {
|
|
throw ValidationError("url must be absolute.")
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- **Handling of the `IGNORE` conflict policy**
|
|
|
|
The SQLite [IGNORE](https://www.sqlite.org/lang_conflict.html) conflict policy has SQLite skip insertions and updates that violate a schema constraint, without reporting any error. You can skip this paragraph if you do not use this policy.
|
|
|
|
GRDB 6 has slightly changed the handling of the `IGNORE` policy.
|
|
|
|
The `didInsert` callback is now always called on `INSERT OR IGNORE` insertions. In GRDB 5, `didInsert` was not called for record types that specify the `.ignore` conflict policy on inserts:
|
|
|
|
```swift
|
|
// Given a record with ignore conflict policy for inserts...
|
|
struct Player: TableRecord, FetchableRecord {
|
|
static let persistenceConflictPolicy = PersistenceConflictPolicy(insert: .ignore)
|
|
}
|
|
|
|
// GRDB 5: Does not call didInsert
|
|
// GRDB 6: Calls didInsert
|
|
try player.insert(db)
|
|
```
|
|
|
|
Since `INSERT OR IGNORE` may silently fail, the `didInsert` method will be called with some random rowid in case of failed insert. You can detect failed insertions with the new method `insertAndFetch`:
|
|
|
|
```swift
|
|
// How to detect failed `INSERT OR IGNORE`:
|
|
// INSERT OR IGNORE INTO player ... RETURNING *
|
|
if let insertedPlayer = try player.insertAndFetch(db) {
|
|
// Succesful insertion
|
|
} else {
|
|
// Ignored failure
|
|
}
|
|
```
|
|
|
|
## Other Changes
|
|
|
|
- The initializer of in-memory databases can now throw errors:
|
|
|
|
```diff
|
|
-let dbQueue = DatabaseQueue()
|
|
+let dbQueue = try DatabaseQueue()
|
|
```
|
|
|
|
- The `selectID()` method is removed. You can provide your own implementation, based on the new `selectPrimaryKey(as:)` method:
|
|
|
|
```swift
|
|
extension QueryInterfaceRequest<Player> {
|
|
func selectID() -> QueryInterfaceRequest<Int64> {
|
|
selectPrimaryKey()
|
|
}
|
|
}
|
|
```
|
|
|
|
- `Cursor.isEmpty` is now a throwing property, instead of a method:
|
|
|
|
```diff
|
|
-if try cursor.isEmpty() { ... }
|
|
+if try cursor.isEmpty { ... }
|
|
```
|
|
|
|
- The `Record.copy()` method was removed, without replacement.
|
|
|
|
- The `DerivableRequest.limit(_:offset_:)` method was removed, without replacement.
|
|
|
|
You can still limit `QueryInterfaceRequest`, but associations can no longer be limited:
|
|
|
|
```swift
|
|
// Still OK: a limited request of authors
|
|
let request = Author.limit(10)
|
|
|
|
// Still OK: a limited request of books
|
|
let request = author.request(for: Author.books).limit(10)
|
|
|
|
// No longer possible: including a limited association
|
|
let request = Author.including(all: Author.books.limit(10))
|
|
```
|
|
|
|
- `DatabaseRegionObservation.start(in:onError:onChange:)` now returns a cancellable.
|
|
|
|
```swift
|
|
let observation = DatabaseRegionObservation.tracking(Player.all())
|
|
|
|
// GRDB 5
|
|
do {
|
|
let observer = try observation.start(in: dbQueue) { db in
|
|
print("Players were modified")
|
|
}
|
|
} catch {
|
|
// handle error
|
|
}
|
|
|
|
// GRDB 6
|
|
let cancellable = observation.start(
|
|
in: dbQueue,
|
|
onError: { error in /* handle error */ },
|
|
onChange: { db in
|
|
print("Players were modified")
|
|
})
|
|
```
|
|
|
|
The `DatabaseRegionObservation.extent` property was removed. You now control the duration of the observation with the cancellable returned from the `start` method.
|
|
|
|
- Database cursors no longer have a `statement` property. When you want information about the database statement used by a cursor, use dedicated cursor properties. For example:
|
|
|
|
```diff
|
|
-let sql = cursor.statement.sql
|
|
-let columns = cursor.statement.columnNames
|
|
+let sql = cursor.sql
|
|
+let columns = cursor.columnNames
|
|
```
|
|
|
|
- The transaction hook `Database.afterNextTransactionCommit(_:)` was renamed `Database.afterNextTransaction(onCommit:onRollback:)`, and is now able to report rollbacks as well as commits.
|
|
|
|
```diff
|
|
-db.afterNextTransactionCommit { db in
|
|
+db.afterNextTransaction { db in
|
|
print("Succesful commit")
|
|
}
|
|
```
|
|
|
|
- If your application directly embeds the `GRDB.xcodeproj` or `GRDBCustom.xcodeproj` project, then you have to update your dependencies. Those projects now define cross-platform targets, and you must perform the following actions:
|
|
|
|
- In the **Target Dependencies** section of the **Build Phases** tab of your **application targets**, replace the GRDB target with `GRDB` or `GRDBCustom`.
|
|
- In the **Embedded Binaries** section of the **General** tab of your **application target**, replace the GRDB framework with `GRDB.framework` or `GRDBCustom.framework`.
|
|
|
|
[persistence callbacks]: ../README.md#persistence-callbacks
|