From 8a8c0e161b0a192c21668b838ea6c95624434bd2 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 2 Jan 2022 15:54:41 -0500 Subject: [PATCH 1/5] feat: add isNull QueryConstraint and set/foreset null operations --- Sources/ParseSwift/Types/ParseOperation.swift | 39 +++++-- .../ParseSwift/Types/QueryConstraint.swift | 43 ++++++- Sources/ParseSwift/Types/QueryWhere.swift | 4 +- .../ParseSwiftTests/ParseOperationTests.swift | 108 ++++++++++++++---- Tests/ParseSwiftTests/ParseQueryTests.swift | 8 ++ 5 files changed, 162 insertions(+), 40 deletions(-) diff --git a/Sources/ParseSwift/Types/ParseOperation.swift b/Sources/ParseSwift/Types/ParseOperation.swift index 22052e0bf..ef732b706 100644 --- a/Sources/ParseSwift/Types/ParseOperation.swift +++ b/Sources/ParseSwift/Types/ParseOperation.swift @@ -20,6 +20,7 @@ public struct ParseOperation: Savable where T: ParseObject { var target: T var operations = [String: Encodable]() + var keysToNull = Set() public init(target: T) { self.target = target @@ -31,11 +32,15 @@ public struct ParseOperation: Savable where T: ParseObject { - key: A tuple consisting of the key and the respective KeyPath of the object. - value: The value to set it to. - returns: The updated operations. + - Note: Set the value to "nil" if you want it to be "null" on the Parse Server. */ - public func set(_ key: (String, WritableKeyPath), - value: W) throws -> Self where W: Encodable { + public func set(_ key: (String, WritableKeyPath), + value: W?) -> Self where W: Encodable { var mutableOperation = self - if !target[keyPath: key.1].isEqual(value) { + if value == nil && target[keyPath: key.1] != nil { + mutableOperation.keysToNull.insert(key.0) + mutableOperation.target[keyPath: key.1] = value + } else if !target[keyPath: key.1].isEqual(value) { mutableOperation.operations[key.0] = value mutableOperation.target[keyPath: key.1] = value } @@ -48,11 +53,16 @@ public struct ParseOperation: Savable where T: ParseObject { - key: A tuple consisting of the key and the respective KeyPath of the object. - value: The value to set it to. - returns: The updated operations. + - Note: Set the value to "nil" if you want it to be "null" on the Parse Server. */ - public func forceSet(_ key: (String, WritableKeyPath), - value: W) throws -> Self where W: Encodable { + public func forceSet(_ key: (String, WritableKeyPath), + value: W?) -> Self where W: Encodable { var mutableOperation = self - mutableOperation.operations[key.0] = value + if value != nil { + mutableOperation.operations[key.0] = value + } else { + mutableOperation.keysToNull.insert(key.0) + } mutableOperation.target[keyPath: key.1] = value return mutableOperation } @@ -93,7 +103,7 @@ public struct ParseOperation: Savable where T: ParseObject { - returns: The updated operations. */ public func addUnique(_ key: (String, WritableKeyPath), - objects: [V]) throws -> Self where V: Encodable, V: Hashable { + objects: [V]) -> Self where V: Encodable, V: Hashable { var mutableOperation = self mutableOperation.operations[key.0] = AddUnique(objects: objects) var values = target[keyPath: key.1] @@ -111,7 +121,7 @@ public struct ParseOperation: Savable where T: ParseObject { - returns: The updated operations. */ public func addUnique(_ key: (String, WritableKeyPath), - objects: [V]) throws -> Self where V: Encodable, V: Hashable { + objects: [V]) -> Self where V: Encodable, V: Hashable { var mutableOperation = self mutableOperation.operations[key.0] = AddUnique(objects: objects) var values = target[keyPath: key.1] ?? [] @@ -141,7 +151,7 @@ public struct ParseOperation: Savable where T: ParseObject { - returns: The updated operations. */ public func add(_ key: (String, WritableKeyPath), - objects: [V]) throws -> Self where V: Encodable { + objects: [V]) -> Self where V: Encodable { var mutableOperation = self mutableOperation.operations[key.0] = Add(objects: objects) var values = target[keyPath: key.1] @@ -158,7 +168,7 @@ public struct ParseOperation: Savable where T: ParseObject { - returns: The updated operations. */ public func add(_ key: (String, WritableKeyPath), - objects: [V]) throws -> Self where V: Encodable { + objects: [V]) -> Self where V: Encodable { var mutableOperation = self mutableOperation.operations[key.0] = Add(objects: objects) var values = target[keyPath: key.1] ?? [] @@ -237,7 +247,7 @@ public struct ParseOperation: Savable where T: ParseObject { - returns: The updated operations. */ public func remove(_ key: (String, WritableKeyPath), - objects: [V]) throws -> Self where V: Encodable, V: Hashable { + objects: [V]) -> Self where V: Encodable, V: Hashable { var mutableOperation = self mutableOperation.operations[key.0] = Remove(objects: objects) let values = target[keyPath: key.1] @@ -258,7 +268,7 @@ public struct ParseOperation: Savable where T: ParseObject { - returns: The updated operations. */ public func remove(_ key: (String, WritableKeyPath), - objects: [V]) throws -> Self where V: Encodable, V: Hashable { + objects: [V]) -> Self where V: Encodable, V: Hashable { var mutableOperation = self mutableOperation.operations[key.0] = Remove(objects: objects) let values = target[keyPath: key.1] @@ -357,6 +367,11 @@ public struct ParseOperation: Savable where T: ParseObject { let encoder = container.superEncoder(forKey: .key(key)) try value.encode(to: encoder) } + try keysToNull.forEach { key in + let encoder = container.superEncoder(forKey: .key(key)) + var container = encoder.singleValueContainer() + try container.encodeNil() + } } } diff --git a/Sources/ParseSwift/Types/QueryConstraint.swift b/Sources/ParseSwift/Types/QueryConstraint.swift index 8fa6eed1b..8af32527a 100644 --- a/Sources/ParseSwift/Types/QueryConstraint.swift +++ b/Sources/ParseSwift/Types/QueryConstraint.swift @@ -9,7 +9,7 @@ import Foundation /// Used to constrain a query. -public struct QueryConstraint: Encodable, Equatable { +public struct QueryConstraint: Encodable, Hashable { enum Comparator: String, CodingKey, Encodable { case lessThan = "$lt" case lessThanOrEqualTo = "$lte" @@ -48,24 +48,46 @@ public struct QueryConstraint: Encodable, Equatable { } var key: String - var value: Encodable + var value: Encodable? var comparator: Comparator? + var isNull: Bool = false public func encode(to encoder: Encoder) throws { - if let value = value as? Date { + if isNull { + var container = encoder.singleValueContainer() + try container.encodeNil() + } else if let value = value as? Date { // Parse uses special case for date try value.parseRepresentation.encode(to: encoder) } else { - try value.encode(to: encoder) + try value?.encode(to: encoder) } } public static func == (lhs: QueryConstraint, rhs: QueryConstraint) -> Bool { guard lhs.key == rhs.key, - lhs.comparator == rhs.comparator else { + lhs.comparator == rhs.comparator, + lhs.isNull == rhs.isNull else { return false } - return lhs.value.isEqual(rhs.value) + guard let lhsValue = lhs.value, + let rhsValue = rhs.value else { + guard lhs.value == nil, + rhs.value == nil else { + return false + } + return true + } + return lhsValue.isEqual(rhsValue) + } + + public func hash(into hasher: inout Hasher) { + do { + let encodedData = try ParseCoding.jsonEncoder().encode(self) + hasher.combine(encodedData) + } catch { + hasher.combine(0) + } } } @@ -150,6 +172,15 @@ public func != (key: String, value: T) throws -> QueryConstraint where T: Par try QueryConstraint(key: key, value: value.toPointer(), comparator: .notEqualTo) } +/** + Add a constraint that requires that a key is equal to **null**. + - parameter key: The key that the value is stored in. + - returns: The same instance of `QueryConstraint` as the receiver. + */ +public func isNull (key: String) -> QueryConstraint { + QueryConstraint(key: key, isNull: true) +} + internal struct InQuery: Encodable where T: ParseObject { let query: Query var className: String { diff --git a/Sources/ParseSwift/Types/QueryWhere.swift b/Sources/ParseSwift/Types/QueryWhere.swift index 4f79459e6..695b676f1 100644 --- a/Sources/ParseSwift/Types/QueryWhere.swift +++ b/Sources/ParseSwift/Types/QueryWhere.swift @@ -9,11 +9,11 @@ import Foundation struct QueryWhere: Encodable, Equatable { - var constraints = [String: [QueryConstraint]]() + var constraints = [String: Set]() mutating func add(_ constraint: QueryConstraint) { var existing = constraints[constraint.key] ?? [] - existing.append(constraint) + existing.insert(constraint) constraints[constraint.key] = existing } diff --git a/Tests/ParseSwiftTests/ParseOperationTests.swift b/Tests/ParseSwiftTests/ParseOperationTests.swift index f5933c6a6..fed4bc66f 100644 --- a/Tests/ParseSwiftTests/ParseOperationTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationTests.swift @@ -20,8 +20,8 @@ class ParseOperationTests: XCTestCase { var score: Double? //: Your own properties - var points: Int - var members = [String]() + var points: Int? + var members: [String] = [String]() var levels: [String]? var previous: [Level]? var next: [Level] @@ -138,13 +138,14 @@ class ParseOperationTests: XCTestCase { XCTFail("Should unwrap dates") return } - guard let originalUpdatedAt = scoreOnServer.updatedAt else { + guard let originalUpdatedAt = scoreOnServer.updatedAt, + let originalPoints = scoreOnServer.points else { XCTFail("Should unwrap dates") return } XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) XCTAssertEqual(saved.ACL, scoreOnServer.ACL) - XCTAssertEqual(saved.points+1, scoreOnServer.points) + XCTAssertEqual(saved.points, originalPoints-1) } catch { XCTFail(error.localizedDescription) } @@ -187,14 +188,15 @@ class ParseOperationTests: XCTestCase { expectation1.fulfill() return } - guard let originalUpdatedAt = scoreOnServer.updatedAt else { + guard let originalUpdatedAt = scoreOnServer.updatedAt, + let originalPoints = scoreOnServer.points else { XCTFail("Should unwrap dates") expectation1.fulfill() return } XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) XCTAssertEqual(saved.ACL, scoreOnServer.ACL) - XCTAssertEqual(saved.points+1, scoreOnServer.points) + XCTAssertEqual(saved.points, originalPoints-1) case .failure(let error): XCTFail(error.localizedDescription) } @@ -206,7 +208,7 @@ class ParseOperationTests: XCTestCase { func testSaveSet() throws { // swiftlint:disable:this function_body_length var score = GameScore(points: 10) score.objectId = "yarr" - let operations = try score.operation + let operations = score.operation .set(("points", \.points), value: 15) var scoreOnServer = score @@ -245,10 +247,52 @@ class ParseOperationTests: XCTestCase { } } + func testSaveSetToNull() throws { // swiftlint:disable:this function_body_length + var score = GameScore(points: 10) + score.objectId = "yarr" + let operations = score.operation + .set(("points", \.points), value: nil) + + var scoreOnServer = score + scoreOnServer.points = nil + scoreOnServer.updatedAt = Date() + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try operations.save() + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + guard let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(saved.ACL, scoreOnServer.ACL) + XCTAssertEqual(saved.points, scoreOnServer.points) + } catch { + XCTFail(error.localizedDescription) + } + } + func testSaveSetAsyncMainQueue() throws { var score = GameScore(points: 10) score.objectId = "yarr" - let operations = try score.operation + let operations = score.operation .set(("points", \.points), value: 15) var scoreOnServer = score @@ -324,7 +368,7 @@ class ParseOperationTests: XCTestCase { func testAddKeypath() throws { let score = GameScore(points: 10) - let operations = try score.operation + let operations = score.operation .add(("test", \.members), objects: ["hello"]) let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"Add\"}}" let encoded = try ParseCoding.parseEncoder() @@ -336,7 +380,7 @@ class ParseOperationTests: XCTestCase { func testAddOptionalKeypath() throws { let score = GameScore(points: 10) - let operations = try score.operation + let operations = score.operation .add(("test", \.levels), objects: ["hello"]) let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"Add\"}}" let encoded = try ParseCoding.parseEncoder() @@ -358,7 +402,7 @@ class ParseOperationTests: XCTestCase { func testAddUniqueKeypath() throws { let score = GameScore(points: 10) - let operations = try score.operation + let operations = score.operation .addUnique(("test", \.members), objects: ["hello"]) let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"AddUnique\"}}" let encoded = try ParseCoding.parseEncoder() @@ -369,7 +413,7 @@ class ParseOperationTests: XCTestCase { func testAddUniqueOptionalKeypath() throws { let score = GameScore(points: 10) - let operations = try score.operation + let operations = score.operation .addUnique(("test", \.levels), objects: ["hello"]) let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"AddUnique\"}}" let encoded = try ParseCoding.parseEncoder() @@ -433,7 +477,7 @@ class ParseOperationTests: XCTestCase { func testRemoveKeypath() throws { let score = GameScore(points: 10) - let operations = try score.operation + let operations = score.operation .remove(("test", \.members), objects: ["hello"]) let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"Remove\"}}" let encoded = try ParseCoding.parseEncoder() @@ -444,7 +488,7 @@ class ParseOperationTests: XCTestCase { func testRemoveOptionalKeypath() throws { let score = GameScore(points: 10) - let operations = try score.operation + let operations = score.operation .remove(("test", \.levels), objects: ["hello"]) let expected = "{\"test\":{\"objects\":[\"hello\"],\"__op\":\"Remove\"}}" let encoded = try ParseCoding.parseEncoder() @@ -497,7 +541,7 @@ class ParseOperationTests: XCTestCase { func testSet() throws { let score = GameScore(points: 10) - let operations = try score.operation.set(("points", \.points), value: 15) + let operations = score.operation.set(("points", \.points), value: 15) .set(("levels", \.levels), value: ["hello"]) let expected = "{\"points\":15,\"levels\":[\"hello\"]}" let encoded = try ParseCoding.parseEncoder() @@ -507,20 +551,28 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(operations.target.points, 15) var level = Level(level: 12) level.members = ["hello", "world"] - let operations2 = try score.operation.set(("previous", \.previous), value: [level]) + let operations2 = score.operation.set(("previous", \.previous), value: [level]) let expected2 = "{\"previous\":[{\"level\":12,\"members\":[\"hello\",\"world\"]}]}" let encoded2 = try ParseCoding.parseEncoder() .encode(operations2) let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) XCTAssertEqual(decoded2, expected2) XCTAssertEqual(operations2.target.previous, [level]) + let operations3 = score.operation.set(("points", \.points), value: nil) + .set(("levels", \.levels), value: ["hello"]) + let expected3 = "{\"points\":null,\"levels\":[\"hello\"]}" + let encoded3 = try ParseCoding.parseEncoder() + .encode(operations3) + let decoded3 = try XCTUnwrap(String(data: encoded3, encoding: .utf8)) + XCTAssertEqual(decoded3, expected3) + XCTAssertNil(operations3.target.points) } func testObjectIdSet() throws { var score = GameScore() score.objectId = "test" score.levels = nil - let operations = try score.operation.set(("objectId", \.objectId), value: "test") + let operations = score.operation.set(("objectId", \.objectId), value: "test") let expected = "{}" let encoded = try ParseCoding.parseEncoder() .encode(operations) @@ -531,7 +583,7 @@ class ParseOperationTests: XCTestCase { level.members = ["hello", "world"] score.previous = [level] let expected2 = "{}" - let operations2 = try score.operation.set(("previous", \.previous), value: [level]) + let operations2 = score.operation.set(("previous", \.previous), value: [level]) let encoded2 = try ParseCoding.parseEncoder() .encode(operations2) let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) @@ -542,22 +594,38 @@ class ParseOperationTests: XCTestCase { func testUnchangedSet() throws { let score = GameScore(points: 10) - let operations = try score.operation.set(("points", \.points), value: 10) + let operations = score.operation.set(("points", \.points), value: 10) let expected = "{}" let encoded = try ParseCoding.parseEncoder() .encode(operations) let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) XCTAssertEqual(decoded, expected) + let operations2 = score.operation + .set(("levels", \.levels), value: nil) + let expected2 = "{}" + let encoded2 = try ParseCoding.parseEncoder() + .encode(operations2) + let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) + XCTAssertEqual(decoded2, expected2) + XCTAssertNil(operations2.target.levels) } func testForceSet() throws { let score = GameScore(points: 10) - let operations = try score.operation.forceSet(("points", \.points), value: 10) + let operations = score.operation.forceSet(("points", \.points), value: 10) let expected = "{\"points\":10}" let encoded = try ParseCoding.parseEncoder() .encode(operations) let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) XCTAssertEqual(decoded, expected) + let operations2 = score.operation + .forceSet(("points", \.points), value: nil) + let expected2 = "{\"points\":null}" + let encoded2 = try ParseCoding.parseEncoder() + .encode(operations2) + let decoded2 = try XCTUnwrap(String(data: encoded2, encoding: .utf8)) + XCTAssertEqual(decoded2, expected2) + XCTAssertNil(operations2.target.points) } func testUnset() throws { diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index d3b358b95..6d88f004b 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -107,10 +107,18 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length substring: "world"), "points" > 101, "createdAt" > Date()]) + let query4 = GameScore.query([containsString(key: "hello", + substring: "world"), + "points" > 101, + "createdAt" > Date(), + isNull(key: "points")]) + let query5 = GameScore.query(isNull(key: "points")) XCTAssertEqual(query1, query1) XCTAssertEqual(query2, query2) XCTAssertNotEqual(query1, query2) XCTAssertNotEqual(query2, query3) + XCTAssertNotEqual(query3, query4) + XCTAssertEqual(query5, query5) } func testEndPoints() { From ab410a00817e669e0f66479a6d2ff462b844d29e Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 2 Jan 2022 19:28:05 -0500 Subject: [PATCH 2/5] add more tests --- CHANGELOG.md | 12 +++-- Tests/ParseSwiftTests/ParseQueryTests.swift | 60 +++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c45601cd..785578fc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,14 @@ ### 3.0.0 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.5.1...3.0.0) -__Improvements__ -- (Breaking Change) Adds options to matchesText query constraint along with the ability to see matching score. The compiler should recommend the new score property to all ParseObjects ([#306](https://github.com/parse-community/Parse-Swift/pull/306)), thanks to [Corey Baker](https://github.com/cbaker6). +__New features__ +- (Breaking Change) Adds options to matchesText QueryConstraint along with the ability to see matching score. The compiler should recommend the new score property to all ParseObjects ([#306](https://github.com/parse-community/Parse-Swift/pull/306)), thanks to [Corey Baker](https://github.com/cbaker6). - Adds withCount query ([#306](https://github.com/parse-community/Parse-Swift/pull/306)), thanks to [Corey Baker](https://github.com/cbaker6). - Adds auth support for GitHub, Google, and LinkedIn ([#307](https://github.com/parse-community/Parse-Swift/pull/307)), thanks to [Corey Baker](https://github.com/cbaker6). +- Adds isNull QueryConstraint along with the ability set/forceSet null using ParseOperation ([#308](https://github.com/parse-community/Parse-Swift/pull/308)), thanks to [Corey Baker](https://github.com/cbaker6). + +__Improvements__ +- Improve QueryWhere by making at a set of QueryConstraint's instead of any array. This dedupes the same constraint when encoding the query; improving the encoding speed when the same constraints are added ([#308](https://github.com/parse-community/Parse-Swift/pull/308)), thanks to [Corey Baker](https://github.com/cbaker6). ### 2.5.1 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.5.0...2.5.1) @@ -26,14 +30,14 @@ __Fixes__ ### 2.5.0 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.4.0...2.5.0) -__Improvements__ +__New features__ - Added create(), replace(), update(), createAll(), replaceAll(), and updateAll() to ParseObjects. Currently, update() and updateAll() are unavaivalble due to limitations of PATCH on the Parse Server ([#299](https://github.com/parse-community/Parse-Swift/pull/299)), thanks to [Corey Baker](https://github.com/cbaker6). - Added convenience methods to convert ParseObject's to Pointer's for QueryConstraint's: !=, containedIn, notContainedIn, containedBy, containsAll ([#298](https://github.com/parse-community/Parse-Swift/pull/298)), thanks to [Corey Baker](https://github.com/cbaker6). ### 2.4.0 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/2.3.1...2.4.0) -__Improvements__ +__New features__ - Added additional methods to ParseRelation to make it easier to create and query relations ([#294](https://github.com/parse-community/Parse-Swift/pull/294)), thanks to [Corey Baker](https://github.com/cbaker6). - Enable async/await for iOS13, tvOS13, watchOS6, and macOS10_15. All async/await methods are MainActor's. Requires Xcode 13.2 or above to use async/await. Not compatible with Xcode 13.0/1, will need to upgrade to 13.2+. Still works with Xcode 11/12 ([#278](https://github.com/parse-community/Parse-Swift/pull/278)), thanks to [Corey Baker](https://github.com/cbaker6). diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index 6d88f004b..d2ed9cc77 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -113,12 +113,14 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length "createdAt" > Date(), isNull(key: "points")]) let query5 = GameScore.query(isNull(key: "points")) + let query6 = GameScore.query(isNull(key: "hello")) XCTAssertEqual(query1, query1) XCTAssertEqual(query2, query2) XCTAssertNotEqual(query1, query2) XCTAssertNotEqual(query2, query3) XCTAssertNotEqual(query3, query4) XCTAssertEqual(query5, query5) + XCTAssertNotEqual(query5, query6) } func testEndPoints() { @@ -1193,6 +1195,16 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(query.debugDescription, expected) } + func testWhereKeyEqualToParseObjectDuplicateConstraint() throws { + var compareObject = GameScore(points: 11) + compareObject.objectId = "hello" + let query = try GameScore.query("yolo" == compareObject, + "yolo" == compareObject) + // swiftlint:disable:next line_length + let expected = "GameScore ({\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"yolo\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"hello\"}}})" + XCTAssertEqual(query.debugDescription, expected) + } + func testWhereKeyEqualToParseObjectPointer() throws { var compareObject = GameScore(points: 11) compareObject.objectId = "hello" @@ -1211,6 +1223,54 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let expected = "GameScore ({\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"yolo\":{\"$ne\":{\"__type\":\"Pointer\",\"className\":\"GameScore\",\"objectId\":\"hello\"}}}})" XCTAssertEqual(query.debugDescription, expected) } + + func testWhereKeyIsNull() throws { + var compareObject = GameScore(points: 11) + compareObject.objectId = "hello" + let query = GameScore.query(isNull(key: "yolo")) + let expected = "GameScore ({\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"yolo\":null}})" + XCTAssertEqual(query.debugDescription, expected) + } + + func testWhereKeyIsNullDuplicateConstraint() throws { + var compareObject = GameScore(points: 11) + compareObject.objectId = "hello" + let query = GameScore.query(isNull(key: "yolo"), + isNull(key: "yolo")) + let expected = "GameScore ({\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"yolo\":null}})" + XCTAssertEqual(query.debugDescription, expected) + } + + func testWhereKeyIsNullMultipleKey() throws { + var compareObject = GameScore(points: 11) + compareObject.objectId = "hello" + let query = GameScore.query(isNull(key: "yolo"), + isNull(key: "hello")) + // swiftlint:disable:next line_length + let expected = "GameScore ({\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"hello\":null,\"yolo\":null}})" + XCTAssertEqual(query.debugDescription, expected) + } + + func testWhereKeyComparatorMultipleSameKey() throws { + var compareObject = GameScore(points: 11) + compareObject.objectId = "hello" + let query = GameScore.query("yolo" >= 5, + "yolo" <= 10) + // swiftlint:disable:next line_length + let expected = "GameScore ({\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"yolo\":{\"$lte\":10,\"$gte\":5}}})" + XCTAssertEqual(query.debugDescription, expected) + } + + func testWhereKeyComparatorMultipleSameKeyDuplicate() throws { + var compareObject = GameScore(points: 11) + compareObject.objectId = "hello" + let query = GameScore.query("yolo" >= 5, + "yolo" >= 5, + "yolo" <= 10) + // swiftlint:disable:next line_length + let expected = "GameScore ({\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"yolo\":{\"$lte\":10,\"$gte\":5}}})" + XCTAssertEqual(query.debugDescription, expected) + } #endif func testWhereKeyNotEqualTo() { From a3ad717b0990ccc324abaf20e63250dcb5351ed2 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 2 Jan 2022 20:48:16 -0500 Subject: [PATCH 3/5] Add notNull constraint and playgrounds --- .../Contents.swift | 15 +++++++- .../Contents.swift | 37 +++++++++++++++++++ .../ParseSwift/Types/QueryConstraint.swift | 9 +++++ Tests/ParseSwiftTests/ParseQueryTests.swift | 8 ++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift index 3249d4ac9..2a32f9f92 100644 --- a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift @@ -23,6 +23,7 @@ struct GameScore: ParseObject { //: Your own properties. var points: Int? = 0 + var name: String? } //: It's recommended to place custom initializers in an extension @@ -43,7 +44,8 @@ extension GameScore { //: First lets create another GameScore. let savedScore: GameScore! do { - savedScore = try GameScore(points: 102).save() + let score = GameScore(points: 102, name: "player1") + savedScore = try score.save() } catch { savedScore = nil assertionFailure("Error saving: \(error)") @@ -80,6 +82,17 @@ do { print(error) } +//: There may be cases where you want to set/forceSet a value to null +//: instead of unsetting +let setToNullOperation = savedScore + .operation.set(("name", \.name), value: nil) +do { + let updatedScore = try setToNullOperation.save() + print("Updated score: \(updatedScore). Check the new score on Parse Dashboard.") +} catch { + print(error) +} + //: There are other operations: set/forceSet/unset/add/remove, etc. objects from `ParseObject`s. //: In fact, the `users` and `roles` relations from `ParseRoles` used the add/remove operations. //: Multiple operations can be chained together. See: diff --git a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift index 1f9d28f6a..7a0e27d21 100644 --- a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift @@ -154,6 +154,24 @@ query3.find { results in } } +//: Get the same results as the previous query using `isNull`. +var anotherQuery3 = GameScore.query("points" > 50, isNull(key: "location")) +anotherQuery3.find { results in + switch results { + case .success(let scores): + + scores.forEach { (score) in + print(""" + Someone has a points value of \"\(String(describing: score.points))\" + with no geopoint \(String(describing: score.location)) + """) + } + + case .failure(let error): + assertionFailure("Error querying: \(error)") + } +} + //: If you want to query for points > 9 and have a `ParseGeoPoint`. var query4 = GameScore.query("points" > 9, exists(key: "location")) query4.find { results in @@ -173,6 +191,25 @@ query4.find { results in } } +//: Get the same results as the previous query using `notNull`. +var anotherQuery4 = GameScore.query("points" > 9, notNull(key: "location")) +anotherQuery4.find { results in + switch results { + case .success(let scores): + + assert(scores.count >= 1) + scores.forEach { (score) in + print(""" + Someone has a points of \"\(String(describing: score.points))\" + with geopoint \(String(describing: score.location)) + """) + } + + case .failure(let error): + assertionFailure("Error querying: \(error)") + } +} + let query5 = GameScore.query("points" == 50) let query6 = GameScore.query("points" == 200) diff --git a/Sources/ParseSwift/Types/QueryConstraint.swift b/Sources/ParseSwift/Types/QueryConstraint.swift index 8af32527a..4f1501565 100644 --- a/Sources/ParseSwift/Types/QueryConstraint.swift +++ b/Sources/ParseSwift/Types/QueryConstraint.swift @@ -181,6 +181,15 @@ public func isNull (key: String) -> QueryConstraint { QueryConstraint(key: key, isNull: true) } +/** + Add a constraint that requires that a key is not equal to **null**. + - parameter key: The key that the value is stored in. + - returns: The same instance of `QueryConstraint` as the receiver. + */ +public func notNull (key: String) -> QueryConstraint { + QueryConstraint(key: key, comparator: .notEqualTo, isNull: true) +} + internal struct InQuery: Encodable where T: ParseObject { let query: Query var className: String { diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index d2ed9cc77..bb9101ebd 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -1232,6 +1232,14 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(query.debugDescription, expected) } + func testWhereKeyNotNull() throws { + var compareObject = GameScore(points: 11) + compareObject.objectId = "hello" + let query = GameScore.query(notNull(key: "yolo")) + let expected = "GameScore ({\"limit\":100,\"skip\":0,\"_method\":\"GET\",\"where\":{\"yolo\":{\"$ne\":null}}})" + XCTAssertEqual(query.debugDescription, expected) + } + func testWhereKeyIsNullDuplicateConstraint() throws { var compareObject = GameScore(points: 11) compareObject.objectId = "hello" From d7ba63e1e45e364e19501ce5913cd11a8636dd20 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 2 Jan 2022 21:39:47 -0500 Subject: [PATCH 4/5] Update playgrounds --- .../Contents.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift index 2a32f9f92..04ba14e0f 100644 --- a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift @@ -72,6 +72,13 @@ do { print(error) } +//: Query all scores who have a name. +let query1 = GameScore.query(notNull(key: "name")) +let results1 = try query1.find() +results1.forEach { score in + print("Found score with a name: \(score)") +} + //: You can also remove a value for a property using unset. let unsetOperation = savedScore .operation.unset(("points", \.points)) @@ -93,6 +100,13 @@ do { print(error) } +//: Query synchronously (not preferred - all operations on main queue). +let query2 = GameScore.query(isNull(key: "name")) +let results2 = try query2.find() +results2.forEach { score in + print("Found score with name is null: \(score)") +} + //: There are other operations: set/forceSet/unset/add/remove, etc. objects from `ParseObject`s. //: In fact, the `users` and `roles` relations from `ParseRoles` used the add/remove operations. //: Multiple operations can be chained together. See: From 04d74db93b97d37411d479c0208bc80c0ce23957 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 2 Jan 2022 22:28:47 -0500 Subject: [PATCH 5/5] update docs --- .../Contents.swift | 28 ++++++++++--- .../Contents.swift | 8 ++-- .../ParseSwift/Types/QueryConstraint.swift | 40 +++++++++---------- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift index 04ba14e0f..c53882961 100644 --- a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift @@ -72,13 +72,22 @@ do { print(error) } -//: Query all scores who have a name. +//: Query all scores whose is null or undefined. let query1 = GameScore.query(notNull(key: "name")) let results1 = try query1.find() +print("Total found: \(results1.count)") results1.forEach { score in print("Found score with a name: \(score)") } +//: Query all scores whose name is undefined. +let query2 = GameScore.query(exists(key: "name")) +let results2 = try query2.find() +print("Total found: \(results2.count)") +results2.forEach { score in + print("Found score with a name: \(score)") +} + //: You can also remove a value for a property using unset. let unsetOperation = savedScore .operation.unset(("points", \.points)) @@ -100,13 +109,22 @@ do { print(error) } -//: Query synchronously (not preferred - all operations on main queue). -let query2 = GameScore.query(isNull(key: "name")) -let results2 = try query2.find() -results2.forEach { score in +//: Query all scores whose name is null or undefined. +let query3 = GameScore.query(isNull(key: "name")) +let results3 = try query3.find() +print("Total found: \(results3.count)") +results3.forEach { score in print("Found score with name is null: \(score)") } +//: Query all scores whose name is undefined. +let query4 = GameScore.query(doesNotExist(key: "name")) +let results4 = try query4.find() +print("Total found: \(results4.count)") +results4.forEach { score in + print("Found score with name does not exist: \(score)") +} + //: There are other operations: set/forceSet/unset/add/remove, etc. objects from `ParseObject`s. //: In fact, the `users` and `roles` relations from `ParseRoles` used the add/remove operations. //: Multiple operations can be chained together. See: diff --git a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift index 7a0e27d21..f5a9c5646 100644 --- a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift @@ -136,7 +136,7 @@ query2.find { results in } } -//: If you want to query for points > 50 and don't have a `ParseGeoPoint`. +//: If you want to query for points > 50 and whose location is undefined. var query3 = GameScore.query("points" > 50, doesNotExist(key: "location")) query3.find { results in switch results { @@ -154,7 +154,7 @@ query3.find { results in } } -//: Get the same results as the previous query using `isNull`. +//: If you want to query for points > 50 and whose location is null or undefined. var anotherQuery3 = GameScore.query("points" > 50, isNull(key: "location")) anotherQuery3.find { results in switch results { @@ -172,7 +172,7 @@ anotherQuery3.find { results in } } -//: If you want to query for points > 9 and have a `ParseGeoPoint`. +//: If you want to query for points > 9 and whose location is not undefined. var query4 = GameScore.query("points" > 9, exists(key: "location")) query4.find { results in switch results { @@ -191,7 +191,7 @@ query4.find { results in } } -//: Get the same results as the previous query using `notNull`. +//: Get the same results as the previous query whose location is not null or undefined. var anotherQuery4 = GameScore.query("points" > 9, notNull(key: "location")) anotherQuery4.find { results in switch results { diff --git a/Sources/ParseSwift/Types/QueryConstraint.swift b/Sources/ParseSwift/Types/QueryConstraint.swift index 4f1501565..f9fe9d87d 100644 --- a/Sources/ParseSwift/Types/QueryConstraint.swift +++ b/Sources/ParseSwift/Types/QueryConstraint.swift @@ -172,24 +172,6 @@ public func != (key: String, value: T) throws -> QueryConstraint where T: Par try QueryConstraint(key: key, value: value.toPointer(), comparator: .notEqualTo) } -/** - Add a constraint that requires that a key is equal to **null**. - - parameter key: The key that the value is stored in. - - returns: The same instance of `QueryConstraint` as the receiver. - */ -public func isNull (key: String) -> QueryConstraint { - QueryConstraint(key: key, isNull: true) -} - -/** - Add a constraint that requires that a key is not equal to **null**. - - parameter key: The key that the value is stored in. - - returns: The same instance of `QueryConstraint` as the receiver. - */ -public func notNull (key: String) -> QueryConstraint { - QueryConstraint(key: key, comparator: .notEqualTo, isNull: true) -} - internal struct InQuery: Encodable where T: ParseObject { let query: Query var className: String { @@ -708,7 +690,25 @@ public func hasSuffix(key: String, suffix: String, modifiers: String? = nil) -> } /** - Add a constraint that requires a particular key exists. + Add a constraint that requires that a key is equal to **null** or **undefined**. + - parameter key: The key that the value is stored in. + - returns: The same instance of `QueryConstraint` as the receiver. + */ +public func isNull (key: String) -> QueryConstraint { + QueryConstraint(key: key, isNull: true) +} + +/** + Add a constraint that requires that a key is not equal to **null** or **undefined**. + - parameter key: The key that the value is stored in. + - returns: The same instance of `QueryConstraint` as the receiver. + */ +public func notNull (key: String) -> QueryConstraint { + QueryConstraint(key: key, comparator: .notEqualTo, isNull: true) +} + +/** + Add a constraint that requires a particular key to be equal to **undefined**. - parameter key: The key that should exist. - returns: The resulting `QueryConstraint`. */ @@ -717,7 +717,7 @@ public func exists(key: String) -> QueryConstraint { } /** - Add a constraint that requires a key not exist. + Add a constraint that requires a key to not be equal to **undefined**. - parameter key: The key that should not exist. - returns: The resulting `QueryConstraint`. */