Skip to content

Commit d3d2ad1

Browse files
committed
Merge branch 'dev/upsert-documentation' into development
2 parents 327911d + 55f3c2f commit d3d2ad1

File tree

2 files changed

+111
-7
lines changed

2 files changed

+111
-7
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
77

88
#### 6.x Releases
99

10-
- `6.0.0` Betas - [6.0.0-beta](#600-beta)
10+
- `6.0.0` Betas - [6.0.0-beta](#600-beta) | [6.0.0-beta.2](#600-beta2)
1111

1212
#### 5.x Releases
1313

@@ -97,6 +97,11 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
9797

9898
---
9999

100+
## 6.0.0-beta.2
101+
102+
- **New**: Extended UPSERT apis with the ability to define the conflict target, and the assignments performed in case of conflicts.
103+
- **Documentation**: A new [Upsert](README.md#upsert) chapter describes upserts in detail.
104+
100105
## 6.0.0-beta
101106

102107
Released August 21, 2022 • [diff](https://github.com/groue/GRDB.swift/compare/v5.26.0...v6.0.0-beta)

README.md

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2924,6 +2924,8 @@ try place.delete(db)
29242924
let exists = try place.exists(db)
29252925
```
29262926

2927+
See [Upsert](#upsert) below for more information about upserts.
2928+
29272929
**The [TableRecord] protocol comes with batch operations**:
29282930

29292931
```swift
@@ -2946,17 +2948,111 @@ For more information about batch updates, see [Update Requests](#update-requests
29462948

29472949
- `save` makes sure your values are stored in the database. It performs an UPDATE if the record has a non-null primary key, and then, if no row was modified, an INSERT. It directly performs an INSERT if the record has no primary key, or a null primary key.
29482950

2949-
- `upsert` requires SQLite 3.35.0+ (iOS 15.0+, macOS 12.0+, tvOS 15.0+, watchOS 8.0+, or [custom SQLite build]).
2950-
29512951
- `delete` and `deleteOne` returns whether a database row was deleted or not. `deleteAll` returns the number of deleted rows. `updateAll` returns the number of updated rows. `updateChanges` returns whether a database row was updated or not.
29522952

29532953
**All primary keys are supported**, including composite primary keys that span several columns, and the [implicit rowid primary key](#the-implicit-rowid-primary-key).
29542954

29552955
**To customize persistence methods**, you provide [Persistence Callbacks], described below. Do not attempt at overriding the ready-made persistence methods.
29562956

2957+
### Upsert
2958+
2959+
[UPSERT](https://www.sqlite.org/lang_UPSERT.html) is an SQLite feature that causes an INSERT to behave as an UPDATE or a no-op if the INSERT would violate a uniqueness constraint (primary key or unique index).
2960+
2961+
> **Note**: Upsert apis are available from SQLite 3.35.0+: iOS 15.0+, macOS 12.0+, tvOS 15.0+, watchOS 8.0+, or with a [custom SQLite build] or [SQLCipher](#encryption).
2962+
>
2963+
> **Note**: With regard to [persistence callbacks](#available-callbacks), an upsert behaves exactly like an insert. In particular: the `aroundInsert(_:)` and `didInsert(_:)` callbacks reports the rowid of the inserted or updated row; `willUpdate`, `aroundUdate`, `didUdate` are not called.
2964+
2965+
[PersistableRecord] provides three upsert methods:
2966+
2967+
- `upsert(_:)`
2968+
2969+
Inserts or updates a record.
2970+
2971+
The upsert behavior is triggered by a violation of any uniqueness constraint on the table (primary key or unique index). In case of conflict, all columns but the primary key are overwritten with the inserted values:
2972+
2973+
```swift
2974+
struct Player: Encodable, PersistableRecord {
2975+
var id: Int64
2976+
var name: String
2977+
var score: Int
2978+
}
2979+
2980+
// INSERT INTO player (id, name, score)
2981+
// VALUES (1, 'Arthur', 1000)
2982+
// ON CONFLICT DO UPDATE SET
2983+
// name = excluded.name,
2984+
// score = excluded.score
2985+
let player = Player(id: 1, name: "Arthur", score: 1000)
2986+
try player.upsert(db)
2987+
```
2988+
2989+
- `upsertAndFetch(_:onConflict:doUpdate:)` (requires [FetchableRecord] conformance)
2990+
2991+
Inserts or updates a record, and returns the upserted record.
2992+
2993+
The `onConflict` and `doUpdate` arguments let you further control the upsert behavior. Make sure you check the [SQLite UPSERT documentation](https://www.sqlite.org/lang_UPSERT.html) for detailed information.
2994+
2995+
- `onConflict`: the "conflict target" is the array of columns in the uniqueness constraint (primary key or unique index) that triggers the upsert.
2996+
2997+
If empty (the default), all uniqueness constraint are considered.
2998+
2999+
- `doUpdate`: a closure that returns columns assignments to perform in case of conflict. Other columns are overwritten with the inserted values.
3000+
3001+
By default, all inserted columns but the primary key and the conflict target are overwritten.
3002+
3003+
In the example below, we upsert the new vocabulary word "jovial". It is inserted if that word is not already in the dictionary. Otherwise, `count` is incremented, `isTainted` is not overwritten, and `kind` is overwritten:
3004+
3005+
```swift
3006+
// CREATE TABLE vocabulary(
3007+
// word TEXT NOT NULL PRIMARY KEY,
3008+
// kind TEXT NOT NULL,
3009+
// isTainted BOOLEAN DEFAULT 0,
3010+
// count INT DEFAULT 1))
3011+
struct Vocabulary: Encodable, PersistableRecord {
3012+
var word: String
3013+
var kind: String
3014+
var isTainted: Bool
3015+
}
3016+
3017+
// INSERT INTO vocabulary(word, kind, isTainted)
3018+
// VALUES('jovial', 'adjective', 0)
3019+
// ON CONFLICT(word) DO UPDATE SET \
3020+
// count = count + 1, -- on conflict, count is incremented
3021+
// kind = excluded.kind -- on conflict, kind is overwritten
3022+
// RETURNING *
3023+
let vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false)
3024+
let upserted = try vocabulary.upsertAndFetch(
3025+
db, onConflict: ["word"],
3026+
doUpdate: { _ in
3027+
[Column("count") += 1, // on conflict, count is incremented
3028+
Column("isTainted").noOverwrite] // on conflict, isTainted is NOT overwritten
3029+
})
3030+
```
3031+
3032+
The `doUpdate` closure accepts an `excluded` TableAlias argument that refers to the inserted values that trigger the conflict. You can use it to specify an explicit overwrite, or to perform a computation. In the next example, the upsert keeps the maximum date in case of conflict:
3033+
3034+
```swift
3035+
// INSERT INTO message(id, text, date)
3036+
// VALUES(...)
3037+
// ON CONFLICT DO UPDATE SET \
3038+
// text = excluded.text,
3039+
// date = MAX(date, excluded.date)
3040+
// RETURNING *
3041+
let upserted = try message.upsertAndFetch(doUpdate: { excluded in
3042+
// keep the maximum date in case of conflict
3043+
[Column("date").set(to: max(Column("date"), excluded["date"]))]
3044+
})
3045+
```
3046+
3047+
- `upsertAndFetch(_:as:onConflict:doUpdate:)` (does not require [FetchableRecord] conformance)
3048+
3049+
This method is identical to `upsertAndFetch(_:onConflict:doUpdate:)` described above, but you can provide a distinct [FetchableRecord] record type as a result, in order to specify the returned columns.
3050+
29573051
### Persistence Methods and the `RETURNING` clause
29583052

2959-
SQLite 3.35.0+ is able to return values from a inserted or updated row, with 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]).
3053+
SQLite is able to return values from a inserted, updated, or deleted row, with the [`RETURNING` clause](https://www.sqlite.org/lang_returning.html).
3054+
3055+
> **Note**: Support for the `RETURNING` clause is available from SQLite 3.35.0+: iOS 15.0+, macOS 12.0+, tvOS 15.0+, watchOS 8.0+, or with a [custom SQLite build] or [SQLCipher](#encryption).
29603056

29613057
The `RETURNING` clause helps dealing with database features such as auto-incremented ids, default values, and [generated columns](https://sqlite.org/gencol.html). You can, for example, insert a few columns and fetch the default or generated ones in one step.
29623058

@@ -3021,13 +3117,15 @@ try dbQueue.write { db in
30213117
}
30223118
```
30233119

3024-
There are other similar persistence methods, such as `saveAndFetch`, `updateAndFetch`, `updateChangesAndFetch`, etc. They all behave like `save`, `update`, `updateChanges` (see [Persistence Methods] and the [`updateChanges` methods](#the-updatechanges-methods)), except that they return saved values. For example:
3120+
There are other similar persistence methods, such as `upsertAndFetch`, `saveAndFetch`, `updateAndFetch`, `updateChangesAndFetch`, etc. They all behave like `upsert`, `save`, `update`, `updateChanges`, except that they return saved values. For example:
30253121

30263122
```swift
30273123
// Save and return the saved player
30283124
let savedPlayer = try player.saveAndFetch(db)
30293125
```
30303126

3127+
See [Persistence Methods], [Upsert](#upsert), and [`updateChanges` methods](#the-updatechanges-methods) for more information.
3128+
30313129
**Batch operations** can return updated or deleted values:
30323130

30333131
> **Warning**: Make sure you check the [documentation of the `RETURNING` clause](https://www.sqlite.org/lang_returning.html), which describes important limitations and caveats for batch operations.
@@ -3117,7 +3215,7 @@ try link.upsert(db) // Calls the willSave callback
31173215

31183216
#### Available Callbacks
31193217

3120-
Here is a list with all the available callbacks, listed in the same order in which they will get called during the respective operations:
3218+
Here is a list with all the available [persistence callbacks], listed in the same order in which they will get called during the respective operations:
31213219

31223220
- Inserting a record (all `record.insert` and `record.upsert` methods)
31233221
- `willSave`
@@ -3140,7 +3238,7 @@ Here is a list with all the available callbacks, listed in the same order in whi
31403238
- `aroundDelete`
31413239
- `didDelete`
31423240

3143-
Make sure you provide implementations that match the exact callback signatures. When in doubt, check the [reference](http://groue.github.io/GRDB.swift/docs/6.0.0-beta/Protocols/MutablePersistableRecord.html).
3241+
For detailed information about each callback, check the [reference](http://groue.github.io/GRDB.swift/docs/6.0.0-beta/Protocols/MutablePersistableRecord.html).
31443242

31453243
In the `MutablePersistableRecord` protocol, `willInsert` and `didInsert` are mutating methods. In `PersistableRecord`, they are not mutating.
31463244

@@ -8704,6 +8802,7 @@ This chapter was renamed to [Embedding SQL in Query Interface Requests].
87048802
[Record Comparison]: #record-comparison
87058803
[Record Customization Options]: #record-customization-options
87068804
[Persistence Callbacks]: #persistence-callbacks
8805+
[persistence callbacks]: #persistence-callbacks
87078806
[TableRecord]: #tablerecord-protocol
87088807
[ValueObservation]: #valueobservation
87098808
[DatabaseRegionObservation]: #databaseregionobservation

0 commit comments

Comments
 (0)