Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.

Commit 6b6ec61

Browse files
Iron-HamMaxim Globak
authored andcommitted
Allow periods in arguments to be ignored when parsing cacheKeys (apollographql#2057)
* Allow commas in arguments to be ignored when parsing cacheKeys * Update * More tests * No-split case
1 parent e931c51 commit 6b6ec61

4 files changed

Lines changed: 160 additions & 1 deletion

File tree

Apollo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/* Begin PBXBuildFile section */
1010
19E9F6AC26D58A9A003AB80E /* OperationMessageIdCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E9F6AA26D58A92003AB80E /* OperationMessageIdCreatorTests.swift */; };
1111
19E9F6B526D6BF25003AB80E /* OperationMessageIdCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E9F6A826D5867E003AB80E /* OperationMessageIdCreator.swift */; };
12+
2EE7FFD0276802E30035DC39 /* CacheKeyConstructionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE7FFCF276802E30035DC39 /* CacheKeyConstructionTests.swift */; };
1213
54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */; };
1314
5AC6CA4322AAF7B200B7C94D /* GraphQLHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC6CA4222AAF7B200B7C94D /* GraphQLHTTPMethod.swift */; };
1415
5BB2C0232380836100774170 /* VersionNumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB2C0222380836100774170 /* VersionNumberTests.swift */; };
@@ -497,6 +498,7 @@
497498
/* Begin PBXFileReference section */
498499
19E9F6A826D5867E003AB80E /* OperationMessageIdCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMessageIdCreator.swift; sourceTree = "<group>"; };
499500
19E9F6AA26D58A92003AB80E /* OperationMessageIdCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMessageIdCreatorTests.swift; sourceTree = "<group>"; };
501+
2EE7FFCF276802E30035DC39 /* CacheKeyConstructionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheKeyConstructionTests.swift; sourceTree = "<group>"; };
500502
54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryNormalizedCache.swift; sourceTree = "<group>"; };
501503
5AC6CA4222AAF7B200B7C94D /* GraphQLHTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLHTTPMethod.swift; sourceTree = "<group>"; };
502504
5BB2C0222380836100774170 /* VersionNumberTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionNumberTests.swift; sourceTree = "<group>"; };
@@ -1733,6 +1735,7 @@
17331735
9F8622F71EC2004200C38162 /* ReadWriteFromStoreTests.swift */,
17341736
9FD03C2D25527CE6002227DC /* StoreConcurrencyTests.swift */,
17351737
9FA6ABCB1EC0A9F7000017BE /* WatchQueryTests.swift */,
1738+
2EE7FFCF276802E30035DC39 /* CacheKeyConstructionTests.swift */,
17361739
);
17371740
path = Cache;
17381741
sourceTree = "<group>";
@@ -2682,6 +2685,7 @@
26822685
DED45EC4261BA0ED0086EF63 /* SplitNetworkTransportTests.swift in Sources */,
26832686
9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */,
26842687
E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */,
2688+
2EE7FFD0276802E30035DC39 /* CacheKeyConstructionTests.swift in Sources */,
26852689
9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */,
26862690
DED45C2A2615319E0086EF63 /* DefaultInterceptorProviderTests.swift in Sources */,
26872691
9F21730E2567E6F000566121 /* DataLoaderTests.swift in Sources */,

Sources/ApolloSQLite/SQLiteNormalizedCache.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public final class SQLiteNormalizedCache {
3737
}
3838

3939
private func recordCacheKey(forFieldCacheKey fieldCacheKey: CacheKey) -> CacheKey {
40-
let components = fieldCacheKey.components(separatedBy: ".")
40+
let components = fieldCacheKey.splitIntoCacheKeyComponents()
4141
var updatedComponents = [String]()
4242
if components.first?.contains("_ROOT") == true {
4343
for component in components {
@@ -117,3 +117,44 @@ extension SQLiteNormalizedCache: NormalizedCache {
117117
try self.database.clearDatabase(shouldVacuumOnClear: self.shouldVacuumOnClear)
118118
}
119119
}
120+
121+
extension String {
122+
private var isBalanced: Bool {
123+
guard contains("(") || contains(")") else { return true }
124+
125+
var stack = [Character]()
126+
for character in self where ["(", ")"].contains(character) {
127+
if character == "(" {
128+
stack.append(character)
129+
} else if !stack.isEmpty && character == ")" {
130+
_ = stack.popLast()
131+
}
132+
}
133+
134+
return stack.isEmpty
135+
}
136+
137+
func splitIntoCacheKeyComponents() -> [String] {
138+
var result = [String]()
139+
var unbalancedString = ""
140+
let tmp = split(separator: ".", omittingEmptySubsequences: false)
141+
tmp
142+
.enumerated()
143+
.forEach { index, item in
144+
let value = String(item)
145+
if value.isBalanced && unbalancedString == "" {
146+
result.append(value)
147+
} else {
148+
unbalancedString += unbalancedString == "" ? value : ".\(value)"
149+
if unbalancedString.isBalanced {
150+
result.append(unbalancedString)
151+
unbalancedString = ""
152+
}
153+
}
154+
if unbalancedString != "" && index == tmp.count - 1 {
155+
result.append(unbalancedString)
156+
}
157+
}
158+
return result
159+
}
160+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import XCTest
2+
@testable import ApolloSQLite
3+
4+
final class CacheKeyConstructionTests: XCTestCase {
5+
func testCacheKeySplitsPeriods() {
6+
let input = "my.chemical.romance"
7+
let expected = ["my", "chemical", "romance"]
8+
9+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
10+
}
11+
12+
func testCacheKeySplitsPeriodsButIgnoresParentheses() {
13+
let input = "my.chemical.romance(xWv.CD-RIP.whole-album)"
14+
let expected = ["my", "chemical", "romance(xWv.CD-RIP.whole-album)"]
15+
16+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
17+
}
18+
19+
func testCacheKeyIgnoresNestedParentheses() {
20+
let input = "my.chemical.romance(the.(very)hidden.albums)"
21+
let expected = ["my", "chemical", "romance(the.(very)hidden.albums)"]
22+
23+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
24+
}
25+
26+
func testDoubleNestedInput() {
27+
let input = "my.chemical.romance(name:imnotokay.rip(xWv(the.original).HIGH-QUALITY)).mp3"
28+
let expected = ["my", "chemical", "romance(name:imnotokay.rip(xWv(the.original).HIGH-QUALITY))", "mp3"]
29+
30+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
31+
}
32+
33+
func testUnbalancedInput() {
34+
let input = "my.chemical.romance(name: )(.thebest.)()"
35+
let expected = ["my", "chemical", "romance(name: )(.thebest.)()"]
36+
37+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
38+
}
39+
40+
func testUnbalancedInputContinued() {
41+
let input = "my.chemical.romance(name: )(.thebest.)().count"
42+
let expected = ["my", "chemical", "romance(name: )(.thebest.)()", "count"]
43+
44+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
45+
}
46+
47+
func testNoSplits() {
48+
let input = "mychemicalromance"
49+
let expected = ["mychemicalromance"]
50+
51+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
52+
}
53+
}

Tests/ApolloTests/Cache/SQLite/CachePersistenceTests.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,67 @@ class CachePersistenceTests: XCTestCase {
6666
}
6767
}
6868

69+
func testFetchAndPersistWithPeriodArguments() throws {
70+
let query = SearchQuery(term: "Luke.Skywalker")
71+
let sqliteFileURL = SQLiteTestCacheProvider.temporarySQLiteFileURL()
72+
73+
try SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { (cache) in
74+
let store = ApolloStore(cache: cache)
75+
76+
let server = MockGraphQLServer()
77+
let networkTransport = MockNetworkTransport(server: server, store: store)
78+
79+
let client = ApolloClient(networkTransport: networkTransport, store: store)
80+
81+
_ = server.expect(SearchQuery.self) { request in
82+
[
83+
"data": [
84+
"search": [
85+
[
86+
"id": "1000",
87+
"name": "Luke Skywalker",
88+
"__typename": "Human"
89+
]
90+
]
91+
]
92+
]
93+
}
94+
let networkExpectation = self.expectation(description: "Fetching query from network")
95+
let newCacheExpectation = self.expectation(description: "Fetch query from new cache")
96+
97+
client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { outerResult in
98+
defer { networkExpectation.fulfill() }
99+
100+
switch outerResult {
101+
case .failure(let error):
102+
XCTFail("Unexpected error: \(error)")
103+
return
104+
case .success(let graphQLResult):
105+
XCTAssertEqual(graphQLResult.data?.search?.first??.asHuman?.name, "Luke Skywalker")
106+
// Do another fetch from cache to ensure that data is cached before creating new cache
107+
client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in
108+
try! SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { cache in
109+
let newStore = ApolloStore(cache: cache)
110+
let newClient = ApolloClient(networkTransport: networkTransport, store: newStore)
111+
newClient.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { newClientResult in
112+
defer { newCacheExpectation.fulfill() }
113+
switch newClientResult {
114+
case .success(let newClientGraphQLResult):
115+
XCTAssertEqual(newClientGraphQLResult.data?.search?.first??.asHuman?.name, "Luke Skywalker")
116+
case .failure(let error):
117+
XCTFail("Unexpected error with new client: \(error)")
118+
}
119+
_ = newClient // Workaround for a bug - ensure that newClient is retained until this block is run
120+
}
121+
}
122+
}
123+
}
124+
}
125+
126+
self.waitForExpectations(timeout: 2, handler: nil)
127+
}
128+
}
129+
69130
func testPassInConnectionDoesNotThrow() {
70131
do {
71132
let database = try SQLiteDotSwiftDatabase(connection: Connection())

0 commit comments

Comments
 (0)