Documentation/GRDB6MigrationGuide.md
This guide aims at helping you upgrading your applications from GRDB 5 to GRDB 6.
If you haven't made it yet, upgrade to the latest GRDB 5 release 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.
GRDB requirements have been bumped:
Request protocols now come with a primary associated type, enabled by SE-0346. This is a great opportunity to simplify your extensions:
-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:
-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).
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.
-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:):
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:
For example:
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 ...
}
}
}
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:):
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:
-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:
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:
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:
// 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 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:
// 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:
// 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:
// 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 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:
// 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:
// 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
}
The initializer of in-memory databases can now throw errors:
-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:
extension QueryInterfaceRequest<Player> {
func selectID() -> QueryInterfaceRequest<Int64> {
selectPrimaryKey()
}
}
Cursor.isEmpty is now a throwing property, instead of a method:
-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:
// 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.
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:
-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.
-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:
GRDB or GRDBCustom.GRDB.framework or GRDBCustom.framework.