Skip to content

Commit f4752e1

Browse files
gh-action-runnergh-action-runner
authored andcommitted
Squashed 'apollo-ios/' changes from 59a26da3..52c5a41b
52c5a41b feature: Remove SQLite.swift (#635) d1830b90 Update ROADMAP.md git-subtree-dir: apollo-ios git-subtree-split: 52c5a41bbdf470539d3731757d2528ece9739557
1 parent f45d701 commit f4752e1

File tree

8 files changed

+226
-103
lines changed

8 files changed

+226
-103
lines changed

Apollo.podspec

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ Pod::Spec.new do |s|
3131
s.subspec 'SQLite' do |ss|
3232
ss.source_files = 'Sources/ApolloSQLite/*.swift'
3333
ss.dependency 'Apollo/Core'
34-
ss.dependency 'SQLite.swift', '~>0.15.1'
3534
ss.resource_bundles = {
3635
'ApolloSQLite' => ['Sources/ApolloSQLite/Resources/PrivacyInfo.xcprivacy']
3736
}

Package.resolved

Lines changed: 0 additions & 14 deletions
This file was deleted.

Package.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ let package = Package(
2525
.plugin(name: "InstallCLI", targets: ["Install CLI"])
2626
],
2727
dependencies: [
28-
.package(
29-
url: "https://github.com/stephencelis/SQLite.swift.git",
30-
.upToNextMajor(from: "0.15.1")),
3128
],
3229
targets: [
3330
.target(
@@ -52,7 +49,6 @@ let package = Package(
5249
name: "ApolloSQLite",
5350
dependencies: [
5451
"Apollo",
55-
.product(name: "SQLite", package: "SQLite.swift"),
5652
],
5753
resources: [
5854
.copy("Resources/PrivacyInfo.xcprivacy")

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 🔮 Apollo iOS Roadmap
22

3-
**Last updated: 2025-04-16**
3+
**Last updated: 2025-04-29**
44

55
For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-ios/blob/main/CHANGELOG.md).
66

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import Foundation
2+
#if !COCOAPODS
3+
import Apollo
4+
#endif
5+
import SQLite3
6+
7+
public final class ApolloSQLiteDatabase: SQLiteDatabase {
8+
9+
private final class DBContextToken: Sendable {}
10+
11+
private var db: OpaquePointer?
12+
private let dbURL: URL
13+
14+
private let dbQueue = DispatchQueue(label: "com.apollo.sqlite.database")
15+
private static let dbContextKey = DispatchSpecificKey<DBContextToken>()
16+
private let dbContextValue = DBContextToken()
17+
18+
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
19+
20+
public init(fileURL: URL) throws {
21+
self.dbURL = fileURL
22+
try openConnection()
23+
dbQueue.setSpecific(key: Self.dbContextKey, value: dbContextValue)
24+
}
25+
26+
deinit {
27+
sqlite3_close(db)
28+
}
29+
30+
// MARK: - Internal Helpers
31+
32+
private func performSync<T>(_ block: () throws -> T) throws -> T {
33+
if DispatchQueue.getSpecific(key: Self.dbContextKey) === dbContextValue {
34+
return try block()
35+
} else {
36+
return try dbQueue.sync(execute: block)
37+
}
38+
}
39+
40+
private func openConnection() throws {
41+
let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_URI
42+
if sqlite3_open_v2(dbURL.path, &db, flags, nil) != SQLITE_OK {
43+
throw SQLiteError.open(path: dbURL.path)
44+
}
45+
}
46+
47+
private func rollbackTransaction() {
48+
sqlite3_exec(db, "ROLLBACK TRANSACTION", nil, nil, nil)
49+
}
50+
51+
private func sqliteErrorMessage() -> String {
52+
return String(cString: sqlite3_errmsg(db))
53+
}
54+
55+
@discardableResult
56+
private func exec(_ sql: String, errorMessage: @autoclosure () -> String) throws -> Int32 {
57+
let result = sqlite3_exec(db, sql, nil, nil, nil)
58+
if result != SQLITE_OK {
59+
throw SQLiteError.execution(message: "\(errorMessage()): \(sqliteErrorMessage())")
60+
}
61+
return result
62+
}
63+
64+
private func prepareStatement(_ sql: String, errorMessage: @autoclosure () -> String) throws -> OpaquePointer? {
65+
var stmt: OpaquePointer?
66+
if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) != SQLITE_OK {
67+
throw SQLiteError.prepare(message: "\(errorMessage()): \(sqliteErrorMessage())")
68+
}
69+
return stmt
70+
}
71+
72+
// MARK: - SQLiteDatabase Protocol
73+
74+
public func createRecordsTableIfNeeded() throws {
75+
try performSync {
76+
let sql = """
77+
CREATE TABLE IF NOT EXISTS "records" (
78+
"_id" INTEGER,
79+
"key" TEXT UNIQUE,
80+
"record" TEXT,
81+
PRIMARY KEY("_id" AUTOINCREMENT)
82+
);
83+
"""
84+
try exec(sql, errorMessage: "Failed to create 'records' database table")
85+
}
86+
}
87+
88+
public func selectRawRows(forKeys keys: Set<CacheKey>) throws -> [DatabaseRow] {
89+
guard !keys.isEmpty else { return [] }
90+
91+
return try performSync {
92+
let placeholders = keys.map { _ in "?" }.joined(separator: ", ")
93+
let sql = """
94+
SELECT \(Self.keyColumnName), \(Self.recordColumName) FROM \(Self.tableName)
95+
WHERE \(Self.keyColumnName) IN (\(placeholders))
96+
"""
97+
98+
let stmt = try prepareStatement(sql, errorMessage: "Failed to prepare select statement")
99+
defer { sqlite3_finalize(stmt) }
100+
101+
for (index, key) in keys.enumerated() {
102+
sqlite3_bind_text(stmt, Int32(index + 1), key, -1, SQLITE_TRANSIENT)
103+
}
104+
105+
var rows = [DatabaseRow]()
106+
var result: Int32
107+
repeat {
108+
result = sqlite3_step(stmt)
109+
if result == SQLITE_ROW {
110+
let key = String(cString: sqlite3_column_text(stmt, 0))
111+
let record = String(cString: sqlite3_column_text(stmt, 1))
112+
rows.append(DatabaseRow(cacheKey: key, storedInfo: record))
113+
} else if result != SQLITE_DONE {
114+
let errorMsg = String(cString: sqlite3_errmsg(db))
115+
throw SQLiteError.step(message: "Failed to step raw row select: \(errorMsg)")
116+
}
117+
} while result != SQLITE_DONE
118+
119+
120+
return rows
121+
}
122+
}
123+
124+
public func addOrUpdate(records: [(cacheKey: CacheKey, recordString: String)]) throws {
125+
guard !records.isEmpty else { return }
126+
127+
try performSync {
128+
let sql = """
129+
INSERT INTO \(Self.tableName) (\(Self.keyColumnName), \(Self.recordColumName))
130+
VALUES (?, ?)
131+
ON CONFLICT(\(Self.keyColumnName)) DO UPDATE SET \(Self.recordColumName) = excluded.\(Self.recordColumName)
132+
"""
133+
134+
try exec("BEGIN TRANSACTION", errorMessage: "Failed to begin insert/update transaction")
135+
136+
let stmt = try prepareStatement(sql, errorMessage: "Failed to prepare insert/update statement")
137+
defer { sqlite3_finalize(stmt) }
138+
139+
for (key, record) in records {
140+
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT)
141+
sqlite3_bind_text(stmt, 2, record, -1, SQLITE_TRANSIENT)
142+
143+
if sqlite3_step(stmt) != SQLITE_DONE {
144+
rollbackTransaction()
145+
throw SQLiteError.step(message: "Insert/update failed: \(sqliteErrorMessage())")
146+
}
147+
148+
sqlite3_reset(stmt)
149+
sqlite3_clear_bindings(stmt)
150+
}
151+
152+
do {
153+
try exec("COMMIT TRANSACTION", errorMessage: "Failed to commit transaction")
154+
} catch {
155+
rollbackTransaction()
156+
throw error
157+
}
158+
}
159+
}
160+
161+
public func deleteRecord(for cacheKey: CacheKey) throws {
162+
try performSync {
163+
let sql = "DELETE FROM \(Self.tableName) WHERE \(Self.keyColumnName) = ?"
164+
let stmt = try prepareStatement(sql, errorMessage: "Failed to prepare delete statement")
165+
defer { sqlite3_finalize(stmt) }
166+
167+
sqlite3_bind_text(stmt, 1, cacheKey, -1, SQLITE_TRANSIENT)
168+
if sqlite3_step(stmt) != SQLITE_DONE {
169+
throw SQLiteError.step(message: "Delete failed: \(sqliteErrorMessage())")
170+
}
171+
}
172+
}
173+
174+
public func deleteRecords(matching pattern: CacheKey) throws {
175+
guard !pattern.isEmpty else { return }
176+
let wildcardPattern = "%\(pattern)%"
177+
178+
try performSync {
179+
let sql = "DELETE FROM \(Self.tableName) WHERE \(Self.keyColumnName) LIKE ? COLLATE NOCASE"
180+
let stmt = try prepareStatement(sql, errorMessage: "Failed to prepare delete pattern statement")
181+
defer { sqlite3_finalize(stmt) }
182+
183+
sqlite3_bind_text(stmt, 1, wildcardPattern, -1, SQLITE_TRANSIENT)
184+
if sqlite3_step(stmt) != SQLITE_DONE {
185+
throw SQLiteError.step(message: "Pattern delete failed: \(sqliteErrorMessage())")
186+
}
187+
}
188+
}
189+
190+
public func clearDatabase(shouldVacuumOnClear: Bool) throws {
191+
try performSync {
192+
try exec("DELETE FROM \(Self.tableName)", errorMessage: "Failed to clear database")
193+
if shouldVacuumOnClear {
194+
try exec("VACUUM;", errorMessage: "Failed to vacuum database")
195+
}
196+
}
197+
}
198+
199+
public func setJournalMode(mode: JournalMode) throws {
200+
try performSync {
201+
try exec("PRAGMA journal_mode = \(mode.rawValue);", errorMessage: "Failed to set journal mode")
202+
}
203+
}
204+
}

Sources/ApolloSQLite/SQLiteDatabase.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ public struct DatabaseRow {
88
let storedInfo: String
99
}
1010

11+
public enum SQLiteError: Error, CustomStringConvertible {
12+
case execution(message: String)
13+
case open(path: String)
14+
case prepare(message: String)
15+
case step(message: String)
16+
17+
public var description: String {
18+
switch self {
19+
case .execution(let message):
20+
return message
21+
case .open(let path):
22+
return "Failed to open SQLite database connection at path: \(path)"
23+
case .prepare(let message):
24+
return message
25+
case .step(let message):
26+
return message
27+
}
28+
}
29+
}
30+
1131
public protocol SQLiteDatabase {
1232

1333
init(fileURL: URL) throws

Sources/ApolloSQLite/SQLiteDotSwiftDatabase.swift

Lines changed: 0 additions & 82 deletions
This file was deleted.

Sources/ApolloSQLite/SQLiteNormalizedCache.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public final class SQLiteNormalizedCache {
2222
/// - shouldVacuumOnClear: If the database should also be `VACCUM`ed on clear to remove all traces of info. Defaults to `false` since this involves a performance hit, but this should be used if you are storing any Personally Identifiable Information in the cache.
2323
/// - Throws: Any errors attempting to open or create the database.
2424
public init(fileURL: URL,
25-
databaseType: any SQLiteDatabase.Type = SQLiteDotSwiftDatabase.self,
25+
databaseType: any SQLiteDatabase.Type = ApolloSQLiteDatabase.self,
2626
shouldVacuumOnClear: Bool = false) throws {
2727
self.database = try databaseType.init(fileURL: fileURL)
2828
self.shouldVacuumOnClear = shouldVacuumOnClear

0 commit comments

Comments
 (0)