Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 1.11.0 (unreleased)

* Add support for [sync streams](https://docs.powersync.com/sync/streams/overview).
* Add `dbDirectory` parameter to `PowerSyncDatabase()` to support custom database storage locations. This enables storing the database in a shared App Group container for access from Share Extensions and other app extensions.

## 1.10.0

Expand Down
19 changes: 13 additions & 6 deletions Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
private let encoder = JSONEncoder()
let currentStatus: SyncStatus
private let dbFilename: String
private let dbDirectory: String?

init(
kotlinDatabase: PowerSyncKotlin.PowerSyncDatabase,
dbFilename: String,
dbDirectory: String? = nil,
logger: DatabaseLogger
) {
self.logger = logger
self.kotlinDatabase = kotlinDatabase
/// We currently use the dbFilename to delete the database files when the database is closed
/// The kotlin PowerSyncDatabase.identifier currently prepends `null` to the dbFilename (for the directory).
/// FIXME. Update this once we support database directory configuration.
self.dbFilename = dbFilename
self.dbDirectory = dbDirectory
currentStatus = KotlinSyncStatus(
baseStatus: kotlinDatabase.currentStatus
)
Expand Down Expand Up @@ -340,8 +340,12 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
}

private func deleteDatabase() async throws {
// We can use the supplied dbLocation when we support that in future
let directory = try appleDefaultDatabaseDirectory()
let directory: URL
if let dbDirectory {
directory = URL(fileURLWithPath: dbDirectory, isDirectory: true)
} else {
directory = try appleDefaultDatabaseDirectory()
}
try deleteSQLiteFiles(dbFilename: dbFilename, in: directory)
}

Expand Down Expand Up @@ -420,6 +424,7 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
func openKotlinDBDefault(
schema: Schema,
dbFilename: String,
dbDirectory: String? = nil,
logger: DatabaseLogger,
initialStatements: [String] = []
) -> PowerSyncDatabaseProtocol {
Expand All @@ -434,9 +439,11 @@ func openKotlinDBDefault(
factory: factory,
schema: KotlinAdapter.Schema.toKotlin(schema),
dbFilename: dbFilename,
logger: logger.kLogger
logger: logger.kLogger,
dbDirectory: dbDirectory
),
dbFilename: dbFilename,
dbDirectory: dbDirectory,
logger: logger
)
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/PowerSync/PowerSyncDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,29 @@ public let DEFAULT_DB_FILENAME = "powersync.db"
/// - Parameters:
/// - schema: The database schema
/// - dbFilename: The database filename. Defaults to "powersync.db"
/// - dbDirectory: Optional custom directory path for the database file.
/// When `nil`, the database is stored in the default application support directory.
/// Use this to store the database in a shared App Group container, e.g.:
/// ```swift
/// let containerURL = FileManager.default.containerURL(
/// forSecurityApplicationGroupIdentifier: "group.com.example.app"
/// )
/// let dbDirectory = containerURL?.path
/// ```
/// - logger: Optional logging interface
/// - initialStatements: An optional list of statements to run as the database is opened.
/// - Returns: A configured PowerSyncDatabase instance
public func PowerSyncDatabase(
schema: Schema,
dbFilename: String = DEFAULT_DB_FILENAME,
dbDirectory: String? = nil,
logger: (any LoggerProtocol) = DefaultLogger(),
initialStatements: [String] = []
) -> PowerSyncDatabaseProtocol {
return openKotlinDBDefault(
schema: schema,
dbFilename: dbFilename,
dbDirectory: dbDirectory,
logger: DatabaseLogger(logger),
initialStatements: initialStatements
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,84 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
// Clean up: delete all SQLite files using the helper function
try deleteSQLiteFiles(dbFilename: testDbFilename, in: databaseDirectory)
}


func testCustomDbDirectory() async throws {
let fileManager = FileManager.default
let testDbFilename = "test_custom_dir_\(UUID().uuidString).db"
let customDirectory = fileManager.temporaryDirectory
.appendingPathComponent("powersync_test_\(UUID().uuidString)")

// Create the custom directory
try fileManager.createDirectory(at: customDirectory, withIntermediateDirectories: true)

let testDatabase = PowerSyncDatabase(
schema: schema,
dbFilename: testDbFilename,
dbDirectory: customDirectory.path,
logger: DatabaseLogger(DefaultLogger())
)

// Perform an operation to ensure the database file is created
try await testDatabase.execute(
sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)",
parameters: ["1", "Test User", "test@example.com"]
)

// Verify the database file exists in the custom directory
let dbFile = customDirectory.appendingPathComponent(testDbFilename)
XCTAssertTrue(fileManager.fileExists(atPath: dbFile.path), "Database file should exist in custom directory")

// Verify the file does NOT exist in the default directory
let defaultDirectory = try appleDefaultDatabaseDirectory()
let defaultDbFile = defaultDirectory.appendingPathComponent(testDbFilename)
XCTAssertFalse(fileManager.fileExists(atPath: defaultDbFile.path), "Database file should not exist in default directory")

// Close and clean up
try await testDatabase.close(deleteDatabase: true)

// Verify the database file is deleted from the custom directory
XCTAssertFalse(fileManager.fileExists(atPath: dbFile.path), "Database file should be deleted from custom directory")

// Clean up the temporary directory
try? fileManager.removeItem(at: customDirectory)
}

func testCustomDbDirectoryCloseWithDeleteDatabase() async throws {
let fileManager = FileManager.default
let testDbFilename = "test_custom_delete_\(UUID().uuidString).db"
let customDirectory = fileManager.temporaryDirectory
.appendingPathComponent("powersync_test_\(UUID().uuidString)")

try fileManager.createDirectory(at: customDirectory, withIntermediateDirectories: true)

let testDatabase = PowerSyncDatabase(
schema: schema,
dbFilename: testDbFilename,
dbDirectory: customDirectory.path,
logger: DatabaseLogger(DefaultLogger())
)

try await testDatabase.execute(
sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)",
parameters: ["1", "Test User", "test@example.com"]
)

let dbFile = customDirectory.appendingPathComponent(testDbFilename)
let walFile = customDirectory.appendingPathComponent("\(testDbFilename)-wal")
let shmFile = customDirectory.appendingPathComponent("\(testDbFilename)-shm")

XCTAssertTrue(fileManager.fileExists(atPath: dbFile.path), "Database file should exist")

try await testDatabase.close(deleteDatabase: true)

// Verify all SQLite files are deleted from the custom directory
XCTAssertFalse(fileManager.fileExists(atPath: dbFile.path), "Database file should be deleted")
XCTAssertFalse(fileManager.fileExists(atPath: walFile.path), "WAL file should be deleted")
XCTAssertFalse(fileManager.fileExists(atPath: shmFile.path), "SHM file should be deleted")

try? fileManager.removeItem(at: customDirectory)
}

func testSubscriptionsUpdateStateWhileOffline() async throws {
var streams = database.currentStatus.asFlow().makeAsyncIterator()
let initialStatus = await streams.next(); // Ignore initial
Expand Down