Skip to content

Commit

Permalink
Merge pull request #647 from groue/dev/batch-update-policy
Browse files Browse the repository at this point in the history
Honor conflict resolution for batch updates
  • Loading branch information
groue authored Nov 10, 2019
2 parents d5c6dde + e1ff862 commit 51a1c41
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 18 deletions.
8 changes: 4 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ All notable changes to this project will be documented in this file.
GRDB adheres to [Semantic Versioning](https://semver.org/), with one expection: APIs flagged [**:fire: EXPERIMENTAL**](README.md#what-are-experimental-features). Those are unstable, and may break between any two minor releases of the library.


<!--
[Next Release](#next-release)
-->


#### 4.x Releases
Expand Down Expand Up @@ -59,9 +57,11 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one expection:
- [0.110.0](#01100), ...


<!--
## Next Release
-->

**Fixed**

- [#647](https://github.com/groue/GRDB.swift/pull/647): Honor conflict resolution for batch updates


## 4.6.0
Expand Down
27 changes: 22 additions & 5 deletions GRDB/QueryInterface/Request/QueryInterfaceRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -503,11 +503,23 @@ extension QueryInterfaceRequest where T: MutablePersistableRecord {
/// }
///
/// - parameter db: A database connection.
/// - returns: The number of updated rows
/// - parameter conflictResolution: A policy for conflict resolution,
/// defaulting to the record's persistenceConflictPolicy.
/// - parameter assignments: An array of column assignments.
/// - returns: The number of updated rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@discardableResult
public func updateAll(_ db: Database, _ assignments: [ColumnAssignment]) throws -> Int {
guard let updateStatement = try SQLQueryGenerator(query).makeUpdateStatement(db, assignments) else {
public func updateAll(
_ db: Database,
onConflict conflictResolution: Database.ConflictResolution? = nil,
_ assignments: [ColumnAssignment]) throws -> Int
{
let conflictResolution = conflictResolution ?? RowDecoder.persistenceConflictPolicy.conflictResolutionForUpdate
guard let updateStatement = try SQLQueryGenerator(query).makeUpdateStatement(
db,
conflictResolution: conflictResolution,
assignments: assignments) else
{
// database not hit
return 0
}
Expand All @@ -525,16 +537,21 @@ extension QueryInterfaceRequest where T: MutablePersistableRecord {
/// }
///
/// - parameter db: A database connection.
/// - returns: The number of updated rows
/// - parameter conflictResolution: A policy for conflict resolution,
/// defaulting to the record's persistenceConflictPolicy.
/// - parameter assignment: A column assignment.
/// - parameter otherAssignments: Eventual other column assignments.
/// - returns: The number of updated rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@discardableResult
public func updateAll(
_ db: Database,
onConflict conflictResolution: Database.ConflictResolution? = nil,
_ assignment: ColumnAssignment,
_ otherAssignments: ColumnAssignment...)
throws -> Int
{
return try updateAll(db, [assignment] + otherAssignments)
return try updateAll(db, onConflict: conflictResolution, [assignment] + otherAssignments)
}
}

Expand Down
15 changes: 13 additions & 2 deletions GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,12 @@ struct SQLQueryGenerator {
}

/// Returns nil if assignments is empty
func makeUpdateStatement(_ db: Database, _ assignments: [ColumnAssignment]) throws -> UpdateStatement? {
func makeUpdateStatement(
_ db: Database,
conflictResolution: Database.ConflictResolution,
assignments: [ColumnAssignment])
throws -> UpdateStatement?
{
if let groupExpressions = try groupPromise?.resolve(db), !groupExpressions.isEmpty {
// Programmer error
fatalError("Can't update query with GROUP BY clause")
Expand All @@ -197,7 +202,13 @@ struct SQLQueryGenerator {

var context = SQLGenerationContext.queryGenerationContext(aliases: relation.allAliases)

var sql = try "UPDATE " + relation.source.sql(db, &context)
var sql = "UPDATE "

if conflictResolution != .abort {
sql += "OR \(conflictResolution.rawValue) "
}

sql += try relation.source.sql(db, &context)

let assignmentsSQL = assignments
.map({ assignment in
Expand Down
23 changes: 18 additions & 5 deletions GRDB/Record/PersistableRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -530,11 +530,19 @@ extension MutablePersistableRecord {
/// }
///
/// - parameter db: A database connection.
/// - returns: The number of updated rows
/// - parameter conflictResolution: A policy for conflict resolution,
/// defaulting to the record's persistenceConflictPolicy.
/// - parameter assignments: An array of column assignments.
/// - returns: The number of updated rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@discardableResult
public static func updateAll(_ db: Database, _ assignments: [ColumnAssignment]) throws -> Int {
return try all().updateAll(db, assignments)
public static func updateAll(
_ db: Database,
onConflict conflictResolution: Database.ConflictResolution? = nil,
_ assignments: [ColumnAssignment])
throws -> Int
{
return try all().updateAll(db, onConflict: conflictResolution, assignments)
}

/// Updates all records; returns the number of updated records.
Expand All @@ -547,16 +555,21 @@ extension MutablePersistableRecord {
/// }
///
/// - parameter db: A database connection.
/// - returns: The number of updated rows
/// - parameter conflictResolution: A policy for conflict resolution,
/// defaulting to the record's persistenceConflictPolicy.
/// - parameter assignment: A column assignment.
/// - parameter otherAssignments: Eventual other column assignments.
/// - returns: The number of updated rows.
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@discardableResult
public static func updateAll(
_ db: Database,
onConflict conflictResolution: Database.ConflictResolution? = nil,
_ assignment: ColumnAssignment,
_ otherAssignments: ColumnAssignment...)
throws -> Int
{
return try updateAll(db, [assignment] + otherAssignments)
return try updateAll(db, onConflict: conflictResolution, [assignment] + otherAssignments)
}
}

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4659,6 +4659,13 @@ As a convenience, you can also use the `+=`, `-=`, `*=`, or `/=` operators:
try Player.updateAll(db, scoreColumn += bonusColumn * 2)
```

Default [Conflict Resolution] rules apply, and you may also provide a specific one:

```swift
// UPDATE OR IGNORE player SET ...
try Player.updateAll(db, onConflict: .ignore, /* assignments... */)
```

> :point_up: **Note** The `updateAll` method is only available for records that adopts the [PersistableRecord] protocol.


Expand Down
90 changes: 88 additions & 2 deletions Tests/GRDBTests/MutablePersistableRecordUpdateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ class MutablePersistableRecordUpdateTests: GRDBTestCase {
}

func testRequestUpdateAll() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
try makeDatabaseQueue().write { db in
let assignment = Columns.score <- 0

try Player.updateAll(db, assignment)
Expand Down Expand Up @@ -282,4 +281,91 @@ class MutablePersistableRecordUpdateTests: GRDBTestCase {
try XCTAssertEqual(Player.fetchOne(db, key: 1)!.score, 2)
}
}

func testConflictPolicyAbort() throws {
struct AbortPlayer: PersistableRecord {
static let databaseTableName = "player"
static let persistenceConflictPolicy = PersistenceConflictPolicy(insert: .abort, update: .abort)
func encode(to container: inout PersistenceContainer) { }
}
try makeDatabaseQueue().write { db in
try AbortPlayer.updateAll(db, Column("score") <- 0)
XCTAssertEqual(self.lastSQLQuery, """
UPDATE "player" SET "score" = 0
""")

try AbortPlayer.updateAll(db, [Column("score") <- 0])
XCTAssertEqual(self.lastSQLQuery, """
UPDATE "player" SET "score" = 0
""")

try AbortPlayer.all().updateAll(db, Column("score") <- 0)
XCTAssertEqual(self.lastSQLQuery, """
UPDATE "player" SET "score" = 0
""")

try AbortPlayer.all().updateAll(db, [Column("score") <- 0])
XCTAssertEqual(self.lastSQLQuery, """
UPDATE "player" SET "score" = 0
""")
}
}

func testConflictPolicyIgnore() throws {
struct IgnorePlayer: PersistableRecord {
static let databaseTableName = "player"
static let persistenceConflictPolicy = PersistenceConflictPolicy(insert: .abort, update: .ignore)
func encode(to container: inout PersistenceContainer) { }
}
try makeDatabaseQueue().write { db in
try IgnorePlayer.updateAll(db, Column("score") <- 0)
XCTAssertEqual(self.lastSQLQuery, """
UPDATE OR IGNORE "player" SET "score" = 0
""")

try IgnorePlayer.updateAll(db, [Column("score") <- 0])
XCTAssertEqual(self.lastSQLQuery, """
UPDATE OR IGNORE "player" SET "score" = 0
""")

try IgnorePlayer.all().updateAll(db, Column("score") <- 0)
XCTAssertEqual(self.lastSQLQuery, """
UPDATE OR IGNORE "player" SET "score" = 0
""")

try IgnorePlayer.all().updateAll(db, [Column("score") <- 0])
XCTAssertEqual(self.lastSQLQuery, """
UPDATE OR IGNORE "player" SET "score" = 0
""")
}
}

func testConflictPolicyCustom() throws {
try makeDatabaseQueue().write { db in
try Player.updateAll(db, Column("score") <- 0)
XCTAssertEqual(self.lastSQLQuery, """
UPDATE "player" SET "score" = 0
""")

try Player.updateAll(db, onConflict: .ignore, Column("score") <- 0)
XCTAssertEqual(self.lastSQLQuery, """
UPDATE OR IGNORE "player" SET "score" = 0
""")

try Player.updateAll(db, onConflict: .ignore, [Column("score") <- 0])
XCTAssertEqual(self.lastSQLQuery, """
UPDATE OR IGNORE "player" SET "score" = 0
""")

try Player.all().updateAll(db, onConflict: .ignore, Column("score") <- 0)
XCTAssertEqual(self.lastSQLQuery, """
UPDATE OR IGNORE "player" SET "score" = 0
""")

try Player.all().updateAll(db, onConflict: .ignore, [Column("score") <- 0])
XCTAssertEqual(self.lastSQLQuery, """
UPDATE OR IGNORE "player" SET "score" = 0
""")
}
}
}

0 comments on commit 51a1c41

Please sign in to comment.