Skip to content

Commit d6a94a1

Browse files
authored
Merge pull request #1048 from geoffmacd/insert-many
implements batch insert rows, insertMany()
2 parents 3dce02e + 75a177a commit d6a94a1

File tree

6 files changed

+113
-6
lines changed

6 files changed

+113
-6
lines changed

Documentation/Index.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,18 @@ do {
639639
}
640640
```
641641

642+
Multiple rows can be inserted at once by similarily calling `insertMany` with an array of per-row [setters](#setters).
643+
644+
```swift
645+
do {
646+
let rowid = try db.run(users.insertMany([mail <- "alice@mac.com"], [email <- "geoff@mac.com"]))
647+
print("inserted id: \(rowid)")
648+
} catch {
649+
print("insertion failed: \(error)")
650+
}
651+
```
652+
653+
642654
The [`update`](#updating-rows) and [`delete`](#deleting-rows) functions
643655
follow similar patterns.
644656

Documentation/Planning.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,3 @@ be referred to when it comes time to add the corresponding feature._
3333
_Features that are not actively being considered, perhaps because of no clean
3434
type-safe way to implement them with the current Swift, or bugs, or just
3535
general uncertainty._
36-
37-
* provide a mechanism for INSERT INTO multiple values, per
38-
[#168](https://github.com/stephencelis/SQLite.swift/issues/168)

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
BUILD_TOOL = xcodebuild
22
BUILD_SCHEME = SQLite Mac
3-
IOS_SIMULATOR = iPhone XS
4-
IOS_VERSION = 12.4
3+
IOS_SIMULATOR = iPhone 12
4+
IOS_VERSION = 14.4
55
ifeq ($(BUILD_SCHEME),SQLite iOS)
66
BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" -destination "platform=iOS Simulator,name=$(IOS_SIMULATOR),OS=$(IOS_VERSION)"
77
else

Sources/SQLite/Typed/Coding.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,35 @@ extension QueryType {
3838
///
3939
/// - otherSetters: Any other setters to include in the insert
4040
///
41-
/// - Returns: An `INSERT` statement fort the encodable object
41+
/// - Returns: An `INSERT` statement for the encodable object
4242
public func insert(_ encodable: Encodable, userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = []) throws -> Insert {
4343
let encoder = SQLiteEncoder(userInfo: userInfo)
4444
try encodable.encode(to: encoder)
4545
return self.insert(encoder.setters + otherSetters)
4646
}
4747

48+
/// Creates a batch `INSERT` statement by encoding the array of given objects
49+
/// This method converts any custom nested types to JSON data and does not handle any sort
50+
/// of object relationships. If you want to support relationships between objects you will
51+
/// have to provide your own Encodable implementations that encode the correct ids.
52+
///
53+
/// - Parameters:
54+
///
55+
/// - encodables: Encodable objects to insert
56+
///
57+
/// - userInfo: User info to be passed to encoder
58+
///
59+
/// - otherSetters: Any other setters to include in the inserts, per row/object.
60+
///
61+
/// - Returns: An `INSERT` statement for the encodable objects
62+
public func insertMany(_ encodables: [Encodable], userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = []) throws -> Insert {
63+
let combinedSetters = try encodables.map { encodable -> [Setter] in
64+
let encoder = SQLiteEncoder(userInfo: userInfo)
65+
try encodable.encode(to: encoder)
66+
return encoder.setters + otherSetters
67+
}
68+
return self.insertMany(combinedSetters)
69+
}
4870

4971
/// Creates an `INSERT ON CONFLICT DO UPDATE` statement, aka upsert, by encoding the given object
5072
/// This method converts any custom nested types to JSON data and does not handle any sort

Sources/SQLite/Typed/Query.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,18 @@ extension QueryType {
631631
return insert(onConflict, values)
632632
}
633633

634+
public func insertMany( _ values: [[Setter]]) -> Insert {
635+
return insertMany(nil, values)
636+
}
637+
638+
public func insertMany(or onConflict: OnConflict, _ values: [[Setter]]) -> Insert {
639+
return insertMany(onConflict, values)
640+
}
641+
642+
public func insertMany(or onConflict: OnConflict, _ values: [Setter]...) -> Insert {
643+
return insertMany(onConflict, values)
644+
}
645+
634646
fileprivate func insert(_ or: OnConflict?, _ values: [Setter]) -> Insert {
635647
let insert = values.reduce((columns: [Expressible](), values: [Expressible]())) { insert, setter in
636648
(insert.columns + [setter.column], insert.values + [setter.value])
@@ -650,6 +662,31 @@ extension QueryType {
650662
return Insert(" ".join(clauses.compactMap { $0 }).expression)
651663
}
652664

665+
fileprivate func insertMany(_ or: OnConflict?, _ values: [[Setter]]) -> Insert {
666+
guard let firstInsert = values.first else {
667+
// must be at least 1 object or else we don't know columns. Default to default inserts.
668+
return insert()
669+
}
670+
let columns = firstInsert.map { $0.column }
671+
let insertValues = values.map { rowValues in
672+
rowValues.reduce([Expressible]()) { insert, setter in
673+
insert + [setter.value]
674+
}
675+
}
676+
677+
let clauses: [Expressible?] = [
678+
Expression<Void>(literal: "INSERT"),
679+
or.map { Expression<Void>(literal: "OR \($0.rawValue)") },
680+
Expression<Void>(literal: "INTO"),
681+
tableName(),
682+
"".wrap(columns) as Expression<Void>,
683+
Expression<Void>(literal: "VALUES"),
684+
", ".join(insertValues.map({ "".wrap($0) as Expression<Void> })),
685+
whereClause
686+
]
687+
return Insert(" ".join(clauses.compactMap { $0 }).expression)
688+
}
689+
653690
/// Runs an `INSERT` statement against the query with `DEFAULT VALUES`.
654691
public func insert() -> Insert {
655692
return Insert(" ".join([
@@ -1048,6 +1085,8 @@ extension Connection {
10481085
/// - SeeAlso: `QueryType.insert(value:_:)`
10491086
/// - SeeAlso: `QueryType.insert(values:)`
10501087
/// - SeeAlso: `QueryType.insert(or:_:)`
1088+
/// - SeeAlso: `QueryType.insertMany(values:)`
1089+
/// - SeeAlso: `QueryType.insertMany(or:_:)`
10511090
/// - SeeAlso: `QueryType.insert()`
10521091
///
10531092
/// - Parameter query: An insert query.

Tests/SQLiteTests/QueryTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,26 @@ class QueryTests : XCTestCase {
247247
)
248248
}
249249

250+
func test_insert_many_compilesInsertManyExpression() {
251+
AssertSQL(
252+
"INSERT INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30), ('geoff@example.com', 32), ('alex@example.com', 83)",
253+
users.insertMany([[email <- "alice@example.com", age <- 30], [email <- "geoff@example.com", age <- 32], [email <- "alex@example.com", age <- 83]])
254+
)
255+
}
256+
func test_insert_many_compilesInsertManyNoneExpression() {
257+
AssertSQL(
258+
"INSERT INTO \"users\" DEFAULT VALUES",
259+
users.insertMany([])
260+
)
261+
}
262+
263+
func test_insert_many_withOnConflict_compilesInsertManyOrOnConflictExpression() {
264+
AssertSQL(
265+
"INSERT OR REPLACE INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30), ('geoff@example.com', 32), ('alex@example.com', 83)",
266+
users.insertMany(or: .replace, [[email <- "alice@example.com", age <- 30], [email <- "geoff@example.com", age <- 32], [email <- "alex@example.com", age <- 83]])
267+
)
268+
}
269+
250270
func test_insert_encodable() throws {
251271
let emails = Table("emails")
252272
let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil)
@@ -288,6 +308,18 @@ class QueryTests : XCTestCase {
288308
)
289309
}
290310

311+
func test_insert_many_encodable() throws {
312+
let emails = Table("emails")
313+
let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil)
314+
let value2 = TestCodable(int: 2, string: "3", bool: true, float: 3, double: 5, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil)
315+
let value3 = TestCodable(int: 3, string: "4", bool: true, float: 3, double: 6, date: Date(timeIntervalSince1970: 0), optional: nil, sub: nil)
316+
let insert = try emails.insertMany([value1, value2, value3])
317+
AssertSQL(
318+
"INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\") VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000'), (2, '3', 1, 3.0, 5.0, '1970-01-01T00:00:00.000'), (3, '4', 1, 3.0, 6.0, '1970-01-01T00:00:00.000')",
319+
insert
320+
)
321+
}
322+
291323
func test_update_compilesUpdateExpression() {
292324
AssertSQL(
293325
"UPDATE \"users\" SET \"age\" = 30, \"admin\" = 1 WHERE (\"id\" = 1)",
@@ -505,6 +537,11 @@ class QueryIntegrationTests : SQLiteTestCase {
505537
XCTAssertEqual(1, id)
506538
}
507539

540+
func test_insert_many() {
541+
let id = try! db.run(users.insertMany([[email <- "alice@example.com"], [email <- "geoff@example.com"]]))
542+
XCTAssertEqual(2, id)
543+
}
544+
508545
func test_upsert() throws {
509546
let fetchAge = { () throws -> Int? in
510547
return try self.db.pluck(self.users.filter(self.email == "alice@example.com")).flatMap { $0[self.age] }

0 commit comments

Comments
 (0)