Tables & Collections
An app’s state is the fields of its #[app::state] struct. Each field is a
table or a collection, a handle over the app’s ordered key-value store
(crates/sdk/src/table.rs, crates/sdk/src/collections). Loading the state
reads nothing; every operation reads or writes only the keys it needs.
One leader applies every write in log order, so these are plain data
structures, not CRDTs: there is nothing to merge. The exception is Text,
which stays a sequence CRDT so that edits typed against a stale view still land
where they were meant (see Collaborative text).
Tables
Section titled “Tables”A Table<R> holds records:
rows with a primary key, unique constraints, indexes, maintained counts and,
optionally, full-text search.
#[derive(Record)]#[table( key = "id", unique(slot = ["room_id", "start"]), index(by_start = ["checked_in", "start"]), count(by_room = ["room_id"]),)]pub struct Booking { pub id: String, pub room_id: String, pub start: u64, pub title: String, pub checked_in: bool,}| Method | Does |
|---|---|
insert(record) |
Inserts; an existing key is Duplicate { index: "key" }, a unique clash Duplicate { index: <name> }. |
upsert(record) |
Inserts or replaces; unique constraints still apply. |
get(&key) / contains(&key) |
The record with this key / whether it exists. |
update(&key, |r| ..) |
Changes the record and re-indexes what changed; false when absent. |
remove(&key) |
Removes the record and its index, unique and count entries; false when absent. |
find(index, &prefix, opts) |
Records whose index fields start with prefix, in index order, one page at a time. |
scan(opts) |
All records in key order, one page at a time. |
count(name, &prefix) |
A maintained count: one read when prefix names every field of the count. |
search(query, ..) |
Full-text search over #[table(search = [..])] fields, best match first (views only). |
Index and count names are camelCase (by_start → "byStart"). Prefixes and
bounds are KeyParts: Str, Int, Bool, Bytes.
let due = self.bookings.find( "byStart", &[KeyPart::Bool(false)], FindOpts::default().to(vec![KeyPart::Int(i128::from(tick.scheduled))]),)?;for b in due.items { self.bookings.remove(&b.id)?;}Every multi-row read returns a Page { items, next }. FindOpts sets
limit (100 when unset, at most 1,000), cursor (the previous page’s
next), reverse, and from (inclusive) / to (exclusive) bounds after the
prefix. A page reads at most limit + 1 entries.
Collections
Section titled “Collections”| Type | Operations |
|---|---|
Cell<T> |
get, set, clear |
UnorderedMap<K, V> |
get, contains, insert / remove (return the old value), len, is_empty, clear; with K: FromKey: page, range, prefix, starts_with, first, last |
Set<T> |
insert, remove (bool), contains, len, is_empty, clear, page, range, prefix, starts_with, first, last |
Vector<T> |
push (returns the position), get, set (OutOfBounds past the end), pop, len, is_empty, page, clear |
Counter |
get, add, increment |
Frozen<T, H = CborSha256> |
Content-addressed and write-once: insert (returns the hex hash and whether it was new), get(hex), contains(hex), hash_of |
PerUser<T> |
get, set, remove on the caller’s own value; get_for(account) reads another’s |
Text |
insert_after(id, str), delete_ids, insert(index, str), delete(index, n), read, chars, id_at, index_of, len, clear |
RichText |
text() / text_mut() (a Text), mark(start, end, key, value, expand), mark_ids, marks, spans, clear |
Despite its name, UnorderedMap reads in key order (page, range, first,
last), so it also serves as a sorted map; tuple keys make nested maps.
#[app::state(version = 1)]pub struct Store { items: UnorderedMap<String, String>, tags: Set<String>, visits: Counter, profile: PerUser<String>,}Costs and limits
Section titled “Costs and limits”- Lengths and counts are stored and kept with an atomic add on every write, so
lenandcountare one read, never a scan. cleardeletes in batches of at most 1,000 until nothing is left.- A key over 1 KiB is refused (
KeyTooLong) before any store call. - Values are deterministic CBOR; keys are order-preserving tuples, the contract the TypeScript SDK produces the same bytes for.
Textreads and edits load every node (1,000 per host read); itslenis one read.