From 4cffcd31ec000013c16efa5292ec87b7c5a3997e Mon Sep 17 00:00:00 2001 From: Amp Date: Mon, 26 Oct 2026 19:44:08 +0000 Subject: [PATCH 1/6] feat(zero-swift): typed tx.mutate., as in JS tx.mutate.issue was the untyped TableMutator (dynamic member lookup), so update and delete took dictionaries and only insert and upsert had a typed form, without the table name. Codegen now emits, per table, a TransactionMutate member returning TypedTableMutator, and nested Row.Update and Row.Key structs: - insert and upsert take the row struct; - update takes Update: the primary key, and the columns to set. A nil column keeps its value, as an undefined one does in JS. An optional column is a ColumnUpdate (.unchanged, .null, .set, or a literal), so leaving it differs from setting it to null; - delete takes Key, the primary key. Rows conform to ZeroTable (a ZeroRow with Update and Key). The untyped TableMutator stays as tx.mutate.table(name), and for tables without a generated member. Code that passed dictionaries to tx.mutate.
moves to the typed calls or table(name). Amp-Thread-ID: https://ampcode.com/threads/T-01a17664-8fd0-76be-8fd5-75ac6e7aa0d8 Co-authored-by: tmm --- .changeset/typed-table-mutators.md | 5 + packages/zero-swift-cli/README.md | 3 +- packages/zero-swift-cli/src/generate.ts | 104 ++++++- packages/zero-swift-cli/test/sweep-schema.ts | 25 ++ swift/Sources/Zero/Client/Transaction.swift | 95 +++++- .../Sources/Zero/Zero.docc/GettingStarted.md | 9 + swift/Sources/Zero/Zero.docc/JSParity.md | 12 +- swift/Sources/Zero/Zero.docc/Zero.md | 4 + swift/TestHost/App/Generated/TestSchema.swift | 294 +++++++++++++++++- swift/Tests/ConformanceTests/ClientApp.swift | 20 +- .../Generated/TestSchema.swift | 294 +++++++++++++++++- swift/Tests/E2ETests/ClientE2ETests.swift | 2 +- .../Tests/E2ETests/Generated/TestSchema.swift | 294 +++++++++++++++++- swift/Tests/ZeroTests/ClientAPITests.swift | 62 +++- .../ZeroTests/Generated/FixtureSchema.swift | 208 ++++++++++++- .../ZeroTests/Generated/TestSchema.swift | 294 +++++++++++++++++- swift/Tests/ZeroTests/LifecycleTests.swift | 6 +- swift/Tests/ZeroTests/StorageTests.swift | 6 +- .../ZeroTests/SweepActiveClientsTests.swift | 6 +- swift/Tests/ZeroTests/SweepClientTests.swift | 12 +- swift/Tests/ZeroTests/SweepStorageTests.swift | 6 +- swift/Tests/ZeroTests/ViewTests.swift | 16 +- .../Generated/TestSchema.swift | 294 +++++++++++++++++- 23 files changed, 1979 insertions(+), 92 deletions(-) create mode 100644 .changeset/typed-table-mutators.md diff --git a/.changeset/typed-table-mutators.md b/.changeset/typed-table-mutators.md new file mode 100644 index 0000000000..68ce471107 --- /dev/null +++ b/.changeset/typed-table-mutators.md @@ -0,0 +1,5 @@ +--- +'zero-swift': minor +--- + +`tx.mutate.
` is typed, as in JS: `insert` and `upsert` take the row struct, `update` its generated `Update` (the primary key and the columns to set), and `delete` its generated `Key`. An optional column in an `Update` is a `ColumnUpdate`, so leaving it (`.unchanged`) differs from setting it to null (`.null`). Breaking: code that passed a dictionary to `tx.mutate.
` now uses these types, or `tx.mutate.table("
")` for the untyped `TableMutator`. Regenerate with `zero-swift codegen`. diff --git a/packages/zero-swift-cli/README.md b/packages/zero-swift-cli/README.md index 451fcffc76..8206c21c1f 100644 --- a/packages/zero-swift-cli/README.md +++ b/packages/zero-swift-cli/README.md @@ -15,7 +15,8 @@ needs the app's own `@rocicorp/zero`, 1.11 or later. ## `zero-swift codegen` Generates Swift from the TypeScript schema. For each table it writes a row -struct, a `ZeroSchema`, and a `zql` builder with a `Query`. +struct with its `Update` and `Key`, a `ZeroSchema`, a `zql` builder with a +`Query`, and a typed `tx.mutate.
` for mutators. ```sh npx zero-swift codegen # reads zero-swift.json diff --git a/packages/zero-swift-cli/src/generate.ts b/packages/zero-swift-cli/src/generate.ts index 306c91dfc2..d63b57617a 100644 --- a/packages/zero-swift-cli/src/generate.ts +++ b/packages/zero-swift-cli/src/generate.ts @@ -118,8 +118,14 @@ const rowMembers = [ 'CodingKeys', // The parameter of `init(entry:)`, which assigns the columns. 'entry', + // What `tx.mutate.
.update` and `.delete` take. + 'Update', + 'Key', ]; +/** The members of `TransactionMutate` besides the tables. */ +const mutateMembers = ['tx', 'table', 'insert', 'upsert']; + /** The members of a `json()` object struct besides its fields. */ const objectMembers = [ 'zeroValue', @@ -559,7 +565,7 @@ export function generate( table.primaryKey.length === 1 && table.primaryKey[0] === 'id' && columns.some(c => c.name === 'id' && c.id === 'id' && !c.optional); - const conformances = ['ZeroRow', 'Codable', 'Hashable']; + const conformances = ['ZeroTable', 'Codable', 'Hashable']; if (identifiable) { conformances.push('Identifiable'); } @@ -642,6 +648,25 @@ export function generate( lines.push(' ]'); lines.push(' }'); lines.push(...codingKeys([...columns, ...relationships])); + const keyColumns = table.primaryKey.map(k => + columns.find(c => c.name === k)!, + ); + lines.push( + '', + ...columnsDecl( + pub, + 'Update', + `What \`tx.mutate.${t}.update\` takes: the primary key, and the other columns to set. A column that is nil, or \`.unchanged\`, keeps its value.`, + columns.map(c => ({...c, key: keyColumns.includes(c)})), + ), + '', + ...columnsDecl( + pub, + 'Key', + `What \`tx.mutate.${t}.delete\` takes: the primary key.`, + keyColumns.map(c => ({...c, key: true})), + ), + ); lines.push('}'); typeDecls.push(lines.join('\n')); } @@ -679,9 +704,86 @@ export function generate( out.push(''); out.push(`${pub}let ${identifier(zqlName)} = ${zqlType}()`); out.push(''); + out.push( + `/// The typed CRUD operations in a mutator, one per table: \`tx.mutate.${tableNames[0] ?? 'table'}.insert(…)\`.`, + ); + out.push('extension TransactionMutate {'); + const mutateIds = new Members(mutateMembers).claimAll( + tableNames.map(t => ({name: t, id: sanitize(t)})), + ); + for (const [i, t] of tableNames.entries()) { + const row = rowNames.get(t)!; + out.push( + ` ${pub}var ${ref(mutateIds[i])}: TypedTableMutator<${row}> { TypedTableMutator(self) }`, + ); + } + out.push('}'); + out.push(''); return out.join('\n'); } +/** + * A struct of some of a row's columns, nested in the row struct: the + * primary key's columns are required, the others optional. An optional + * column is a `ColumnUpdate`, so leaving it differs from setting it to + * null. + */ +function columnsDecl( + pub: string, + name: string, + doc: string, + columns: readonly (Column & {key: boolean})[], +): string[] { + const nullable = (c: Column) => c.swiftType.endsWith('?'); + const type = (c: Column & {key: boolean}) => + c.key + ? c.swiftType + : nullable(c) + ? `ColumnUpdate<${c.swiftType.slice(0, -1)}>` + : `${c.swiftType}?`; + const fallback = (c: Column & {key: boolean}) => + c.key ? '' : nullable(c) ? ' = .unchanged' : ' = nil'; + const lines = [ + ` /// ${doc}`, + ` ${pub}struct ${name}: ZeroColumnValues, Hashable {`, + ]; + for (const c of columns) { + lines.push(` ${pub}var ${c.property}: ${type(c)}`); + } + lines.push(''); + lines.push(` ${pub}init(`); + lines.push( + columns + .map(c => ` ${c.property}: ${type(c)}${fallback(c)}`) + .join(',\n'), + ); + lines.push(' ) {'); + for (const c of columns) { + lines.push(` self.${c.property} = ${c.property}`); + } + lines.push(' }'); + lines.push(''); + lines.push(` ${pub}var columnValues: Row {`); + lines.push(' var row = Row()'); + for (const c of columns) { + if (c.key) { + lines.push(` row[${str(c.name)}] = self.${c.property}.zeroValue`); + } else if (nullable(c)) { + lines.push( + ` if let value = self.${c.property}.zeroValue { row[${str(c.name)}] = value }`, + ); + } else { + lines.push( + ` if let value = self.${c.property} { row[${str(c.name)}] = value.zeroValue }`, + ); + } + } + lines.push(' return row'); + lines.push(' }'); + lines.push(' }'); + return lines; +} + /** * The `CodingKeys` of a struct whose properties aren't all named as their * keys, so `Codable` uses the names on the wire; none when they are. diff --git a/packages/zero-swift-cli/test/sweep-schema.ts b/packages/zero-swift-cli/test/sweep-schema.ts index 44ce95eabc..e2bdfe157f 100644 --- a/packages/zero-swift-cli/test/sweep-schema.ts +++ b/packages/zero-swift-cli/test/sweep-schema.ts @@ -173,3 +173,28 @@ export const memberEnum = createSchema({ export const clashingQueries = createSchema({ tables: [one('a-b'), one('a_b'), one('init')], }); + +/** Tables named after the members `TransactionMutate` already has. */ +export const mutateTables = createSchema({ + tables: [one('table'), one('insert'), one('upsert'), one('tx')], +}); + +/** + * Columns named after a row's `Update` and `Key` and the locals of their + * `columnValues`, with optional and nullable columns and a compound key. + */ +export const updateColumns = createSchema({ + tables: [ + table('t') + .columns({ + Update: string(), + Key: string(), + row: string(), + value: string().optional(), + kind: enumeration<'a' | 'b'>().optional(), + doc: json<{n: number} | null>(), + tags: json().optional(), + }) + .primaryKey('Update', 'Key'), + ], +}); diff --git a/swift/Sources/Zero/Client/Transaction.swift b/swift/Sources/Zero/Client/Transaction.swift index 7bc6a7143c..7857be21b4 100644 --- a/swift/Sources/Zero/Client/Transaction.swift +++ b/swift/Sources/Zero/Client/Transaction.swift @@ -116,7 +116,9 @@ public final class Transaction { } } -/// `tx.mutate`: the tables of the schema. +/// `tx.mutate`: the tables of the schema. `zero-swift codegen` adds a +/// typed member per table, `tx.mutate.issue`, as in JS. A table without +/// one, or `table(_:)`, gives the untyped ``TableMutator``. @MainActor @dynamicMemberLookup public struct TransactionMutate { @@ -126,6 +128,8 @@ public struct TransactionMutate { TableMutator(tx: tx, table: table) } + /// The untyped CRUD operations on `name`, with values keyed by client + /// column name. public func table(_ name: String) -> TableMutator { TableMutator(tx: tx, table: name) } /// Inserts a row unless one with its primary key exists. @@ -167,3 +171,92 @@ public struct TableMutator { public func update(_ value: JSONObject) throws { try update(Row(value)) } public func delete(_ id: JSONObject) throws { try delete(Row(id)) } } + +/// A row struct with typed writes, as `zero-swift codegen` generates it: +/// `Update` is what `update` takes (`UpdateValue` in JS) and `Key` what +/// `delete` takes (`DeleteID`). +public protocol ZeroTable: ZeroRow { + associatedtype Update: ZeroColumnValues + associatedtype Key: ZeroColumnValues +} + +/// Some of a table's columns, keyed by client column name. +public protocol ZeroColumnValues: Sendable { + var columnValues: Row { get } +} + +/// The CRUD operations on one table, typed by its row struct (`TableCRUD` +/// in `packages/zql/src/mutate/crud.ts`): `tx.mutate.issue`. +@MainActor +public struct TypedTableMutator { + let tx: Transaction + + public init(_ mutate: TransactionMutate) { tx = mutate.tx } + + /// Inserts a row unless one with its primary key exists. + public func insert(_ row: R) throws { try tx.insert(R.tableName, row.columnValues) } + /// Inserts a row, or replaces the columns of the existing one. + public func upsert(_ row: R) throws { try tx.upsert(R.tableName, row.columnValues) } + /// Sets the columns of the row with the primary key in `value` that + /// `value` sets, if the row exists. + public func update(_ value: R.Update) throws { try tx.update(R.tableName, value.columnValues) } + /// Deletes the row with the primary key `id`, if any. + public func delete(_ id: R.Key) throws { try tx.delete(R.tableName, id.columnValues) } +} + +/// An optional column in an update: left as it is (the default), set to +/// null, or set to a value. A literal sets the value: `ownerID: "u1"`. +public enum ColumnUpdate: Sendable, Hashable { + case unchanged + case null + case set(Value) + + /// Sets the column to `value`, or to null when it is nil. + public init(_ value: Value?) { + self = value.map { .set($0) } ?? .null + } + + /// What the update writes to the column; nil when it leaves it. + public var zeroValue: JSONValue? { + switch self { + case .unchanged: nil + case .null: .null + case .set(let value): value.zeroValue + } + } +} + +extension ColumnUpdate: ExpressibleByUnicodeScalarLiteral +where Value: ExpressibleByStringLiteral, Value.StringLiteralType == String { + public init(unicodeScalarLiteral value: String) { self = .set(Value(stringLiteral: value)) } +} + +extension ColumnUpdate: ExpressibleByExtendedGraphemeClusterLiteral +where Value: ExpressibleByStringLiteral, Value.StringLiteralType == String { + public init(extendedGraphemeClusterLiteral value: String) { + self = .set(Value(stringLiteral: value)) + } +} + +extension ColumnUpdate: ExpressibleByStringLiteral +where Value: ExpressibleByStringLiteral, Value.StringLiteralType == String { + public init(stringLiteral value: String) { self = .set(Value(stringLiteral: value)) } +} + +extension ColumnUpdate: ExpressibleByIntegerLiteral where Value: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Value.IntegerLiteralType) { + self = .set(Value(integerLiteral: value)) + } +} + +extension ColumnUpdate: ExpressibleByFloatLiteral where Value: ExpressibleByFloatLiteral { + public init(floatLiteral value: Value.FloatLiteralType) { + self = .set(Value(floatLiteral: value)) + } +} + +extension ColumnUpdate: ExpressibleByBooleanLiteral where Value: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: Value.BooleanLiteralType) { + self = .set(Value(booleanLiteral: value)) + } +} diff --git a/swift/Sources/Zero/Zero.docc/GettingStarted.md b/swift/Sources/Zero/Zero.docc/GettingStarted.md index 20fb0be3c7..ef988ebbde 100644 --- a/swift/Sources/Zero/Zero.docc/GettingStarted.md +++ b/swift/Sources/Zero/Zero.docc/GettingStarted.md @@ -63,12 +63,21 @@ enum IssueMutators { var title: String var created: Double } + struct Rename: Codable, Sendable { + var id: String + var title: String + } static let create = defineMutator("issue.create") { (tx, args: Create, auth: AuthData?) in guard let auth else { throw ApplicationError("Not logged in") } try tx.mutate.issue.insert( Issue(id: args.id, title: args.title, creatorID: auth.sub, created: args.created)) } + + static let rename = defineMutator("issue.rename") { (tx, args: Rename, _: AuthData?) in + // Sets the title and nothing else; `assigneeID: .null` would unassign. + try tx.mutate.issue.update(.init(id: args.id, title: args.title)) + } } ``` diff --git a/swift/Sources/Zero/Zero.docc/JSParity.md b/swift/Sources/Zero/Zero.docc/JSParity.md index ed5ffe1898..aacee6e8b6 100644 --- a/swift/Sources/Zero/Zero.docc/JSParity.md +++ b/swift/Sources/Zero/Zero.docc/JSParity.md @@ -101,11 +101,21 @@ query. | JS | Swift | | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `defineMutators({issue: {create: defineMutator(async ({tx, args, ctx}) => ...)}})` | `defineMutator("issue.create") { (tx, args: Args, ctx: Context) in ... }`, a plain `throws` function | -| `await tx.mutate.issue.insert(row)` | `try tx.mutate.issue.insert(row)` | +| `await tx.mutate.issue.insert(row)`, `.upsert(row)` | `try tx.mutate.issue.insert(Issue(...))`, `.upsert(...)` | +| `await tx.mutate.issue.update({id, title})` | `try tx.mutate.issue.update(.init(id: id, title: title))` | +| `await tx.mutate.issue.delete({id})` | `try tx.mutate.issue.delete(.init(id: id))` | | `await tx.run(query)` | `try tx.run(query)` | | `tx.location`, `tx.reason`, `tx.clientID`, `tx.mutationID` | the same | | `throw new ApplicationError(message, {details})` | `throw ApplicationError(message, details:)` | +`update` takes the row struct's generated `Update`: the primary key, and +the columns to set. A column left nil keeps its value, as an `undefined` +one does in JS. An optional column is a ``ColumnUpdate``, so leaving it +(`.unchanged`, the default) differs from setting it to null (`.null`): +`.init(id: id, assigneeID: .null)` unassigns, and a literal or `.set(x)` +sets it. `tx.mutate.table("issue")` is the untyped ``TableMutator``, with +values keyed by column name. + Calling a mutator that isn't registered with the `Zero` throws in JS. In Swift `mutate` returns a ``MutatorResult`` whose `client` and `server` are both `.zeroError` with the same message, and logs it as an error. Nothing diff --git a/swift/Sources/Zero/Zero.docc/Zero.md b/swift/Sources/Zero/Zero.docc/Zero.md index 64d7ca4331..d1696ccdd5 100644 --- a/swift/Sources/Zero/Zero.docc/Zero.md +++ b/swift/Sources/Zero/Zero.docc/Zero.md @@ -59,6 +59,8 @@ your TypeScript registries. - ``NoArgs`` - ``Transaction`` - ``TransactionMutate`` +- ``TypedTableMutator`` +- ``ColumnUpdate`` - ``TableMutator`` - ``ApplicationError`` @@ -118,6 +120,8 @@ These are what `zero-swift codegen` writes; apps rarely use them directly. - ``Relationship`` - ``Cardinality`` - ``ZeroRow`` +- ``ZeroTable`` +- ``ZeroColumnValues`` - ``ZeroValue`` - ``ZeroEnum`` - ``Nullable`` diff --git a/swift/TestHost/App/Generated/TestSchema.swift b/swift/TestHost/App/Generated/TestSchema.swift index 04e7b05ab3..0dc63657b3 100644 --- a/swift/TestHost/App/Generated/TestSchema.swift +++ b/swift/TestHost/App/Generated/TestSchema.swift @@ -3,7 +3,7 @@ import Zero -struct TestIssue: ZeroRow, Codable, Hashable, Identifiable { +struct TestIssue: ZeroTable, Codable, Hashable, Identifiable { var id: String var title: String var description: String @@ -87,9 +87,63 @@ struct TestIssue: ZeroRow, Codable, Hashable, Identifiable { "createdAt": createdAt.zeroValue, ] } + + /// What `tx.mutate.issue.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var title: String? + var description: String? + var closed: Bool? + var ownerId: ColumnUpdate + var createdAt: Double? + + init( + id: String, + title: String? = nil, + description: String? = nil, + closed: Bool? = nil, + ownerId: ColumnUpdate = .unchanged, + createdAt: Double? = nil + ) { + self.id = id + self.title = title + self.description = description + self.closed = closed + self.ownerId = ownerId + self.createdAt = createdAt + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.title { row["title"] = value.zeroValue } + if let value = self.description { row["description"] = value.zeroValue } + if let value = self.closed { row["closed"] = value.zeroValue } + if let value = self.ownerId.zeroValue { row["ownerId"] = value } + if let value = self.createdAt { row["createdAt"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.issue.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestUser: ZeroRow, Codable, Hashable, Identifiable { +struct TestUser: ZeroTable, Codable, Hashable, Identifiable { var id: String var name: String var metadata: TestUserMetadata? @@ -135,9 +189,51 @@ struct TestUser: ZeroRow, Codable, Hashable, Identifiable { "metadata": metadata.zeroValue, ] } + + /// What `tx.mutate.user.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var name: String? + var metadata: ColumnUpdate + + init( + id: String, + name: String? = nil, + metadata: ColumnUpdate = .unchanged + ) { + self.id = id + self.name = name + self.metadata = metadata + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + if let value = self.metadata.zeroValue { row["metadata"] = value } + return row + } + } + + /// What `tx.mutate.user.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestComment: ZeroRow, Codable, Hashable, Identifiable { +struct TestComment: ZeroTable, Codable, Hashable, Identifiable { var id: String var authorId: String var issueId: String @@ -205,9 +301,59 @@ struct TestComment: ZeroRow, Codable, Hashable, Identifiable { "createdAt": createdAt.zeroValue, ] } + + /// What `tx.mutate.comment.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var authorId: String? + var issueId: String? + var text: String? + var createdAt: Double? + + init( + id: String, + authorId: String? = nil, + issueId: String? = nil, + text: String? = nil, + createdAt: Double? = nil + ) { + self.id = id + self.authorId = authorId + self.issueId = issueId + self.text = text + self.createdAt = createdAt + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.authorId { row["authorId"] = value.zeroValue } + if let value = self.issueId { row["issueId"] = value.zeroValue } + if let value = self.text { row["text"] = value.zeroValue } + if let value = self.createdAt { row["createdAt"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.comment.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestRevision: ZeroRow, Codable, Hashable, Identifiable { +struct TestRevision: ZeroTable, Codable, Hashable, Identifiable { var id: String var authorId: String var commentId: String @@ -264,9 +410,55 @@ struct TestRevision: ZeroRow, Codable, Hashable, Identifiable { "text": text.zeroValue, ] } + + /// What `tx.mutate.revision.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var authorId: String? + var commentId: String? + var text: String? + + init( + id: String, + authorId: String? = nil, + commentId: String? = nil, + text: String? = nil + ) { + self.id = id + self.authorId = authorId + self.commentId = commentId + self.text = text + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.authorId { row["authorId"] = value.zeroValue } + if let value = self.commentId { row["commentId"] = value.zeroValue } + if let value = self.text { row["text"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.revision.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestLabel: ZeroRow, Codable, Hashable, Identifiable { +struct TestLabel: ZeroTable, Codable, Hashable, Identifiable { var id: String var name: String @@ -306,9 +498,47 @@ struct TestLabel: ZeroRow, Codable, Hashable, Identifiable { "name": name.zeroValue, ] } + + /// What `tx.mutate.label.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var name: String? + + init( + id: String, + name: String? = nil + ) { + self.id = id + self.name = name + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.label.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestIssueLabel: ZeroRow, Codable, Hashable { +struct TestIssueLabel: ZeroTable, Codable, Hashable { var issueId: String var labelId: String @@ -340,6 +570,48 @@ struct TestIssueLabel: ZeroRow, Codable, Hashable { "labelId": labelId.zeroValue, ] } + + /// What `tx.mutate.issueLabel.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var issueId: String + var labelId: String + + init( + issueId: String, + labelId: String + ) { + self.issueId = issueId + self.labelId = labelId + } + + var columnValues: Row { + var row = Row() + row["issueId"] = self.issueId.zeroValue + row["labelId"] = self.labelId.zeroValue + return row + } + } + + /// What `tx.mutate.issueLabel.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var issueId: String + var labelId: String + + init( + issueId: String, + labelId: String + ) { + self.issueId = issueId + self.labelId = labelId + } + + var columnValues: Row { + var row = Row() + row["issueId"] = self.issueId.zeroValue + row["labelId"] = self.labelId.zeroValue + return row + } + } } struct TestUserMetadata: ZeroValue, Codable, Hashable { @@ -505,3 +777,13 @@ struct TestZQL: Sendable { } let testZQL = TestZQL() + +/// The typed CRUD operations in a mutator, one per table: `tx.mutate.issue.insert(…)`. +extension TransactionMutate { + var issue: TypedTableMutator { TypedTableMutator(self) } + var user: TypedTableMutator { TypedTableMutator(self) } + var comment: TypedTableMutator { TypedTableMutator(self) } + var revision: TypedTableMutator { TypedTableMutator(self) } + var label: TypedTableMutator { TypedTableMutator(self) } + var issueLabel: TypedTableMutator { TypedTableMutator(self) } +} diff --git a/swift/Tests/ConformanceTests/ClientApp.swift b/swift/Tests/ConformanceTests/ClientApp.swift index cea485cb52..8aa02dd43f 100644 --- a/swift/Tests/ConformanceTests/ClientApp.swift +++ b/swift/Tests/ConformanceTests/ClientApp.swift @@ -78,30 +78,30 @@ enum ClientApp { static let mutators: [JSONMutator] = [ JSONMutator(name: "issue.create") { tx, args in - try tx.mutate.issue.insert(issueRow(args)) + try tx.mutate.table("issue").insert(issueRow(args)) return nil }, JSONMutator(name: "issue.upsert") { tx, args in - try tx.mutate.issue.upsert(issueRow(args)) + try tx.mutate.table("issue").upsert(issueRow(args)) return nil }, JSONMutator(name: "issue.update") { tx, args in - try tx.mutate.issue.update(Row(args?.objectValue ?? [:])) + try tx.mutate.table("issue").update(Row(args?.objectValue ?? [:])) return nil }, JSONMutator(name: "issue.delete") { tx, args in - try tx.mutate.issue.delete(["id": args ?? .null]) + try tx.mutate.table("issue").delete(["id": args ?? .null]) return nil }, JSONMutator(name: "issue.close") { tx, args in - try tx.mutate.issue.update(["id": args ?? .null, "closed": true]) + try tx.mutate.table("issue").update(["id": args ?? .null, "closed": true]) return nil }, // Reads through the transaction, so a rebase can change what it writes. JSONMutator(name: "issue.closeAllOpen") { tx, _ in let open = try tx.run(zql("issue").where("closed", false)) for issue in open.entries { - try tx.mutate.issue.update(["id": issue.row["id"] ?? .null, "closed": true]) + try tx.mutate.table("issue").update(["id": issue.row["id"] ?? .null, "closed": true]) } return nil }, @@ -111,12 +111,12 @@ enum ClientApp { if !existing.entries.isEmpty { throw ApplicationError("issue exists", details: ["id": field(args, "id", .null)]) } - try tx.mutate.issue.insert(issueRow(args)) + try tx.mutate.table("issue").insert(issueRow(args)) return nil }, // Returns data to the caller of the optimistic run. JSONMutator(name: "issue.createReturning") { tx, args in - try tx.mutate.issue.insert(issueRow(args)) + try tx.mutate.table("issue").insert(issueRow(args)) return ["created": field(args, "id", .null)] }, JSONMutator(name: "issue.fail") { _, args in @@ -124,7 +124,7 @@ enum ClientApp { }, // Writes and then throws, so nothing it wrote may stay. JSONMutator(name: "issue.createThenFail") { tx, args in - try tx.mutate.issue.insert(issueRow(args)) + try tx.mutate.table("issue").insert(issueRow(args)) throw ApplicationError("failed after writing") }, // Runs CRUD operations on any table, then throws, reads tables back, or @@ -162,7 +162,7 @@ enum ClientApp { ] if let id = args?["id"] { row["id"] = id } if let issueID = args?["issueId"] { row["issueId"] = issueID } - try tx.mutate.comment.insert(row) + try tx.mutate.table("comment").insert(row) return nil }, ] diff --git a/swift/Tests/ConformanceTests/Generated/TestSchema.swift b/swift/Tests/ConformanceTests/Generated/TestSchema.swift index 04e7b05ab3..0dc63657b3 100644 --- a/swift/Tests/ConformanceTests/Generated/TestSchema.swift +++ b/swift/Tests/ConformanceTests/Generated/TestSchema.swift @@ -3,7 +3,7 @@ import Zero -struct TestIssue: ZeroRow, Codable, Hashable, Identifiable { +struct TestIssue: ZeroTable, Codable, Hashable, Identifiable { var id: String var title: String var description: String @@ -87,9 +87,63 @@ struct TestIssue: ZeroRow, Codable, Hashable, Identifiable { "createdAt": createdAt.zeroValue, ] } + + /// What `tx.mutate.issue.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var title: String? + var description: String? + var closed: Bool? + var ownerId: ColumnUpdate + var createdAt: Double? + + init( + id: String, + title: String? = nil, + description: String? = nil, + closed: Bool? = nil, + ownerId: ColumnUpdate = .unchanged, + createdAt: Double? = nil + ) { + self.id = id + self.title = title + self.description = description + self.closed = closed + self.ownerId = ownerId + self.createdAt = createdAt + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.title { row["title"] = value.zeroValue } + if let value = self.description { row["description"] = value.zeroValue } + if let value = self.closed { row["closed"] = value.zeroValue } + if let value = self.ownerId.zeroValue { row["ownerId"] = value } + if let value = self.createdAt { row["createdAt"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.issue.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestUser: ZeroRow, Codable, Hashable, Identifiable { +struct TestUser: ZeroTable, Codable, Hashable, Identifiable { var id: String var name: String var metadata: TestUserMetadata? @@ -135,9 +189,51 @@ struct TestUser: ZeroRow, Codable, Hashable, Identifiable { "metadata": metadata.zeroValue, ] } + + /// What `tx.mutate.user.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var name: String? + var metadata: ColumnUpdate + + init( + id: String, + name: String? = nil, + metadata: ColumnUpdate = .unchanged + ) { + self.id = id + self.name = name + self.metadata = metadata + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + if let value = self.metadata.zeroValue { row["metadata"] = value } + return row + } + } + + /// What `tx.mutate.user.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestComment: ZeroRow, Codable, Hashable, Identifiable { +struct TestComment: ZeroTable, Codable, Hashable, Identifiable { var id: String var authorId: String var issueId: String @@ -205,9 +301,59 @@ struct TestComment: ZeroRow, Codable, Hashable, Identifiable { "createdAt": createdAt.zeroValue, ] } + + /// What `tx.mutate.comment.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var authorId: String? + var issueId: String? + var text: String? + var createdAt: Double? + + init( + id: String, + authorId: String? = nil, + issueId: String? = nil, + text: String? = nil, + createdAt: Double? = nil + ) { + self.id = id + self.authorId = authorId + self.issueId = issueId + self.text = text + self.createdAt = createdAt + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.authorId { row["authorId"] = value.zeroValue } + if let value = self.issueId { row["issueId"] = value.zeroValue } + if let value = self.text { row["text"] = value.zeroValue } + if let value = self.createdAt { row["createdAt"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.comment.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestRevision: ZeroRow, Codable, Hashable, Identifiable { +struct TestRevision: ZeroTable, Codable, Hashable, Identifiable { var id: String var authorId: String var commentId: String @@ -264,9 +410,55 @@ struct TestRevision: ZeroRow, Codable, Hashable, Identifiable { "text": text.zeroValue, ] } + + /// What `tx.mutate.revision.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var authorId: String? + var commentId: String? + var text: String? + + init( + id: String, + authorId: String? = nil, + commentId: String? = nil, + text: String? = nil + ) { + self.id = id + self.authorId = authorId + self.commentId = commentId + self.text = text + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.authorId { row["authorId"] = value.zeroValue } + if let value = self.commentId { row["commentId"] = value.zeroValue } + if let value = self.text { row["text"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.revision.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestLabel: ZeroRow, Codable, Hashable, Identifiable { +struct TestLabel: ZeroTable, Codable, Hashable, Identifiable { var id: String var name: String @@ -306,9 +498,47 @@ struct TestLabel: ZeroRow, Codable, Hashable, Identifiable { "name": name.zeroValue, ] } + + /// What `tx.mutate.label.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var name: String? + + init( + id: String, + name: String? = nil + ) { + self.id = id + self.name = name + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.label.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestIssueLabel: ZeroRow, Codable, Hashable { +struct TestIssueLabel: ZeroTable, Codable, Hashable { var issueId: String var labelId: String @@ -340,6 +570,48 @@ struct TestIssueLabel: ZeroRow, Codable, Hashable { "labelId": labelId.zeroValue, ] } + + /// What `tx.mutate.issueLabel.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var issueId: String + var labelId: String + + init( + issueId: String, + labelId: String + ) { + self.issueId = issueId + self.labelId = labelId + } + + var columnValues: Row { + var row = Row() + row["issueId"] = self.issueId.zeroValue + row["labelId"] = self.labelId.zeroValue + return row + } + } + + /// What `tx.mutate.issueLabel.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var issueId: String + var labelId: String + + init( + issueId: String, + labelId: String + ) { + self.issueId = issueId + self.labelId = labelId + } + + var columnValues: Row { + var row = Row() + row["issueId"] = self.issueId.zeroValue + row["labelId"] = self.labelId.zeroValue + return row + } + } } struct TestUserMetadata: ZeroValue, Codable, Hashable { @@ -505,3 +777,13 @@ struct TestZQL: Sendable { } let testZQL = TestZQL() + +/// The typed CRUD operations in a mutator, one per table: `tx.mutate.issue.insert(…)`. +extension TransactionMutate { + var issue: TypedTableMutator { TypedTableMutator(self) } + var user: TypedTableMutator { TypedTableMutator(self) } + var comment: TypedTableMutator { TypedTableMutator(self) } + var revision: TypedTableMutator { TypedTableMutator(self) } + var label: TypedTableMutator { TypedTableMutator(self) } + var issueLabel: TypedTableMutator { TypedTableMutator(self) } +} diff --git a/swift/Tests/E2ETests/ClientE2ETests.swift b/swift/Tests/E2ETests/ClientE2ETests.swift index 0c244cae05..2ee4f23359 100644 --- a/swift/Tests/E2ETests/ClientE2ETests.swift +++ b/swift/Tests/E2ETests/ClientE2ETests.swift @@ -26,7 +26,7 @@ enum E2EApp { createdAt: 100)) } static let close = defineMutator("issue.close") { (tx, id: String) in - try tx.mutate.issue.update(["id": .string(id), "closed": true]) + try tx.mutate.issue.update(.init(id: id, closed: true)) } } diff --git a/swift/Tests/E2ETests/Generated/TestSchema.swift b/swift/Tests/E2ETests/Generated/TestSchema.swift index 04e7b05ab3..0dc63657b3 100644 --- a/swift/Tests/E2ETests/Generated/TestSchema.swift +++ b/swift/Tests/E2ETests/Generated/TestSchema.swift @@ -3,7 +3,7 @@ import Zero -struct TestIssue: ZeroRow, Codable, Hashable, Identifiable { +struct TestIssue: ZeroTable, Codable, Hashable, Identifiable { var id: String var title: String var description: String @@ -87,9 +87,63 @@ struct TestIssue: ZeroRow, Codable, Hashable, Identifiable { "createdAt": createdAt.zeroValue, ] } + + /// What `tx.mutate.issue.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var title: String? + var description: String? + var closed: Bool? + var ownerId: ColumnUpdate + var createdAt: Double? + + init( + id: String, + title: String? = nil, + description: String? = nil, + closed: Bool? = nil, + ownerId: ColumnUpdate = .unchanged, + createdAt: Double? = nil + ) { + self.id = id + self.title = title + self.description = description + self.closed = closed + self.ownerId = ownerId + self.createdAt = createdAt + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.title { row["title"] = value.zeroValue } + if let value = self.description { row["description"] = value.zeroValue } + if let value = self.closed { row["closed"] = value.zeroValue } + if let value = self.ownerId.zeroValue { row["ownerId"] = value } + if let value = self.createdAt { row["createdAt"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.issue.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestUser: ZeroRow, Codable, Hashable, Identifiable { +struct TestUser: ZeroTable, Codable, Hashable, Identifiable { var id: String var name: String var metadata: TestUserMetadata? @@ -135,9 +189,51 @@ struct TestUser: ZeroRow, Codable, Hashable, Identifiable { "metadata": metadata.zeroValue, ] } + + /// What `tx.mutate.user.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var name: String? + var metadata: ColumnUpdate + + init( + id: String, + name: String? = nil, + metadata: ColumnUpdate = .unchanged + ) { + self.id = id + self.name = name + self.metadata = metadata + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + if let value = self.metadata.zeroValue { row["metadata"] = value } + return row + } + } + + /// What `tx.mutate.user.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestComment: ZeroRow, Codable, Hashable, Identifiable { +struct TestComment: ZeroTable, Codable, Hashable, Identifiable { var id: String var authorId: String var issueId: String @@ -205,9 +301,59 @@ struct TestComment: ZeroRow, Codable, Hashable, Identifiable { "createdAt": createdAt.zeroValue, ] } + + /// What `tx.mutate.comment.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var authorId: String? + var issueId: String? + var text: String? + var createdAt: Double? + + init( + id: String, + authorId: String? = nil, + issueId: String? = nil, + text: String? = nil, + createdAt: Double? = nil + ) { + self.id = id + self.authorId = authorId + self.issueId = issueId + self.text = text + self.createdAt = createdAt + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.authorId { row["authorId"] = value.zeroValue } + if let value = self.issueId { row["issueId"] = value.zeroValue } + if let value = self.text { row["text"] = value.zeroValue } + if let value = self.createdAt { row["createdAt"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.comment.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestRevision: ZeroRow, Codable, Hashable, Identifiable { +struct TestRevision: ZeroTable, Codable, Hashable, Identifiable { var id: String var authorId: String var commentId: String @@ -264,9 +410,55 @@ struct TestRevision: ZeroRow, Codable, Hashable, Identifiable { "text": text.zeroValue, ] } + + /// What `tx.mutate.revision.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var authorId: String? + var commentId: String? + var text: String? + + init( + id: String, + authorId: String? = nil, + commentId: String? = nil, + text: String? = nil + ) { + self.id = id + self.authorId = authorId + self.commentId = commentId + self.text = text + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.authorId { row["authorId"] = value.zeroValue } + if let value = self.commentId { row["commentId"] = value.zeroValue } + if let value = self.text { row["text"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.revision.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestLabel: ZeroRow, Codable, Hashable, Identifiable { +struct TestLabel: ZeroTable, Codable, Hashable, Identifiable { var id: String var name: String @@ -306,9 +498,47 @@ struct TestLabel: ZeroRow, Codable, Hashable, Identifiable { "name": name.zeroValue, ] } + + /// What `tx.mutate.label.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var id: String + var name: String? + + init( + id: String, + name: String? = nil + ) { + self.id = id + self.name = name + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.label.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var id: String + + init( + id: String + ) { + self.id = id + } + + var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -struct TestIssueLabel: ZeroRow, Codable, Hashable { +struct TestIssueLabel: ZeroTable, Codable, Hashable { var issueId: String var labelId: String @@ -340,6 +570,48 @@ struct TestIssueLabel: ZeroRow, Codable, Hashable { "labelId": labelId.zeroValue, ] } + + /// What `tx.mutate.issueLabel.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + struct Update: ZeroColumnValues, Hashable { + var issueId: String + var labelId: String + + init( + issueId: String, + labelId: String + ) { + self.issueId = issueId + self.labelId = labelId + } + + var columnValues: Row { + var row = Row() + row["issueId"] = self.issueId.zeroValue + row["labelId"] = self.labelId.zeroValue + return row + } + } + + /// What `tx.mutate.issueLabel.delete` takes: the primary key. + struct Key: ZeroColumnValues, Hashable { + var issueId: String + var labelId: String + + init( + issueId: String, + labelId: String + ) { + self.issueId = issueId + self.labelId = labelId + } + + var columnValues: Row { + var row = Row() + row["issueId"] = self.issueId.zeroValue + row["labelId"] = self.labelId.zeroValue + return row + } + } } struct TestUserMetadata: ZeroValue, Codable, Hashable { @@ -505,3 +777,13 @@ struct TestZQL: Sendable { } let testZQL = TestZQL() + +/// The typed CRUD operations in a mutator, one per table: `tx.mutate.issue.insert(…)`. +extension TransactionMutate { + var issue: TypedTableMutator { TypedTableMutator(self) } + var user: TypedTableMutator { TypedTableMutator(self) } + var comment: TypedTableMutator { TypedTableMutator(self) } + var revision: TypedTableMutator { TypedTableMutator(self) } + var label: TypedTableMutator { TypedTableMutator(self) } + var issueLabel: TypedTableMutator { TypedTableMutator(self) } +} diff --git a/swift/Tests/ZeroTests/ClientAPITests.swift b/swift/Tests/ZeroTests/ClientAPITests.swift index 614372dc10..d6cea9a136 100644 --- a/swift/Tests/ZeroTests/ClientAPITests.swift +++ b/swift/Tests/ZeroTests/ClientAPITests.swift @@ -29,16 +29,14 @@ import ZeroTesting } static let closeMine = defineMutator("issue.closeMine") { (tx, _: NoArgs, auth: Auth) in for issue in try tx.run(zql.issue.where(\.ownerId, auth.userID)) { - try tx.mutate.issue.update(["id": .string(issue.id), "closed": true]) + try tx.mutate.issue.update(.init(id: issue.id, closed: true)) } } struct Created: Encodable { var created: String } static let createReturning = defineMutator("issue.createReturning") { (tx, args: CreateIssue, _: Auth) -> Created in - try tx.mutate.issue.insert([ - "id": .string(args.id), "title": .string(args.title), "description": "", - "closed": false, "ownerId": .null, "createdAt": 1, - ]) + try tx.mutate.issue.insert( + Issue(id: args.id, title: args.title, description: "", closed: false, createdAt: 1)) return Created(created: args.id) } @@ -115,6 +113,54 @@ import ZeroTesting ]) } + static let seed = defineMutator("t.seed") { (tx, _: NoArgs, _: Auth) in + try tx.mutate.issue.insert( + Issue(id: "i1", title: "a", description: "", closed: false, ownerId: "u1", createdAt: 1)) + try tx.mutate.issueLabel.insert(IssueLabel(issueId: "i1", labelId: "l1")) + try tx.mutate.issueLabel.insert(IssueLabel(issueId: "i1", labelId: "l2")) + } + static let retitle = defineMutator("t.retitle") { (tx, _: NoArgs, _: Auth) in + try tx.mutate.issue.update(.init(id: "i1", title: "b")) + } + static let clearOwner = defineMutator("t.clearOwner") { (tx, _: NoArgs, _: Auth) in + try tx.mutate.issue.update(.init(id: "i1", ownerId: .null)) + } + static let setOwner = defineMutator("t.setOwner") { (tx, _: NoArgs, _: Auth) in + try tx.mutate.issue.update(.init(id: "i1", closed: true, ownerId: "u3")) + } + static let unlabel = defineMutator("t.unlabel") { (tx, _: NoArgs, _: Auth) in + try tx.mutate.issueLabel.delete(.init(issueId: "i1", labelId: "l1")) + } + + /// `tx.mutate.
` takes the generated row, `Update`, and `Key`, and + /// writes what JS's `TableCRUD` writes: an update sets only the columns + /// it names (JS skips `undefined`), and `.null` sets null. + @Test func typedTableMutatorsWriteOnlyTheColumnsTheyName() async throws { + let zero = try Zero( + schema: .test, context: Auth(userID: "u2"), + mutators: [Self.seed, Self.retitle, Self.clearOwner, Self.setOwner, Self.unlabel], + options: Self.options, scheduler: scheduler, socketFactory: sockets) + func issue() async throws -> Issue? { try await zero.run(zql.issue.one()) } + + zero.mutate(Self.seed()) + zero.mutate(Self.retitle()) + var i = try #require(try await issue()) + #expect([i.title, i.ownerId] == ["b", "u1"]) + #expect(i.closed == false) + + zero.mutate(Self.clearOwner()) + i = try #require(try await issue()) + #expect([i.title, i.ownerId] == ["b", nil]) + + zero.mutate(Self.setOwner()) + i = try #require(try await issue()) + #expect([i.title, i.ownerId] == ["b", "u3"]) + #expect(i.closed == true) + + zero.mutate(Self.unlabel()) + #expect(try await zero.run(zql.issueLabel).map(\.labelId) == ["l2"]) + } + @Test func returnsTheEncodedResultOfAMutator() async throws { let zero = try makeZero() let result = zero.mutate(Self.createReturning(CreateIssue(id: "i3", title: "t"))) @@ -190,10 +236,8 @@ import ZeroTesting socket.receive(text: #"["connected",{"wsid":"w1"}]"#) scheduler.runDueTimers() let unregistered = defineMutator("issue.unregistered") { (tx, args: CreateIssue, _: Auth) in - try tx.mutate.issue.insert([ - "id": .string(args.id), "title": .string(args.title), "description": "", - "closed": false, "ownerId": .null, "createdAt": 1, - ]) + try tx.mutate.issue.insert( + Issue(id: args.id, title: args.title, description: "", closed: false, createdAt: 1)) } let result = zero.mutate(unregistered(CreateIssue(id: "i4", title: "t"))) diff --git a/swift/Tests/ZeroTests/Generated/FixtureSchema.swift b/swift/Tests/ZeroTests/Generated/FixtureSchema.swift index 5108e65ca7..f3385250d4 100644 --- a/swift/Tests/ZeroTests/Generated/FixtureSchema.swift +++ b/swift/Tests/ZeroTests/Generated/FixtureSchema.swift @@ -3,7 +3,7 @@ import Zero -public struct FxProject: ZeroRow, Codable, Hashable, Identifiable { +public struct FxProject: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var name: String public var `default`: Bool @@ -78,9 +78,67 @@ public struct FxProject: ZeroRow, Codable, Hashable, Identifiable { "archivedStatus": archivedStatus.zeroValue, ] } + + /// What `tx.mutate.project.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var name: String? + public var `default`: Bool? + public var status: FxStatus? + public var settings: FxSettings? + public var position: Int? + public var archivedStatus: ColumnUpdate + + public init( + id: String, + name: String? = nil, + `default`: Bool? = nil, + status: FxStatus? = nil, + settings: FxSettings? = nil, + position: Int? = nil, + archivedStatus: ColumnUpdate = .unchanged + ) { + self.id = id + self.name = name + self.`default` = `default` + self.status = status + self.settings = settings + self.position = position + self.archivedStatus = archivedStatus + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + if let value = self.`default` { row["default"] = value.zeroValue } + if let value = self.status { row["status"] = value.zeroValue } + if let value = self.settings { row["settings"] = value.zeroValue } + if let value = self.position { row["position"] = value.zeroValue } + if let value = self.archivedStatus.zeroValue { row["archivedStatus"] = value } + return row + } + } + + /// What `tx.mutate.project.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct FxQuery: ZeroRow, Codable, Hashable, Identifiable { +public struct FxQuery: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var projectID: String public var text: String @@ -132,9 +190,55 @@ public struct FxQuery: ZeroRow, Codable, Hashable, Identifiable { "class": `class`.zeroValue, ] } + + /// What `tx.mutate.query.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var projectID: String? + public var text: String? + public var `class`: ColumnUpdate + + public init( + id: String, + projectID: String? = nil, + text: String? = nil, + `class`: ColumnUpdate = .unchanged + ) { + self.id = id + self.projectID = projectID + self.text = text + self.`class` = `class` + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.projectID { row["projectID"] = value.zeroValue } + if let value = self.text { row["text"] = value.zeroValue } + if let value = self.`class`.zeroValue { row["class"] = value } + return row + } + } + + /// What `tx.mutate.query.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct FxMember: ZeroRow, Codable, Hashable { +public struct FxMember: ZeroTable, Codable, Hashable { public var projectID: String public var userID: String public var role: FxMemberRole @@ -172,9 +276,55 @@ public struct FxMember: ZeroRow, Codable, Hashable { "role": role.zeroValue, ] } + + /// What `tx.mutate.member.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var projectID: String + public var userID: String + public var role: FxMemberRole? + + public init( + projectID: String, + userID: String, + role: FxMemberRole? = nil + ) { + self.projectID = projectID + self.userID = userID + self.role = role + } + + public var columnValues: Row { + var row = Row() + row["projectID"] = self.projectID.zeroValue + row["userID"] = self.userID.zeroValue + if let value = self.role { row["role"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.member.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var projectID: String + public var userID: String + + public init( + projectID: String, + userID: String + ) { + self.projectID = projectID + self.userID = userID + } + + public var columnValues: Row { + var row = Row() + row["projectID"] = self.projectID.zeroValue + row["userID"] = self.userID.zeroValue + return row + } + } } -public struct FxPerson: ZeroRow, Codable, Hashable, Identifiable { +public struct FxPerson: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var name: String public var nickname: String? @@ -212,6 +362,48 @@ public struct FxPerson: ZeroRow, Codable, Hashable, Identifiable { "nickname": nickname.zeroValue, ] } + + /// What `tx.mutate.person.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var name: String? + public var nickname: ColumnUpdate + + public init( + id: String, + name: String? = nil, + nickname: ColumnUpdate = .unchanged + ) { + self.id = id + self.name = name + self.nickname = nickname + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + if let value = self.nickname.zeroValue { row["nickname"] = value } + return row + } + } + + /// What `tx.mutate.person.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } public enum FxStatus: String, ZeroEnum, Codable, CaseIterable { @@ -372,3 +564,11 @@ public struct FixtureZQL: Sendable { } public let fixtureZQL = FixtureZQL() + +/// The typed CRUD operations in a mutator, one per table: `tx.mutate.project.insert(…)`. +extension TransactionMutate { + public var project: TypedTableMutator { TypedTableMutator(self) } + public var query: TypedTableMutator { TypedTableMutator(self) } + public var member: TypedTableMutator { TypedTableMutator(self) } + public var person: TypedTableMutator { TypedTableMutator(self) } +} diff --git a/swift/Tests/ZeroTests/Generated/TestSchema.swift b/swift/Tests/ZeroTests/Generated/TestSchema.swift index b98b915e65..c251d2aafb 100644 --- a/swift/Tests/ZeroTests/Generated/TestSchema.swift +++ b/swift/Tests/ZeroTests/Generated/TestSchema.swift @@ -3,7 +3,7 @@ import Zero -public struct Issue: ZeroRow, Codable, Hashable, Identifiable { +public struct Issue: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var title: String public var description: String @@ -87,9 +87,63 @@ public struct Issue: ZeroRow, Codable, Hashable, Identifiable { "createdAt": createdAt.zeroValue, ] } + + /// What `tx.mutate.issue.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var title: String? + public var description: String? + public var closed: Bool? + public var ownerId: ColumnUpdate + public var createdAt: Double? + + public init( + id: String, + title: String? = nil, + description: String? = nil, + closed: Bool? = nil, + ownerId: ColumnUpdate = .unchanged, + createdAt: Double? = nil + ) { + self.id = id + self.title = title + self.description = description + self.closed = closed + self.ownerId = ownerId + self.createdAt = createdAt + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.title { row["title"] = value.zeroValue } + if let value = self.description { row["description"] = value.zeroValue } + if let value = self.closed { row["closed"] = value.zeroValue } + if let value = self.ownerId.zeroValue { row["ownerId"] = value } + if let value = self.createdAt { row["createdAt"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.issue.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct User: ZeroRow, Codable, Hashable, Identifiable { +public struct User: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var name: String public var metadata: UserMetadata? @@ -135,9 +189,51 @@ public struct User: ZeroRow, Codable, Hashable, Identifiable { "metadata": metadata.zeroValue, ] } + + /// What `tx.mutate.user.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var name: String? + public var metadata: ColumnUpdate + + public init( + id: String, + name: String? = nil, + metadata: ColumnUpdate = .unchanged + ) { + self.id = id + self.name = name + self.metadata = metadata + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + if let value = self.metadata.zeroValue { row["metadata"] = value } + return row + } + } + + /// What `tx.mutate.user.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct Comment: ZeroRow, Codable, Hashable, Identifiable { +public struct Comment: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var authorId: String public var issueId: String @@ -205,9 +301,59 @@ public struct Comment: ZeroRow, Codable, Hashable, Identifiable { "createdAt": createdAt.zeroValue, ] } + + /// What `tx.mutate.comment.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var authorId: String? + public var issueId: String? + public var text: String? + public var createdAt: Double? + + public init( + id: String, + authorId: String? = nil, + issueId: String? = nil, + text: String? = nil, + createdAt: Double? = nil + ) { + self.id = id + self.authorId = authorId + self.issueId = issueId + self.text = text + self.createdAt = createdAt + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.authorId { row["authorId"] = value.zeroValue } + if let value = self.issueId { row["issueId"] = value.zeroValue } + if let value = self.text { row["text"] = value.zeroValue } + if let value = self.createdAt { row["createdAt"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.comment.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct Revision: ZeroRow, Codable, Hashable, Identifiable { +public struct Revision: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var authorId: String public var commentId: String @@ -264,9 +410,55 @@ public struct Revision: ZeroRow, Codable, Hashable, Identifiable { "text": text.zeroValue, ] } + + /// What `tx.mutate.revision.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var authorId: String? + public var commentId: String? + public var text: String? + + public init( + id: String, + authorId: String? = nil, + commentId: String? = nil, + text: String? = nil + ) { + self.id = id + self.authorId = authorId + self.commentId = commentId + self.text = text + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.authorId { row["authorId"] = value.zeroValue } + if let value = self.commentId { row["commentId"] = value.zeroValue } + if let value = self.text { row["text"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.revision.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct Label: ZeroRow, Codable, Hashable, Identifiable { +public struct Label: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var name: String @@ -306,9 +498,47 @@ public struct Label: ZeroRow, Codable, Hashable, Identifiable { "name": name.zeroValue, ] } + + /// What `tx.mutate.label.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var name: String? + + public init( + id: String, + name: String? = nil + ) { + self.id = id + self.name = name + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.label.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct IssueLabel: ZeroRow, Codable, Hashable { +public struct IssueLabel: ZeroTable, Codable, Hashable { public var issueId: String public var labelId: String @@ -340,6 +570,48 @@ public struct IssueLabel: ZeroRow, Codable, Hashable { "labelId": labelId.zeroValue, ] } + + /// What `tx.mutate.issueLabel.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var issueId: String + public var labelId: String + + public init( + issueId: String, + labelId: String + ) { + self.issueId = issueId + self.labelId = labelId + } + + public var columnValues: Row { + var row = Row() + row["issueId"] = self.issueId.zeroValue + row["labelId"] = self.labelId.zeroValue + return row + } + } + + /// What `tx.mutate.issueLabel.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var issueId: String + public var labelId: String + + public init( + issueId: String, + labelId: String + ) { + self.issueId = issueId + self.labelId = labelId + } + + public var columnValues: Row { + var row = Row() + row["issueId"] = self.issueId.zeroValue + row["labelId"] = self.labelId.zeroValue + return row + } + } } public struct UserMetadata: ZeroValue, Codable, Hashable { @@ -505,3 +777,13 @@ public struct ZQL: Sendable { } public let zql = ZQL() + +/// The typed CRUD operations in a mutator, one per table: `tx.mutate.issue.insert(…)`. +extension TransactionMutate { + public var issue: TypedTableMutator { TypedTableMutator(self) } + public var user: TypedTableMutator { TypedTableMutator(self) } + public var comment: TypedTableMutator { TypedTableMutator(self) } + public var revision: TypedTableMutator { TypedTableMutator(self) } + public var label: TypedTableMutator
Amp-Thread-ID: https://ampcode.com/threads/T-01a17664-8fd0-76be-8fd5-75ac6e7aa0d8 Co-authored-by: tmm --- apps/zbugs-swift/Sources/ZBugs/Mutators.swift | 43 +- apps/zbugs-swift/Sources/ZBugs/Schema.swift | 550 +++++++++++++++++- 2 files changed, 563 insertions(+), 30 deletions(-) diff --git a/apps/zbugs-swift/Sources/ZBugs/Mutators.swift b/apps/zbugs-swift/Sources/ZBugs/Mutators.swift index 38da63c834..83f2711504 100644 --- a/apps/zbugs-swift/Sources/ZBugs/Mutators.swift +++ b/apps/zbugs-swift/Sources/ZBugs/Mutators.swift @@ -200,7 +200,7 @@ public enum Mutators { public static let createIssue = defineMutator("issue.create") { (tx, args: CreateIssueArgs, auth: AuthData?) in let auth = try assertIsLoggedIn(auth) - try tx.mutate.insert( + try tx.mutate.issue.insert( Issue( id: args.id, title: args.title, open: true, modified: args.modified, created: args.created, projectID: args.projectID, creatorID: auth.sub, @@ -219,7 +219,11 @@ public enum Mutators { // An admin skips the creator check; the same error as for anyone else. throw mutationError("Issue not found or not authorized", .notAuthorized, change.id) } - try tx.mutate.issue.update(JSONValueEncoder.encode(change).objectValue ?? [:]) + try tx.mutate.issue.update( + .init( + id: change.id, title: change.title, open: change.open, modified: change.modified, + assigneeID: change.assigneeID.map(ColumnUpdate.init) ?? .unchanged, + description: change.description, visibility: change.visibility)) var previousAssigneeID: String? if case .some(let assigneeID) = change.assigneeID, assigneeID != oldIssue.assigneeID { @@ -241,7 +245,7 @@ public enum Mutators { public static let deleteIssue = defineMutator("issue.delete") { (tx, id: String, auth: AuthData?) in try assertIsCreatorOrAdmin(tx, auth, zql.issue, id) - try tx.mutate.issue.delete(["id": .string(id)]) + try tx.mutate.issue.delete(.init(id: id)) } /// Deletes the issues if the user created every one of them (or is an @@ -253,14 +257,14 @@ public enum Mutators { try assertIsCreatorOrAdmin(tx, auth, zql.issue, id) } for id in ids { - try tx.mutate.issue.delete(["id": .string(id)]) + try tx.mutate.issue.delete(.init(id: id)) } } public static let addLabel = defineMutator("issue.addLabel") { (tx, args: IssueLabelArgs, auth: AuthData?) in try assertIsCreatorOrAdmin(tx, auth, zql.issue, args.issueID) - try tx.mutate.insert( + try tx.mutate.issueLabel.insert( IssueLabel( issueID: args.issueID, labelID: args.labelID, projectID: issueProjectID(tx, args.issueID))) @@ -269,9 +273,7 @@ public enum Mutators { public static let removeLabel = defineMutator("issue.removeLabel") { (tx, args: RemoveLabelArgs, auth: AuthData?) in try assertIsCreatorOrAdmin(tx, auth, zql.issue, args.issueID) - try tx.mutate.issueLabel.delete([ - "issueID": .string(args.issueID), "labelID": .string(args.labelID), - ]) + try tx.mutate.issueLabel.delete(.init(labelID: args.labelID, issueID: args.issueID)) } public static let updateNotification = defineMutator("notification.update") { @@ -295,14 +297,14 @@ public enum Mutators { public static let removeEmoji = defineMutator("emoji.remove") { (tx, id: String, auth: AuthData?) in try assertIsCreatorOrAdmin(tx, auth, zql.emoji, id) - try tx.mutate.emoji.delete(["id": .string(id)]) + try tx.mutate.emoji.delete(.init(id: id)) } public static let addComment = defineMutator("comment.add") { (tx, args: AddCommentArgs, auth: AuthData?) in let creatorID = try assertIsLoggedIn(auth).sub try assertUserCanSeeIssue(tx, creatorID, args.issueID) - try tx.mutate.insert( + try tx.mutate.comment.insert( Comment( id: args.id, issueID: args.issueID, created: args.created, body: args.body, creatorID: creatorID)) @@ -314,13 +316,13 @@ public enum Mutators { public static let editComment = defineMutator("comment.edit") { (tx, args: EditCommentArgs, auth: AuthData?) in try assertIsCreatorOrAdmin(tx, auth, zql.comment, args.id) - try tx.mutate.comment.update(["id": .string(args.id), "body": .string(args.body)]) + try tx.mutate.comment.update(.init(id: args.id, body: args.body)) } public static let removeComment = defineMutator("comment.remove") { (tx, id: String, auth: AuthData?) in try assertIsCreatorOrAdmin(tx, auth, zql.comment, id) - try tx.mutate.comment.delete(["id": .string(id)]) + try tx.mutate.comment.delete(.init(id: id)) } public static let createLabel = defineMutator("label.create") { @@ -328,7 +330,7 @@ public enum Mutators { guard try isAdmin(auth) else { throw mutationError("Only admins can create labels", .notAuthorized, args.id) } - try tx.mutate.insert(Label(id: args.id, name: args.name, projectID: args.projectID)) + try tx.mutate.label.insert(Label(id: args.id, name: args.name, projectID: args.projectID)) } public static let createAndAddLabel = defineMutator("label.createAndAddToIssue") { @@ -337,21 +339,22 @@ public enum Mutators { throw mutationError("Only admins can create labels", .notAuthorized, args.labelID) } let projectID = try issueProjectID(tx, args.issueID) - try tx.mutate.insert(Label(id: args.labelID, name: args.labelName, projectID: projectID)) - try tx.mutate.insert( + try tx.mutate.label.insert(Label(id: args.labelID, name: args.labelName, projectID: projectID)) + try tx.mutate.issueLabel.insert( IssueLabel(issueID: args.issueID, labelID: args.labelID, projectID: projectID)) } public static let setViewState = defineMutator("viewState.set") { (tx, args: ViewStateArgs, auth: AuthData?) in let userID = try assertIsLoggedIn(auth).sub - try tx.mutate.upsert(ViewState(issueID: args.issueID, userID: userID, viewed: args.viewed)) + try tx.mutate.viewState.upsert( + ViewState(issueID: args.issueID, userID: userID, viewed: args.viewed)) } public static let setUserPref = defineMutator("userPref.set") { (tx, args: UserPrefArgs, auth: AuthData?) in let userID = try assertIsLoggedIn(auth).sub - try tx.mutate.upsert(UserPref(key: args.key, userID: userID, value: args.value)) + try tx.mutate.userPref.upsert(UserPref(key: args.key, userID: userID, value: args.value)) } /// Every mutator, for the client and the drift test. @@ -384,7 +387,7 @@ func addEmoji(_ tx: Transaction, _ subject: EmojiSubject, _ args: AddEmojiArgs, case .issue: try assertUserCanSeeIssue(tx, creatorID, args.subjectID) case .comment: try assertUserCanSeeComment(tx, creatorID, args.subjectID) } - try tx.mutate.insert( + try tx.mutate.emoji.insert( Emoji( id: args.id, value: args.unicode, annotation: args.annotation, subjectID: args.subjectID, creatorID: creatorID, created: args.created)) @@ -407,10 +410,10 @@ func updateIssueNotification( let existing = try tx.run( zql.issueNotifications.where(\.userID, userID).where(\.issueID, issueID).one()) if subscribed == .subscribe && (existing == nil || forceUpdate) { - try tx.mutate.upsert( + try tx.mutate.issueNotifications.upsert( IssueNotifications(userID: userID, issueID: issueID, subscribed: true, created: created)) } else if subscribed == .unsubscribe { - try tx.mutate.upsert( + try tx.mutate.issueNotifications.upsert( IssueNotifications(userID: userID, issueID: issueID, subscribed: false, created: created)) } } diff --git a/apps/zbugs-swift/Sources/ZBugs/Schema.swift b/apps/zbugs-swift/Sources/ZBugs/Schema.swift index bd72d46ea7..b3712c0aae 100644 --- a/apps/zbugs-swift/Sources/ZBugs/Schema.swift +++ b/apps/zbugs-swift/Sources/ZBugs/Schema.swift @@ -3,7 +3,7 @@ import Zero -public struct User: ZeroRow, Codable, Hashable, Identifiable { +public struct User: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var login: String public var name: String? @@ -66,9 +66,59 @@ public struct User: ZeroRow, Codable, Hashable, Identifiable { "role": role.zeroValue, ] } + + /// What `tx.mutate.user.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var login: String? + public var name: ColumnUpdate + public var avatar: String? + public var role: UserRole? + + public init( + id: String, + login: String? = nil, + name: ColumnUpdate = .unchanged, + avatar: String? = nil, + role: UserRole? = nil + ) { + self.id = id + self.login = login + self.name = name + self.avatar = avatar + self.role = role + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.login { row["login"] = value.zeroValue } + if let value = self.name.zeroValue { row["name"] = value } + if let value = self.avatar { row["avatar"] = value.zeroValue } + if let value = self.role { row["role"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.user.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct Project: ZeroRow, Codable, Hashable, Identifiable { +public struct Project: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var name: String public var lowerCaseName: String @@ -143,9 +193,67 @@ public struct Project: ZeroRow, Codable, Hashable, Identifiable { "logoURL": logoURL.zeroValue, ] } + + /// What `tx.mutate.project.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var name: String? + public var lowerCaseName: String? + public var issueCountEstimate: ColumnUpdate + public var supportsSearch: Bool? + public var markURL: ColumnUpdate + public var logoURL: ColumnUpdate + + public init( + id: String, + name: String? = nil, + lowerCaseName: String? = nil, + issueCountEstimate: ColumnUpdate = .unchanged, + supportsSearch: Bool? = nil, + markURL: ColumnUpdate = .unchanged, + logoURL: ColumnUpdate = .unchanged + ) { + self.id = id + self.name = name + self.lowerCaseName = lowerCaseName + self.issueCountEstimate = issueCountEstimate + self.supportsSearch = supportsSearch + self.markURL = markURL + self.logoURL = logoURL + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + if let value = self.lowerCaseName { row["lowerCaseName"] = value.zeroValue } + if let value = self.issueCountEstimate.zeroValue { row["issueCountEstimate"] = value } + if let value = self.supportsSearch { row["supportsSearch"] = value.zeroValue } + if let value = self.markURL.zeroValue { row["markURL"] = value } + if let value = self.logoURL.zeroValue { row["logoURL"] = value } + return row + } + } + + /// What `tx.mutate.project.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct Issue: ZeroRow, Codable, Hashable, Identifiable { +public struct Issue: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var shortID: Int? public var title: String @@ -279,9 +387,83 @@ public struct Issue: ZeroRow, Codable, Hashable, Identifiable { "visibility": visibility.zeroValue, ] } + + /// What `tx.mutate.issue.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var shortID: ColumnUpdate + public var title: String? + public var `open`: Bool? + public var modified: Double? + public var created: Double? + public var projectID: String? + public var creatorID: String? + public var assigneeID: ColumnUpdate + public var description: String? + public var visibility: IssueVisibility? + + public init( + id: String, + shortID: ColumnUpdate = .unchanged, + title: String? = nil, + `open`: Bool? = nil, + modified: Double? = nil, + created: Double? = nil, + projectID: String? = nil, + creatorID: String? = nil, + assigneeID: ColumnUpdate = .unchanged, + description: String? = nil, + visibility: IssueVisibility? = nil + ) { + self.id = id + self.shortID = shortID + self.title = title + self.`open` = `open` + self.modified = modified + self.created = created + self.projectID = projectID + self.creatorID = creatorID + self.assigneeID = assigneeID + self.description = description + self.visibility = visibility + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.shortID.zeroValue { row["shortID"] = value } + if let value = self.title { row["title"] = value.zeroValue } + if let value = self.`open` { row["open"] = value.zeroValue } + if let value = self.modified { row["modified"] = value.zeroValue } + if let value = self.created { row["created"] = value.zeroValue } + if let value = self.projectID { row["projectID"] = value.zeroValue } + if let value = self.creatorID { row["creatorID"] = value.zeroValue } + if let value = self.assigneeID.zeroValue { row["assigneeID"] = value } + if let value = self.description { row["description"] = value.zeroValue } + if let value = self.visibility { row["visibility"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.issue.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct Comment: ZeroRow, Codable, Hashable, Identifiable { +public struct Comment: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var issueID: String public var created: Double @@ -349,9 +531,59 @@ public struct Comment: ZeroRow, Codable, Hashable, Identifiable { "creatorID": creatorID.zeroValue, ] } + + /// What `tx.mutate.comment.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var issueID: String? + public var created: Double? + public var body: String? + public var creatorID: String? + + public init( + id: String, + issueID: String? = nil, + created: Double? = nil, + body: String? = nil, + creatorID: String? = nil + ) { + self.id = id + self.issueID = issueID + self.created = created + self.body = body + self.creatorID = creatorID + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.issueID { row["issueID"] = value.zeroValue } + if let value = self.created { row["created"] = value.zeroValue } + if let value = self.body { row["body"] = value.zeroValue } + if let value = self.creatorID { row["creatorID"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.comment.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct Label: ZeroRow, Codable, Hashable, Identifiable { +public struct Label: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var name: String public var projectID: String @@ -397,9 +629,51 @@ public struct Label: ZeroRow, Codable, Hashable, Identifiable { "projectID": projectID.zeroValue, ] } + + /// What `tx.mutate.label.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var name: String? + public var projectID: String? + + public init( + id: String, + name: String? = nil, + projectID: String? = nil + ) { + self.id = id + self.name = name + self.projectID = projectID + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.name { row["name"] = value.zeroValue } + if let value = self.projectID { row["projectID"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.label.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct IssueLabel: ZeroRow, Codable, Hashable { +public struct IssueLabel: ZeroTable, Codable, Hashable { public var issueID: String public var labelID: String public var projectID: String @@ -450,9 +724,55 @@ public struct IssueLabel: ZeroRow, Codable, Hashable { "projectID": projectID.zeroValue, ] } + + /// What `tx.mutate.issueLabel.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var issueID: String + public var labelID: String + public var projectID: String? + + public init( + issueID: String, + labelID: String, + projectID: String? = nil + ) { + self.issueID = issueID + self.labelID = labelID + self.projectID = projectID + } + + public var columnValues: Row { + var row = Row() + row["issueID"] = self.issueID.zeroValue + row["labelID"] = self.labelID.zeroValue + if let value = self.projectID { row["projectID"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.issueLabel.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var labelID: String + public var issueID: String + + public init( + labelID: String, + issueID: String + ) { + self.labelID = labelID + self.issueID = issueID + } + + public var columnValues: Row { + var row = Row() + row["labelID"] = self.labelID.zeroValue + row["issueID"] = self.issueID.zeroValue + return row + } + } } -public struct ViewState: ZeroRow, Codable, Hashable { +public struct ViewState: ZeroTable, Codable, Hashable { public var issueID: String public var userID: String public var viewed: Double @@ -490,9 +810,55 @@ public struct ViewState: ZeroRow, Codable, Hashable { "viewed": viewed.zeroValue, ] } + + /// What `tx.mutate.viewState.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var issueID: String + public var userID: String + public var viewed: Double? + + public init( + issueID: String, + userID: String, + viewed: Double? = nil + ) { + self.issueID = issueID + self.userID = userID + self.viewed = viewed + } + + public var columnValues: Row { + var row = Row() + row["issueID"] = self.issueID.zeroValue + row["userID"] = self.userID.zeroValue + if let value = self.viewed { row["viewed"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.viewState.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var userID: String + public var issueID: String + + public init( + userID: String, + issueID: String + ) { + self.userID = userID + self.issueID = issueID + } + + public var columnValues: Row { + var row = Row() + row["userID"] = self.userID.zeroValue + row["issueID"] = self.issueID.zeroValue + return row + } + } } -public struct Emoji: ZeroRow, Codable, Hashable, Identifiable { +public struct Emoji: ZeroTable, Codable, Hashable, Identifiable { public var id: String public var value: String public var annotation: String @@ -566,9 +932,63 @@ public struct Emoji: ZeroRow, Codable, Hashable, Identifiable { "created": created.zeroValue, ] } + + /// What `tx.mutate.emoji.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var id: String + public var value: String? + public var annotation: String? + public var subjectID: String? + public var creatorID: String? + public var created: Double? + + public init( + id: String, + value: String? = nil, + annotation: String? = nil, + subjectID: String? = nil, + creatorID: String? = nil, + created: Double? = nil + ) { + self.id = id + self.value = value + self.annotation = annotation + self.subjectID = subjectID + self.creatorID = creatorID + self.created = created + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + if let value = self.value { row["value"] = value.zeroValue } + if let value = self.annotation { row["annotation"] = value.zeroValue } + if let value = self.subjectID { row["subjectID"] = value.zeroValue } + if let value = self.creatorID { row["creatorID"] = value.zeroValue } + if let value = self.created { row["created"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.emoji.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var id: String + + public init( + id: String + ) { + self.id = id + } + + public var columnValues: Row { + var row = Row() + row["id"] = self.id.zeroValue + return row + } + } } -public struct UserPref: ZeroRow, Codable, Hashable { +public struct UserPref: ZeroTable, Codable, Hashable { public var key: String public var userID: String public var value: String @@ -606,9 +1026,55 @@ public struct UserPref: ZeroRow, Codable, Hashable { "value": value.zeroValue, ] } + + /// What `tx.mutate.userPref.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var key: String + public var userID: String + public var value: String? + + public init( + key: String, + userID: String, + value: String? = nil + ) { + self.key = key + self.userID = userID + self.value = value + } + + public var columnValues: Row { + var row = Row() + row["key"] = self.key.zeroValue + row["userID"] = self.userID.zeroValue + if let value = self.value { row["value"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.userPref.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var userID: String + public var key: String + + public init( + userID: String, + key: String + ) { + self.userID = userID + self.key = key + } + + public var columnValues: Row { + var row = Row() + row["userID"] = self.userID.zeroValue + row["key"] = self.key.zeroValue + return row + } + } } -public struct IssueNotifications: ZeroRow, Codable, Hashable { +public struct IssueNotifications: ZeroTable, Codable, Hashable { public var userID: String public var issueID: String public var subscribed: Bool @@ -652,6 +1118,56 @@ public struct IssueNotifications: ZeroRow, Codable, Hashable { "created": created.zeroValue, ] } + + /// What `tx.mutate.issueNotifications.update` takes: the primary key, and the other columns to set. A column that is nil, or `.unchanged`, keeps its value. + public struct Update: ZeroColumnValues, Hashable { + public var userID: String + public var issueID: String + public var subscribed: Bool? + public var created: Double? + + public init( + userID: String, + issueID: String, + subscribed: Bool? = nil, + created: Double? = nil + ) { + self.userID = userID + self.issueID = issueID + self.subscribed = subscribed + self.created = created + } + + public var columnValues: Row { + var row = Row() + row["userID"] = self.userID.zeroValue + row["issueID"] = self.issueID.zeroValue + if let value = self.subscribed { row["subscribed"] = value.zeroValue } + if let value = self.created { row["created"] = value.zeroValue } + return row + } + } + + /// What `tx.mutate.issueNotifications.delete` takes: the primary key. + public struct Key: ZeroColumnValues, Hashable { + public var userID: String + public var issueID: String + + public init( + userID: String, + issueID: String + ) { + self.userID = userID + self.issueID = issueID + } + + public var columnValues: Row { + var row = Row() + row["userID"] = self.userID.zeroValue + row["issueID"] = self.issueID.zeroValue + return row + } + } } public enum UserRole: String, ZeroEnum, Codable, CaseIterable { @@ -882,3 +1398,17 @@ public struct ZQL: Sendable { } public let zql = ZQL() + +/// The typed CRUD operations in a mutator, one per table: `tx.mutate.user.insert(…)`. +extension TransactionMutate { + public var user: TypedTableMutator { TypedTableMutator(self) } + public var project: TypedTableMutator { TypedTableMutator(self) } + public var issue: TypedTableMutator { TypedTableMutator(self) } + public var comment: TypedTableMutator { TypedTableMutator(self) } + public var label: TypedTableMutator
` is typed, as in JS: `insert` and `upsert` take the row struct, `update` its generated `Update` (the primary key and the columns to set), and `delete` its generated `Key`. An optional column in an `Update` is a `ColumnUpdate`, so leaving it (`.unchanged`) differs from setting it to null (`.null`). Breaking: code that passed a dictionary to `tx.mutate.
` now uses these types, or `tx.mutate.table("
")` for the untyped `TableMutator`. Regenerate with `zero-swift codegen`. +**Breaking:** `tx.mutate.
` is typed, as in JS. `insert` and `upsert` take the row struct, `update` its generated `Update` (the primary key and the columns to set), and `delete` its generated `Key`. An optional column in an `Update` is a `ColumnUpdate`, so leaving it (`.unchanged`, the default) differs from setting it to null (`.null`). + +To migrate, regenerate with `npx zero-swift codegen`, then replace dictionaries passed to `tx.mutate.
` with the typed values: + +```diff +- try tx.mutate.issue.insert(["id": .string(id), "title": .string(title), ...]) ++ try tx.mutate.issue.insert(.init(id: id, title: title, ...)) + +- try tx.mutate.issue.update(["id": .string(id), "title": .string(title)]) ++ try tx.mutate.issue.update(.init(id: id, title: title)) + +- try tx.mutate.issue.update(["id": .string(id), "assigneeID": .null]) ++ try tx.mutate.issue.update(.init(id: id, assigneeID: .null)) + +- try tx.mutate.issue.delete(["id": .string(id)]) ++ try tx.mutate.issue.delete(.init(id: id)) +``` + +`tx.mutate.insert(row)` and `tx.mutate.upsert(row)` still work. For untyped writes, `tx.mutate.table("issue")` takes dictionaries as before. From 408860e82c6347158651bb5e31d0c4c2f75e0b69 Mon Sep 17 00:00:00 2001 From: Amp Date: Mon, 26 Oct 2026 23:17:11 +0000 Subject: [PATCH 6/6] feat(zero-swift): drop the generic tx.mutate.insert and upsert tx.mutate.
is now the one way to write, as in JS. Generic code uses TypedTableMutator(tx.mutate); hand-written rows without a generated member use the untyped tx.mutate.
with columnValues. Amp-Thread-ID: https://ampcode.com/threads/T-01a17664-8fd0-76be-8fd5-75ac6e7aa0d8 Co-authored-by: tmm --- .changeset/typed-table-mutators.md | 5 ++++- swift/Sources/Zero/Client/Transaction.swift | 10 ---------- swift/Sources/Zero/Zero.docc/JSParity.md | 7 ++++--- swift/Sources/ZeroBenchmarks/Fixtures.swift | 4 ++-- swift/Tests/AppleTests/Notes.swift | 2 +- swift/Tests/E2ETests/ClientE2ETests.swift | 4 ++-- swift/Tests/ZeroTests/ClientAPITests.swift | 4 ++-- swift/Tests/ZeroTests/HardenConnectionTests.swift | 2 +- swift/Tests/ZeroTests/HardenCrashTests.swift | 2 +- swift/Tests/ZeroTests/HardenMutationTests.swift | 12 ++++++------ swift/Tests/ZeroTests/HardenStorageTests.swift | 2 +- 11 files changed, 24 insertions(+), 30 deletions(-) diff --git a/.changeset/typed-table-mutators.md b/.changeset/typed-table-mutators.md index 51d00c030a..287ad5faf6 100644 --- a/.changeset/typed-table-mutators.md +++ b/.changeset/typed-table-mutators.md @@ -7,6 +7,9 @@ To migrate, regenerate with `npx zero-swift codegen`, then replace dictionaries passed to `tx.mutate.
` with the typed values: ```diff +- try tx.mutate.insert(Issue(id: id, title: title, ...)) ++ try tx.mutate.issue.insert(.init(id: id, title: title, ...)) + - try tx.mutate.issue.insert(["id": .string(id), "title": .string(title), ...]) + try tx.mutate.issue.insert(.init(id: id, title: title, ...)) @@ -20,4 +23,4 @@ To migrate, regenerate with `npx zero-swift codegen`, then replace dictionaries + try tx.mutate.issue.delete(.init(id: id)) ``` -`tx.mutate.insert(row)` and `tx.mutate.upsert(row)` still work. For untyped writes, `tx.mutate.table("issue")` takes dictionaries as before. +`tx.mutate.insert(row)` and `tx.mutate.upsert(row)` are gone: `tx.mutate.
` is the one way to write. Generic code can use `TypedTableMutator(tx.mutate)`, and `tx.mutate.table("issue")` takes dictionaries for untyped writes. diff --git a/swift/Sources/Zero/Client/Transaction.swift b/swift/Sources/Zero/Client/Transaction.swift index b10ec37ca1..fac128a160 100644 --- a/swift/Sources/Zero/Client/Transaction.swift +++ b/swift/Sources/Zero/Client/Transaction.swift @@ -131,16 +131,6 @@ public struct TransactionMutate { /// The untyped CRUD operations on `name`, with values keyed by client /// column name. public func table(_ name: String) -> TableMutator { TableMutator(tx: tx, table: name) } - - /// Inserts a row unless one with its primary key exists. - public func insert(_ row: R) throws { - try tx.insert(R.tableName, row.columnValues) - } - - /// Inserts a row, or replaces the columns of the existing one. - public func upsert(_ row: R) throws { - try tx.upsert(R.tableName, row.columnValues) - } } /// The CRUD operations on one table (`TableCRUD` in diff --git a/swift/Sources/Zero/Zero.docc/JSParity.md b/swift/Sources/Zero/Zero.docc/JSParity.md index f2356cdf83..86c3524a43 100644 --- a/swift/Sources/Zero/Zero.docc/JSParity.md +++ b/swift/Sources/Zero/Zero.docc/JSParity.md @@ -172,9 +172,10 @@ is written or sent. that writes one to a column a view sorts by fails, and so does every mutation after it, and a poke that brings one disconnects the client again after every reconnect. Swift sorts such a value with the others - (by type, then by its JSON) and keeps working. A mutator can only write - one through the untyped `tx.mutate.
` calls; the server rejects - such a mutation, and its row goes when the next poke comes. + (by type, then by its JSON) and keeps working. The typed + `tx.mutate.
` calls can't write one; a mutator can only write one + through the untyped `tx.mutate.table(_:)`, and the server rejects such a + mutation, so its row goes when the next poke comes. - **Error messages.** Messages about data the client can't read follow valita's wording, as JS's do. A server frame that isn't JSON is the exception: JS shows the engine's `JSON.parse` message, and Swift its diff --git a/swift/Sources/ZeroBenchmarks/Fixtures.swift b/swift/Sources/ZeroBenchmarks/Fixtures.swift index 5fca155247..8596ce7d71 100644 --- a/swift/Sources/ZeroBenchmarks/Fixtures.swift +++ b/swift/Sources/ZeroBenchmarks/Fixtures.swift @@ -78,14 +78,14 @@ struct UpdateUser: Codable, Sendable { enum Mutators { static let insertUsers = defineMutator("insertUsers") { (tx, args: InsertUsers) in for n in args.from..] = [insertUsers, updateUser] } diff --git a/swift/Tests/AppleTests/Notes.swift b/swift/Tests/AppleTests/Notes.swift index f483ab2a79..7a3029f3b6 100644 --- a/swift/Tests/AppleTests/Notes.swift +++ b/swift/Tests/AppleTests/Notes.swift @@ -40,7 +40,7 @@ enum Notes { } static let put = defineMutator("note.put") { (tx, note: Put) in - try tx.mutate.upsert(Note(id: note.id, text: note.text)) + try tx.mutate.note.upsert(Note(id: note.id, text: note.text).columnValues) } static let remove = defineMutator("note.remove") { (tx, id: String) in try tx.mutate.note.delete(["id": .string(id)]) diff --git a/swift/Tests/E2ETests/ClientE2ETests.swift b/swift/Tests/E2ETests/ClientE2ETests.swift index 2ee4f23359..2f31392019 100644 --- a/swift/Tests/E2ETests/ClientE2ETests.swift +++ b/swift/Tests/E2ETests/ClientE2ETests.swift @@ -20,8 +20,8 @@ enum E2EApp { } static let create = defineMutator("issue.create") { (tx, args: CreateIssue) in - try tx.mutate.insert( - TestIssue( + try tx.mutate.issue.insert( + .init( id: args.id, title: args.title, description: "", closed: false, ownerId: args.ownerId, createdAt: 100)) } diff --git a/swift/Tests/ZeroTests/ClientAPITests.swift b/swift/Tests/ZeroTests/ClientAPITests.swift index d6cea9a136..fabb25862f 100644 --- a/swift/Tests/ZeroTests/ClientAPITests.swift +++ b/swift/Tests/ZeroTests/ClientAPITests.swift @@ -22,8 +22,8 @@ import ZeroTesting zql.issue.where(\.ownerId, auth.userID).orderBy(\.id, .asc) } static let create = defineMutator("issue.create") { (tx, args: CreateIssue, auth: Auth) in - try tx.mutate.insert( - Issue( + try tx.mutate.issue.insert( + .init( id: args.id, title: args.title, description: "", closed: false, ownerId: args.ownerId ?? auth.userID, createdAt: 1)) } diff --git a/swift/Tests/ZeroTests/HardenConnectionTests.swift b/swift/Tests/ZeroTests/HardenConnectionTests.swift index 011dc7ee07..e38c80d86d 100644 --- a/swift/Tests/ZeroTests/HardenConnectionTests.swift +++ b/swift/Tests/ZeroTests/HardenConnectionTests.swift @@ -8,7 +8,7 @@ import ZeroTesting /// inside `send`. Expected values follow `packages/zero-client/src/client/zero.ts`. @Suite @MainActor struct HardenConnectionTests { static let create = defineMutator("issue.create") { (tx, id: String) in - try tx.mutate.insert( + try tx.mutate.issue.insert( Issue(id: id, title: "", description: "", closed: false, createdAt: 1)) } diff --git a/swift/Tests/ZeroTests/HardenCrashTests.swift b/swift/Tests/ZeroTests/HardenCrashTests.swift index e9e3c551f3..375e0a1c20 100644 --- a/swift/Tests/ZeroTests/HardenCrashTests.swift +++ b/swift/Tests/ZeroTests/HardenCrashTests.swift @@ -13,7 +13,7 @@ import ZeroTesting /// `precondition`, which fails the child. @Suite @MainActor struct HardenCrashTests { static let create = defineMutator("issue.create") { (tx, id: String) in - try tx.mutate.insert( + try tx.mutate.issue.insert( Issue(id: id, title: "", description: "", closed: false, createdAt: 1)) } diff --git a/swift/Tests/ZeroTests/HardenMutationTests.swift b/swift/Tests/ZeroTests/HardenMutationTests.swift index ee9f8379e1..22d909c97c 100644 --- a/swift/Tests/ZeroTests/HardenMutationTests.swift +++ b/swift/Tests/ZeroTests/HardenMutationTests.swift @@ -28,26 +28,26 @@ import ZeroTesting } static let create = defineMutator("t.create") { (tx, id: String) -> Seen in - try tx.mutate.insert(row(id)) + try tx.mutate.issue.insert(row(id)) return Seen(mid: tx.mutationID, seen: try ids(tx)) } static let createThenThrow = defineMutator("t.createThenThrow") { (tx, id: String) in - try tx.mutate.insert(row(id)) + try tx.mutate.issue.insert(row(id)) throw ApplicationError("midway") } /// Writes, calls `zero.mutate` without waiting for it, and writes again. static let outer = defineMutator("t.outer") { (tx, _: String) -> Seen in - try tx.mutate.insert(row("outer1")) + try tx.mutate.issue.insert(row("outer1")) Nested.inner = Nested.zero?.mutate(create("inner")) - try tx.mutate.insert(row("outer2")) + try tx.mutate.issue.insert(row("outer2")) return Seen(mid: tx.mutationID, seen: try ids(tx)) } /// Closes the `Zero` it runs in, then writes again. static let closing = defineMutator("t.closing") { (tx, _: String) -> String in - try tx.mutate.insert(row("a")) + try tx.mutate.issue.insert(row("a")) Nested.zero?.close() - try tx.mutate.insert(row("b")) + try tx.mutate.issue.insert(row("b")) return "done" } diff --git a/swift/Tests/ZeroTests/HardenStorageTests.swift b/swift/Tests/ZeroTests/HardenStorageTests.swift index 73bbcc430c..03e8fd38e0 100644 --- a/swift/Tests/ZeroTests/HardenStorageTests.swift +++ b/swift/Tests/ZeroTests/HardenStorageTests.swift @@ -14,7 +14,7 @@ import ZeroTesting @Suite(.serialized) @MainActor struct HardenStorageTests { static let day: Double = 24 * 60 * 60_000 static let create = defineMutator("issue.create") { (tx, id: String) in - try tx.mutate.insert( + try tx.mutate.issue.insert( Issue(id: id, title: "", description: "", closed: false, createdAt: 1)) }