Skip to main content

Round-Trip Order Editing with Generated Q and E APIs on SQLite

· 7 min read
TeaQL Team
Core Team

An order-editing page looks ordinary: load an order, show its lines in a table, let a user change quantities or remove a line, and save the result.

The difficult part is everything hidden behind that table. The server must know which rows were actually issued, which fields were editable, whether the user is still authorized, whether the order or an individual line changed in the meantime, and whether all writes committed together. Omitting a row must not be silently interpreted as deleting it.

TeaQL now has a runnable Rust and SQLite example that proves this complete round trip with generated Q, E, and mutation APIs. It is not a mock repository or an in-memory approximation: it creates a real SQLite database, loads a generated object graph, issues a context-bound document, accepts an edited projection, and verifies the persisted result.

The boundary we want​

External users and systems normally identify the order by a stable business identifier such as WEB-DOC-2026-001. The editable document may contain rows for internal objects, but raw database IDs should not become the public editing contract.

The example therefore separates two kinds of identity:

  • the order business ID is suitable for communication across a boundary;
  • context-bound row references are valid only inside the issued document.

The document body remains convenient for a browser or another client:

{
"orderNumber": "WEB-DOC-2026-001",
"items": [
{
"ref": "item:0",
"productName": "Context document product",
"sku": "DOC-LINE-1",
"quantity": 4
}
],
"removedRefs": []
}

The meaningful item:0 reference is resolved through an authenticated, context-bound document token. That protected snapshot records the real entity type, internal ID, optimistic version, projected fields, writable fields, allowed mutations, aggregate scope, purpose, actor binding, model fingerprint, and expiry.

Copying the document to another actor does not transfer its authority.

Generated Q defines the graph​

Opening the document begins with a generated Q request:

Q::customer_orders()
.with_order_number_is(order_number)
.select_order_line_list_with(
Q::order_lines()
.select_self_fields()
.order_by_id_asc()
.limit(100),
)
.limit(2)
.comment("what: load a generated Order and its complete item projection")
.purpose("why: open or validate a context-bound editable document")
.execute_for_one(context)
.await?;

This is more than a typed replacement for a SQL string.

The request states the root object, nested relation, ordering, bounds, what the query does, and why the application is allowed to execute it. The same request can be reviewed as a business-shaped query rather than reconstructed from table aliases and join conditions.

The nested line selection is also important to document semantics. TeaQL knows that this relation was deliberately loaded as a bounded, complete projection; it does not infer completeness from whatever JSON happened to return later.

Generated E preserves load-state truth​

After Q loads the graph, generated E expressions read it:

let order_id = required(E::customer_order(order).get_id().eval())?;
let order_version = required(E::customer_order(order).get_version().eval())?;
let order_number = required(E::customer_order(order).get_order_number().eval())?;
let lines = required(E::customer_order(order).get_order_line_list().eval())?;

for line in lines.iter() {
let line_id = required(E::order_line(line).get_id().eval())?;
let line_version = required(E::order_line(line).get_version().eval())?;
let sku = required(E::order_line(line).get_sku().eval())?;
let quantity = required(E::order_line(line).get_quantity().eval())?;
// Build the controlled external projection.
}

E does not collapse NotLoaded, explicit null, and a loaded value into one ambiguous result. If application code attempts to use a field that the Q projection did not load, the failure occurs where that assumption is made.

That property matters during document construction. A server should not issue an editable field merely because ordinary language defaults or a serializer made a missing value look harmless.

Accepting a document is not deserializing an entity​

When the edited document returns, TeaQL does not deserialize the JSON into a trusted entity graph and save it. The example performs a governed acceptance sequence:

consume and authenticate the document token
-> verify aggregate type, purpose, actor and model fingerprint
-> reload the current Order graph with generated Q
-> re-check current authorization
-> compare aggregate and child optimistic versions
-> reject fields and row references outside the issued projection
-> apply allowed generated mutations
-> save every change in one provider transaction
-> issue a refreshed document token

Only quantity is writable in the example. productName and sku are shown to the user but are checked against current values rather than accepted as mutations. Unknown properties, duplicate row references, rows outside the issued relation, invalid quantities, and stale versions fail closed.

The generated mutation remains direct and readable:

if E::order_line(line).get_quantity().eval() != Some(quantity) {
line.update_quantity(quantity);
}

Removal is explicit:

line.mark_for_deletion();

Leaving a row out of items is not enough to delete it. The caller must include its issued reference in removedRefs, and the protected relation snapshot must allow RemoveChild. This distinction prevents a partial client projection from accidentally erasing data it never loaded.

One transaction, including the failure path​

The aggregate revision and child mutations execute inside one TeaQL provider transaction:

context
.execute_in_send_transaction::<DataServiceExecutor, _, _>(|scope| {
Box::pin(async move {
let saved_order = scope
.save_audited(order.audit_as("accept context-bound command"))
.await?;

for line in changed {
scope
.save_audited(line.audit_as("update projected order item"))
.await?;
}

for line in removed {
scope
.save_audited(line.audit_as("remove projected order item"))
.await?;
}

Ok(saved_order.version())
})
})
.await?;

The example deliberately installs a temporary SQLite trigger that rejects a child update after the parent write has begun. The operation fails, the parent version remains 1, and no child change escapes. After the trigger is removed, the exact same submitted document succeeds and advances the aggregate version to 2.

Raw SQL is used only for this failure-injection trigger. Domain loading and mutation use generated TeaQL APIs.

What the executable proves​

The retained acceptance scenario checks each of these cases:

CapabilityEvidence in the example
Generated graph queryQ loads the Order and bounded OrderLine relation from SQLite
Loaded-field safetyE reads selected scalars and relations without hiding NotLoaded
Actor bindingBob cannot submit Alice's issued document
Runtime continuityA recreated UserContext can consume the token using the same key material
Current authorizationPermission revoked after open is rejected before write
Aggregate concurrencyA stale Order revision is rejected
Child concurrencyAn independently changed OrderLine is rejected even when the root is otherwise current
Explicit removalOnly a row named in removedRefs is deleted
Atomic persistenceInjected child failure rolls back the earlier parent update
Continued editingA successful acceptance returns a refreshed context-bound document

The final deterministic marker records the version transitions:

CONTEXT_BOUND_SQLITE_QE_PASS order_id=1 update_version=2 delete_version=3 remaining_line_id=2

Reproduce it locally​

Clone teaql-rs, then run:

TEAQL_CONTEXT_DOCUMENT_DATABASE="sqlite:file:/tmp/teaql-context-document.sqlite" \
cargo run \
--manifest-path examples/order-management/rust-app-console/Cargo.toml \
--example context_bound_document_sqlite

The complete source is the context_bound_document_sqlite Cargo example. It is also part of the controlled runtime example suite, which runs against a fresh temporary SQLite file and checks the acceptance marker. The promotion to a first-class example and its independent CI evidence are recorded in teaql-rs PR #238.

Why this is a runtime capability, not a UI trick​

A frontend can assign temporary row keys. An API can encrypt an ID. An ORM can update a row. None of those pieces alone establishes a safe round trip for a business document.

The useful capability comes from composing them under one runtime contract:

  • generated Q controls what graph is loaded;
  • generated E makes partial-load assumptions explicit;
  • the document snapshot controls what crossed the boundary;
  • current policy and optimistic versions control what may return;
  • generated mutation APIs express the permitted change;
  • audited transactional Save controls how it becomes durable.

This is the larger TeaQL goal: application code should remain close to the business operation while the runtime preserves the security, concurrency, audit, and transaction properties that are easy to lose in hand-written boundary code.