Swift Advanced Order Mutations
Remove one line without losing the total, enforce the allowed lifecycle of an Order, and handle a rejected stale save without overwriting the winner. Start with the Swift Order tutorial for workspace setup, Q/E result handling and the first audited edit.
The retained native SQLite recipes use published Swift 0.3.1. Existing generator/adapter qualifications remain scoped, and the other six runtimes’ lifecycle/soak workspace RELEASE PENDING marks remain unchanged. This console path does not certify Apple UI or HTTP lifecycle behavior.
Prepare three independent recipes
Use the v1.0 model and original three-order fixture. The target is
ORD-2026-1001: DRAFT (1001), total 3850 USD cents, Notebook quantity 2 at 1250,
Pen quantity 3 at 450; Order and items start at version 1. Do not first run the
introductory quantity edit: that changes the total to 5100 and invalidates these
recipe starting values. The export, isolation and stock fixtures are different.
The complete native application
contains startup, helpers and phase handlers. Code below is quoted from that
application and assumes its trusted context, imports and validation helpers.
These are function-body excerpts or complete functions, not replacement standalone
programs. removeLine,
transition, worker, status, report and their casing variants are application
helpers where present; they are not extra generated API methods.
Each recipe needs a fresh disposable database initialized with bootstrap
and seeded once. The retained built workspace/toolchain is required; this is
not a clean-machine bootstrap. The original receipts keep each control on a
separate seed copy, so an omission probe, removal and stale-removal probe do not
silently alter each other's starting state.
1. Explicitly remove Pen and recompute the total
- Load all Order scalars and its complete bounded item list. The selected list
has a 101-item overflow sentinel; reject more than 100 and require a DRAFT
Order, at least two items, and exactly one
SKU-PEN. - Validate each price, quantity and stored line total. Sum only the retained Notebook line: 2 × 1250 = 2500 cents. Do not calculate from a one-item preview.
- Mark Pen for deletion with the generated deletion API, update the Order total, and save both changes in one provider transaction.
- Read again through a normal query and a deleted-only query in a new process. Preserve tombstone identity; this is soft deletion, not physical row removal.
order.orderItemList[index].markForDeletion();order.updateTotalMinor(total);print("ORDER_REMOVAL_ATTEMPT")
_ = try await order.auditAs("Remove Order line and recompute total atomically").save(context)
Generated entities use value semantics. Mark order.orderItemList[index] inside the parent value that will be saved and await the audited Order graph save. Mutating a separate local child copy is insufficient. Keep the reporting-overflow arithmetic and complete-item checks from removeLine.
The snippet assumes the full function has already selected the unique target and
validated total. A generated delete marker does not validate prices, quantities,
Order state or authorization. The fixture's business-number query is not a
production buyer/tenant policy.
Read the tombstone deliberately
The existing complete-read handler uses this explicit deleted-only request:
let deleted=try await Q.orderItems().deletedRowsOnly().withSkuCodeIs("SKU-PEN").limit(10)
.comment("Read retained deleted Order lines").purpose("Verify explicit soft deletion after restart").executeForList(context)
This is a diagnostic query across the synthetic fixture, not an authorized cross-tenant endpoint. Use the separate isolation model and trusted scope for protected access. Ordinary relation loading excludes the tombstone.
| Seed-based control | Expected durable result | Failure/recovery decision |
|---|---|---|
remove-line | Order total 2500, version 2; Notebook remains quantity 2/version 1; Pen remains in storage at version -2 and is absent from normal item reads. Other Orders stay unchanged. | Treat the returned audited save/readback as the accepted removal; keep the tombstone ID for diagnosis. |
omission-probe, on a separate fresh seed | Saving the Notebook-only preview changes only the Order note/version. Total remains 3850 and both items stay live at version 1. | Omission is not a delete command; explicitly mark the intended line. |
remove-conflict, on a separate fresh seed | A competing Pen label save advances Pen to version 2 with Pen (concurrent winner). The attempted total/deletion fails: Order stays total 3850, version 1, with original timestamp; both items remain live. | Preserve the competing item. Reload the full Order/items and ask whether removal is still intended; do not publish a 2500 total after a failed transaction. |
For the stale-removal control, the native log contains the Order UPDATE before the failing item UPDATE. The independent snapshot proves the earlier Order write rolled back. The winner's Pen update is intentionally retained; “rollback” does not mean erasing another transaction's commit. Do not catch a child failure inside the transaction and return a successful removal.
Walk through the successful removal
From the site root, using the retained built workspace. Stop if a native command fails; expected business rejections are reported as normal JSON, not process failures.
export ORDER_WORKSPACE="$PWD/artifacts/order/2026-10-27/swift/swift-app-console"
export ORDER_FIXTURE="$PWD/static/examples/order/fixture.json"
export ORDER_DATABASE="$(mktemp -d)/orders.sqlite"
cd "$ORDER_WORKSPACE"
for phase in bootstrap seed remove-line complete-read; do
./.build/debug/TeaQLConsole "$phase"
done
Inspect ORDER_CASE for remove-line and complete-read, not only the exit
status. To inspect omission-probe or remove-conflict, start another fresh
seed and substitute that phase for remove-line; never chain all three on the
same edited database. Version -2 is the result for this version-1 fixture, not a
constant to assign to arbitrary deleted entities.
2. Apply an allowed state transition
The model declares DRAFT (1001), CONFIRMED (1002) and CANCELLED (1003). The application implements this policy before calling a generated setter:
| Current status | To DRAFT | To CONFIRMED | To CANCELLED |
|---|---|---|---|
| DRAFT | Reject | Commit | Commit |
| CONFIRMED | Reject | Reject | Commit |
| CANCELLED | Reject | Reject | Reject |
Read the complete current Order with its version; require one target. Reject unknown target 9999, repeated states and prohibited reversals without saving. A generated status setter expresses a value change; it does not itself enforce this transition matrix or your service's authorization.
func transition(_ context: UserContext) async throws -> [String:Any] {
let target=try required(ProcessInfo.processInfo.environment["ORDER_TARGET"])
let next=try required(Int64(required(ProcessInfo.processInfo.environment["ORDER_NEXT_STATUS"])))
let rows=try await Q.customerOrders().withOrderNumberIs(target).limit(2)
.comment("Load full Order before state transition").purpose("Apply the Order lifecycle policy with original version").executeForList(context)
try require(rows.count==1,"Expected one target");var order=rows[0]
let current=try required(E.customerOrder(order).orderStatusId().eval())
var result:[String:Any]=try ["target":target,"from":current,"to":next,"observedVersion":required(E.customerOrder(order).version().eval())]
guard (current==1001 && (next==1002 || next==1003)) || (current==1002 && next==1003) else {result["outcome"]="rejected";result["reason"]="Transition not allowed";return result}
if next==1002 {order.updateOrderStatusToConfirmed()} else {order.updateOrderStatusToCancelled()}
_ = try await order.auditAs("Apply an allowed Order status transition").save(context);result["outcome"]="committed";result["reason"]="";return result
}
ORDER_TARGET and ORDER_NEXT_STATUS are console inputs used to demonstrate the
policy. A service should validate its request before passing the values to this
operation. The returned observedVersion is the version read before save,
not the version of the committed result; complete-read obtains current state.
On a separate fresh database, run the allowed path and rejection guards from the
prepared application directory (the previous section established the same
workspace and absolute ORDER_FIXTURE):
export ORDER_DATABASE="$(mktemp -d)/orders.sqlite"
run_order_phase() { ./.build/debug/TeaQLConsole "$1"; }
run_order_phase bootstrap
run_order_phase seed
export ORDER_TARGET=ORD-2026-1001
export ORDER_NEXT_STATUS=1002
run_order_phase transition
run_order_phase guard-edit
export ORDER_NEXT_STATUS=1003
run_order_phase transition
run_order_phase transition
run_order_phase complete-read
| Operation in that sequence | Expected result |
|---|---|
| DRAFT → CONFIRMED | outcome=committed, observed version 1; subsequent read shows state 1002/version 2. |
guard-edit while CONFIRMED | rejected=true, reason Only DRAFT orders can be edited; no quantity, total or version write. |
| CONFIRMED → CANCELLED | Commit with observed version 2; subsequent read shows state 1003/version 3. |
| CANCELLED → CANCELLED again | outcome=rejected, Transition not allowed; state/version remain 1003/3. |
These transitions preserve the 3850 total and all item values/versions. Setting
CANCELLED does not delete items, release stock or deliver an external event: the
v1.0 recipe has no inventory reservation workflow. The retained state controls also
cover DRAFT → CANCELLED, all prohibited pairs, unknown state and item edits on a
CANCELLED Order. A rejected policy decision is a normal business result, not proof
of a provider failure. If save itself fails, preserve that native error and use
the conflict/provider decision below; do not return committed in a finally block.
3. Handle a stale Order version
The existing stale-order control isolates the version decision from business
math: two native processes change only the Order note. Both fully load the same
existing version-1 Order before either is allowed to write.
| Step | Writer A | Writer B | Authoritative database |
|---|---|---|---|
| Load | Retain original Order/version 1. | Retain original Order/version 1. | Still seed state; no writes before the readiness barrier. |
| Release B | Keep waiting with version 1. | Save Delivery note from writer B. | B's note at version 2, independently checked before A is released. |
| Release A | Attempt audited save with the original version 1; native rejection. | Already finished. | B's note, version and timestamp remain unchanged; amounts/items stay at seed values. |
| Read again | Reload authoritative state before any new business decision. | — | Fresh process sees B's committed version 2. |
After its readiness/release barrier, this language's worker executes:
order.updateOrderNote(note);var outcome="committed",reason=""
do { _ = try await order.auditAs("Revise Order note with optimistic version").save(context) }
catch { outcome="conflict";reason=String(describing:error) }
result["outcome"]=outcome;result["conflictReason"]=reason
print("ORDER_WORKER "+String(decoding:try JSONSerialization.data(withJSONObject:result,options:[.sortedKeys]),as:UTF8.self))
The test worker collects thrown save errors under one label. The parent controller verifies the native error and winner; do not classify every thrown provider error as a conflict.
Classify the error, then decide
The native stale-run diagnostic in the existing swift receipt is:
optimisticLock(entity: "CustomerOrder", id: TeaQLCore.TeaQLValue.int(1), expectedVersion: 1)
Preserve the associated entity and expected version in the native error. Await the save before returning or ending the provider scope; a cancelled or failed Task is not a committed transition.
The numeric entity ID in the recorded diagnostic belongs to that database; use the scoped business identity rather than copying it as a portable Order key.
| Outcome | Application action |
|---|---|
| Confirmed optimistic version rejection | Reload the complete authoritative Order and needed items in the same trusted scope. Revalidate current state and amounts. Reject the stale command or reconcile the user's intent before creating a new audited attempt. |
Policy says Transition not allowed / non-DRAFT edit | Return that business rejection without a write. Do not retry it as a transient database problem. |
| Busy/locked, timeout, provider statement or COMMIT failure | Preserve the native cause and diagnose transaction/provider cleanup. These errors do not count as version conflicts. |
| Missing Order after reload | Handle not-found in the same scope. Do not fabricate a successful edit or borrow an ID from another fixture. |
Do not overwrite a losing object's version with the winner's version and resubmit its old fields. Do not put every failure into an unbounded retry loop. A reloaded Order may now be CONFIRMED/CANCELLED, or the Pen line may have changed; repeat the business checks before deciding whether to retry. A newly accepted edit needs its own audit reason and one complete transaction.
The worker files use a 45-second readiness/release protocol and are coordinated
by the existing P1 runner. Running the bare worker phase without its controller
will wait for release; use the stored round trace to understand the timeline.
The same-Order control releases both writers together and permits one winner;
the distinct-Order control expects two commits. Use the recorded ready/result snapshots to distinguish
the one-target and two-target outcomes.
The retained stale race changes notes. It does not supply a complete service-level retry/merge policy for two concurrent state-transition commands. Keep the save failure boundary explicit and design that business decision at the service layer.
Existing evidence and next steps
| Topic | Where to inspect the retained result |
|---|---|
| Exact snippets and runtime/package qualification | Current core receipt: runtimes.swift.verification.cases and the source-bound application above. |
| Explicit removal, omission, stale child and state matrix | P1 completion receipt, with its original sources; the current core receipt reruns those cases with the quoted source. |
| Ordered stale writer and same/different Order races | P1 concurrency receipt, plus cases.stale-order in the current core receipt. |
| Failures and database invariants | Native errors, provider UPDATE/audit logs, before/after/fresh-read snapshots, and the removal/state expectations. |
Use the existing receipt's controls for the three removal branches, steps for
state decisions and rounds for writer ordering. A positive process exit from a
negative-test reporter is not a successful business mutation. Do not relabel
observedVersion as a returned committed version.
Continue with the Swift troubleshooting table, API usage guide and documentation coverage matrix. The first-edit tutorial and School regression baseline remain separate paths.
Continue with Order queries
Use Swift Order Queries for continuous export, retained-ID page jumps and provider summaries. The query recipes use separate nine-/ten-order datasets; keep the fixture and context/store lifetime with the operation you are learning.
Continue with buyer and tenant isolation
Use Swift Order Isolation to upgrade the model to v1.1 and build root/buyer scope from an application-owned trusted session.