diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index f13117d..9a8ff59 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -14,25 +14,13 @@ jobs: uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - - name: Build and Test + + - name: Test on iOS simulator run: | xcodebuild test -scheme PowerSync-Package -destination "platform=iOS Simulator,name=iPhone 16" - xcodebuild test -scheme PowerSync-Package -destination "platform=macOS,arch=arm64,name=My Mac" - xcodebuild test -scheme PowerSync-Package -destination "platform=watchOS Simulator,arch=arm64,name=Apple Watch Ultra 2 (49mm)" - - buildSwift6: - name: Build and test with Swift 6 - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Set up XCode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - name: Use Swift 6 + - name: Test on macOS run: | - sed -i '' 's|^// swift-tools-version:.*$|// swift-tools-version:6.1|' Package.swift - - name: Build and Test + xcodebuild test -scheme PowerSync-Package -destination "platform=macOS,arch=arm64,name=My Mac" + - name: Test on watchOS simulator run: | - swift build -Xswiftc -strict-concurrency=complete - swift test -Xswiftc -strict-concurrency=complete + xcodebuild test -scheme PowerSync-Package -destination "platform=watchOS Simulator,arch=arm64,name=Apple Watch Ultra 2 (49mm)" diff --git a/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a9e2dc7..9d69379 100644 --- a/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "state" : { - "revision" : "b2a81af14e9ad83393eb187bb02e62e6db8b5ad6", - "version" : "0.4.6" + "revision" : "9801f4aa0923c7f33fa479a01e643d00e7764f0b", + "version" : "0.4.8" + } + }, + { + "identity" : "sqlcipher.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sqlcipher/SQLCipher.swift.git", + "state" : { + "revision" : "247b042574b4838c7eefdb95d6a250a843dcf1ad", + "version" : "4.11.0" } }, { diff --git a/Demo/GRDB Demo/GRDB Demo/GRDB_DemoApp.swift b/Demo/GRDB Demo/GRDB Demo/GRDB_DemoApp.swift index 0252d25..a4aef19 100644 --- a/Demo/GRDB Demo/GRDB Demo/GRDB_DemoApp.swift +++ b/Demo/GRDB Demo/GRDB Demo/GRDB_DemoApp.swift @@ -31,12 +31,11 @@ func openDatabase() ).first! .appendingPathComponent("test.sqlite") - var config = Configuration() - config.configurePowerSync( - schema: schema - ) - do { + var config = Configuration() + try config.configurePowerSync( + schema: schema + ) let grdb = try DatabasePool( path: dbUrl.path, configuration: config diff --git a/Package.resolved b/Package.resolved index 72e6cdb..729992b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "b4fa79e5a90b91cdc3279611b8f3e24faa521cbd12185e9002013aa958561945", "pins" : [ { "identity" : "grdb.swift", @@ -14,10 +15,19 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "state" : { - "revision" : "b2a81af14e9ad83393eb187bb02e62e6db8b5ad6", - "version" : "0.4.6" + "revision" : "9801f4aa0923c7f33fa479a01e643d00e7764f0b", + "version" : "0.4.8" + } + }, + { + "identity" : "sqlcipher.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sqlcipher/SQLCipher.swift.git", + "state" : { + "revision" : "247b042574b4838c7eefdb95d6a250a843dcf1ad", + "version" : "4.11.0" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 316de00..f6efb00 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -7,33 +7,43 @@ let packageName = "PowerSync" // Set this to the absolute path of your Kotlin SDK checkout if you want to use a local Kotlin // build. Also see docs/LocalBuild.md for details -let localKotlinSdkOverride: String? = "/Users/stevenontong/Documents/platform_code/powersync/powersync-kotlin/internal" - +let localKotlinSdkOverride: String? = nil // Set this to the absolute path of your powersync-sqlite-core checkout if you want to use a // local build of the core extension. let localCoreExtension: String? = nil +// Currently encryption is required for GRDB integration +let encryption = true + // Our target and dependency setup is different when a local Kotlin SDK is used. Without the local // SDK, we have no package dependency on Kotlin and download the XCFramework from Kotlin releases as // a binary target. // With a local SDK, we point to a `Package.swift` within the Kotlin SDK containing a target pointing // towards a local framework build -var conditionalDependencies: [Package.Dependency] = [] +var conditionalDependencies: [Package.Dependency] = [ +] + var conditionalTargets: [Target] = [] var kotlinTargetDependency = Target.Dependency.target(name: "PowerSyncKotlin") +if encryption { + conditionalDependencies.append(.package(url: "https://github.com/sqlcipher/SQLCipher.swift.git", from: "4.10.0")) +} else { + conditionalDependencies.append(.package(url: "https://github.com/sbooth/CSQLite.git", from: "3.50.4")) +} + if let kotlinSdkPath = localKotlinSdkOverride { // We can't depend on local XCFrameworks outside of this project's root, so there's a Package.swift // in the PowerSyncKotlin project pointing towards a local build. - conditionalDependencies.append(.package(path: "\(kotlinSdkPath)/PowerSyncKotlin")) + conditionalDependencies.append(.package(path: "\(kotlinSdkPath)/internal/PowerSyncKotlin")) kotlinTargetDependency = .product(name: "PowerSyncKotlin", package: "PowerSyncKotlin") } else { // Not using a local build, so download from releases conditionalTargets.append(.binaryTarget( name: "PowerSyncKotlin", - url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.7.0/PowersyncKotlinRelease.zip", - checksum: "836ac106c26a184c10373c862745d9af195737ad01505bb965f197797aa88535" + url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.8.0/PowersyncKotlinRelease.zip", + checksum: "31ac7c5e11d747e11bceb0b34f30438d37033e700c621b0a468aa308d887587f" )) } @@ -45,7 +55,7 @@ if let corePath = localCoreExtension { // Not using a local build, so download from releases conditionalDependencies.append(.package( url: "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", - exact: "0.4.6" + exact: "0.4.8" )) } @@ -87,7 +97,10 @@ let package = Package( name: packageName, dependencies: [ kotlinTargetDependency, - .product(name: "PowerSyncSQLiteCore", package: corePackageName) + .product(name: "PowerSyncSQLiteCore", package: corePackageName), + encryption ? + .product(name: "SQLCipher", package: "SQLCipher.swift") : + .product(name: "CSQLite", package: "CSQLite") ] ), .target( @@ -95,6 +108,7 @@ let package = Package( dependencies: [ .target(name: "PowerSync"), .product(name: "GRDB", package: "GRDB.swift") + ] ), .testTarget( diff --git a/README.md b/README.md index 54feb27..82bdbec 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ import GRDB // Configure GRDB with PowerSync support var config = Configuration() -config.configurePowerSync(schema: mySchema) +try config.configurePowerSync(schema: mySchema) // Create database with PowerSync enabled let dbPool = try DatabasePool( diff --git a/Sources/PowerSync/Kotlin/KotlinAdapter.swift b/Sources/PowerSync/Kotlin/KotlinAdapter.swift index 8e85220..2d73ab3 100644 --- a/Sources/PowerSync/Kotlin/KotlinAdapter.swift +++ b/Sources/PowerSync/Kotlin/KotlinAdapter.swift @@ -49,7 +49,8 @@ enum KotlinAdapter { return PowerSyncKotlin.RawTable( name: table.name, put: translateStatement(table.put), - delete: translateStatement(table.delete) + delete: translateStatement(table.delete), + clear: table.clear, ); } diff --git a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift index 4393aab..dd8bf6c 100644 --- a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -1,3 +1,4 @@ +import SQLCipher import Foundation import PowerSyncKotlin @@ -80,9 +81,10 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol, try await kotlinDatabase.disconnect() } - func disconnectAndClear(clearLocal: Bool = true) async throws { + func disconnectAndClear(clearLocal: Bool, soft: Bool) async throws { try await kotlinDatabase.disconnectAndClear( - clearLocal: clearLocal + clearLocal: clearLocal, + soft: soft ) } @@ -391,11 +393,18 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol, func openKotlinDBDefault( schema: Schema, dbFilename: String, - logger: DatabaseLogger + logger: DatabaseLogger, + initialStatements: [String] = [] ) -> PowerSyncDatabaseProtocol { + let rc = sqlite3_initialize() + if rc != 0 { + fatalError("Call to sqlite3_initialize() failed with \(rc)") + } + + let factory = sqlite3DatabaseFactory(initialStatements: initialStatements) return KotlinPowerSyncDatabaseImpl( kotlinDatabase: PowerSyncDatabase( - factory: PowerSyncKotlin.DatabaseDriverFactory(), + factory: factory, schema: KotlinAdapter.Schema.toKotlin(schema), dbFilename: dbFilename, logger: logger.kLogger diff --git a/Sources/PowerSync/Kotlin/KotlinSQLiteConnectionPool.swift b/Sources/PowerSync/Kotlin/KotlinSQLiteConnectionPool.swift index 339f663..c113659 100644 --- a/Sources/PowerSync/Kotlin/KotlinSQLiteConnectionPool.swift +++ b/Sources/PowerSync/Kotlin/KotlinSQLiteConnectionPool.swift @@ -71,12 +71,12 @@ final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter { // We currently only use this for schema updates return try await wrapExceptions { let sendableCallback = SendableAllLeaseCallback(callback) - try await pool.write { lease in + try await pool.withAllConnections { writer, readers in try sendableCallback.execute( writeLease: KotlinLeaseAdapter( - lease: lease + lease: writer ), - readLeases: [] + readLeases: readers.map { KotlinLeaseAdapter(lease: $0) } ) } } diff --git a/Sources/PowerSync/Kotlin/kotlinWithSession.swift b/Sources/PowerSync/Kotlin/kotlinWithSession.swift index c467af8..6ec03d0 100644 --- a/Sources/PowerSync/Kotlin/kotlinWithSession.swift +++ b/Sources/PowerSync/Kotlin/kotlinWithSession.swift @@ -4,11 +4,14 @@ func kotlinWithSession( db: OpaquePointer, action: @escaping () throws -> ReturnType, ) throws -> WithSessionResult { + var innerResult: ReturnType? let baseResult = try withSession( db: UnsafeMutableRawPointer(db), block: { do { - return try PowerSyncResult.Success(value: action()) + innerResult = try action() + // We'll use the innerResult closure above to return the result + return PowerSyncResult.Success(value: nil) } catch { return PowerSyncResult.Failure(exception: error.toPowerSyncError()) } @@ -16,20 +19,17 @@ func kotlinWithSession( ) var outputResult: Result - switch baseResult.blockResult { - case let success as PowerSyncResult.Success: - do { - let casted = try safeCast(success.value, to: ReturnType.self) - outputResult = .success(casted) - } catch { - outputResult = .failure(error) - } - - case let failure as PowerSyncResult.Failure: + if let failure = baseResult.blockResult as? PowerSyncResult.Failure { outputResult = .failure(failure.exception.asError()) - - default: - outputResult = .failure(PowerSyncError.operationFailed(message: "Unknown error encountered when processing session")) + } else if let result = innerResult { + outputResult = .success(result) + } else { + // The return type is not nullable, so we should have a result + outputResult = .failure( + PowerSyncError.operationFailed( + message: "Unknown error encountered when processing session", + ) + ) } return WithSessionResult( diff --git a/Sources/PowerSync/PowerSyncDatabase.swift b/Sources/PowerSync/PowerSyncDatabase.swift index 099b48f..a5da231 100644 --- a/Sources/PowerSync/PowerSyncDatabase.swift +++ b/Sources/PowerSync/PowerSyncDatabase.swift @@ -12,12 +12,14 @@ public let DEFAULT_DB_FILENAME = "powersync.db" public func PowerSyncDatabase( schema: Schema, dbFilename: String = DEFAULT_DB_FILENAME, - logger: (any LoggerProtocol) = DefaultLogger() + logger: (any LoggerProtocol) = DefaultLogger(), + initialStatements: [String] = [] ) -> PowerSyncDatabaseProtocol { return openKotlinDBDefault( schema: schema, dbFilename: dbFilename, - logger: DatabaseLogger(logger) + logger: DatabaseLogger(logger), + initialStatements: initialStatements ) } diff --git a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift index cd112a5..d0a7161 100644 --- a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift +++ b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift @@ -216,12 +216,20 @@ public protocol PowerSyncDatabaseProtocol: Queries, Sendable { func disconnect() async throws /// Disconnect and clear the database. - /// Use this when logging out. - /// The database can still be queried after this is called, but the tables - /// would be empty. /// - /// - Parameter clearLocal: Set to false to preserve data in local-only tables. Defaults to `true`. - func disconnectAndClear(clearLocal: Bool) async throws + /// Clearing the database is useful when a user logs out, to ensure another user logging in later would not see + /// previous data. + /// + /// The database can still be queried after this is called, but the tables would be empty. + /// + /// To perserve data in local-only tables, set `clearLocal` to `false`. + /// + /// A `soft` clear deletes publicly visible data, but keeps internal copies of data synced in the database. This + /// usually means that if the same user logs out and back in again, the first sync is very fast because all internal + /// data is still available. When a different user logs in, no old data would be visible at any point. + /// Using soft clears is recommended where it's not a security issue that old data could be reconstructed from + /// the database. + func disconnectAndClear(clearLocal: Bool, soft: Bool) async throws /// Close the database, releasing resources. /// Also disconnects any active connection. @@ -290,7 +298,15 @@ public extension PowerSyncDatabaseProtocol { } func disconnectAndClear() async throws { - try await disconnectAndClear(clearLocal: true) + try await disconnectAndClear(clearLocal: true, soft: false) + } + + func disconnectAndClear(clearLocal: Bool) async throws { + try await disconnectAndClear(clearLocal: clearLocal, soft: false) + } + + func disconnectAndClear(soft: Bool) async throws { + try await disconnectAndClear(clearLocal: true, soft: soft) } func getCrudBatch(limit: Int32 = 100) async throws -> CrudBatch? { diff --git a/Sources/PowerSync/Protocol/Schema/RawTable.swift b/Sources/PowerSync/Protocol/Schema/RawTable.swift index b209583..f8fbcd0 100644 --- a/Sources/PowerSync/Protocol/Schema/RawTable.swift +++ b/Sources/PowerSync/Protocol/Schema/RawTable.swift @@ -24,11 +24,15 @@ public struct RawTable: BaseTableProtocol { /// The statement to run when the sync client has to delete a row. public let delete: PendingStatement + + /// An optional statement to run when the database is cleared. + public let clear: String? - public init(name: String, put: PendingStatement, delete: PendingStatement) { + public init(name: String, put: PendingStatement, delete: PendingStatement, clear: String? = nil) { self.name = name self.put = put self.delete = delete + self.clear = clear } } diff --git a/Sources/PowerSync/resolvePowerSyncLoadableExtensionPath.swift b/Sources/PowerSync/resolvePowerSyncLoadableExtensionPath.swift index ac28035..99a3fb1 100644 --- a/Sources/PowerSync/resolvePowerSyncLoadableExtensionPath.swift +++ b/Sources/PowerSync/resolvePowerSyncLoadableExtensionPath.swift @@ -55,7 +55,8 @@ /// /// - Returns: The file system path to the PowerSync SQLite extension, or `nil` on watchOS /// (where the extension is statically loaded and doesn't require a path) -/// - Throws: An error if the extension path cannot be resolved on platforms that require it +/// - Throws: An error if the extension path cannot be resolved on platforms that require it or +/// if the extension could not be registered on watchOS. public func resolvePowerSyncLoadableExtensionPath() throws -> String? { return try kotlinResolvePowerSyncLoadableExtensionPath() } diff --git a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift index f1ce149..7ef982f 100644 --- a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift +++ b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift @@ -15,17 +15,25 @@ public extension Configuration { /// Example usage: /// ```swift /// var config = Configuration() - /// config.configurePowerSync(schema: mySchema) + /// try config.configurePowerSync(schema: mySchema) /// let dbQueue = try DatabaseQueue(path: dbPath, configuration: config) /// ``` /// /// - Parameter schema: The PowerSync `Schema` describing your sync views. + /// - Throws: An error if the PowerSync extension path cannot be resolved, + /// if extension loading cannot be enabled, or if the PowerSync extension + /// fails to load or initialize. mutating func configurePowerSync( schema: Schema - ) { + ) throws { + // Handles the case on WatchOS where the extension is statically loaded. + // We need to register the extension before SQLite connections are established. + // This should only throw on non-WatchOS platforms if the extension path cannot be resolved. So we catch and ignore the error. + let extensionPath = try resolvePowerSyncLoadableExtensionPath() + // Register the PowerSync core extension prepareDatabase { database in - guard let extensionPath = try resolvePowerSyncLoadableExtensionPath() else { + guard let extensionPath = extensionPath else { // We get the extension path for non WatchOS platforms. // The Kotlin registration for automatically loading the extension does not seem to work. // We explicitly initialize the extension here. diff --git a/Sources/PowerSyncGRDB/Connections/GRDBConnectionPool.swift b/Sources/PowerSyncGRDB/Connections/GRDBConnectionPool.swift index 6b9cabe..356d5ff 100644 --- a/Sources/PowerSyncGRDB/Connections/GRDBConnectionPool.swift +++ b/Sources/PowerSyncGRDB/Connections/GRDBConnectionPool.swift @@ -63,25 +63,29 @@ actor GRDBConnectionPool: SQLiteConnectionPoolProtocol { GRDBConnectionLease(database: database) ) } - + return sessionResult } // Notify PowerSync of these changes tableUpdatesContinuation?.yield(result.affectedTables) // Notify GRDB, this needs to be a write (transaction) - try await pool.write { database in + try await pool.write { database in // Notify GRDB about these changes for table in result.affectedTables { try database.notifyChanges(in: Table(table)) } } + + if case let .failure(error) = result.blockResult { + throw error + } } func withAllConnections( onConnection: @Sendable @escaping (SQLiteConnectionLease, [SQLiteConnectionLease]) throws -> Void ) async throws { // FIXME, we currently don't support updating the schema - try await pool.write { database in + try await pool.writeWithoutTransaction { database in let lease = try GRDBConnectionLease(database: database) try onConnection(lease, []) } diff --git a/Sources/PowerSyncGRDB/GRDBPowerSyncDatabase.swift b/Sources/PowerSyncGRDB/GRDBPowerSyncDatabase.swift index 3d92967..7f425d6 100644 --- a/Sources/PowerSyncGRDB/GRDBPowerSyncDatabase.swift +++ b/Sources/PowerSyncGRDB/GRDBPowerSyncDatabase.swift @@ -21,7 +21,7 @@ import PowerSync /// /// // Configure GRDB with PowerSync support /// var config = Configuration() -/// config.configurePowerSync(schema: schema) +/// try config.configurePowerSync(schema: schema) /// /// // Create the database pool /// let dbPool = try DatabasePool(path: "path/to/db", configuration: config) diff --git a/Tests/PowerSyncGRDBTests/BasicTest.swift b/Tests/PowerSyncGRDBTests/BasicTest.swift index 41f1176..7806e87 100644 --- a/Tests/PowerSyncGRDBTests/BasicTest.swift +++ b/Tests/PowerSyncGRDBTests/BasicTest.swift @@ -59,7 +59,7 @@ final class GRDBTests: XCTestCase { var config = Configuration() - config.configurePowerSync( + try config.configurePowerSync( schema: schema ) diff --git a/Tests/PowerSyncTests/CrudTests.swift b/Tests/PowerSyncTests/CrudTests.swift index 065fb7a..4c23d2c 100644 --- a/Tests/PowerSyncTests/CrudTests.swift +++ b/Tests/PowerSyncTests/CrudTests.swift @@ -238,4 +238,19 @@ final class CrudTests: XCTestCase { let finalTx = try await database.getNextCrudTransaction() XCTAssertEqual(finalTx!.crud.count, 15) } + + func testSoftClear() async throws { + try await database.execute(sql: "INSERT INTO users (id, name) VALUES (uuid(), ?)", parameters: ["test"]); + try await database.execute(sql: "INSERT INTO ps_buckets (name, last_applied_op) VALUES (?, ?)", parameters: ["bkt", 10]) + + // Doing a soft-clear should delete data but keep the bucket around. + try await database.disconnectAndClear(soft: true) + let entries = try await database.getAll("SELECT name FROM ps_buckets", mapper: { cursor in try cursor.getString(index: 0) }) + XCTAssertEqual(entries.count, 1) + + // Doing a default clear also deletes buckets. + try await database.disconnectAndClear(); + let newEntries = try await database.getAll("SELECT name FROM ps_buckets", mapper: { cursor in try cursor.getString(index: 0) }) + XCTAssertEqual(newEntries.count, 0) + } } diff --git a/Tests/PowerSyncTests/EncryptionTests.swift b/Tests/PowerSyncTests/EncryptionTests.swift new file mode 100644 index 0000000..6538f00 --- /dev/null +++ b/Tests/PowerSyncTests/EncryptionTests.swift @@ -0,0 +1,67 @@ +@testable import PowerSync +import XCTest + + +final class EncryptionTests: XCTestCase { + + func testLinksSqlcipher() async throws { + let database = openKotlinDBDefault( + schema: Schema(), + dbFilename: ":memory:", + logger: DatabaseLogger(DefaultLogger()) + ) + + let version = try await database.get("pragma cipher_version", mapper: {cursor in + try cursor.getString(index: 0) + }); + + XCTAssertEqual(version, "4.11.0 community") + try await database.close() + } + + func testEncryption() async throws { + let database = openKotlinDBDefault( + schema: Schema(tables: [ + Table( + name: "users", + columns: [ + .text("name") + ] + ), + ]), + dbFilename: "encrypted.db", + logger: DatabaseLogger(DefaultLogger()), + initialStatements: [ + "pragma key = 'foobar'" + ], + ) + + try await database.execute("INSERT INTO users (id, name) VALUES (uuid(), 'test')") + try await database.close() + + let another = openKotlinDBDefault( + schema: Schema(tables: [ + Table( + name: "users", + columns: [ + .text("name") + ] + ), + ]), + dbFilename: "encrypted.db", + logger: DatabaseLogger(DefaultLogger()), + initialStatements: [ + "pragma key = 'wrong password'" + ], + ) + + var hadError = false + do { + try await database.execute("DELETE FROM users") + } catch let error { + hadError = true + } + + XCTAssertTrue(hadError) + } +}