Skip to content

Commit 613de0a

Browse files
BobaFettersgh-action-runner
authored andcommitted
feature: Remove SQLite.swift (#635)
1 parent 6d81ebb commit 613de0a

File tree

12 files changed

+244
-131
lines changed

12 files changed

+244
-131
lines changed

.package.resolved

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,6 @@
3636
"version" : "13.3.0"
3737
}
3838
},
39-
{
40-
"identity" : "sqlite.swift",
41-
"kind" : "remoteSourceControl",
42-
"location" : "https://github.com/stephencelis/SQLite.swift.git",
43-
"state" : {
44-
"revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8",
45-
"version" : "0.15.3"
46-
}
47-
},
4839
{
4940
"identity" : "swift-argument-parser",
5041
"kind" : "remoteSourceControl",

Project.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ let project = Project(
88
organizationName: "apollographql",
99
packages: [
1010
.package(url: "https://github.com/Quick/Nimble.git", from: "13.2.0"),
11-
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.1"),
1211
.package(path: "apollo-ios"),
1312
.package(path: "apollo-ios-codegen"),
1413
.package(path: "apollo-ios-pagination"),

Tests/ApolloTests/Cache/SQLite/CachePersistenceTests.swift

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import XCTest
33
import ApolloAPI
44
@testable import ApolloSQLite
55
import ApolloInternalTestHelpers
6-
import SQLite
6+
import SQLite3
77
import StarWarsAPI
88

99
class CachePersistenceTests: XCTestCase {
@@ -162,16 +162,6 @@ class CachePersistenceTests: XCTestCase {
162162
await fulfillment(of: [networkExpectation, newCacheExpectation], timeout: 2)
163163
}
164164

165-
func testPassInConnectionDoesNotThrow() {
166-
do {
167-
let database = try SQLiteDotSwiftDatabase(connection: Connection())
168-
_ = try SQLiteNormalizedCache(database: database)
169-
170-
} catch {
171-
XCTFail("Passing in connection failed with error: \(error)")
172-
}
173-
}
174-
175165
func testClearCache() async throws {
176166
// given
177167
class GivenSelectionSet: MockSelectionSet {
Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import XCTest
22
@testable import ApolloSQLite
33
import ApolloInternalTestHelpers
4-
import SQLite
4+
import SQLite3
55

6-
class SQLiteDotSwiftDatabaseBehaviorTests: XCTestCase {
6+
class ApolloSQLiteDatabaseBehaviorTests: XCTestCase {
77

88
func testSelection_withForcedError_shouldThrow() throws {
99
let sqliteFileURL = SQLiteTestCacheProvider.temporarySQLiteFileURL()
10-
let db = try! SQLiteDotSwiftDatabase(fileURL: sqliteFileURL)
10+
let db = try! ApolloSQLiteDatabase(fileURL: sqliteFileURL)
1111

1212
try! db.createRecordsTableIfNeeded()
1313
try! db.addOrUpdateRecordString("record", for: "key")
@@ -16,11 +16,22 @@ class SQLiteDotSwiftDatabaseBehaviorTests: XCTestCase {
1616
XCTAssertNoThrow(rows = try db.selectRawRows(forKeys: ["key"]))
1717
XCTAssertEqual(rows.count, 1)
1818

19-
// Use SQLite directly to manipulate the database (cannot be done with SQLiteDotSwiftDatabase)
20-
let connection = try Connection(.uri(sqliteFileURL.absoluteString), readonly: false)
21-
let table = Table(SQLiteDotSwiftDatabase.tableName)
22-
try! connection.run(table.drop(ifExists: false))
19+
// Use SQLite directly to manipulate the database (cannot be done with ApolloSQLiteDatabase)
20+
try dropSQLiteTable(dbURL: sqliteFileURL, tableName: ApolloSQLiteDatabase.tableName)
2321

2422
XCTAssertThrowsError(try db.selectRawRows(forKeys: ["key"]))
2523
}
24+
25+
private func dropSQLiteTable(dbURL: URL, tableName: String) throws {
26+
var db: OpaquePointer?
27+
let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_URI
28+
if sqlite3_open_v2(dbURL.path, &db, flags, nil) != SQLITE_OK {
29+
throw SQLiteError.open(path: dbURL.path)
30+
}
31+
32+
let sql = "DROP TABLE IF EXISTS \(tableName)"
33+
if sqlite3_exec(db, sql, nil, nil, nil) != SQLITE_OK {
34+
throw SQLiteError.execution(message: "Failed to drop table: \(tableName)")
35+
}
36+
}
2637
}

Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ extension Target {
2323
.package(product: "ApolloSQLite"),
2424
.package(product: "ApolloWebSocket"),
2525
.package(product: "ApolloTestSupport"),
26-
.package(product: "SQLite"),
2726
.package(product: "Nimble")
2827
],
2928
settings: .forTarget(target)

apollo-ios/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
}

apollo-ios/Package.resolved

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

apollo-ios/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")
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+
}

apollo-ios/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

0 commit comments

Comments
 (0)