Skip to content

Commit a6898a4

Browse files
authored
fix(datastore): v1 swap like for instr in sql queries (#2819)
1 parent 430d53f commit a6898a4

File tree

4 files changed

+201
-28
lines changed

4 files changed

+201
-28
lines changed

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Storage/SQLite/QueryPredicate+SQLite.swift

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,26 @@ import SQLite
1111

1212
extension QueryOperator {
1313

14-
var sqlOperation: String {
14+
func sqlOperation(column: String) -> String {
1515
switch self {
1616
case .notEqual(let value):
17-
return value == nil ? "is not null" : "<> ?"
17+
return value == nil ? "\(column) is not null" : "\(column) <> ?"
1818
case .equals(let value):
19-
return value == nil ? "is null" : "= ?"
19+
return value == nil ? "\(column) is null" : "\(column) = ?"
2020
case .lessOrEqual:
21-
return "<= ?"
21+
return "\(column) <= ?"
2222
case .lessThan:
23-
return "< ?"
23+
return "\(column) < ?"
2424
case .greaterOrEqual:
25-
return ">= ?"
25+
return "\(column) >= ?"
2626
case .greaterThan:
27-
return "> ?"
27+
return "\(column) > ?"
2828
case .between:
29-
return "between ? and ?"
30-
case .beginsWith, .contains:
31-
return "like ?"
29+
return "\(column) between ? and ?"
30+
case .beginsWith:
31+
return "instr(\(column), ?) = 1"
32+
case .contains:
33+
return "instr(\(column), ?) > 0"
3234
}
3335
}
3436

@@ -43,10 +45,9 @@ extension QueryOperator {
4345
.greaterOrEqual(let value),
4446
.greaterThan(let value):
4547
return [value.asBinding()]
46-
case .contains(let value):
47-
return ["%\(value)%"]
48-
case .beginsWith(let value):
49-
return ["\(value)%"]
48+
case .contains(let value),
49+
.beginsWith(let value):
50+
return [value.asBinding()]
5051
}
5152
}
5253
}

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Storage/SQLite/SQLStatement+Condition.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ private func translateQueryPredicate(from modelSchema: ModelSchema,
3131
if let operation = pred as? QueryPredicateOperation {
3232
let column = resolveColumn(operation)
3333
if predicateIndex == 0 {
34-
sql.append("\(indent)\(column) \(operation.operator.sqlOperation)")
34+
sql.append("\(indent)\(operation.operator.sqlOperation(column: column))")
3535
} else {
36-
sql.append("\(indent)\(groupType.rawValue) \(column) \(operation.operator.sqlOperation)")
36+
sql.append("\(indent)\(groupType.rawValue) \(operation.operator.sqlOperation(column: column))")
3737
}
3838

3939
bindings.append(contentsOf: operation.operator.bindings)

AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginIntegrationTests/DataStoreLocalStoreTests.swift

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,177 @@ class DataStoreLocalStoreTests: LocalStoreIntegrationTestBase {
442442
sink.cancel()
443443
}
444444

445+
/// Given: Saved `Post`s containing SQL pattern matching symbols `%` and `_`
446+
/// When: Querying with predicates containing those symbols.
447+
/// Then: The query results should only contain values matching the predicate without
448+
/// treating `%` and `_` as pattern matching symbols.
449+
func testQueryPatternMatchingSymbols() async throws {
450+
setUp(withModels: TestModelRegistration())
451+
452+
let posts = [
453+
Post(
454+
title: "_bc",
455+
content: "",
456+
createdAt: .now()
457+
),
458+
Post(
459+
title: "abc",
460+
content: "",
461+
createdAt: .now()
462+
),
463+
Post(
464+
title: "de%",
465+
content: "",
466+
createdAt: .now()
467+
),
468+
Post(
469+
title: "def",
470+
content: "",
471+
createdAt: .now()
472+
)
473+
]
474+
475+
for post in posts {
476+
let saveExpectation = expectation(description: "Save post completed")
477+
Amplify.DataStore.save(post) { result in
478+
switch result {
479+
case .success:
480+
saveExpectation.fulfill()
481+
case .failure(let error):
482+
XCTFail("Error saving post: \(post) - \(error)")
483+
}
484+
}
485+
wait(for: [saveExpectation], timeout: 2)
486+
}
487+
488+
let beginsWithExpectation = expectation(description: "query with beginsWith predicate completed")
489+
// This should only contain 1 Post with the title "_bc"
490+
// It should not contain the Post with the title "abc"
491+
Amplify.DataStore.query(
492+
Post.self,
493+
where: Post.keys.title.beginsWith("_b")
494+
) { result in
495+
switch result {
496+
case .success(let posts):
497+
XCTAssertEqual(posts.count, 1)
498+
XCTAssertEqual(posts[0].title, "_bc")
499+
beginsWithExpectation.fulfill()
500+
case .failure(let error):
501+
XCTFail("\(error)")
502+
}
503+
}
504+
wait(for: [beginsWithExpectation], timeout: 1)
505+
506+
let containsExpectation = expectation(description: "query with contains predicate completed")
507+
// This should only contain the Post with the title "de%"
508+
// It should not contain the Post with the title "def"
509+
Amplify.DataStore.query(
510+
Post.self,
511+
where: Post.keys.title.contains("%")
512+
) { result in
513+
switch result {
514+
case .success(let posts):
515+
XCTAssertEqual(posts.count, 1)
516+
XCTAssertEqual(posts[0].title, "de%")
517+
case .failure(let error):
518+
XCTFail("\(error)")
519+
}
520+
}
521+
wait(for: [containsExpectation], timeout: 1)
522+
}
523+
524+
/// Given: Saved `Post`s containing SQL pattern matching symbols `%` and `_`
525+
/// When: Deleting with predicates containing those symbols and subsequently querying
526+
/// for all `Post`s.
527+
/// Then: The query results should only contain values matching the predicate without
528+
/// treating `%` and `_` as pattern matching symbols.
529+
func testDeletePatternMatchingSymbols() async throws {
530+
setUp(withModels: TestModelRegistration())
531+
532+
let posts = [
533+
Post(
534+
title: "_bc",
535+
content: "",
536+
createdAt: .now()
537+
),
538+
Post(
539+
title: "abc",
540+
content: "",
541+
createdAt: .now()
542+
),
543+
Post(
544+
title: "de%",
545+
content: "",
546+
createdAt: .now()
547+
),
548+
Post(
549+
title: "def",
550+
content: "",
551+
createdAt: .now()
552+
)
553+
]
554+
555+
for post in posts {
556+
let saveExpectation = expectation(description: "Save post completed")
557+
Amplify.DataStore.save(post) { result in
558+
switch result {
559+
case .success:
560+
saveExpectation.fulfill()
561+
case .failure(let error):
562+
XCTFail("Error saving post: \(post) - \(error)")
563+
}
564+
}
565+
wait(for: [saveExpectation], timeout: 2)
566+
}
567+
568+
let beginsWithExpectation = expectation(description: "delete + query with beginsWith predicate completed")
569+
// This should only delete 1 Post with the title "_bc"
570+
// It should not delete the Post with the title "abc"
571+
Amplify.DataStore.delete(
572+
Post.self,
573+
where: Post.keys.title.beginsWith("_b")
574+
) { deleteResult in
575+
guard case .success = deleteResult else {
576+
return XCTFail("Delete request failed")
577+
}
578+
Amplify.DataStore.query(Post.self) { result in
579+
switch result {
580+
case .success(let posts):
581+
XCTAssertEqual(posts.count, 3)
582+
XCTAssertTrue(posts.filter { $0.title == "_bc" }.isEmpty)
583+
beginsWithExpectation.fulfill()
584+
case .failure(let error):
585+
XCTFail("\(error)")
586+
587+
}
588+
}
589+
}
590+
wait(for: [beginsWithExpectation], timeout: 1)
591+
592+
let containsExpectation = expectation(description: "delete + query with contains predicate completed")
593+
// This should only delete the Post with the title "de%"
594+
// It should not delete the Post with the title "def"
595+
Amplify.DataStore.delete(
596+
Post.self,
597+
where: Post.keys.title.contains("%")
598+
) { deleteResult in
599+
guard case .success = deleteResult else {
600+
return XCTFail("Delete request failed")
601+
}
602+
Amplify.DataStore.query(Post.self) { result in
603+
switch result {
604+
case .success(let posts):
605+
XCTAssertEqual(posts.count, 2)
606+
XCTAssertTrue(posts.filter { $0.title == "de%" }.isEmpty)
607+
containsExpectation.fulfill()
608+
case .failure(let error):
609+
XCTFail("\(error)")
610+
}
611+
}
612+
}
613+
wait(for: [containsExpectation], timeout: 1)
614+
}
615+
445616
func testDeleteModelTypeWithPredicate() {
446617
setUp(withModels: TestModelRegistration())
447618
_ = setUpLocalStore(numberOfPosts: 5)

AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Core/SQLStatementTests.swift

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,11 +1047,11 @@ class SQLStatementTests: XCTestCase {
10471047
matches: "\"root\".\"rating\" between ? and ?",
10481048
bindings: [3, 5])
10491049
assertPredicate(post.title.beginsWith("gelato"),
1050-
matches: "\"root\".\"title\" like ?",
1051-
bindings: ["gelato%"])
1050+
matches: "instr(\"root\".\"title\", ?) = 1",
1051+
bindings: ["gelato"])
10521052
assertPredicate(post.title ~= "gelato",
1053-
matches: "\"root\".\"title\" like ?",
1054-
bindings: ["%gelato%"])
1053+
matches: "instr(\"root\".\"title\", ?) > 0",
1054+
bindings: ["gelato"])
10551055
}
10561056

10571057
/// - Given: a grouped predicate
@@ -1081,8 +1081,8 @@ class SQLStatementTests: XCTestCase {
10811081
and "status" <> ?
10821082
and "updatedAt" is null
10831083
and (
1084-
"content" like ?
1085-
or "title" like ?
1084+
instr("content", ?) > 0
1085+
or instr("title", ?) = 1
10861086
)
10871087
)
10881088
""", statement.stringValue)
@@ -1093,8 +1093,8 @@ class SQLStatementTests: XCTestCase {
10931093
XCTAssertEqual(variables[2] as? Int, 2)
10941094
XCTAssertEqual(variables[3] as? Int, 4)
10951095
XCTAssertEqual(variables[4] as? String, PostStatus.draft.rawValue)
1096-
XCTAssertEqual(variables[5] as? String, "%gelato%")
1097-
XCTAssertEqual(variables[6] as? String, "ice cream%")
1096+
XCTAssertEqual(variables[5] as? String, "gelato")
1097+
XCTAssertEqual(variables[6] as? String, "ice cream")
10981098
}
10991099

11001100
/// - Given: a `Model` type
@@ -1314,20 +1314,21 @@ class SQLStatementTests: XCTestCase {
13141314
and "root"."rating" between ? and ?
13151315
and "root"."updatedAt" is null
13161316
and (
1317-
"root"."content" like ?
1318-
or "root"."title" like ?
1317+
instr("root"."content", ?) > 0
1318+
or instr("root"."title", ?) = 1
13191319
)
13201320
)
13211321
"""
1322+
13221323
XCTAssertEqual(statement.stringValue, expectedStatement)
13231324

13241325
let variables = statement.variables
13251326
XCTAssertEqual(variables[0] as? Int, 1)
13261327
XCTAssertEqual(variables[1] as? Int, 0)
13271328
XCTAssertEqual(variables[2] as? Int, 2)
13281329
XCTAssertEqual(variables[3] as? Int, 4)
1329-
XCTAssertEqual(variables[4] as? String, "%gelato%")
1330-
XCTAssertEqual(variables[5] as? String, "ice cream%")
1330+
XCTAssertEqual(variables[4] as? String, "gelato")
1331+
XCTAssertEqual(variables[5] as? String, "ice cream")
13311332
}
13321333

13331334
func testTranslateQueryPredicateWithNameSpaceWhenFieldNameSpecified() {

0 commit comments

Comments
 (0)