Skip to content

Commit b584331

Browse files
authored
fix(datastore): swap like for instr in sql queries (#2818)
1 parent b890172 commit b584331

File tree

4 files changed

+146
-31
lines changed

4 files changed

+146
-31
lines changed

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

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,28 @@ 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
case .notContains:
33-
return "not like ?"
35+
return "instr(\(column), ?) = 0"
3436
}
3537
}
3638

@@ -45,12 +47,10 @@ extension QueryOperator {
4547
.greaterOrEqual(let value),
4648
.greaterThan(let value):
4749
return [value.asBinding()]
48-
case .contains(let value):
49-
return ["%\(value)%"]
50-
case .beginsWith(let value):
51-
return ["\(value)%"]
52-
case .notContains(let value):
53-
return ["%\(value)%"]
50+
case .contains(let value),
51+
.beginsWith(let value),
52+
.notContains(let value):
53+
return [value.asBinding()]
5454
}
5555
}
5656
}

AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/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/Tests/AWSDataStorePluginTests/AWSDataStoreLocalStoreTests.swift

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,120 @@ class AWSDataStoreLocalStoreTests: LocalStoreIntegrationTestBase {
527527
XCTAssertEqual(postsAfterDeletingNotContains1andNotContainsA[0].content, "a")
528528
}
529529

530+
/// Given: Saved `Post`s containing SQL pattern matching symbols `%` and `_`
531+
/// When: Querying with predicates containing those symbols.
532+
/// Then: The query results should only contain values matching the predicate without
533+
/// treating `%` and `_` as pattern matching symbols.
534+
func testQueryPatternMatchingSymbols() async throws {
535+
setUp(withModels: TestModelRegistration())
536+
537+
let posts = [
538+
Post(
539+
title: "_bc",
540+
content: "",
541+
createdAt: .now()
542+
),
543+
Post(
544+
title: "abc",
545+
content: "",
546+
createdAt: .now()
547+
),
548+
Post(
549+
title: "de%",
550+
content: "",
551+
createdAt: .now()
552+
),
553+
Post(
554+
title: "def",
555+
content: "",
556+
createdAt: .now()
557+
)
558+
]
559+
560+
for post in posts {
561+
try await Amplify.DataStore.save(post)
562+
}
563+
564+
// This should only contain 1 Post with the title "_bc"
565+
// It should not contain the Post with the title "abc"
566+
let postBeginsWithUnderscore = try await Amplify.DataStore.query(
567+
Post.self,
568+
where: Post.keys.title.beginsWith("_b")
569+
)
570+
571+
XCTAssertEqual(postBeginsWithUnderscore.count, 1)
572+
XCTAssertEqual(postBeginsWithUnderscore[0].title, "_bc")
573+
574+
// This should only contain the Post with the title "de%"
575+
// It should not contain the Post with the title "def"
576+
let postContainingPercent = try await Amplify.DataStore.query(
577+
Post.self,
578+
where: Post.keys.title.contains("%")
579+
)
580+
581+
XCTAssertEqual(postContainingPercent.count, 1)
582+
XCTAssertEqual(postContainingPercent[0].title, "de%")
583+
}
584+
585+
/// Given: Saved `Post`s containing SQL pattern matching symbols `%` and `_`
586+
/// When: Deleting with predicates containing those symbols and subsequently querying
587+
/// for all `Post`s.
588+
/// Then: The query results should only contain values matching the predicate without
589+
/// treating `%` and `_` as pattern matching symbols.
590+
func testDeletePatternMatchingSymbols() async throws {
591+
setUp(withModels: TestModelRegistration())
592+
593+
let posts = [
594+
Post(
595+
title: "_bc",
596+
content: "",
597+
createdAt: .now()
598+
),
599+
Post(
600+
title: "abc",
601+
content: "",
602+
createdAt: .now()
603+
),
604+
Post(
605+
title: "de%",
606+
content: "",
607+
createdAt: .now()
608+
),
609+
Post(
610+
title: "def",
611+
content: "",
612+
createdAt: .now()
613+
)
614+
]
615+
616+
for post in posts {
617+
try await Amplify.DataStore.save(post)
618+
}
619+
620+
// This should only delete 1 Post with the title "_bc"
621+
// It should not delete the Post with the title "abc"
622+
try await Amplify.DataStore.delete(
623+
Post.self,
624+
where: Post.keys.title.beginsWith("_b")
625+
)
626+
627+
let p1 = try await Amplify.DataStore.query(Post.self)
628+
XCTAssertEqual(p1.count, 3)
629+
XCTAssertTrue(p1.filter { $0.title == "_bc" }.isEmpty)
630+
631+
632+
try await Amplify.DataStore.delete(
633+
Post.self,
634+
where: Post.keys.title.contains("%")
635+
)
636+
637+
// This should only delete the Post with the title "de%"
638+
// It should not delete the Post with the title "def"
639+
let p2 = try await Amplify.DataStore.query(Post.self)
640+
641+
XCTAssertEqual(p2.count, 2)
642+
XCTAssertTrue(p2.filter { $0.title == "de%" }.isEmpty)
643+
}
530644

531645
func setUpLocalStore(numberOfPosts: Int) async throws -> [Post] {
532646
let posts = (0..<numberOfPosts).map {

AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/SQLStatementTests.swift

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,11 +1052,11 @@ class SQLStatementTests: XCTestCase {
10521052
matches: "\"root\".\"rating\" between ? and ?",
10531053
bindings: [3, 5])
10541054
assertPredicate(post.title.beginsWith("gelato"),
1055-
matches: "\"root\".\"title\" like ?",
1056-
bindings: ["gelato%"])
1055+
matches: "instr(\"root\".\"title\", ?) = 1",
1056+
bindings: ["gelato"])
10571057
assertPredicate(post.title ~= "gelato",
1058-
matches: "\"root\".\"title\" like ?",
1059-
bindings: ["%gelato%"])
1058+
matches: "instr(\"root\".\"title\", ?) > 0",
1059+
bindings: ["gelato"])
10601060
}
10611061

10621062
/// - Given: a grouped predicate
@@ -1086,8 +1086,8 @@ class SQLStatementTests: XCTestCase {
10861086
and "status" <> ?
10871087
and "updatedAt" is null
10881088
and (
1089-
"content" like ?
1090-
or "title" like ?
1089+
instr("content", ?) > 0
1090+
or instr("title", ?) = 1
10911091
)
10921092
)
10931093
""", statement.stringValue)
@@ -1098,8 +1098,8 @@ class SQLStatementTests: XCTestCase {
10981098
XCTAssertEqual(variables[2] as? Int, 2)
10991099
XCTAssertEqual(variables[3] as? Int, 4)
11001100
XCTAssertEqual(variables[4] as? String, PostStatus.draft.rawValue)
1101-
XCTAssertEqual(variables[5] as? String, "%gelato%")
1102-
XCTAssertEqual(variables[6] as? String, "ice cream%")
1101+
XCTAssertEqual(variables[5] as? String, "gelato")
1102+
XCTAssertEqual(variables[6] as? String, "ice cream")
11031103
}
11041104

11051105
/// - Given: a `Model` type
@@ -1319,20 +1319,21 @@ class SQLStatementTests: XCTestCase {
13191319
and "root"."rating" between ? and ?
13201320
and "root"."updatedAt" is null
13211321
and (
1322-
"root"."content" like ?
1323-
or "root"."title" like ?
1322+
instr("root"."content", ?) > 0
1323+
or instr("root"."title", ?) = 1
13241324
)
13251325
)
13261326
"""
1327+
13271328
XCTAssertEqual(statement.stringValue, expectedStatement)
13281329

13291330
let variables = statement.variables
13301331
XCTAssertEqual(variables[0] as? Int, 1)
13311332
XCTAssertEqual(variables[1] as? Int, 0)
13321333
XCTAssertEqual(variables[2] as? Int, 2)
13331334
XCTAssertEqual(variables[3] as? Int, 4)
1334-
XCTAssertEqual(variables[4] as? String, "%gelato%")
1335-
XCTAssertEqual(variables[5] as? String, "ice cream%")
1335+
XCTAssertEqual(variables[4] as? String, "gelato")
1336+
XCTAssertEqual(variables[5] as? String, "ice cream")
13361337
}
13371338

13381339
func testTranslateQueryPredicateWithNameSpaceWhenFieldNameSpecified() {

0 commit comments

Comments
 (0)