Skip to content

Commit fa770b1

Browse files
authored
feat(datastore): add notContains query operator (#2789)
1 parent 143c7c0 commit fa770b1

File tree

6 files changed

+219
-0
lines changed

6 files changed

+219
-0
lines changed

Amplify/Categories/DataStore/Query/ModelKey.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ extension CodingKey where Self: ModelKey {
5656
return key.contains(value)
5757
}
5858

59+
// MARK: - not contains
60+
public func notContains(_ value: String) -> QueryPredicateOperation {
61+
return field(stringValue).notContains(value)
62+
}
63+
5964
// MARK: - eq
6065

6166
public func eq(_ value: Persistable?) -> QueryPredicateOperation {

Amplify/Categories/DataStore/Query/QueryField.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public protocol QueryFieldOperation {
3434
func beginsWith(_ value: String) -> QueryPredicateOperation
3535
func between(start: Persistable, end: Persistable) -> QueryPredicateOperation
3636
func contains(_ value: String) -> QueryPredicateOperation
37+
func notContains(_ value: String) -> QueryPredicateOperation
3738
func eq(_ value: Persistable?) -> QueryPredicateOperation
3839
func eq(_ value: EnumPersistable) -> QueryPredicateOperation
3940
func ge(_ value: Persistable) -> QueryPredicateOperation
@@ -84,6 +85,11 @@ public struct QueryField: QueryFieldOperation {
8485
return key.contains(value)
8586
}
8687

88+
// MARK: - not contains
89+
public func notContains(_ value: String) -> QueryPredicateOperation {
90+
return QueryPredicateOperation(field: name, operator: .notContains(value))
91+
}
92+
8793
// MARK: - eq
8894

8995
public func eq(_ value: Persistable?) -> QueryPredicateOperation {

Amplify/Categories/DataStore/Query/QueryOperator.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public enum QueryOperator {
1515
case greaterOrEqual(_ value: Persistable)
1616
case greaterThan(_ value: Persistable)
1717
case contains(_ value: String)
18+
case notContains(_ value: String)
1819
case between(start: Persistable, end: Persistable)
1920
case beginsWith(_ value: String)
2021

@@ -37,6 +38,10 @@ public enum QueryOperator {
3738
return targetString.contains(predicateString)
3839
}
3940
return false
41+
case .notContains(let predicateString):
42+
if let targetString = target as? String {
43+
return !targetString.contains(predicateString)
44+
}
4045
case .between(let start, let end):
4146
return PersistableHelper.isBetween(start, end, target)
4247
case .beginsWith(let predicateValue):

AmplifyPlugins/Core/AWSPluginsCore/Model/Support/QueryPredicate+GraphQL.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ extension QueryOperator {
185185
return "between"
186186
case .beginsWith:
187187
return "beginsWith"
188+
case .notContains:
189+
return "notContains"
188190
}
189191
}
190192

@@ -208,6 +210,8 @@ extension QueryOperator {
208210
return [start.graphQLValue(), end.graphQLValue()]
209211
case .beginsWith(let value):
210212
return value
213+
case .notContains(let value):
214+
return value
211215
}
212216
}
213217
}

AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/QueryPredicate+SQLite.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ extension QueryOperator {
2929
return "between ? and ?"
3030
case .beginsWith, .contains:
3131
return "like ?"
32+
case .notContains:
33+
return "not like ?"
3234
}
3335
}
3436

@@ -47,6 +49,8 @@ extension QueryOperator {
4749
return ["%\(value)%"]
4850
case .beginsWith(let value):
4951
return ["\(value)%"]
52+
case .notContains(let value):
53+
return ["%\(value)%"]
5054
}
5155
}
5256
}

AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/AWSDataStoreLocalStoreTests.swift

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,201 @@ class AWSDataStoreLocalStoreTests: LocalStoreIntegrationTestBase {
333333
XCTAssertEqual(posts.count, 0)
334334
}
335335

336+
337+
/// Given: 5 `Posts` with titles containing 0...4
338+
/// When: Querying `Posts` where title`notContains("1")`
339+
/// Then: 4 posts should be returned, none of which contain `"1"` in the title
340+
func testQueryNotContains() async throws {
341+
setUp(withModels: TestModelRegistration())
342+
_ = try await setUpLocalStore(numberOfPosts: 5)
343+
let posts = try await Amplify.DataStore.query(Post.self)
344+
XCTAssertEqual(posts.count, 5)
345+
346+
let postsContaining1InTitle = try await Amplify.DataStore.query(
347+
Post.self,
348+
where: Post.keys.title.contains("1")
349+
)
350+
XCTAssertEqual(postsContaining1InTitle.count, 1)
351+
352+
let postsNotContaining1InTitle = try await Amplify.DataStore.query(
353+
Post.self,
354+
where: Post.keys.title.notContains("1")
355+
)
356+
XCTAssertEqual(
357+
posts.count - postsContaining1InTitle.count,
358+
postsNotContaining1InTitle.count
359+
)
360+
361+
XCTAssertTrue(
362+
postsNotContaining1InTitle.filter(
363+
{ $0.title.contains("1") }
364+
).isEmpty
365+
)
366+
}
367+
368+
369+
/// Given: 50 `Posts` with titles containing 0...49
370+
/// When: Querying posts with multiple `notContains(...)` chained with `&&`
371+
/// e.g. `notContains(a) && notContains(b)`
372+
/// Then: Posts returned should not contain either `a` or `b`
373+
func testQueryMultipleNotContains() async throws {
374+
setUp(withModels: TestModelRegistration())
375+
_ = try await setUpLocalStore(numberOfPosts: 50)
376+
let posts = try await Amplify.DataStore.query(Post.self)
377+
XCTAssertEqual(posts.count, 50)
378+
379+
let titleValueOne = "25", titleValueTwo = "42"
380+
381+
let postsContaining25or42InTitle = try await Amplify.DataStore.query(
382+
Post.self,
383+
where: Post.keys.title.contains(titleValueOne)
384+
|| Post.keys.title.contains(titleValueTwo)
385+
)
386+
XCTAssertEqual(postsContaining25or42InTitle.count, 2)
387+
388+
let postsNotContaining25or42InTitle = try await Amplify.DataStore.query(
389+
Post.self,
390+
where: Post.keys.title.notContains(titleValueOne)
391+
&& Post.keys.title.notContains(titleValueTwo)
392+
)
393+
394+
XCTAssertEqual(
395+
posts.count - postsContaining25or42InTitle.count,
396+
postsNotContaining25or42InTitle.count
397+
)
398+
399+
XCTAssertTrue(
400+
postsNotContaining25or42InTitle.filter(
401+
{ $0.title.contains(titleValueOne) }
402+
).isEmpty
403+
)
404+
405+
XCTAssertTrue(
406+
postsNotContaining25or42InTitle.filter(
407+
{ $0.title.contains(titleValueTwo) }
408+
).isEmpty
409+
)
410+
}
411+
412+
/// Given: 50 saved `Post`s
413+
/// When: Querying for posts with `contains(a)` and `notContains()`
414+
/// where `a` == `<char in 2 posts>` and `b` == `<status of 1 / 2 posts>`
415+
/// Then: The result should contain a single `Post` that contains `a` but doesn't contain `b`
416+
func testQueryNotContainsAndContains() async throws {
417+
let numberOfPosts = 50
418+
setUp(withModels: TestModelRegistration())
419+
_ = try await setUpLocalStore(numberOfPosts: numberOfPosts)
420+
let posts = try await Amplify.DataStore.query(Post.self)
421+
XCTAssertEqual(posts.count, numberOfPosts)
422+
423+
let randomTitleNumber = String(Int.random(in: 0..<numberOfPosts))
424+
425+
let postWithDuplicateTitleAndDifferentStatus = Post(
426+
title: "title_\(randomTitleNumber)",
427+
content: "content",
428+
createdAt: .now(),
429+
status: .published
430+
)
431+
432+
_ = try await Amplify.DataStore.save(postWithDuplicateTitleAndDifferentStatus)
433+
434+
let postsContainingRandomTitleNumber = try await Amplify.DataStore.query(
435+
Post.self,
436+
where: Post.keys.title.contains(randomTitleNumber)
437+
)
438+
439+
XCTAssertEqual(postsContainingRandomTitleNumber.count, 2)
440+
XCTAssertEqual(
441+
postsContainingRandomTitleNumber
442+
.lazy
443+
.filter({ $0.status == .draft })
444+
.count,
445+
1
446+
)
447+
448+
XCTAssertEqual(
449+
postsContainingRandomTitleNumber
450+
.lazy
451+
.filter({ $0.status == .published })
452+
.count,
453+
1
454+
)
455+
456+
let postsContainingRandomTitleNumberAndNotContainingDraftStatus = try await Amplify.DataStore.query(
457+
Post.self,
458+
where: Post.keys.title.contains(randomTitleNumber)
459+
&& Post.keys.status.notContains(PostStatus.published.rawValue)
460+
)
461+
462+
XCTAssertEqual(
463+
postsContainingRandomTitleNumberAndNotContainingDraftStatus.count,
464+
1
465+
)
466+
467+
XCTAssertEqual(
468+
postsContainingRandomTitleNumberAndNotContainingDraftStatus[0].title,
469+
"title_\(randomTitleNumber)"
470+
)
471+
472+
XCTAssertNotEqual(
473+
postsContainingRandomTitleNumberAndNotContainingDraftStatus[0].status,
474+
.published
475+
)
476+
}
477+
478+
/// Given: 5 saved `Post`s
479+
/// When: Deleting with `notContains(a)` where `a` is contained in only one post
480+
/// Then: All but one `Post` should be deleted. The `Post` containing `a` should remain.
481+
func testDeleteNotContains() async throws {
482+
setUp(withModels: TestModelRegistration())
483+
_ = try await setUpLocalStore(numberOfPosts: 5)
484+
let posts = try await Amplify.DataStore.query(Post.self)
485+
XCTAssertEqual(posts.count, 5)
486+
487+
try await Amplify.DataStore.delete(
488+
Post.self,
489+
where: Post.keys.title.notContains("1")
490+
)
491+
492+
let postsIncluding1InTitle = try await Amplify.DataStore.query(Post.self)
493+
XCTAssertEqual(postsIncluding1InTitle.count, 1)
494+
XCTAssertEqual(postsIncluding1InTitle[0].title, "title_1")
495+
}
496+
497+
498+
/// Given: 3 saved `Post`s where 2 `Post` titles contain `x` and 1 of those `Post`s content field contain `y`
499+
/// When: Deleting with `notContains(x) || notContains(y)`. Then querying for remaining `Post`s
500+
/// Then: The query should return a single `Post` that does **not** contain `x` in the title but **does** contain `y` in the content.
501+
func testDeleteJoinedOrNotContains() async throws {
502+
setUp(withModels: TestModelRegistration())
503+
504+
func post(title: String, content: String) -> Post {
505+
.init(title: title, content: content, createdAt: .now())
506+
}
507+
508+
let post1 = post(title: "title_1", content: "a")
509+
let post2 = post(title: "title_1", content: "b")
510+
let post3 = post(title: "title_3", content: "c")
511+
512+
_ = try await Amplify.DataStore.save(post1)
513+
_ = try await Amplify.DataStore.save(post2)
514+
_ = try await Amplify.DataStore.save(post3)
515+
516+
let posts = try await Amplify.DataStore.query(Post.self)
517+
XCTAssertEqual(posts.count, 3)
518+
519+
try await Amplify.DataStore.delete(
520+
Post.self,
521+
where: Post.keys.title.notContains("1")
522+
|| Post.keys.content.notContains("a")
523+
)
524+
525+
let postsAfterDeletingNotContains1andNotContainsA = try await Amplify.DataStore.query(Post.self)
526+
XCTAssertEqual(postsAfterDeletingNotContains1andNotContainsA.count, 1)
527+
XCTAssertEqual(postsAfterDeletingNotContains1andNotContainsA[0].content, "a")
528+
}
529+
530+
336531
func setUpLocalStore(numberOfPosts: Int) async throws -> [Post] {
337532
let posts = (0..<numberOfPosts).map {
338533
Post(

0 commit comments

Comments
 (0)