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 .github/workflows/build_and_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ jobs:
build:
name: Build and test
runs-on: macos-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Set up XCode
Expand Down
16 changes: 14 additions & 2 deletions Sources/PowerSync/Protocol/Schema/Column.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,26 @@ public protocol ColumnProtocol: Equatable, Sendable {
var type: ColumnData { get }
}

public enum ColumnData: Sendable {
public enum ColumnData: Sendable, Encodable {
case text
case integer
case real

public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .text:
try container.encode("text")
case .integer:
try container.encode("integer")
case .real:
try container.encode("real")
}
}
}

/// A single column in a table schema.
public struct Column: ColumnProtocol {
public struct Column: ColumnProtocol, Encodable {
public let name: String
public let type: ColumnData

Expand Down
23 changes: 22 additions & 1 deletion Sources/PowerSync/Protocol/Schema/Index.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public protocol IndexProtocol: Sendable {
var columns: [IndexedColumnProtocol] { get }
}

public struct Index: IndexProtocol {
public struct Index: IndexProtocol, Encodable {
public let name: String
public let columns: [IndexedColumnProtocol]

Expand Down Expand Up @@ -47,4 +47,25 @@ public struct Index: IndexProtocol {
) -> Index {
return ascending(name: name, columns: [column])
}

public func encode(to encoder: any Encoder) throws {
enum CodingKeys: CodingKey {
case name
case columns
}

var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
var columnsContainer = container.nestedUnkeyedContainer(forKey: .columns)
for column in columns {
enum IndexedColumnCodingKeys: CodingKey {
case name
case ascending
}

var container = columnsContainer.nestedContainer(keyedBy: IndexedColumnCodingKeys.self)
try container.encode(column.column, forKey: .name)
try container.encode(column.ascending, forKey: .ascending)
}
}
}
82 changes: 77 additions & 5 deletions Sources/PowerSync/Protocol/Schema/RawTable.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Foundation

/// A table that is managed by the user instead of being auto-created and migrated by the PowerSync SDK.
///
/// These tables give application developers full control over the table (including table and column constraints).
Expand All @@ -11,7 +13,7 @@
///
/// Note that raw tables are only supported when ``ConnectOptions/newClientImplementation``
/// is enabled.
public struct RawTable: BaseTableProtocol {
public struct RawTable: BaseTableProtocol, Encodable {
/// The name of the table as it appears in sync rules.
///
/// This doesn't necessarily have to match the statement that ``RawTable/put`` and ``RawTable/delete``
Expand Down Expand Up @@ -58,11 +60,52 @@ public struct RawTable: BaseTableProtocol {
/// 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()
let encoder = JSONEncoder()
do {
let serialized = try encoder.encode(self)
return String(data: serialized, encoding: .utf8)!
} catch {
// An older version of the Swift SDK used to implement this method in Kotlin.
// That could also throw an exception (which would crash the process), but that
// was overlooked due to a missing @Throws annotation.
// Now, we can't mark this as throws without breaking backwards compatibility.
// We should conver this to be throwing in a future major release.
fatalError("Serializing a raw table failed: \(error)")
}
}

internal func validate() throws(TableError) {
if let schema {
try schema.options.validate(tableName: name)
}
}

public func encode(to encoder: any Encoder) throws {
enum CodingKeys: String, CodingKey {
case name
case put
case delete
case clear
// For schema
case tableName = "table_name"
case syncedColumns = "synced_columns"
}
typealias Keys = TableOptionsCodingKeys<CodingKeys>

var container = encoder.container(keyedBy: Keys.self)
try container.encode(name, forKey: .outer(.name))
try container.encodeIfPresent(put, forKey: .outer(.put))
try container.encodeIfPresent(delete, forKey: .outer(.delete))
try container.encodeIfPresent(clear, forKey: .outer(.clear))
if let schema {
try container.encode(schema.tableName ?? name, forKey: .outer(.tableName))
try container.encodeIfPresent(schema.syncedColumns, forKey: .outer(.syncedColumns))
try schema.options.serializeTo(container)
}
}
}

/// THe schema of a ``RawTable`` in the local database.
/// 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.
Expand Down Expand Up @@ -95,7 +138,7 @@ public struct RawTableSchema: Sendable {
}

/// A statement to run to sync server-side changes into a local raw table.
public struct PendingStatement: Sendable {
public struct PendingStatement: Sendable, Encodable {
/// The SQL statement to execute.
public let sql: String
/// For parameters in the prepared statement, the values to fill in.
Expand All @@ -108,10 +151,21 @@ public struct PendingStatement: Sendable {
self.sql = sql
self.parameters = parameters
}

public func encode(to encoder: any Encoder) throws {
enum CodingKeys: String, CodingKey {
case sql
case parameters = "params"
}

var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.sql, forKey: .sql)
try container.encode(self.parameters, forKey: .parameters)
}
}

/// A parameter that can be used in a ``PendingStatement``.
public enum PendingStatementParameter: Sendable {
public enum PendingStatementParameter: Sendable, Encodable {
/// A value that resolves to the textual id of the row to insert, update or delete.
case id
/// A value that resolves to the value of a column in a `PUT` operation for inserts or updates.
Expand All @@ -122,4 +176,22 @@ public enum PendingStatementParameter: Sendable {
/// 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

public func encode(to encoder: any Encoder) throws {
switch self {
case .id:
var container = encoder.singleValueContainer()
try container.encode("Id")
case .column(let name):
enum CodingKeys: String, CodingKey {
case column = "Column"
}

var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .column)
case .rest:
var container = encoder.singleValueContainer()
try container.encode("Rest")
}
}
}
28 changes: 27 additions & 1 deletion Sources/PowerSync/Protocol/Schema/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public protocol SchemaProtocol: Sendable {
func validate() throws
}

public struct Schema: SchemaProtocol {
public struct Schema: SchemaProtocol, Encodable {
public let tables: [Table]
public let rawTables: [RawTable]

Expand Down Expand Up @@ -50,6 +50,32 @@ public struct Schema: SchemaProtocol {
}
try table.validate()
}

for table in rawTables {
// Only check for duplicate names if the raw table has a fixed local schema
// name. By default, the name in raw tables refers to the name of the table as
// defined in Sync Streams. The local table populated by put/delete statements
// might be different and we can't check that.
if let schema = table.schema {
let name = schema.tableName ?? table.name
if !tableNames.insert(name).inserted {
throw SchemaError.duplicateTableName(name)
}
}

try table.validate()
}
}

public func encode(to encoder: any Encoder) throws {
enum CodingKeys: String, CodingKey {
case tables
case rawTables = "raw_tables"
}

var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.tables, forKey: .tables)
try container.encode(self.rawTables, forKey: .rawTables)
}
}

Expand Down
29 changes: 19 additions & 10 deletions Sources/PowerSync/Protocol/Schema/Table.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private let MAX_AMOUNT_OF_COLUMNS = 63
///
/// A single table in the schema.
///
public struct Table: TableProtocol {
public struct Table: TableProtocol, Encodable {
public let name: String
public let columns: [Column]
public let indexes: [Index]
Expand Down Expand Up @@ -119,15 +119,7 @@ public struct Table: TableProtocol {
throw TableError.invalidViewName(viewName: viewNameOverride)
}

if localOnly {
if trackPreviousValues != nil {
throw TableError.trackPreviousForLocalTable(tableName: name)
}
if trackMetadata {
throw TableError.metadataForLocalTable(tableName: name)
}
}

try options.validate(tableName: name)
var columnNames = Set<String>(["id"])

for column in columns {
Expand Down Expand Up @@ -184,6 +176,23 @@ public struct Table: TableProtocol {
indexNames.insert(index.name)
}
}

public func encode(to encoder: any Encoder) throws {
enum CodingKeys: String, CodingKey {
case name
case viewName = "view_name"
case columns
case indexes
}
typealias Keys = TableOptionsCodingKeys<CodingKeys>

var container = encoder.container(keyedBy: Keys.self)
try container.encode(name, forKey: .outer(.name))
try container.encodeIfPresent(viewNameOverride, forKey: .outer(.viewName))
try container.encode(columns, forKey: .outer(.columns))
try container.encode(indexes, forKey: .outer(.indexes))
try options.serializeTo(container)
}
}

public enum TableError: Error {
Expand Down
73 changes: 73 additions & 0 deletions Sources/PowerSync/Protocol/Schema/TableOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ public struct TableOptions: TableOptionsProtocol {
self.trackPreviousValues = trackPreviousValues
self.ignoreEmptyUpdates = ignoreEmptyUpdates
}

internal func validate(tableName: String) throws(TableError) {
if localOnly {
if trackPreviousValues != nil {
throw TableError.trackPreviousForLocalTable(tableName: tableName)
}
if trackMetadata {
throw TableError.metadataForLocalTable(tableName: tableName)
}
}
}

internal func serializeTo<T: CodingKey>(_ container: KeyedEncodingContainer<TableOptionsCodingKeys<T>>) throws {
var container = container
try container.encode(localOnly, forKey: .localOnly)
try container.encode(insertOnly, forKey: .insertOnly)
try container.encode(trackMetadata, forKey: .includeMetadata)
try container.encode(ignoreEmptyUpdates, forKey: .ignoreEmptyUpdate)
try trackPreviousValues?.serializeTo(container)
}
}

/// Options to include old values in ``CrudEntry/previousValues`` for update statements.
Expand All @@ -91,4 +111,57 @@ public struct TrackPreviousValuesOptions: Sendable {
self.columnFilter = columnFilter
self.onlyWhenChanged = onlyWhenChanged
}

internal func serializeTo<T: CodingKey>(_ container: KeyedEncodingContainer<TableOptionsCodingKeys<T>>) throws {
var container = container
if let columnFilter {
try container.encode(columnFilter, forKey: .diffIncludeOld)
} else {
try container.encode(true, forKey: .diffIncludeOld)
}
try container.encode(onlyWhenChanged, forKey: .includeOldOnlyWhenChanged)
}
}

/// Coding keys for table options (which are always embedded into another outer object.
internal enum TableOptionsCodingKeys<T: CodingKey>: CodingKey {
case outer(T)
case diffIncludeOld
case localOnly
case insertOnly
case includeMetadata
case includeOldOnlyWhenChanged
case ignoreEmptyUpdate

// We don't use these for decoding, so we can return nil here.
init?(stringValue: String) {
return nil
}
init?(intValue: Int) {
return nil
}

var stringValue: String {
switch self {
case .outer(let field):
return field.stringValue
case .diffIncludeOld:
return "include_old"
case .localOnly:
return "local_only"
case .insertOnly:
return "insert_only"
case .includeMetadata:
return "include_metadata"
case .includeOldOnlyWhenChanged:
return "include_old_only_when_changed"
case .ignoreEmptyUpdate:
return "ignore_empty_update"
}
}

// We'll only encode into string-keyed dictionaries (JSON objects).
var intValue: Int? {
nil
}
}
Loading
Loading