Skip to content

Commit

Permalink
BREAKING: persistence callbacks, and support for RETURNING
Browse files Browse the repository at this point in the history
- Replace performInsert, performUpdate, etc with callbacks: willInsert, aroundInsert, didInsert, etc.
- Always call didInsert, even with INSERT OR IGNORE
- Explicit conflict resolution in persistence methods
- Add ...AndFetch... methods
  • Loading branch information
groue committed Aug 21, 2022
1 parent 0ed9367 commit a702fe6
Show file tree
Hide file tree
Showing 50 changed files with 6,800 additions and 1,648 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,14 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
- **Breaking**: `DatabaseRegionObservation.start(in:onError:onChange:)` now returns a cancellable.
- **Breaking**: The `DatabaseRegionObservation.extent` property was removed.
- **Breaking**: The `statement` property of database cursors was replaced with read-only properties such as `sql` or `columnNames`.
- **Breaking**: Record types can no longer override persistence methods. You use persistence callbacks instead.
- **New**: Request protocols and cursors now define primary associated types, enabled by [SE-0346](https://github.com/apple/swift-evolution/blob/main/proposals/0346-light-weight-same-type-syntax.md).
- **New**: You can append the contents of a cursor to a collection with `RangeReplaceableCollection.append(contentsOf:)`.
- **New**: `ValueObservation.map` now accepts a throwing closure argument.
- **New**: Value requests now throw an error when they find unexpected database values (they used to crash in GRDB 5).
- **New**: Persistence callbacks allow record types to customize persistence methods.
- **New**: All persistence methods now accept an explicit conflict policy: `player.insert(db, onConflict: .replace)`, etc.
- **New**: Support for the [`RETURNING` clause](https://www.sqlite.org/lang_returning.html) (available from iOS 15.0+, macOS 12.0+, tvOS 15.0+, watchOS 8.0+, or with a custom SQLite build): `player.insertAndFetch(db)`, `Player.deleteAndFetchAll(db)`, etc.

## 5.26.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ extension Player: Codable, FetchableRecord, MutablePersistableRecord {
}

/// Updates a player id after it has been inserted in the database.
mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ extension Player: Codable, FetchableRecord, MutablePersistableRecord {
}

/// Updates a player id after it has been inserted in the database.
mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}

Expand Down
4 changes: 2 additions & 2 deletions Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ extension Player: Codable, FetchableRecord, MutablePersistableRecord {
}

/// Updates a player id after it has been inserted in the database.
mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}

Expand Down
128 changes: 128 additions & 0 deletions Documentation/GRDB6MigrationGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,135 @@ The record protocols have been refactored. We tried to keep the amount of modifi

</details>

- **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](../README.md#persistence-callbacks) for the full list of callbacks).

For example, 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:
Expand Down
18 changes: 11 additions & 7 deletions Documentation/GoodPracticesForDesigningRecordTypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,29 +109,33 @@ struct Book: Codable, Identifiable {
var authorId: Int64
var title: String
}
```
We add database powers to our types with [record protocols]. Since our records use auto-incremented ids, we provide an implementation of the `didInsert` method:

```swift
// Add Database access

extension Author: FetchableRecord, MutablePersistableRecord {
// 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
}
}

extension Book: FetchableRecord, MutablePersistableRecord {
// 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
}
}
```

That's it. The `Author` type can read and write in the `author` database table. `Book` as well, in `book`. See [record protocols] for more information.
That's it. The `Author` type can read and write in the `author` database table. `Book` as well, in `book`.

> :bulb: **Tip**: When a column of a database table can't be NULL, store it in a non-optional property of your record type. On the other side, when the database may contain NULL, define an optional property.
>
> :bulb: **Tip**: When a database table uses an auto-incremented identifier, make the `id` property optional (so that you can instantiate a record before it gets inserted and gains an id), and implement the `didInsert(with:for:)` method:
> :bulb: **Tip**: When a database table uses an auto-incremented identifier, make the `id` property optional (so that you can instantiate a record before it gets inserted and gains an id), and implement the `didInsert(_:)` method:
>
> ```swift
> try dbQueue.write { db in
Expand Down Expand Up @@ -234,7 +238,7 @@ Let's look at three examples:
> :bulb: Private properties allow records to choose both their best database representation, and at the same time, their best Swift interface.
**Generally speaking**, record types are the dedicated place, in your code, where you can transform raw database values into well-suited types that the rest of the application will enjoy. When needed, you can even [validate values](../README.md#customizing-the-persistence-methods) before they enter the database.
**Generally speaking**, record types are the dedicated place, in your code, where you can transform raw database values into well-suited types that the rest of the application will enjoy. When needed, you can even [validate values](../README.md#persistence-callbacks) before they enter the database.
## Singleton Records
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ struct Author: Codable, FetchableRecord, MutablePersistableRecord {
var id: Int64?
var name: String

mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}

Expand All @@ -54,8 +54,8 @@ struct Book: Codable, FetchableRecord, MutablePersistableRecord {
var authorId: Int64
var title: String

mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ struct Player: Codable, FetchableRecord, MutablePersistableRecord {
var name: String
var score: Int

mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}

Expand Down
4 changes: 2 additions & 2 deletions Documentation/Playgrounds/Tour.playground/Contents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ extension Place : MutablePersistableRecord {
container["longitude"] = coordinate.longitude
}

mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}

Expand Down
Loading

0 comments on commit a702fe6

Please sign in to comment.