Skip to content

Commit 1248391

Browse files
Add ability to delete SQLite files (#89)
1 parent 6939c94 commit 1248391

File tree

4 files changed

+163
-9
lines changed

4 files changed

+163
-9
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@
33
## 1.6.1 (unreleased)
44

55
* Update Kotlin SDK to 1.7.0.
6+
* Add `close(deleteDatabase:)` method to `PowerSyncDatabaseProtocol` for deleting SQLite database files when closing the database. This includes the main database file and all WAL mode files (.wal, .shm, .journal). Files that don't exist are ignored, but an error is thrown if a file exists but cannot be deleted.
67

8+
```swift
9+
// Close the database and delete all SQLite files
10+
try await database.close(deleteDatabase: true)
11+
12+
// Close the database without deleting files (default behavior)
13+
try await database.close(deleteDatabase: false)
14+
// or simply
15+
try await database.close()
16+
```
717
## 1.6.0
818

919
* Update core extension to 0.4.6 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.6))

Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
99
private let kotlinDatabase: PowerSyncKotlin.PowerSyncDatabase
1010
private let encoder = JSONEncoder()
1111
let currentStatus: SyncStatus
12+
private let dbFilename: String
1213

1314
init(
1415
schema: Schema,
@@ -23,6 +24,7 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
2324
logger: logger.kLogger
2425
)
2526
self.logger = logger
27+
self.dbFilename = dbFilename
2628
currentStatus = KotlinSyncStatus(
2729
baseStatus: kotlinDatabase.currentStatus
2830
)
@@ -74,7 +76,7 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
7476
batch: base
7577
)
7678
}
77-
79+
7880
func getCrudTransactions() -> any CrudTransactions {
7981
return KotlinCrudTransactions(db: kotlinDatabase)
8082
}
@@ -323,6 +325,21 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
323325
try await kotlinDatabase.close()
324326
}
325327

328+
func close(deleteDatabase: Bool = false) async throws {
329+
// Close the SQLite connections
330+
try await close()
331+
332+
if deleteDatabase {
333+
try await self.deleteDatabase()
334+
}
335+
}
336+
337+
private func deleteDatabase() async throws {
338+
// We can use the supplied dbLocation when we support that in future
339+
let directory = try appleDefaultDatabaseDirectory()
340+
try deleteSQLiteFiles(dbFilename: dbFilename, in: directory)
341+
}
342+
326343
/// Tries to convert Kotlin PowerSyncExceptions to Swift Exceptions
327344
private func wrapPowerSyncException<R: Sendable>(
328345
handler: () async throws -> R)
@@ -449,3 +466,46 @@ func wrapTransactionContext(
449466
}
450467
}
451468
}
469+
470+
/// This returns the default directory in which we store SQLite database files.
471+
func appleDefaultDatabaseDirectory() throws -> URL {
472+
let fileManager = FileManager.default
473+
474+
// Get the application support directory
475+
guard let documentsDirectory = fileManager.urls(
476+
for: .applicationSupportDirectory,
477+
in: .userDomainMask
478+
).first else {
479+
throw PowerSyncError.operationFailed(message: "Unable to find application support directory")
480+
}
481+
482+
return documentsDirectory.appendingPathComponent("databases")
483+
}
484+
485+
/// Deletes all SQLite files for a given database filename in the specified directory.
486+
/// This includes the main database file and WAL mode files (.wal, .shm, and .journal if present).
487+
/// Throws an error if a file exists but could not be deleted. Files that don't exist are ignored.
488+
func deleteSQLiteFiles(dbFilename: String, in directory: URL) throws {
489+
let fileManager = FileManager.default
490+
491+
// SQLite files to delete:
492+
// 1. Main database file: dbFilename
493+
// 2. WAL file: dbFilename-wal
494+
// 3. SHM file: dbFilename-shm
495+
// 4. Journal file: dbFilename-journal (for rollback journal mode, though WAL mode typically doesn't use it)
496+
497+
let filesToDelete = [
498+
dbFilename,
499+
"\(dbFilename)-wal",
500+
"\(dbFilename)-shm",
501+
"\(dbFilename)-journal"
502+
]
503+
504+
for filename in filesToDelete {
505+
let fileURL = directory.appendingPathComponent(filename)
506+
if fileManager.fileExists(atPath: fileURL.path) {
507+
try fileManager.removeItem(at: fileURL)
508+
}
509+
// If file doesn't exist, we ignore it and continue
510+
}
511+
}

Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public protocol PowerSyncDatabaseProtocol: Queries, Sendable {
188188
/// data by transaction. One batch may contain data from multiple transactions,
189189
/// and a single transaction may be split over multiple batches.
190190
func getCrudBatch(limit: Int32) async throws -> CrudBatch?
191-
191+
192192
/// Obtains an async iterator of completed transactions with local writes against the database.
193193
///
194194
/// This is typically used from the ``PowerSyncBackendConnectorProtocol/uploadData(database:)`` callback.
@@ -228,6 +228,17 @@ public protocol PowerSyncDatabaseProtocol: Queries, Sendable {
228228
///
229229
/// Once close is called, this database cannot be used again - a new one must be constructed.
230230
func close() async throws
231+
232+
/// Close the database, releasing resources.
233+
/// Also disconnects any active connection.
234+
///
235+
/// Once close is called, this database cannot be used again - a new one must be constructed.
236+
///
237+
/// - Parameter deleteDatabase: Set to true to delete the SQLite database files. Defaults to `false`.
238+
///
239+
/// - Throws: An error if a database file exists but could not be deleted. Files that don't exist are ignored.
240+
/// This includes the main database file and any WAL mode files (.wal, .shm, .journal).
241+
func close(deleteDatabase: Bool) async throws
231242
}
232243

233244
public extension PowerSyncDatabaseProtocol {
@@ -243,13 +254,13 @@ public extension PowerSyncDatabaseProtocol {
243254
/// Unlike `getCrudBatch`, this only returns data from a single transaction at a time.
244255
/// All data for the transaction is loaded into memory.
245256
func getNextCrudTransaction() async throws -> CrudTransaction? {
246-
for try await transaction in self.getCrudTransactions() {
257+
for try await transaction in getCrudTransactions() {
247258
return transaction
248259
}
249-
260+
250261
return nil
251262
}
252-
263+
253264
///
254265
/// The connection is automatically re-opened if it fails for any reason.
255266
///

Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
7171

7272
func testGetError() async throws {
7373
do {
74-
let _ = try await database.get(
74+
_ = try await database.get(
7575
sql: "SELECT id, name, email FROM usersfail WHERE id = ?",
7676
parameters: ["1"]
7777
) { cursor in
@@ -116,7 +116,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
116116

117117
func testGetOptionalError() async throws {
118118
do {
119-
let _ = try await database.getOptional(
119+
_ = try await database.getOptional(
120120
sql: "SELECT id, name, email FROM usersfail WHERE id = ?",
121121
parameters: ["1"]
122122
) { cursor in
@@ -140,7 +140,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
140140
parameters: ["1", "Test User", "test@example.com"]
141141
)
142142
do {
143-
let _ = try await database.getOptional(
143+
_ = try await database.getOptional(
144144
sql: "SELECT id, name, email FROM users WHERE id = ?",
145145
parameters: ["1"]
146146
) { _ throws in
@@ -181,7 +181,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
181181

182182
func testGetAllError() async throws {
183183
do {
184-
let _ = try await database.getAll(
184+
_ = try await database.getAll(
185185
sql: "SELECT id, name, email FROM usersfail WHERE id = ?",
186186
parameters: ["1"]
187187
) { cursor in
@@ -624,4 +624,77 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
624624
XCTAssertEqual(result[0], JoinOutput(name: "Test User", description: "task 1", comment: "comment 1"))
625625
XCTAssertEqual(result[1], JoinOutput(name: "Test User", description: "task 2", comment: "comment 2"))
626626
}
627+
628+
func testCloseWithDeleteDatabase() async throws {
629+
let fileManager = FileManager.default
630+
let testDbFilename = "test_delete_\(UUID().uuidString).db"
631+
632+
// Get the database directory using the helper function
633+
let databaseDirectory = try appleDefaultDatabaseDirectory()
634+
635+
// Create a database with a real file
636+
let testDatabase = PowerSyncDatabase(
637+
schema: schema,
638+
dbFilename: testDbFilename,
639+
logger: DatabaseLogger(DefaultLogger())
640+
)
641+
642+
// Perform some operations to ensure the database file is created
643+
try await testDatabase.execute(
644+
sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)",
645+
parameters: ["1", "Test User", "test@example.com"]
646+
)
647+
648+
// Verify the database file exists
649+
let dbFile = databaseDirectory.appendingPathComponent(testDbFilename)
650+
XCTAssertTrue(fileManager.fileExists(atPath: dbFile.path), "Database file should exist")
651+
652+
// Close with deleteDatabase: true
653+
try await testDatabase.close(deleteDatabase: true)
654+
655+
// Verify the database file and related files are deleted
656+
XCTAssertFalse(fileManager.fileExists(atPath: dbFile.path), "Database file should be deleted")
657+
658+
let walFile = databaseDirectory.appendingPathComponent("\(testDbFilename)-wal")
659+
let shmFile = databaseDirectory.appendingPathComponent("\(testDbFilename)-shm")
660+
let journalFile = databaseDirectory.appendingPathComponent("\(testDbFilename)-journal")
661+
662+
XCTAssertFalse(fileManager.fileExists(atPath: walFile.path), "WAL file should be deleted")
663+
XCTAssertFalse(fileManager.fileExists(atPath: shmFile.path), "SHM file should be deleted")
664+
XCTAssertFalse(fileManager.fileExists(atPath: journalFile.path), "Journal file should be deleted")
665+
}
666+
667+
func testCloseWithoutDeleteDatabase() async throws {
668+
let fileManager = FileManager.default
669+
let testDbFilename = "test_no_delete_\(UUID().uuidString).db"
670+
671+
// Get the database directory using the helper function
672+
let databaseDirectory = try appleDefaultDatabaseDirectory()
673+
674+
// Create a database with a real file
675+
let testDatabase = PowerSyncDatabase(
676+
schema: schema,
677+
dbFilename: testDbFilename,
678+
logger: DatabaseLogger(DefaultLogger())
679+
)
680+
681+
// Perform some operations to ensure the database file is created
682+
try await testDatabase.execute(
683+
sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)",
684+
parameters: ["1", "Test User", "test@example.com"]
685+
)
686+
687+
// Verify the database file exists
688+
let dbFile = databaseDirectory.appendingPathComponent(testDbFilename)
689+
XCTAssertTrue(fileManager.fileExists(atPath: dbFile.path), "Database file should exist")
690+
691+
// Close with deleteDatabase: false (default)
692+
try await testDatabase.close()
693+
694+
// Verify the database file still exists
695+
XCTAssertTrue(fileManager.fileExists(atPath: dbFile.path), "Database file should still exist after close without delete")
696+
697+
// Clean up: delete all SQLite files using the helper function
698+
try deleteSQLiteFiles(dbFilename: testDbFilename, in: databaseDirectory)
699+
}
627700
}

0 commit comments

Comments
 (0)