@@ -333,6 +333,201 @@ class AWSDataStoreLocalStoreTests: LocalStoreIntegrationTestBase {
333
333
XCTAssertEqual ( posts. count, 0 )
334
334
}
335
335
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
+
336
531
func setUpLocalStore( numberOfPosts: Int ) async throws -> [ Post ] {
337
532
let posts = ( 0 ..< numberOfPosts) . map {
338
533
Post (
0 commit comments