diff --git a/.changeset/typed-table-mutators.md b/.changeset/typed-table-mutators.md
new file mode 100644
index 0000000000..287ad5faf6
--- /dev/null
+++ b/.changeset/typed-table-mutators.md
@@ -0,0 +1,26 @@
+---
+'zero-swift': minor
+---
+
+**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.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, ...))
+
+- 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)` 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/apps/zbugs-swift/Sources/ZBugs/Mutators.swift b/apps/zbugs-swift/Sources/ZBugs/Mutators.swift
index 38da63c834..74f17cb316 100644
--- a/apps/zbugs-swift/Sources/ZBugs/Mutators.swift
+++ b/apps/zbugs-swift/Sources/ZBugs/Mutators.swift
@@ -200,8 +200,8 @@ 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(
- Issue(
+ try tx.mutate.issue.insert(
+ .init(
id: args.id, title: args.title, open: true, modified: args.modified,
created: args.created, projectID: args.projectID, creatorID: auth.sub,
description: args.description ?? "", visibility: .public))
@@ -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,15 +257,15 @@ 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(
- IssueLabel(
+ try tx.mutate.issueLabel.insert(
+ .init(
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,15 +297,15 @@ 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(
- Comment(
+ try tx.mutate.comment.insert(
+ .init(
id: args.id, issueID: args.issueID, created: args.created, body: args.body,
creatorID: creatorID))
try updateIssueNotification(
@@ -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(.init(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(
- IssueLabel(issueID: args.issueID, labelID: args.labelID, projectID: projectID))
+ try tx.mutate.label.insert(.init(id: args.labelID, name: args.labelName, projectID: projectID))
+ try tx.mutate.issueLabel.insert(
+ .init(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(
+ .init(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(.init(key: args.key, userID: userID, value: args.value))
}
/// Every mutator, for the client and the drift test.
@@ -384,8 +387,8 @@ 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(
- Emoji(
+ try tx.mutate.emoji.insert(
+ .init(
id: args.id, value: args.unicode, annotation: args.annotation, subjectID: args.subjectID,
creatorID: creatorID, created: args.created))
// An emoji on the issue itself follows the issue.
@@ -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(
- IssueNotifications(userID: userID, issueID: issueID, subscribed: true, created: created))
+ try tx.mutate.issueNotifications.upsert(
+ .init(userID: userID, issueID: issueID, subscribed: true, created: created))
} else if subscribed == .unsubscribe {
- try tx.mutate.upsert(
- IssueNotifications(userID: userID, issueID: issueID, subscribed: false, created: created))
+ try tx.mutate.issueNotifications.upsert(
+ .init(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 { TypedTableMutator(self) }
+ public var issueLabel: TypedTableMutator { TypedTableMutator(self) }
+ public var viewState: TypedTableMutator { TypedTableMutator(self) }
+ public var emoji: TypedTableMutator { TypedTableMutator(self) }
+ public var userPref: TypedTableMutator { TypedTableMutator(self) }
+ public var issueNotifications: TypedTableMutator { TypedTableMutator(self) }
+}
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..fac128a160 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,17 +128,9 @@ 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.
- 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
@@ -167,3 +161,94 @@ 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. Every
+ /// column is written, so an optional one left nil sets null; JS keeps a
+ /// column the value leaves out. Use `update` to set only some columns.
+ 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..b7074df50b 100644
--- a/swift/Sources/Zero/Zero.docc/GettingStarted.md
+++ b/swift/Sources/Zero/Zero.docc/GettingStarted.md
@@ -63,11 +63,20 @@ 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))
+ .init(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..86c3524a43 100644
--- a/swift/Sources/Zero/Zero.docc/JSParity.md
+++ b/swift/Sources/Zero/Zero.docc/JSParity.md
@@ -101,11 +101,30 @@ 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(.init(...))`, `.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:)` |
+`upsert` takes the whole row struct, so it writes every column: an
+optional column left nil sets null on an existing row, where JS keeps a
+column the value leaves out.
+
+Each takes a value of a generated type, which `.init(...)` builds inline.
+Like a JS object, it can also be built first, changed, and passed later:
+`var change = Issue.Update(id: id)`, then `change.title = title`, then
+`try tx.mutate.issue.update(change)`.
+
+`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
@@ -153,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/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/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/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/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/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..2f31392019 100644
--- a/swift/Tests/E2ETests/ClientE2ETests.swift
+++ b/swift/Tests/E2ETests/ClientE2ETests.swift
@@ -20,13 +20,13 @@ 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))
}
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..fabb25862f 100644
--- a/swift/Tests/ZeroTests/ClientAPITests.swift
+++ b/swift/Tests/ZeroTests/ClientAPITests.swift
@@ -22,23 +22,21 @@ 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))
}
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 { TypedTableMutator(self) }
+ public var issueLabel: TypedTableMutator { TypedTableMutator(self) }
+}
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))
}
diff --git a/swift/Tests/ZeroTests/LifecycleTests.swift b/swift/Tests/ZeroTests/LifecycleTests.swift
index eb4a8054d2..3e44fa368b 100644
--- a/swift/Tests/ZeroTests/LifecycleTests.swift
+++ b/swift/Tests/ZeroTests/LifecycleTests.swift
@@ -10,10 +10,8 @@ import ZeroTesting
static let all = defineQuery("issues.all") { () in zql.issue.orderBy(\.id, .asc) }
static let closed = defineQuery("issues.closed") { () in zql.issue.where(\.closed, true) }
static let create = defineMutator("issue.create") { (tx, id: String) in
- try tx.mutate.issue.insert([
- "id": .string(id), "title": "", "description": "", "closed": false, "ownerId": .null,
- "createdAt": 1,
- ])
+ try tx.mutate.issue.insert(
+ Issue(id: id, title: "", description: "", closed: false, createdAt: 1))
}
let scheduler = ManualScheduler(now: 1_700_000_000_000)
diff --git a/swift/Tests/ZeroTests/StorageTests.swift b/swift/Tests/ZeroTests/StorageTests.swift
index c5d52df06a..5143160e90 100644
--- a/swift/Tests/ZeroTests/StorageTests.swift
+++ b/swift/Tests/ZeroTests/StorageTests.swift
@@ -26,10 +26,8 @@ import ZeroTesting
}
let create = defineMutator("issue.create") { (tx, id: String) in
- try tx.mutate.issue.insert([
- "id": .string(id), "title": "", "description": "", "closed": false, "ownerId": .null,
- "createdAt": 1,
- ])
+ try tx.mutate.issue.insert(
+ Issue(id: id, title: "", description: "", closed: false, createdAt: 1))
}
func fileName(_ storageKey: String) -> String {
diff --git a/swift/Tests/ZeroTests/SweepActiveClientsTests.swift b/swift/Tests/ZeroTests/SweepActiveClientsTests.swift
index 08126781e8..05f199cfd0 100644
--- a/swift/Tests/ZeroTests/SweepActiveClientsTests.swift
+++ b/swift/Tests/ZeroTests/SweepActiveClientsTests.swift
@@ -8,10 +8,8 @@ import ZeroTesting
/// client (`packages/zero-client` active clients).
@Suite(.serialized) @MainActor struct SweepActiveClientsTests {
static let create = defineMutator("issue.create") { (tx, id: String) in
- try tx.mutate.issue.insert([
- "id": .string(id), "title": "", "description": "", "closed": false, "ownerId": .null,
- "createdAt": 1,
- ])
+ try tx.mutate.issue.insert(
+ Issue(id: id, title: "", description: "", closed: false, createdAt: 1))
}
func makeZero(
diff --git a/swift/Tests/ZeroTests/SweepClientTests.swift b/swift/Tests/ZeroTests/SweepClientTests.swift
index fbe64bdfff..dfd0be4042 100644
--- a/swift/Tests/ZeroTests/SweepClientTests.swift
+++ b/swift/Tests/ZeroTests/SweepClientTests.swift
@@ -9,16 +9,12 @@ import ZeroTesting
@Suite @MainActor struct SweepClientTests {
static let all = defineQuery("issues.all") { () in zql.issue.orderBy(\.id, .asc) }
static let create = defineMutator("issue.create") { (tx, id: String) in
- try tx.mutate.issue.insert([
- "id": .string(id), "title": "", "description": "", "closed": false, "ownerId": .null,
- "createdAt": 1,
- ])
+ try tx.mutate.issue.insert(
+ Issue(id: id, title: "", description: "", closed: false, createdAt: 1))
}
static let createThenThrow = defineMutator("issue.createThenThrow") { (tx, id: String) in
- try tx.mutate.issue.insert([
- "id": .string(id), "title": "", "description": "", "closed": false, "ownerId": .null,
- "createdAt": 1,
- ])
+ try tx.mutate.issue.insert(
+ Issue(id: id, title: "", description: "", closed: false, createdAt: 1))
throw ApplicationError("midway")
}
diff --git a/swift/Tests/ZeroTests/SweepStorageTests.swift b/swift/Tests/ZeroTests/SweepStorageTests.swift
index 4844b839bc..9372727dd7 100644
--- a/swift/Tests/ZeroTests/SweepStorageTests.swift
+++ b/swift/Tests/ZeroTests/SweepStorageTests.swift
@@ -12,10 +12,8 @@ import ZeroTesting
/// storage, and relaunches.
@Suite(.serialized) @MainActor struct SweepStorageTests {
static let create = defineMutator("issue.create") { (tx, id: String) in
- try tx.mutate.issue.insert([
- "id": .string(id), "title": "", "description": "", "closed": false, "ownerId": .null,
- "createdAt": 1,
- ])
+ try tx.mutate.issue.insert(
+ Issue(id: id, title: "", description: "", closed: false, createdAt: 1))
}
/// Every SQLite file of these tests is in a directory with
diff --git a/swift/Tests/ZeroTests/ViewTests.swift b/swift/Tests/ZeroTests/ViewTests.swift
index 83a147d39b..304e2708f4 100644
--- a/swift/Tests/ZeroTests/ViewTests.swift
+++ b/swift/Tests/ZeroTests/ViewTests.swift
@@ -13,22 +13,18 @@ import ZeroTesting
}
static let create = defineMutator("issue.create") { (tx, args: Edit) 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))
}
static let retitle = defineMutator("issue.retitle") { (tx, args: Edit) in
- try tx.mutate.issue.update(["id": .string(args.id), "title": .string(args.title)])
+ try tx.mutate.issue.update(.init(id: args.id, title: args.title))
}
static let remove = defineMutator("issue.remove") { (tx, id: String) in
- try tx.mutate.issue.delete(["id": .string(id)])
+ try tx.mutate.issue.delete(.init(id: id))
}
static let comment = defineMutator("comment.create") { (tx, args: Edit) in
- try tx.mutate.comment.insert([
- "id": .string("c-\(args.id)"), "authorId": "u", "issueId": .string(args.id),
- "text": .string(args.title), "createdAt": 1,
- ])
+ try tx.mutate.comment.insert(
+ Comment(id: "c-\(args.id)", authorId: "u", issueId: args.id, text: args.title, createdAt: 1))
}
func makeZero() throws -> Zero {
diff --git a/swift/Tests/ZeroVirtualTests/Generated/TestSchema.swift b/swift/Tests/ZeroVirtualTests/Generated/TestSchema.swift
index 04e7b05ab3..0dc63657b3 100644
--- a/swift/Tests/ZeroVirtualTests/Generated/TestSchema.swift
+++ b/swift/Tests/ZeroVirtualTests/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) }
+}