Skip to content

Commit

Permalink
fix: encode pointer object properties properly when batching (#390)
Browse files Browse the repository at this point in the history
* reduce loops in batching calls

* fix saveAll async

* fix: encode pointer object properties properly when batching
  • Loading branch information
cbaker6 authored Aug 17, 2022
1 parent 50f655b commit 675168e
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ let otherBook1 = Book(title: "I like this book")
let otherBook2 = Book(title: "I like this book also")
var author2 = Author(name: "Bruce", book: newBook)
author2.otherBooks = [otherBook1, otherBook2]

author2.save { result in
switch result {
case .success(let savedAuthorAndBook):
Expand All @@ -121,7 +122,10 @@ author2.save { result in
assert(savedAuthorAndBook.updatedAt != nil)
assert(savedAuthorAndBook.otherBooks?.count == 2)

//: Notice the pointer objects haven't been updated on the client.
/*:
Notice the pointer objects haven't been updated on the
client.If you want the latest pointer objects, fetch and include them.
*/
print("Saved \(savedAuthorAndBook)")

case .failure(let error):
Expand Down Expand Up @@ -262,5 +266,68 @@ do {
print("\(error)")
}

//: Batching saves with saved and unsaved pointer items.
var author3 = Author(name: "Logan", book: newBook)
let otherBook3 = Book(title: "I like this book")
let otherBook4 = Book(title: "I like this book also")
author3.otherBooks = [otherBook3, otherBook4]

[author3].saveAll { result in
switch result {
case .success(let savedAuthorsAndBook):
savedAuthorsAndBook.forEach { eachResult in
switch eachResult {
case .success(let savedAuthorAndBook):
assert(savedAuthorAndBook.objectId != nil)
assert(savedAuthorAndBook.createdAt != nil)
assert(savedAuthorAndBook.updatedAt != nil)
assert(savedAuthorAndBook.otherBooks?.count == 2)

/*:
Notice the pointer objects haven't been updated on the
client.If you want the latest pointer objects, fetch and include them.
*/
print("Saved \(savedAuthorAndBook)")
case .failure(let error):
assertionFailure("Error saving: \(error)")
}
}

case .failure(let error):
assertionFailure("Error saving: \(error)")
}
}

//: Batching saves with unsaved pointer items.
var newBook2 = Book(title: "world")
var author4 = Author(name: "Scott", book: newBook2)
author4.otherBooks = [otherBook3, otherBook4]

[author4].saveAll { result in
switch result {
case .success(let savedAuthorsAndBook):
savedAuthorsAndBook.forEach { eachResult in
switch eachResult {
case .success(let savedAuthorAndBook):
assert(savedAuthorAndBook.objectId != nil)
assert(savedAuthorAndBook.createdAt != nil)
assert(savedAuthorAndBook.updatedAt != nil)
assert(savedAuthorAndBook.otherBooks?.count == 2)

/*:
Notice the pointer objects haven't been updated on the
client.If you want the latest pointer objects, fetch and include them.
*/
print("Saved \(savedAuthorAndBook)")
case .failure(let error):
assertionFailure("Error saving: \(error)")
}
}

case .failure(let error):
assertionFailure("Error saving: \(error)")
}
}

PlaygroundPage.current.finishExecution()
//: [Next](@next)
2 changes: 1 addition & 1 deletion ParseSwift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1619,8 +1619,8 @@
7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */,
703B091A26BDE774005A112F /* ParseObjectAsyncTests.swift */,
70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */,
7044C1DE25C5C70D0011F6E7 /* ParseObjectCombineTests.swift */,
70732C592606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift */,
7044C1DE25C5C70D0011F6E7 /* ParseObjectCombineTests.swift */,
911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */,
917BA43D2703E84000F8D747 /* ParseOperationAsyncTests.swift */,
7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */,
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/API/API+Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ internal extension API.Command where T: ParseObject {
}
}

let batchCommand = BatchCommandNoBody(requests: commands, transaction: transaction)
let batchCommand = BatchCommandEncodable(requests: commands, transaction: transaction)
return RESTBatchCommandNoBodyType<NoBody>(method: .POST, path: .batch, body: batchCommand, mapper: mapper)
}
}
5 changes: 2 additions & 3 deletions Sources/ParseSwift/API/API+NonParseBodyCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ internal extension API.NonParseBodyCommand {
internal extension API.NonParseBodyCommand {
// MARK: Batch - Child Objects
static func batch(objects: [ParseEncodable],
transaction: Bool) throws -> RESTBatchCommandTypeEncodable<AnyCodable> {
transaction: Bool) throws -> RESTBatchCommandTypeEncodablePointer<AnyCodable> {
let batchCommands = try objects.compactMap { (object) -> API.BatchCommand<AnyCodable, PointerType>? in
guard var objectable = object as? Objectable else {
return nil
Expand Down Expand Up @@ -193,7 +193,6 @@ internal extension API.NonParseBodyCommand {
guard let parseError = response.error else {
return .failure(ParseError(code: .unknownError, message: "unknown error"))
}

return .failure(parseError)
}
})
Expand All @@ -206,7 +205,7 @@ internal extension API.NonParseBodyCommand {
}
let batchCommand = BatchChildCommand(requests: batchCommands,
transaction: transaction)
return RESTBatchCommandTypeEncodable<AnyCodable>(method: .POST,
return RESTBatchCommandTypeEncodablePointer<AnyCodable>(method: .POST,
path: .batch,
body: batchCommand,
mapper: mapper)
Expand Down
10 changes: 5 additions & 5 deletions Sources/ParseSwift/API/BatchUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@ typealias ParseObjectBatchResponse<T> = [(Result<T, ParseError>)]
// swiftlint:disable line_length
typealias RESTBatchCommandType<T> = API.Command<ParseObjectBatchCommand<T>, ParseObjectBatchResponse<T>> where T: ParseObject

typealias ParseObjectBatchCommandNoBody<T> = BatchCommandNoBody<NoBody, NoBody>
typealias ParseObjectBatchCommandNoBody<T> = BatchCommandEncodable<NoBody, NoBody>
typealias ParseObjectBatchResponseNoBody<NoBody> = [(Result<Void, ParseError>)]
typealias RESTBatchCommandNoBodyType<T> = API.NonParseBodyCommand<ParseObjectBatchCommandNoBody<T>, ParseObjectBatchResponseNoBody<T>> where T: Encodable

typealias ParseObjectBatchCommandEncodable<T> = BatchChildCommand<T, PointerType> where T: Encodable
typealias ParseObjectBatchResponseEncodable<U> = [(Result<PointerType, ParseError>)]
typealias ParseObjectBatchCommandEncodablePointer<T> = BatchChildCommand<T, PointerType> where T: Encodable
typealias ParseObjectBatchResponseEncodablePointer<U> = [(Result<PointerType, ParseError>)]
// swiftlint:disable line_length
typealias RESTBatchCommandTypeEncodable<T> = API.NonParseBodyCommand<ParseObjectBatchCommandEncodable<T>, ParseObjectBatchResponseEncodable<Encodable>> where T: Encodable
typealias RESTBatchCommandTypeEncodablePointer<T> = API.NonParseBodyCommand<ParseObjectBatchCommandEncodablePointer<T>, ParseObjectBatchResponseEncodablePointer<Encodable>> where T: Encodable
// swiftlint:enable line_length

internal struct BatchCommand<T, U>: ParseEncodable where T: ParseEncodable {
let requests: [API.Command<T, U>]
var transaction: Bool
}

internal struct BatchCommandNoBody<T, U>: Encodable where T: Encodable {
internal struct BatchCommandEncodable<T, U>: Encodable where T: Encodable {
let requests: [API.NonParseBodyCommand<T, U>]
var transaction: Bool
}
Expand Down
14 changes: 3 additions & 11 deletions Sources/ParseSwift/Coding/ParseEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,23 +340,15 @@ internal class _ParseEncoder: JSONEncoder, Encoder {
throw ParseError(code: .unknownError,
message: "Found a circular dependency when encoding.")
}
if !self.collectChildren && codingPath.count > 0 {
valueToEncode = value
} else {
valueToEncode = pointer
}
valueToEncode = pointer
} else if let object = value as? Objectable {
if let pointer = try? PointerType(object) {
if let uniquePointer = self.uniquePointer,
uniquePointer.hasSameObjectId(as: pointer) {
throw ParseError(code: .unknownError,
message: "Found a circular dependency when encoding.")
}
if !self.collectChildren && codingPath.count > 0 {
valueToEncode = value
} else {
valueToEncode = pointer
}
valueToEncode = pointer
} else {
var object = object
if object.ACL == nil,
Expand Down Expand Up @@ -959,7 +951,7 @@ extension _ParseEncoder {
// swiftlint:disable:next force_cast
return (value as! NSDecimalNumber)
} else if value is _JSONStringDictionaryEncodableMarker {
//COREY: DON'T remove the force unwrap, it will crash the app
// COREY: DON'T remove the force cast, it will crash the app
// swiftlint:disable:next force_cast
return try self.box(value as! [String : Encodable])
} else if value is ParsePointer {
Expand Down
59 changes: 36 additions & 23 deletions Sources/ParseSwift/Objects/ParseInstallation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -945,14 +945,17 @@ public extension Sequence where Element: ParseInstallation {
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
var childObjects = [String: PointerType]()
var childFiles = [UUID: ParseFile]()
var commands = [API.Command<Self.Element, Self.Element>]()
var error: ParseError?

let installations = map { $0 }
for installation in installations {
try forEach {
let installation = $0
let group = DispatchGroup()
group.enter()
installation.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in
//If an error occurs, everything should be skipped
installation.ensureDeepSave(options: options,
// swiftlint:disable:next line_length
isShouldReturnIfChildObjectsFound: transaction) { (savedChildObjects, savedChildFiles, parseError) -> Void in
// If an error occurs, everything should be skipped
if parseError != nil {
error = parseError
}
Expand Down Expand Up @@ -984,12 +987,10 @@ public extension Sequence where Element: ParseInstallation {
if let error = error {
throw error
}
commands.append(try installation.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig))
}

var returnBatch = [(Result<Self.Element, ParseError>)]()
let commands = try map {
try $0.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)
}
let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit
try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit)
let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit)
Expand Down Expand Up @@ -1171,16 +1172,17 @@ public extension Sequence where Element: ParseInstallation {
var childObjects = [String: PointerType]()
var childFiles = [UUID: ParseFile]()
var error: ParseError?

var commands = [API.Command<Self.Element, Self.Element>]()
let installations = map { $0 }

for installation in installations {
let group = DispatchGroup()
group.enter()
installation
.ensureDeepSave(options: options,
// swiftlint:disable:next line_length
isShouldReturnIfChildObjectsFound: true) { (savedChildObjects, savedChildFiles, parseError) -> Void in
//If an error occurs, everything should be skipped
isShouldReturnIfChildObjectsFound: transaction) { (savedChildObjects, savedChildFiles, parseError) -> Void in
// If an error occurs, everything should be skipped
if parseError != nil {
error = parseError
}
Expand Down Expand Up @@ -1215,23 +1217,34 @@ public extension Sequence where Element: ParseInstallation {
}
return
}

do {
switch method {
case .save:
commands.append(
try installation.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)
)
case .create:
commands.append(installation.createCommand())
case .replace:
commands.append(try installation.replaceCommand())
case .update:
commands.append(try installation.updateCommand())
}
} catch {
callbackQueue.async {
if let parseError = error as? ParseError {
completion(.failure(parseError))
} else {
completion(.failure(.init(code: .unknownError, message: error.localizedDescription)))
}
}
return
}
}

do {
var returnBatch = [(Result<Self.Element, ParseError>)]()
let commands: [API.Command<Self.Element, Self.Element>]!
switch method {
case .save:
commands = try map {
try $0.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)
}
case .create:
commands = map { $0.createCommand() }
case .replace:
commands = try map { try $0.replaceCommand() }
case .update:
commands = try map { try $0.updateCommand() }
}

let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit
try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit)
Expand Down
Loading

0 comments on commit 675168e

Please sign in to comment.