Skip to content
Draft
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Changelog

## 1.11.1 (unreleased)
## 1.12.0 (unreleased)

* Make raw tables easier to use:
* Introduce the `RawTableSchema` struct storing the name of a raw table in the database. When set,
`put` and `delete` statements can be inferred automatically.
* Add `RawTable//jsonDescription`, which can be passed to the `powersync_create_raw_table_crud_trigger`
SQL function to auto-create triggers forwarding writes to `ps_crud`.
* Fix `SyncStreamStatus` fields not being visible.
* Update PowerSync core extension to version 0.4.11.

Expand Down
69 changes: 53 additions & 16 deletions Sources/PowerSync/Kotlin/KotlinAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,72 @@ enum KotlinAdapter {

struct Table {
static func toKotlin(_ table: TableProtocol) -> PowerSyncKotlin.Table {
let trackPreviousKotlin: PowerSyncKotlin.TrackPreviousValuesOptions? = if let track = table.trackPreviousValues {
PowerSyncKotlin.TrackPreviousValuesOptions(
columnFilter: track.columnFilter,
onlyWhenChanged: track.onlyWhenChanged
)
} else {
nil
}

return PowerSyncKotlin.Table(
name: table.name,
columns: table.columns.map { Column.toKotlin($0) },
indexes: table.indexes.map { Index.toKotlin($0) },
localOnly: table.localOnly,
insertOnly: table.insertOnly,
options: translateTableOptions(table),
viewNameOverride: table.viewNameOverride,
trackMetadata: table.trackMetadata,
trackPreviousValues: trackPreviousKotlin,
ignoreEmptyUpdates: table.ignoreEmptyUpdates
)
}

static func toKotlin(_ table: RawTable) -> PowerSyncKotlin.RawTable {
if let schema = table.schema {
var put: PowerSyncKotlin.PendingStatement? = nil
var delete: PowerSyncKotlin.PendingStatement? = nil
if let definedStmt = table.put {
put = translateStatement(definedStmt)
}
if let definedStmt = table.delete {
delete = translateStatement(definedStmt)
}

return PowerSyncKotlin.RawTable(
name: table.name,
schema: translateRawTableSchema(schema),
put: put,
delete: delete,
clear: table.clear,
)
}

// If no schema is given, put and delete statements must be present (an invariant
// matched by constructor overloads on the RawTable struct).
return PowerSyncKotlin.RawTable(
name: table.name,
put: translateStatement(table.put),
delete: translateStatement(table.delete),
put: translateStatement(table.put!),
delete: translateStatement(table.delete!),
clear: table.clear
);
}

private static func translateTableOptions(_ options: TableOptionsProtocol) -> PowerSyncKotlin.TableOptions {
let trackPreviousKotlin: PowerSyncKotlin.TrackPreviousValuesOptions? = if let track = options.trackPreviousValues {
PowerSyncKotlin.TrackPreviousValuesOptions(
columnFilter: track.columnFilter,
onlyWhenChanged: track.onlyWhenChanged
)
} else {
nil
}

return PowerSyncKotlin.TableOptions(
localOnly: options.localOnly,
insertOnly: options.insertOnly,
trackMetadata: options.trackMetadata,
trackPreviousValues: trackPreviousKotlin,
ignoreEmptyUpdates: options.ignoreEmptyUpdates,
)
}

private static func translateRawTableSchema(_ schema: RawTableSchema) -> PowerSyncKotlin.RawTableSchema {
return PowerSyncKotlin.RawTableSchema.init(
tableName: schema.tableName,
syncedColumns: schema.syncedColumns,
options: translateTableOptions(schema.options)
)
}

private static func translateStatement(_ stmt: PendingStatement) -> PowerSyncKotlin.PendingStatement {
return PowerSyncKotlin.PendingStatement(
sql: stmt.sql,
Expand All @@ -67,6 +102,8 @@ enum KotlinAdapter {
return PowerSyncKotlin.PendingStatementParameterId.shared
case .column(let name):
return PowerSyncKotlin.PendingStatementParameterColumn(name: name)
case .rest:
return PowerSyncKotlin.PendingStatementParameterRest.shared
}
}
}
Expand Down
63 changes: 61 additions & 2 deletions Sources/PowerSync/Protocol/Schema/RawTable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,77 @@ public struct RawTable: BaseTableProtocol {
/// Instead, it is used by the sync client to identify which operations need to use which raw table definition.
public let name: String

public let schema: RawTableSchema?

/// The statement to run when the sync client has to insert or update a row.
public let put: PendingStatement
public let put: PendingStatement?

/// The statement to run when the sync client has to delete a row.
public let delete: PendingStatement
public let delete: PendingStatement?

/// An optional statement to run when the database is cleared.
public let clear: String?

/// Creates a raw table from explicit `put` and `delete` statements.
///
/// Alternatively, raw tables can also be constructed with a ``RawTableSchema`` to infer those statements.
public init(name: String, put: PendingStatement, delete: PendingStatement, clear: String? = nil) {
self.name = name
self.schema = nil
self.put = put
self.delete = delete
self.clear = clear
}

/// Creates a raw table where `put` and `delete` statements for the sync client are inferred from a
/// ``RawTableSchema``.
///
/// The statements can still be customized if necessary.
public init(name: String, schema: RawTableSchema, put: PendingStatement? = nil, delete: PendingStatement? = nil, clear: String? = nil) {
self.name = name
self.schema = schema
self.put = put
self.delete = delete
self.clear = clear
}

/// A JSON-serialized representation of this raw table.
///
/// The output of this can be passed to the `powersync_create_raw_table_crud_trigger` SQL
/// function to define triggers for this table.
public func jsonDescription() -> String {
return KotlinAdapter.Table.toKotlin(self).jsonDescription()
}
}

/// THe schema of a ``RawTable`` in the local database.
///
/// This information is optional when declaring raw tables. However, providing it allows the sync
/// client to infer ``RawTable/put`` and ``RawTable/delete`` statements automatically.
public struct RawTableSchema: Sendable {
/// The actual name of the raw table in the local schema.
///
/// Unlike ``RawTable/name``, which describes the name of synced tables to match, this reflects
/// the SQLite table name. This is used to infer ``RawTable/put`` and ``RawTable/delete`` statements
/// for the sync client. It can also be used to auto-generate triggers forwarding writes on raw
/// tables into the CRUD upload queue (using the `powersync_create_raw_table_crud_trigger` SQL function).
public let tableName: String

/// An optional filter of columns that should be synced.
///
/// By default, all columns in a raw table are considered for sync. If a filter is specified,
/// PowerSync treats unmatched columns as local-only and will not attempt to sync them.
public let syncedColumns: [String]?

/// Common options affecting how the `powersync_create_raw_table_crud_trigger` SQL function generates
/// triggers.
public let options: TableOptions

public init(tableName: String, syncedColumns: [String]? = nil, options: TableOptions = TableOptions()) {
self.tableName = tableName
self.syncedColumns = syncedColumns
self.options = options
}
}

/// A statement to run to sync server-side changes into a local raw table.
Expand Down Expand Up @@ -61,4 +117,7 @@ public enum PendingStatementParameter: Sendable {
/// Note that using this parameter is not allowed for ``RawTable/delete`` statements, which only have access
/// to the row's ``PendingStatementParameter/id``.
case column(String)
/// Resolves to a JSON object containing all columns from the synced row that haven't been matched
/// by a ``PendingStatementParameter/column`` value in the same statement.
case rest
}
94 changes: 35 additions & 59 deletions Sources/PowerSync/Protocol/Schema/Table.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public protocol BaseTableProtocol: Sendable {
}

/// Protocol for describing ``Table``s managed by PowerSync.
public protocol TableProtocol: BaseTableProtocol {
public protocol TableProtocol: BaseTableProtocol, TableOptionsProtocol {
///
/// List of columns.
///
Expand All @@ -19,58 +19,10 @@ public protocol TableProtocol: BaseTableProtocol {
///
var indexes: [Index] { get }
///
/// Whether the table only exists locally.
///
var localOnly: Bool { get }
///
/// Whether this is an insert-only table.
///
var insertOnly: Bool { get }
///
/// Override the name for the view
///
var viewNameOverride: String? { get }
var viewName: String { get }

/// Whether to add a hidden `_metadata` column that will ne abled for updates to
/// attach custom information about writes.
///
/// When the `_metadata` column is written to for inserts or updates, its value will not be
/// part of ``CrudEntry/opData``. Instead, it is reported as ``CrudEntry/metadata``,
/// allowing ``PowerSyncBackendConnector``s to handle these updates specially.
var trackMetadata: Bool { get }

/// When set to a non-`nil` value, track old values of columns for ``CrudEntry/previousValues``.
///
/// See ``TrackPreviousValuesOptions`` for details
var trackPreviousValues: TrackPreviousValuesOptions? { get }

/// Whether an `UPDATE` statement that doesn't change any values should be ignored entirely when
/// creating CRUD entries.
///
/// This is disabled by default, meaning that an `UPDATE` on a row that doesn't change values would
/// create a ``CrudEntry`` with an empty ``CrudEntry/opData`` and ``UpdateType/patch``.
var ignoreEmptyUpdates: Bool { get }
}

/// Options to include old values in ``CrudEntry/previousValues`` for update statements.
///
/// These options are enabled by passing them to a non-local ``Table`` constructor.
public struct TrackPreviousValuesOptions: Sendable {
/// A filter of column names for which updates should be tracked.
///
/// When set to a non-`nil` value, columns not included in this list will not appear in
/// ``CrudEntry/previousValues``. By default, all columns are included.
public let columnFilter: [String]?

/// Whether to only include old values when they were changed by an update, instead of always including
/// all old values.
public let onlyWhenChanged: Bool

public init(columnFilter: [String]? = nil, onlyWhenChanged: Bool = false) {
self.columnFilter = columnFilter
self.onlyWhenChanged = onlyWhenChanged
}
}

private let MAX_AMOUNT_OF_COLUMNS = 63
Expand All @@ -82,12 +34,34 @@ public struct Table: TableProtocol {
public let name: String
public let columns: [Column]
public let indexes: [Index]
public let localOnly: Bool
public let insertOnly: Bool
public let options: TableOptions
public let viewNameOverride: String?
public let trackMetadata: Bool
public let trackPreviousValues: TrackPreviousValuesOptions?
public let ignoreEmptyUpdates: Bool

public var localOnly: Bool {
get {
return self.options.localOnly;
}
}
public var insertOnly: Bool {
get {
return self.options.insertOnly
}
}
public var trackMetadata: Bool {
get {
return self.options.trackMetadata
}
}
public var trackPreviousValues: TrackPreviousValuesOptions? {
get {
return self.options.trackPreviousValues
}
}
public var ignoreEmptyUpdates: Bool {
get {
return self.options.ignoreEmptyUpdates
}
}

public var viewName: String {
viewNameOverride ?? name
Expand Down Expand Up @@ -116,12 +90,14 @@ public struct Table: TableProtocol {
self.name = name
self.columns = columns
self.indexes = indexes
self.localOnly = localOnly
self.insertOnly = insertOnly
self.viewNameOverride = viewNameOverride
self.trackMetadata = trackMetadata
self.trackPreviousValues = trackPreviousValues
self.ignoreEmptyUpdates = ignoreEmptyUpdates
self.options = TableOptions(
localOnly: localOnly,
insertOnly: insertOnly,
trackMetadata: trackMetadata,
trackPreviousValues: trackPreviousValues,
ignoreEmptyUpdates: ignoreEmptyUpdates
)
}

private func hasInvalidSqliteCharacters(_ string: String) -> Bool {
Expand Down
Loading
Loading