Skip to content

feat(zero-swift): typed tx.mutate.<table>, as in JS - #36

Merged
tmm merged 6 commits into
mainfrom
swift-typed-mutators
Oct 27, 2026
Merged

tmm merged 6 commits into
mainfrom
swift-typed-mutators

Conversation

@tmm

@tmm tmm commented Oct 26, 2026

Copy link
Copy Markdown
Member

Writes in a Swift mutator now look like JS: all four tx.mutate.<table> calls are typed.

try tx.mutate.issue.insert(.init(id: id, title: "Crash", ...))
try tx.mutate.issue.update(.init(id: id, title: "New title"))   // only title changes
try tx.mutate.issue.update(.init(id: id, assigneeID: .null))    // unassigns
try tx.mutate.issueLabel.delete(.init(labelID: l, issueID: i))  // compound key

var change = Issue.Update(id: id)                               // or build it first
change.title = title
try tx.mutate.issue.update(change)

Before, tx.mutate.issue was the untyped TableMutator (dynamic member lookup): update and delete took dictionaries, and only insert/upsert had a typed form, without the table name.

What changes

  • zero-swift codegen emits, per table, a tx.mutate.<table> member returning TypedTableMutator<Row>, and nested Row.Update (like JS's UpdateValue) and Row.Key (DeleteID). Rows conform to ZeroTable.
  • In an Update, a column left nil keeps its value, as an undefined one does in JS. An optional column is a ColumnUpdate (.unchanged by default, .null, .set(x), or a literal), so leaving it differs from setting it to null.
  • upsert takes the whole row, so it writes every column; JS keeps a column the value leaves out. Documented.
  • The untyped API stays as tx.mutate.table("issue").
  • zbugs-swift writes through the typed calls; its issue.update no longer round-trips its arguments through JSON.

Breaking (minor): passing a dictionary to tx.mutate.<table> no longer compiles. Regenerate, and use the typed calls or tx.mutate.table(...).

Known limit: two generated schemas in one module with the same table name clash on the tx.mutate member, a compile error in the generated file. Nothing does this today.

Verification

  • Swift suite: 220 tests pass on Linux, including the end-to-end tests against zero-cache. A new test covers leaving a column, setting null, setting a value, and deleting by a compound key; it fails if .unchanged writes null.
  • zbugs drift tests pass (they compare the Swift mutators with the TS ones, including issue.update with an assignee set, null, and left out).
  • Codegen sweep: every hostile schema compiles, plus two new ones (tables named table/insert/tx; columns named Update/Key/row/value with optional enums, nullable JSON, and a compound key).
  • Lint, tsc, oxfmt clean; DocC builds with no warnings. macOS and iOS run in CI only.

ampagent and others added 6 commits October 27, 2026 03:18
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<Row>, 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.<table>
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 <tmm@tmm.dev>
tx.mutate.<table> is now the one way to write, as in JS. Generic code
uses TypedTableMutator<R>(tx.mutate); hand-written rows without a
generated member use the untyped tx.mutate.<table> with columnValues.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17664-8fd0-76be-8fd5-75ac6e7aa0d8
Co-authored-by: tmm <tmm@tmm.dev>
@tmm
tmm force-pushed the swift-typed-mutators branch from ae6eaab to 408860e Compare October 27, 2026 03:23
@tmm
tmm merged commit 0dee3a3 into main Oct 27, 2026
19 checks passed
@tmm
tmm deleted the swift-typed-mutators branch October 27, 2026 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants