From e55807cea722be44a4d8d1a4a589dc35ea5fcfda Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 26 Aug 2026 11:50:38 -0700 Subject: [PATCH 1/2] Clean up test databases at exit While test databases are provisioned in the temporary directory, the OS doesn't necessarily actively clean them up till restart. So we've heard a report of a user with a long uptime accumulating many, many gigabytes over many hundreds of test runs. This adds a small `atexit` hook to prune temporary databases. It won't run if tests are cancelled or crashed, but those cases seem rare enough that we can let the OS do that cleanup eventually. --- .../CloudKit/DefaultSyncEngine.swift | 6 +---- .../Internal/TemporaryDatabase.swift | 24 +++++++++++++++++++ .../DefaultDatabase.swift | 5 +--- 3 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 Sources/SQLiteData/Internal/TemporaryDatabase.swift diff --git a/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift b/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift index a8edd53c..af7264f8 100644 --- a/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift @@ -55,11 +55,7 @@ } public static var testValue: SyncEngine { - try! SyncEngine( - for: DatabasePool( - path: URL.temporaryDirectory.appending(path: "\(UUID().uuidString).sqlite").path() - ) - ) + try! SyncEngine(for: temporaryDatabasePool()) } } #endif diff --git a/Sources/SQLiteData/Internal/TemporaryDatabase.swift b/Sources/SQLiteData/Internal/TemporaryDatabase.swift new file mode 100644 index 00000000..d343b5ce --- /dev/null +++ b/Sources/SQLiteData/Internal/TemporaryDatabase.swift @@ -0,0 +1,24 @@ +import Foundation +import GRDB + +func temporaryDatabasePool(configuration: Configuration = Configuration()) throws -> DatabasePool { + try FileManager.default.createDirectory( + at: temporaryDatabaseDirectory, withIntermediateDirectories: true + ) + return try DatabasePool( + path: temporaryDatabaseDirectory + .appending(path: "\(UUID().uuidString).db") + .path(percentEncoded: false), + configuration: configuration + ) +} + +private let temporaryDatabaseDirectory: URL = { + atexit { + try? FileManager.default.removeItem(at: temporaryDatabaseDirectory) + } + return URL.temporaryDirectory.appending( + path: "co.pointfree.SQLiteData/\(ProcessInfo.processInfo.processIdentifier)", + directoryHint: .isDirectory + ) +}() diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift b/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift index 2f70021d..1a6ee3ca 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift @@ -36,10 +36,7 @@ public func defaultDatabase( } database = try DatabasePool(path: path ?? defaultPath, configuration: configuration) case .preview, .test: - database = try DatabasePool( - path: "\(NSTemporaryDirectory())\(UUID().uuidString).db", - configuration: configuration - ) + database = try temporaryDatabasePool(configuration: configuration) } return database } From f47c20b5387aba90b39a33bab06a8983beac58a4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 26 Aug 2026 12:12:41 -0700 Subject: [PATCH 2/2] Fix --- .../Internal/TemporaryDatabase.swift | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Sources/SQLiteData/Internal/TemporaryDatabase.swift b/Sources/SQLiteData/Internal/TemporaryDatabase.swift index d343b5ce..1da57181 100644 --- a/Sources/SQLiteData/Internal/TemporaryDatabase.swift +++ b/Sources/SQLiteData/Internal/TemporaryDatabase.swift @@ -14,11 +14,23 @@ func temporaryDatabasePool(configuration: Configuration = Configuration()) throw } private let temporaryDatabaseDirectory: URL = { - atexit { - try? FileManager.default.removeItem(at: temporaryDatabaseDirectory) + let directory = URL.temporaryDirectory.appending( + path: "co.pointfree.SQLiteData", + directoryHint: .isDirectory + ) + let processDirectories = + (try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil)) + ?? [] + for processDirectory in processDirectories { + guard + let processIdentifier = pid_t(processDirectory.lastPathComponent), + kill(processIdentifier, 0) != 0, + errno == ESRCH + else { continue } + try? FileManager.default.removeItem(at: processDirectory) } - return URL.temporaryDirectory.appending( - path: "co.pointfree.SQLiteData/\(ProcessInfo.processInfo.processIdentifier)", + return directory.appending( + path: "\(ProcessInfo.processInfo.processIdentifier)", directoryHint: .isDirectory ) }()