Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch updates #646

Merged
merged 15 commits into from
Nov 9, 2019
13 changes: 9 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 @@ -58,9 +56,16 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one expection:
- [0.110.0](#01100), ...


<!--
## Next Release
-->

**New**

- [#646](https://github.com/groue/GRDB.swift/pull/646): Batch updates

### Documentation Diff

The [Update Requests](README.md#update-requests) chapter documents the new support for batch updates introduced in the [query interface](README.md#the-query-interface).


## 4.5.0

Expand Down
688 changes: 688 additions & 0 deletions Documentation/FullTextSearch.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions GRDB.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@
56176C7D1EACCD2D000F3F2B /* EncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567156701CB18050007DC145 /* EncryptionTests.swift */; };
56176C7F1EACCD2F000F3F2B /* EncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567156701CB18050007DC145 /* EncryptionTests.swift */; };
56193E8E1CD8A3E200F95862 /* FetchedRecordsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A7787C1C6A4DD600F507F6 /* FetchedRecordsController.swift */; };
561CFA7823735016000C8BAA /* MutablePersistableRecordUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561CFA7123735015000C8BAA /* MutablePersistableRecordUpdateTests.swift */; };
561CFA7923735016000C8BAA /* MutablePersistableRecordUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561CFA7123735015000C8BAA /* MutablePersistableRecordUpdateTests.swift */; };
561CFA7A23735016000C8BAA /* MutablePersistableRecordUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561CFA7123735015000C8BAA /* MutablePersistableRecordUpdateTests.swift */; };
562205F11E420E47005860AC /* DatabasePoolReleaseMemoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563363CF1C943D13000BE133 /* DatabasePoolReleaseMemoryTests.swift */; };
562205F21E420E47005860AC /* DatabasePoolSchemaCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531281C908A5B00CF1A2B /* DatabasePoolSchemaCacheTests.swift */; };
562205F31E420E47005860AC /* DatabaseQueueReleaseMemoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563363D41C94484E000BE133 /* DatabaseQueueReleaseMemoryTests.swift */; };
Expand Down Expand Up @@ -1333,6 +1336,7 @@
561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSDecimalNumberTests.swift; sourceTree = "<group>"; };
5616AAF0207CD45E00AC3664 /* RequestProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProtocols.swift; sourceTree = "<group>"; };
56172947223533F40006E219 /* EncodableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncodableRecord.swift; sourceTree = "<group>"; };
561CFA7123735015000C8BAA /* MutablePersistableRecordUpdateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutablePersistableRecordUpdateTests.swift; sourceTree = "<group>"; };
562393171DECC02000A6B01F /* RowFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFetchTests.swift; sourceTree = "<group>"; };
5623932F1DEDFC5700A6B01F /* AnyCursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyCursorTests.swift; sourceTree = "<group>"; };
5623934D1DEDFEFB00A6B01F /* EnumeratedCursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnumeratedCursorTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1837,6 +1841,7 @@
5674A71C1F30A8DF0095F066 /* MutablePersistableRecordEncodableTests.swift */,
56B021C81D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */,
563363A91C933FF8000BE133 /* MutablePersistableRecordTests.swift */,
561CFA7123735015000C8BAA /* MutablePersistableRecordUpdateTests.swift */,
563363AA1C933FF8000BE133 /* PersistableRecordTests.swift */,
);
name = PersistableRecord;
Expand Down Expand Up @@ -3527,6 +3532,7 @@
56A5EF131EF7F20B00F03071 /* ForeignKeyInfoTests.swift in Sources */,
5653EAE920944B4F00F46237 /* AssociationChainSQLTests.swift in Sources */,
5698ACD21DA8C2620056AF8C /* RecordPrimaryKeyHiddenRowIDTests.swift in Sources */,
561CFA7923735016000C8BAA /* MutablePersistableRecordUpdateTests.swift in Sources */,
56A238621B9C74A90082EB20 /* RecordEditedTests.swift in Sources */,
5653EAE120944B4F00F46237 /* AssociationTableAliasTestsSQLTests.swift in Sources */,
5653EAE720944B4F00F46237 /* AssociationBelongsToFetchableRecordTests.swift in Sources */,
Expand Down Expand Up @@ -3729,6 +3735,7 @@
5698AC401DA2BED90056AF8C /* FTS3PatternTests.swift in Sources */,
562393571DEE013C00A6B01F /* FilterCursorTests.swift in Sources */,
5653EAE820944B4F00F46237 /* AssociationChainSQLTests.swift in Sources */,
561CFA7823735016000C8BAA /* MutablePersistableRecordUpdateTests.swift in Sources */,
56A5EF0F1EF7F20B00F03071 /* ForeignKeyInfoTests.swift in Sources */,
56D496B61D813434008276D7 /* DatabaseRegionTests.swift in Sources */,
5653EAE020944B4F00F46237 /* AssociationTableAliasTestsSQLTests.swift in Sources */,
Expand Down Expand Up @@ -4064,6 +4071,7 @@
AAA4DD8B230F262000C74B15 /* ForeignKeyInfoTests.swift in Sources */,
AAA4DD8C230F262000C74B15 /* AssociationChainSQLTests.swift in Sources */,
AAA4DD8D230F262000C74B15 /* RecordPrimaryKeyHiddenRowIDTests.swift in Sources */,
561CFA7A23735016000C8BAA /* MutablePersistableRecordUpdateTests.swift in Sources */,
AAA4DD8E230F262000C74B15 /* RecordEditedTests.swift in Sources */,
AAA4DD8F230F262000C74B15 /* AssociationTableAliasTestsSQLTests.swift in Sources */,
AAA4DD90230F262000C74B15 /* AssociationBelongsToFetchableRecordTests.swift in Sources */,
Expand Down
139 changes: 138 additions & 1 deletion GRDB/QueryInterface/Request/QueryInterfaceRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ extension QueryInterfaceRequest: DerivableRequest where T: TableRecord { }

extension QueryInterfaceRequest where T: MutablePersistableRecord {

// MARK: Deleting
// MARK: Batch Delete

/// Deletes matching rows; returns the number of deleted rows.
///
Expand All @@ -490,6 +490,52 @@ extension QueryInterfaceRequest where T: MutablePersistableRecord {
try SQLQueryGenerator(query).makeDeleteStatement(db).execute()
return db.changesCount
}

// MARK: Batch Update

/// Updates matching rows; returns the number of updated rows.
///
/// For example:
///
/// try dbQueue.write { db in
/// // UPDATE player SET score = 0
/// try Player.all().updateAll(db, [Column("score") <- 0])
/// }
///
/// - parameter db: A database connection.
/// - 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 {
// database not hit
return 0
}
try updateStatement.execute()
return db.changesCount
}

/// Updates matching rows; returns the number of updated rows.
///
/// For example:
///
/// try dbQueue.write { db in
/// // UPDATE player SET score = 0
/// try Player.all().updateAll(db, Column("score") <- 0)
/// }
///
/// - parameter db: A database connection.
/// - returns: The number of updated rows
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@discardableResult
public func updateAll(
_ db: Database,
_ assignment: ColumnAssignment,
_ otherAssignments: ColumnAssignment...)
throws -> Int
{
return try updateAll(db, [assignment] + otherAssignments)
}
}

// MARK: - Eager loading of hasMany associations
Expand Down Expand Up @@ -574,3 +620,94 @@ extension Row {
}
}
}

// MARK: - ColumnAssignment

precedencegroup ColumnAssignment {
associativity: left
assignment: true
lowerThan: AssignmentPrecedence
}

infix operator <- : ColumnAssignment

/// A ColumnAssignment can update rows in the database.
///
/// You create an assignment from a column and an assignment operator, such as
/// `<-` or `+=`:
///
/// try dbQueue.write { db in
/// // UPDATE player SET score = 0
/// let assignment = Column("score") <- 0
/// try Player.updateAll(db, assignment)
/// }
public struct ColumnAssignment {
var column: ColumnExpression
var value: SQLExpressible
}

/// Creates an assignment to a value.
///
/// Column("valid") <- true
/// Column("score") <- 0
/// Column("score") <- Column("score") + Column("bonus")
///
/// try dbQueue.write { db in
/// // UPDATE player SET score = 0
/// try Player.updateAll(db, Column("score") <- 0)
/// }
public func <- (column: ColumnExpression, value: SQLExpressible) -> ColumnAssignment {
return ColumnAssignment(column: column, value: value)
}

/// Creates an assignment that adds a value
///
/// Column("score") += 1
/// Column("score") += Column("bonus")
///
/// try dbQueue.write { db in
/// // UPDATE player SET score = score + 1
/// try Player.updateAll(db, Column("score") += 1)
/// }
public func += (column: ColumnExpression, value: SQLExpressible) -> ColumnAssignment {
return column <- column + value
}

/// Creates an assignment that subtracts a value
///
/// Column("score") -= 1
/// Column("score") -= Column("bonus")
///
/// try dbQueue.write { db in
/// // UPDATE player SET score = score - 1
/// try Player.updateAll(db, Column("score") -= 1)
/// }
public func -= (column: ColumnExpression, value: SQLExpressible) -> ColumnAssignment {
return column <- column - value
}

/// Creates an assignment that multiplies by a value
///
/// Column("score") *= 2
/// Column("score") *= Column("factor")
///
/// try dbQueue.write { db in
/// // UPDATE player SET score = score * 2
/// try Player.updateAll(db, Column("score") *= 2)
/// }
public func *= (column: ColumnExpression, value: SQLExpressible) -> ColumnAssignment {
return column <- column * value
}

/// Creates an assignment that divides by a value
///
/// Column("score") /= 2
/// Column("score") /= Column("factor")
///
/// try dbQueue.write { db in
/// // UPDATE player SET score = score / 2
/// try Player.updateAll(db, Column("score") /= 2)
/// }
public func /= (column: ColumnExpression, value: SQLExpressible) -> ColumnAssignment {
return column <- column / value
}
64 changes: 58 additions & 6 deletions GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,19 +161,71 @@ struct SQLQueryGenerator {
if !orderings.isEmpty {
sql += " ORDER BY " + orderings.map { $0.orderingTermSQL(&context) }.joined(separator: ", ")
}

if Database.sqliteCompileOptions.contains("ENABLE_UPDATE_DELETE_LIMIT") {
sql += " LIMIT " + limit.sql
} else {
fatalError("Can't delete query with limit")
}
sql += " LIMIT " + limit.sql
}

let statement = try db.makeUpdateStatement(sql: sql)
statement.arguments = context.arguments!
return statement
}

/// Returns nil if assignments is empty
func makeUpdateStatement(_ db: Database, _ assignments: [ColumnAssignment]) throws -> UpdateStatement? {
if let groupExpressions = try groupPromise?.resolve(db), !groupExpressions.isEmpty {
// Programmer error
fatalError("Can't update query with GROUP BY clause")
}

guard havingExpressions.isEmpty else {
// Programmer error
fatalError("Can't update query with HAVING clause")
}

guard relation.joins.isEmpty else {
// Programmer error
fatalError("Can't update query with JOIN clause")
}

guard case .table = relation.source else {
// Programmer error
fatalError("Can't update without any database table")
}

if assignments.isEmpty {
return nil
}

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

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

let assignmentsSQL = assignments
.map({ assignment in
assignment.column.expressionSQL(&context, wrappedInParenthesis: false) +
" = " +
assignment.value.sqlExpression.expressionSQL(&context, wrappedInParenthesis: false)
})
.joined(separator: ", ")
sql += " SET " + assignmentsSQL

let filters = try relation.filtersPromise.resolve(db)
if filters.isEmpty == false {
sql += " WHERE " + SQLExpressionAnd(filters).expressionSQL(&context, wrappedInParenthesis: false)
}

if let limit = limit {
let orderings = try relation.ordering.resolve(db)
if !orderings.isEmpty {
sql += " ORDER BY " + orderings.map { $0.orderingTermSQL(&context) }.joined(separator: ", ")
}
sql += " LIMIT " + limit.sql
}

let statement = try db.makeUpdateStatement(sql: sql)
statement.arguments = context.arguments!
return statement
}

/// Returns a select statement
func makeSelectStatement(_ db: Database) throws -> SelectStatement {
// Build an SQK generation context with all aliases found in the query,
Expand Down
43 changes: 42 additions & 1 deletion GRDB/Record/PersistableRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ extension MutablePersistableRecord where Self: AnyObject {

extension MutablePersistableRecord {

// MARK: - Deleting All
// MARK: Batch Delete

/// Deletes all records; returns the number of deleted rows.
///
Expand All @@ -517,6 +517,47 @@ extension MutablePersistableRecord {
public static func deleteAll(_ db: Database) throws -> Int {
return try all().deleteAll(db)
}

// MARK: Batch Update

/// Updates all records; returns the number of updated records.
///
/// For example:
///
/// try dbQueue.write { db in
/// // UPDATE player SET score = 0
/// try Player.updateAll(db, [Column("score") <- 0])
/// }
///
/// - parameter db: A database connection.
/// - 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)
}

/// Updates all records; returns the number of updated records.
///
/// For example:
///
/// try dbQueue.write { db in
/// // UPDATE player SET score = 0
/// try Player.updateAll(db, Column("score") <- 0)
/// }
///
/// - parameter db: A database connection.
/// - returns: The number of updated rows
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@discardableResult
public static func updateAll(
_ db: Database,
_ assignment: ColumnAssignment,
_ otherAssignments: ColumnAssignment...)
throws -> Int
{
return try updateAll(db, [assignment] + otherAssignments)
}
}

extension MutablePersistableRecord {
Expand Down
Loading