Writes
On a host with Terminals::ReadWrite, a handle also writes. Every write goes through the resolved
Vista, so it follows the backend’s rules and whatever policy the host’s resolver wrapped around it.
The write verbs
let orders = table("order");
let id = orders.insert(#{ client: "c2", total: 30, status: "due" });
orders.patch(id, #{ status: "paid" });
let deleted = orders.delete("o4"); // true: the row was there
let again = orders.delete("o4"); // false: nothing left to delete
let patched = orders.patch("o99", #{ status: "paid" }); // false
#{
status: orders.get(id).status,
deleted: deleted,
again: again,
patched: patched,
}
| Verb | Returns | When the row is missing |
|---|---|---|
insert(map) | the new row’s id | n/a |
upsert(id, map) | id | inserts it |
patch(id, map) | true | returns false |
delete(id) | true | returns false |
import_from(source), import_from(source, mapping) | #{ inserted, skipped, cancelled } | n/a |
patch and delete return false only for a missing row: the backend reported
ErrorKind::NotFound. Any other failure throws. Drivers report not-found for these cases:
vantage-memory, SQL on SQLite, PostgreSQL and MySQL (a patch or delete that matched no row) and
SurrealDB (an update or delete that affected nothing). A Dio Vista checks its cache and then its
master before it queues the write. A backend that doesn’t report not-found returns true for a
missing row.
patch changes only the fields in the map. upsert replaces the whole row, so fields left out of
the map are gone afterwards. Computed columns in the map are dropped before the
write reaches the backend.
Ids
insert looks at the map’s id column. With a value there, that id is used as given, and an
existing row with that id is an error. Without one, the backend assigns the id and insert
returns it.
let orders = table("order");
orders.insert(#{ id: "o10", client: "c3", total: 55, status: "due" });
orders.upsert("o10", #{ client: "c3", total: 60, status: "due" }); // replaces
orders.upsert("o11", #{ client: "c3", total: 5, status: "due" }); // inserts
table("client").where("id", "c3").ref("orders").ids()
Inserting an id that already exists throws:
table("order").insert(#{ id: "o1", client: "c2", total: 1, status: "due" })
Use upsert when a script owns stable ids and may run more than once.
Narrowing doesn’t filter writes
A write goes to the handle’s table. where, sort, search and limit decide what reads
return, not which rows a write may touch:
let paid = table("order").where("status", "paid");
paid.delete("o2"); // o2 is due, and is deleted all the same
let ada = table("client").where("id", "c1");
ada.ref("orders").insert(#{ id: "o20", client: "c1", total: 9, status: "due" });
#{
orders: table("order").ids(),
clients: table("client").count(),
}
The exception is ref: after a ref step, writes go to the relation’s target table. Steps before
the last ref are read (to find the related rows); steps after it are ignored for writes.
insert through a ref doesn’t fill the foreign key. Set it in the map, as above.
Capabilities
Each write checks the target Vista’s capabilities before it runs, and throws an error naming the verb and the table when the backend can’t do it:
| Verb | Needs |
|---|---|
insert | can_insert |
upsert | can_insert and can_update |
patch | can_update |
delete | can_delete |
import_from | can_import, or can_insert for row-by-row inserts |
A host can also turn writes off for every table with Writes::Denied(message). The verbs are
still there, and each throws message. Vantage UI does this for MCP agents unless the “Allow MCP
agents to write data” setting is on.
Importing
import_from(source) copies every row of another handle into this table. source is read through
its narrowing, so it can be any set: a filtered table, the target of a ref, a table on another
datasource. A limit(n) on the source copies only its first n rows:
let biggest = table("order").sort("total", "desc").limit(2);
let report = table("archive").import_from(biggest);
[report.inserted, table("archive").ids()]
returns [2, ["o1", "o3"]]. Without a mapping, each row keeps its id.
With a mapping, each imported row is built from a source row. String values in the mapping may
reference source columns as ${row.<col>}:
let report = table("archive").import_from(
table("order").where("status", "paid"),
#{ id: "a-${row.id}", amount: "${row.total}", note: "paid by ${row.client}" }
);
#{ report: report, rows: table("archive").list() }
- A value that is exactly one reference (
"${row.total}") copies the column’s value with its type:amountstays an integer. - Any other string interpolates into text (
"paid by ${row.client}"). - Non-string values are literals.
- The mapping must set the target’s id column. Two source rows mapping to the same id are an
error, and so is an id whose
table:prefix names a different table.
An id the target already holds is never overwritten; it counts as skipped. When the target can
import (can_import), the rows go through import_values in one call and the backend decides
which ids it already holds. Otherwise each row is looked up with get first and inserted only if
it is missing; any insert failure stops the import and throws. A target narrowed by ref only
sees its own rows in that lookup, so an id held outside the narrowing is sent as an insert, which a
backend that rejects duplicate ids refuses. Importing the same rows twice into a memory table:
let first = table("backup").import_from(table("client"));
let second = table("backup").import_from(table("client"));
[first.inserted, second.inserted, second.skipped]
returns [3, 0, 3].
cancelled is true when the backend stopped part-way: a host’s import can be cancellable (Vantage
UI shows a progress dialog with a cancel button). inserted then counts the rows written before
the stop. A backend signals this by returning an error carrying the
IMPORT_CANCELLED context key (the rows inserted), and
optionally IMPORT_CANCELLED_SKIPPED (the rows
skipped).
Writes through a Dio
A host may hand scripts Vistas whose writes go through a Dio: Dio::vista() is a Vista over the
Dio’s cache whose inserts, patches and deletes are queued as flashes and written through to the
master. To the script it is just a Vista. It is how action bodies in Vantage UI write: the page
sees the change at once, and the master write follows.
An insert without an id on a Dio Vista goes straight to the master (there is no id to stage
until the master assigns one) and then seeds the cache, so an immediate get(id) finds the row.