Skip to content
Merged
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

# 1.0.0-Beta.10 (unreleased)

* Added the ability to specify a custom logging implementation
```swift
let db = PowerSyncDatabase(
schema: Schema(
tables: [
Table(
name: "users",
columns: [
.text("name"),
.text("email")
]
)
]
),
logger: DefaultLogger(minSeverity: .debug)
)
```
* added `.close()` method on `PowerSyncDatabaseProtocol`

## 1.0.0-Beta.9

* Update PowerSync SQLite core extension to 0.3.12.
Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<a href="https://www.powersync.com" target="_blank"><img src="https://github.com/powersync-ja/.github/assets/7372448/d2538c43-c1a0-4c47-9a76-41462dba484f"/></a>
</p>

*[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side.*
_[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side._

# PowerSync Swift

Expand All @@ -16,7 +16,7 @@ This SDK is currently in a beta release it is suitable for production use, given

- [Sources](./Sources/)

- This is the Swift SDK implementation.
- This is the Swift SDK implementation.

## Demo Apps / Example Projects

Expand Down Expand Up @@ -51,11 +51,35 @@ to your `Package.swift` file and pin the dependency to a specific version. The v

to your `Package.swift` file and pin the dependency to a specific version. This is required because the package is in beta.

## Usage

Create a PowerSync client

```swift
import PowerSync

let powersync = PowerSyncDatabase(
schema: Schema(
tables: [
Table(
name: "users",
columns: [
.text("count"),
.integer("is_active"),
.real("weight"),
.text("description")
]
)
]
),
logger: DefaultLogger(minSeverity: .debug)
)
```

## Underlying Kotlin Dependency

The PowerSync Swift SDK currently makes use of the [PowerSync Kotlin Multiplatform SDK](https://github.com/powersync-ja/powersync-kotlin) with the API tool [SKIE](https://skie.touchlab.co/) and KMMBridge under the hood to help generate and publish a native Swift package. We will move to an entirely Swift native API in v1 and do not expect there to be any breaking changes. For more details, see the [Swift SDK reference](https://docs.powersync.com/client-sdk-references/swift).


## Migration from Alpha to Beta

See these [developer notes](https://docs.powersync.com/client-sdk-references/swift#migrating-from-the-alpha-to-the-beta-sdk) if you are migrating from the alpha to the beta version of the Swift SDK.
106 changes: 106 additions & 0 deletions Sources/PowerSync/Kotlin/DatabaseLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import PowerSyncKotlin

/// Adapts a Swift `LoggerProtocol` to Kermit's `LogWriter` interface.
///
/// This allows Kotlin logging (via Kermit) to call into the Swift logging implementation.
private class KermitLogWriterAdapter: Kermit_coreLogWriter {
/// The underlying Swift log writer to forward log messages to.
let logger: any LoggerProtocol

/// Initializes a new adapter.
///
/// - Parameter logger: A Swift log writer that will handle log output.
init(logger: any LoggerProtocol) {
self.logger = logger
super.init()
}

/// Called by Kermit to log a message.
///
/// - Parameters:
/// - severity: The severity level of the log.
/// - message: The content of the log message.
/// - tag: A string categorizing the log.
/// - throwable: An optional Kotlin exception (ignored here).
override func log(severity: Kermit_coreSeverity, message: String, tag: String, throwable: KotlinThrowable?) {
switch severity {
case PowerSyncKotlin.Kermit_coreSeverity.verbose:
return logger.debug(message, tag: tag)
case PowerSyncKotlin.Kermit_coreSeverity.debug:
return logger.debug(message, tag: tag)
case PowerSyncKotlin.Kermit_coreSeverity.info:
return logger.info(message, tag: tag)
case PowerSyncKotlin.Kermit_coreSeverity.warn:
return logger.warning(message, tag: tag)
case PowerSyncKotlin.Kermit_coreSeverity.error:
return logger.error(message, tag: tag)
case PowerSyncKotlin.Kermit_coreSeverity.assert:
return logger.fault(message, tag: tag)
}
}
}

/// A logger implementation that integrates with PowerSync's Kotlin core using Kermit.
///
/// This class bridges Swift log writers with the Kotlin logging system and supports
/// runtime configuration of severity levels and writer lists.
internal class DatabaseLogger: LoggerProtocol {
/// The underlying Kermit logger instance provided by the PowerSyncKotlin SDK.
public let kLogger = PowerSyncKotlin.generateLogger(logger: nil)
public let logger: any LoggerProtocol

/// Initializes a new logger with an optional list of writers.
///
/// - Parameter logger: A logger which will be called for each internal log operation
init(_ logger: any LoggerProtocol) {
self.logger = logger
// Set to the lowest severity. The provided logger should filter by severity
kLogger.mutableConfig.setMinSeverity(Kermit_coreSeverity.verbose)
kLogger.mutableConfig.setLogWriterList(
[KermitLogWriterAdapter(logger: logger)]
)
}

/// Sets the minimum severity level that will be logged.
///
/// Messages below this level will be ignored.
///
/// - Parameter severity: The minimum `LogSeverity` to allow through.
public func setMinSeverity(_ severity: LogSeverity) {
logger.setMinSeverity(severity)
}

/// Sets the list of log writers that will receive log messages.
///
/// This updates both the internal writer list and the Kermit logger's configuration.
///
/// - Parameter writers: An array of Swift `LogWritterProtocol` implementations.
public func setWriters(_ writers: [any LogWriterProtocol]) {
logger.setWriters(writers)
}

/// Logs a debug-level message.
public func debug(_ message: String, tag: String?) {
logger.debug(message, tag: tag)
}

/// Logs an info-level message.
public func info(_ message: String, tag: String?) {
logger.info(message, tag: tag)
}

/// Logs a warning-level message.
public func warning(_ message: String, tag: String?) {
logger.warning(message, tag: tag)
}

/// Logs an error-level message.
public func error(_ message: String, tag: String?) {
logger.error(message, tag: tag)
}

/// Logs a fault (assert-level) message, typically used for critical issues.
public func fault(_ message: String, tag: String?) {
logger.fault(message, tag: tag)
}
}
10 changes: 8 additions & 2 deletions Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {

init(
schema: Schema,
dbFilename: String
dbFilename: String,
logger: DatabaseLogger? = nil
) {
let factory = PowerSyncKotlin.DatabaseDriverFactory()
kotlinDatabase = PowerSyncDatabase(
factory: factory,
schema: KotlinAdapter.Schema.toKotlin(schema),
dbFilename: dbFilename
dbFilename: dbFilename,
logger: logger?.kLogger
)
}

Expand Down Expand Up @@ -232,4 +234,8 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
func readTransaction<R>(callback: @escaping (any PowerSyncTransaction) throws -> R) async throws -> R {
return try safeCast(await kotlinDatabase.readTransaction(callback: TransactionCallback(callback: callback)), to: R.self)
}

func close() async throws{
try await kotlinDatabase.close()
}
}
97 changes: 97 additions & 0 deletions Sources/PowerSync/Logger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import OSLog

/// A log writer which prints to the standard output
///
/// This writer uses `os.Logger` on iOS 14+ and falls back to `print` for earlier versions.
public class PrintLogWriter: LogWriterProtocol {

/// Logs a message with a given severity and optional tag.
///
/// - Parameters:
/// - severity: The severity level of the message.
/// - message: The content of the log message.
/// - tag: An optional tag used to categorize the message. If empty, no brackets are shown.
public func log(severity: LogSeverity, message: String, tag: String?) {
let tagPrefix: String
if let tag, !tag.isEmpty {
tagPrefix = "[\(tag)] "
} else {
tagPrefix = ""
}

let message = "\(tagPrefix) \(message)"
if #available(iOS 14.0, *) {
let l = Logger()

switch severity {
case .info:
l.info("\(message)")
case .error:
l.error("\(message)")
case .debug:
l.debug("\(message)")
case .warning:
l.warning("\(message)")
case .fault:
l.fault("\(message)")
}
} else {
print("\(severity.stringValue): \(message)")
}
}
}

/// A default logger configuration that uses `PrintLogWritter` and filters messages by minimum severity.
public class DefaultLogger: LoggerProtocol {
public var minSeverirty: LogSeverity
public var writers: [any LogWriterProtocol]

/// Initializes the default logger with an optional minimum severity level.
///
/// - Parameters
/// - minSeverity: The minimum severity level to log. Defaults to `.debug`.
/// - writers: Optional writers which logs should be written to. Defaults to a `PrintLogWriter`.
public init(minSeverity: LogSeverity = .debug, writers: [any LogWriterProtocol]? = nil ) {
self.writers = writers ?? [ PrintLogWriter() ]
self.minSeverirty = minSeverity
}

public func setWriters(_ writters: [any LogWriterProtocol]) {
self.writers = writters
}

public func setMinSeverity(_ severity: LogSeverity) {
self.minSeverirty = severity
}


public func debug(_ message: String, tag: String? = nil) {
self.writeLog(message, severity: LogSeverity.debug, tag: tag)
}

public func error(_ message: String, tag: String? = nil) {
self.writeLog(message, severity: LogSeverity.error, tag: tag)
}

public func info(_ message: String, tag: String? = nil) {
self.writeLog(message, severity: LogSeverity.info, tag: tag)
}

public func warning(_ message: String, tag: String? = nil) {
self.writeLog(message, severity: LogSeverity.warning, tag: tag)
}

public func fault(_ message: String, tag: String? = nil) {
self.writeLog(message, severity: LogSeverity.fault, tag: tag)
}

private func writeLog(_ message: String, severity: LogSeverity, tag: String?) {
if (severity.rawValue < self.minSeverirty.rawValue) {
return
}

for writer in self.writers {
writer.log(severity: severity, message: message, tag: tag)
}
}
}
Loading