@@ -73,82 +73,89 @@ extension PersistableRecord {
7373 /// let upsertedPlayer = try player.upsertAndFetch(db)
7474 /// ```
7575 ///
76- /// With `conflictTarget` and `assignments` arguments, you can further
77- /// control the upsert behavior. Make sure you check
76+ /// With the `conflictTarget`, `strategy`, and `assignments` arguments,
77+ /// you can further control the upsert behavior. Make sure you check
7878 /// <https://www.sqlite.org/lang_UPSERT.html> for detailed information.
7979 ///
8080 /// The conflict target are the columns of the uniqueness constraint
8181 /// (primary key or unique index) that triggers the upsert. If empty, all
8282 /// uniqueness constraint are considered.
8383 ///
84- /// The assignments describe how to update columns in case of violation of
85- /// a uniqueness constraint. In the next example, we insert the new
86- /// vocabulary word "jovial" if that word is not already in the dictionary.
87- /// If the word is already in the dictionary, it increments the counter,
88- /// does not overwrite the tainted flag, and overwrites the
89- /// remaining columns:
84+ /// The strategy controls which columns are updated in case of
85+ /// uniqueness constraint violation: all columns unless specified (the
86+ /// default), or only the specified columns.
87+ ///
88+ /// For example, compare:
9089 ///
9190 /// ```swift
92- /// // CREATE TABLE vocabulary(
93- /// // word TEXT PRIMARY KEY,
94- /// // kind TEXT NOT NULL,
95- /// // isTainted BOOLEAN DEFAULT 0,
96- /// // count INT DEFAULT 1))
97- /// struct Vocabulary: Encodable, PersistableRecord {
98- /// var word: String
99- /// var kind: String
100- /// var isTainted: Bool
91+ /// // In case of conflict, updates all columns, including columns added
92+ /// // in a future migration.
93+ /// let upserted = try player.upsertAndFetch(db)
94+ ///
95+ /// // In case of conflict, updates all columns, including future ones,
96+ /// // but 'score'.
97+ /// let upserted = try player.upsertAndFetch(db) { _ in
98+ /// [Column("score").noOverwrite]
10199 /// }
102100 ///
103- /// // INSERT INTO vocabulary(word, kind, isTainted)
104- /// // VALUES('jovial', 'adjective', 0)
105- /// // ON CONFLICT(word) DO UPDATE SET \
106- /// // count = count + 1,
107- /// // kind = excluded.kind
108- /// // RETURNING *
109- /// let vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false)
110- /// let upserted = try vocabulary.upsertAndFetch(
111- /// db,
112- /// onConflict: ["word"],
113- /// doUpdate: { _ in
114- /// [Column("count") += 1,
115- /// Column("isTainted").noOverwrite]
116- /// })
101+ /// // In case of conflict, do not update any column.
102+ /// let upserted = try player.upsertAndFetch(db, updating: .noColumnUnlessSpecified)
103+ ///
104+ /// // In case of conflict, only update 'name'.
105+ /// let upserted = try player.upsertAndFetch(db, updating: .noColumnUnlessSpecified) { excluded in
106+ /// [Column("name").set(to: excluded[Column("name")])]
107+ /// }
117108 /// ```
118109 ///
119110 /// - parameter db: A database connection.
120111 /// - parameter conflictTarget: The conflict target.
112+ /// - parameter strategy: The default strategy, `.allColumns`, updates
113+ /// all columns in case of conflict, unless specified otherwise in the
114+ /// `assignments` parameter. Use `.noColumnUnlessSpecified` to only
115+ /// update the columns assigned in the `assignments` parameter.
121116 /// - parameter assignments: An optional function that returns an array of
122117 /// ``ColumnAssignment``. In case of violation of a uniqueness
123- /// constraints, these assignments are performed, and remaining columns
124- /// are overwritten by inserted values.
118+ /// constraint, these assignments are performed, and remaining columns
119+ /// are overwritten by inserted values, or not, depending on the
120+ /// chosen `strategy`. To use the value that would have been inserted
121+ /// had the constraint not failed, use the `excluded` parameter.
125122 /// - returns: The upserted record.
126123 /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
127124 /// error thrown by the persistence callbacks defined by the record type.
128125 @inlinable // allow specialization so that empty callbacks are removed
129126 public func upsertAndFetch(
130127 _ db: Database ,
131128 onConflict conflictTarget: [ String ] = [ ] ,
129+ updating strategy: UpsertUpdateStrategy = . allColumns,
132130 doUpdate assignments: ( ( _ excluded: TableAlias < Self > ) -> [ ColumnAssignment ] ) ? = nil )
133131 throws -> Self
134132 where Self: FetchableRecord
135133 {
136- try upsertAndFetch ( db, as: Self . self, onConflict: conflictTarget, doUpdate: assignments)
134+ try upsertAndFetch (
135+ db, as: Self . self, onConflict: conflictTarget,
136+ updating: strategy, doUpdate: assignments)
137137 }
138138
139139 /// Executes an `INSERT ON CONFLICT DO UPDATE RETURNING` statement, and
140140 /// returns the upserted record.
141141 ///
142- /// See `upsertAndFetch(_:onConflict:doUpdate:)` for more information about
143- /// the `conflictTarget` and `assignments` parameters.
142+ /// See ``upsertAndFetch(_:onConflict:updating:doUpdate:)`` for more
143+ /// information about the `conflictTarget`, `strategy`, and
144+ /// `assignments` parameters.
144145 ///
145146 /// - parameter db: A database connection.
146147 /// - parameter returnedType: The type of the returned record.
147148 /// - parameter conflictTarget: The conflict target.
149+ /// - parameter strategy: The default strategy, `.allColumns`, updates
150+ /// all columns in case of conflict, unless specified otherwise in the
151+ /// `assignments` parameter. Use `.noColumnUnlessSpecified` to only
152+ /// update the columns assigned in the `assignments` parameter.
148153 /// - parameter assignments: An optional function that returns an array of
149154 /// ``ColumnAssignment``. In case of violation of a uniqueness
150- /// constraints, these assignments are performed, and remaining columns
151- /// are overwritten by inserted values.
155+ /// constraint, these assignments are performed, and remaining columns
156+ /// are overwritten by inserted values, or not, depending on the
157+ /// chosen `strategy`. To use the value that would have been inserted
158+ /// had the constraint not failed, use the `excluded` parameter.
152159 /// - returns: A record of type `returnedType`.
153160 /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
154161 /// error thrown by the persistence callbacks defined by the record type.
@@ -157,6 +164,7 @@ extension PersistableRecord {
157164 _ db: Database ,
158165 as returnedType: T . Type ,
159166 onConflict conflictTarget: [ String ] = [ ] ,
167+ updating strategy: UpsertUpdateStrategy = . allColumns,
160168 doUpdate assignments: ( ( _ excluded: TableAlias < Self > ) -> [ ColumnAssignment ] ) ? = nil )
161169 throws -> T
162170 {
@@ -166,7 +174,7 @@ extension PersistableRecord {
166174 try aroundSave ( db) {
167175 success = try upsertAndFetchWithCallbacks (
168176 db, onConflict: conflictTarget,
169- doUpdate: assignments,
177+ updating : strategy , doUpdate: assignments,
170178 selection: T . databaseSelection,
171179 decode: { try T ( row: $0) } )
172180 return PersistenceSuccess ( success!. inserted)
@@ -250,55 +258,52 @@ extension PersistableRecord {
250258 /// let upsertedPlayer = try player.upsertAndFetch(db)
251259 /// ```
252260 ///
253- /// With `conflictTarget` and `assignments` arguments, you can further
254- /// control the upsert behavior. Make sure you check
261+ /// With the `conflictTarget`, `strategy`, and `assignments` arguments,
262+ /// you can further control the upsert behavior. Make sure you check
255263 /// <https://www.sqlite.org/lang_UPSERT.html> for detailed information.
256264 ///
257265 /// The conflict target are the columns of the uniqueness constraint
258266 /// (primary key or unique index) that triggers the upsert. If empty, all
259267 /// uniqueness constraint are considered.
260268 ///
261- /// The assignments describe how to update columns in case of violation of
262- /// a uniqueness constraint. In the next example, we insert the new
263- /// vocabulary word "jovial" if that word is not already in the dictionary.
264- /// If the word is already in the dictionary, it increments the counter,
265- /// does not overwrite the tainted flag, and overwrites the
266- /// remaining columns:
269+ /// The strategy controls which columns are updated in case of
270+ /// uniqueness constraint violation: all columns unless specified (the
271+ /// default), or only the specified columns.
272+ ///
273+ /// For example, compare:
267274 ///
268275 /// ```swift
269- /// // CREATE TABLE vocabulary(
270- /// // word TEXT PRIMARY KEY,
271- /// // kind TEXT NOT NULL,
272- /// // isTainted BOOLEAN DEFAULT 0,
273- /// // count INT DEFAULT 1))
274- /// struct Vocabulary: Encodable, PersistableRecord {
275- /// var word: String
276- /// var kind: String
277- /// var isTainted: Bool
276+ /// // In case of conflict, updates all columns, including columns added
277+ /// // in a future migration.
278+ /// let upserted = try player.upsertAndFetch(db)
279+ ///
280+ /// // In case of conflict, updates all columns, including future ones,
281+ /// // but 'score'.
282+ /// let upserted = try player.upsertAndFetch(db) { _ in
283+ /// [Column("score").noOverwrite]
278284 /// }
279285 ///
280- /// // INSERT INTO vocabulary(word, kind, isTainted)
281- /// // VALUES('jovial', 'adjective', 0)
282- /// // ON CONFLICT(word) DO UPDATE SET \
283- /// // count = count + 1,
284- /// // kind = excluded.kind
285- /// // RETURNING *
286- /// let vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false)
287- /// let upserted = try vocabulary.upsertAndFetch(
288- /// db,
289- /// onConflict: ["word"],
290- /// doUpdate: { _ in
291- /// [Column("count") += 1,
292- /// Column("isTainted").noOverwrite]
293- /// })
286+ /// // In case of conflict, do not update any column.
287+ /// let upserted = try player.upsertAndFetch(db, updating: .noColumnUnlessSpecified)
288+ ///
289+ /// // In case of conflict, only update 'name'.
290+ /// let upserted = try player.upsertAndFetch(db, updating: .noColumnUnlessSpecified) { excluded in
291+ /// [Column("name").set(to: excluded[Column("name")])]
292+ /// }
294293 /// ```
295294 ///
296295 /// - parameter db: A database connection.
297296 /// - parameter conflictTarget: The conflict target.
297+ /// - parameter strategy: The default strategy, `.allColumns`, updates
298+ /// all columns in case of conflict, unless specified otherwise in the
299+ /// `assignments` parameter. Use `.noColumnUnlessSpecified` to only
300+ /// update the columns assigned in the `assignments` parameter.
298301 /// - parameter assignments: An optional function that returns an array of
299302 /// ``ColumnAssignment``. In case of violation of a uniqueness
300- /// constraints, these assignments are performed, and remaining columns
301- /// are overwritten by inserted values.
303+ /// constraint, these assignments are performed, and remaining columns
304+ /// are overwritten by inserted values, or not, depending on the
305+ /// chosen `strategy`. To use the value that would have been inserted
306+ /// had the constraint not failed, use the `excluded` parameter.
302307 /// - returns: The upserted record.
303308 /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
304309 /// error thrown by the persistence callbacks defined by the record type.
@@ -307,26 +312,36 @@ extension PersistableRecord {
307312 public func upsertAndFetch(
308313 _ db: Database ,
309314 onConflict conflictTarget: [ String ] = [ ] ,
315+ updating strategy: UpsertUpdateStrategy = . allColumns,
310316 doUpdate assignments: ( ( _ excluded: TableAlias < Self > ) -> [ ColumnAssignment ] ) ? = nil )
311317 throws -> Self
312318 where Self: FetchableRecord
313319 {
314- try upsertAndFetch ( db, as: Self . self, onConflict: conflictTarget, doUpdate: assignments)
320+ try upsertAndFetch (
321+ db, as: Self . self, onConflict: conflictTarget,
322+ updating: strategy, doUpdate: assignments)
315323 }
316324
317325 /// Executes an `INSERT ON CONFLICT DO UPDATE RETURNING` statement, and
318326 /// returns the upserted record.
319327 ///
320- /// See ``upsertAndFetch(_:onConflict:doUpdate:)`` for more information
321- /// about the `conflictTarget` and `assignments` parameters.
328+ /// See ``upsertAndFetch(_:onConflict:updating:doUpdate:)`` for more
329+ /// information about the `conflictTarget`, `strategy`, and
330+ /// `assignments` parameters.
322331 ///
323332 /// - parameter db: A database connection.
324333 /// - parameter returnedType: The type of the returned record.
325334 /// - parameter conflictTarget: The conflict target.
335+ /// - parameter strategy: The default strategy, `.allColumns`, updates
336+ /// all columns in case of conflict, unless specified otherwise in the
337+ /// `assignments` parameter. Use `.noColumnUnlessSpecified` to only
338+ /// update the columns assigned in the `assignments` parameter.
326339 /// - parameter assignments: An optional function that returns an array of
327340 /// ``ColumnAssignment``. In case of violation of a uniqueness
328- /// constraints, these assignments are performed, and remaining columns
329- /// are overwritten by inserted values.
341+ /// constraint, these assignments are performed, and remaining columns
342+ /// are overwritten by inserted values, or not, depending on the
343+ /// chosen `strategy`. To use the value that would have been inserted
344+ /// had the constraint not failed, use the `excluded` parameter.
330345 /// - returns: A record of type `returnedType`.
331346 /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any
332347 /// error thrown by the persistence callbacks defined by the record type.
@@ -336,6 +351,7 @@ extension PersistableRecord {
336351 _ db: Database ,
337352 as returnedType: T . Type ,
338353 onConflict conflictTarget: [ String ] = [ ] ,
354+ updating strategy: UpsertUpdateStrategy = . allColumns,
339355 doUpdate assignments: ( ( _ excluded: TableAlias < Self > ) -> [ ColumnAssignment ] ) ? = nil )
340356 throws -> T
341357 {
@@ -345,7 +361,7 @@ extension PersistableRecord {
345361 try aroundSave ( db) {
346362 success = try upsertAndFetchWithCallbacks (
347363 db, onConflict: conflictTarget,
348- doUpdate: assignments,
364+ updating : strategy , doUpdate: assignments,
349365 selection: T . databaseSelection,
350366 decode: { try T ( row: $0) } )
351367 return PersistenceSuccess ( success!. inserted)
@@ -369,6 +385,7 @@ extension PersistableRecord {
369385 {
370386 let ( inserted, _) = try upsertAndFetchWithCallbacks (
371387 db, onConflict: [ ] ,
388+ updating: . allColumns,
372389 doUpdate: nil ,
373390 selection: [ ] ,
374391 decode: { _ in /* Nothing to decode */ } )
@@ -379,6 +396,7 @@ extension PersistableRecord {
379396 func upsertAndFetchWithCallbacks< T> (
380397 _ db: Database ,
381398 onConflict conflictTarget: [ String ] ,
399+ updating strategy: UpsertUpdateStrategy ,
382400 doUpdate assignments: ( ( _ excluded: TableAlias < Self > ) -> [ ColumnAssignment ] ) ? ,
383401 selection: [ any SQLSelectable ] ,
384402 decode: ( Row ) throws -> T )
@@ -390,7 +408,7 @@ extension PersistableRecord {
390408 try aroundInsert ( db) {
391409 success = try upsertAndFetchWithoutCallbacks (
392410 db, onConflict: conflictTarget,
393- doUpdate: assignments,
411+ updating : strategy , doUpdate: assignments,
394412 selection: selection,
395413 decode: decode)
396414 return success!. inserted
0 commit comments