Three admitted reads
Reads use a key, a declared index, or an ordered stream.
- Read request known question
- Primary key one record
- Declared index bounded set
- Stream cursor ordered changes
2027 · MerTek technology
Core system / System overview
Typed records, read only along declared paths
MerDB is the database engine behind MTK applications. It stores typed records in troves, and a program reads them by key, through an index the trove file declares, or from an ordered change feed. Every write to a record passes one commit point, and the store checks the declared schema when a program launches.
Worked example
The Tulsa depot tracks 40,000 part records across 60 vehicles. The 2nd-shift lead keeps a board open that asks one question every minute: which records changed since 14:00? The reorder desk, a separate service, has to act on every status change, in order, and continue after a restart without skipping one.
The team declares the part trove in its own file. Each index row is an access path the store keeps up to date on every write. bySerial stores a lowercased key, so P-204 and p-204 can never both exist.
# _db/troves/part.tri
trove part {
text<class=ascii> serial = ""
text unitId = ""
text status = "ready"
timepoint<unix> changedAt = now()
unique index bySerial = t.text.ascii.lower(serial)
index byChangedAt = changedAt
index byUnitStatus = [unitId, status]
}The board asks its question along byChangedAt. The call names the index, two bounds and a limit, and the store reads only the key range between them. The answer is newest first and never longer than 500 ids.
u64[] changedSince(timepoint<unix> shiftStart) {
chest lower = { timepoint<unix> inclusive = shiftStart }
chest upper = { timepoint<unix> inclusive = t.clock.now() }
return t.db.range("part", "byChangedAt", lower, upper, true, 500)
}
text board() {
timepoint<unix> shiftStart = t.time.unix.add(t.clock.now(), 0s - 8h)
text lines = ""
foreach (id in changedSince(shiftStart)) {
chest row = t.db.get("part", id)
chest doc = row.document
text serial = doc.serial
text status = doc.status
lines = lines + serial + " " + status + "\n"
}
return lines
}A technician marks P-204 installed. The update is conditional on the fields it names. If another technician changed status after this one read it, the volume primary refuses the write and the program takes its else arm.
text install(part p) {
transaction {
part.update(p, { status = "installed", changedAt = t.clock.now() })
} else (f) {
fault reason = f.reason
if (reason == fault.store_conflict) {
return "changed by another technician; reload and retry"
}
return "not saved"
}
return "installed"
}The reorder desk follows the change feed. poll reads a page of events without moving anything, and ack moves the desk’s cursor past what it handled. An event names a record and its version, never its contents, so the desk reads each record under its own read access.
u64 drainOnce(u64 savedThrough) {
resource<t.db.subscription> feed = t.db.subscribe("part", savedThrough)
chest page = t.db.poll("part", feed, 64)
chest[] changes = page.events
foreach (change in changes) {
u64 id = change.id
u8 deleted = change.deleted
if (deleted == 0) {
chest row = t.db.get("part", id)
chest doc = row.document
text status = doc.status
if (status == "installed") {
t.log.info("reorder stock for part " + text(id))
}
}
}
u64 through = page.through
t.db.ack("part", feed, through)
t.db.unsubscribe("part", feed)
return through
}At 16:40 a rollout restarts the desk. It subscribes again from the position drainOnce returned last, and the one page it had read but not acknowledged comes back. A volume primary that takes over after a failure keeps every sequence number, so the saved position still lines up. A position the store can no longer serve is refused by name, store_resync_required, and never skipped past.
The board reads the few hundred records that changed during the shift, at 40,000 records or ten times that. The reorder desk lost no status change across its restart. When a planner asked for every part awaiting inspection depot-wide, the team published a new index row, and no query ever read the whole table.
System diagram
The application names the access path before deployment. A query outside that path is refused instead of becoming an unplanned full scan.
Inside the system
Follow the data, decisions, and access boundaries. Each diagram explains a concept used by applications.
Reads use a key, a declared index, or an ordered stream.
A kind tag separates records without creating a second storage model.
Several update forms meet at one commit point.
A consumer resumes from one ordered position without a silent gap.
The durable declaration controls how stored data is trusted on startup.
Key parts
These are the main boundaries, inputs, outputs, and failure rules. The examples show a specific use of each part.
A program reads a record by its key, through an index its trove file declares, or from the change feed. An index answers a key range or a text search. A scan exists for maintenance and must state a row ceiling and a byte ceiling. Past either one it refuses instead of returning a partial list.
The parts board reads byChangedAt for the last 8 hours. A request for every part awaiting inspection depot-wide waits for an index row; it never becomes a full scan.
Each record belongs to one volume primary, which checks every write against the record’s latest version, commits it in a batch and acknowledges it. A write that spans several primaries commits in two phases under one decision record. A conflicting write faults into the program instead of overwriting.
Two technicians mark P-204 at 14:02. The first update commits; the second takes its else arm and reloads the record.
The trove files are the schema. At launch the store compares their digest with the ones it has published. A match runs, an older known schema reconciles and keeps writing, and an unknown one refuses until a release publishes it. Every stored record carries the schema version it was written under.
A node still on last month’s build keeps writing during a rollout. A laptop build with an unpublished column refuses at launch and names the digest it brought.
Code
Each example is short and complete enough to read on its own. Hover a dotted word for its definition.
A key read returns the document and its version. The version is what a later conditional write compares against.
text statusOf(u64 id) {
chest row = t.db.get("part", id)
chest doc = row.document
u64 version = row.version
text status = doc.status
return status + " at version " + text(version)
}
Both ceilings are required. On a 40,000-record table this call faults when the 501st row arrives; it never hands back the first 500 as if they were everything.
chest[] everyPart() {
# at most 500 rows and 1 MiB in one answer
return t.db.scan("part", 500, 1048576)
}
The untyped t.db surface spells the condition as a version. If the record moved after the read, putIf refuses with store_conflict and writes nothing.
text markInstalled(u64 id) {
chest row = t.db.get("part", id)
chest doc = row.document
u64 readVersion = row.version
doc.status = "installed"
chest written = t.db.putIf("part", id, doc, readVersion)
u64 newVersion = written.version
return "version " + text(newVersion)
}
A migration is one typed step per file. The store fills a shadow column in the background and switches with one publish. A program still on the old release faults only if it writes the renamed column. At launch the store compares each release’s steps with the ones it has applied.
# _db/troves/part.tri, after the change
trove part {
text<class=ascii> serial = ""
text bin = ""
text status = "ready"
unique index bySerial = t.text.ascii.lower(serial)
}
# _db/migrations/part/<stamp>-rename-bin.tri
step rename { from = binCode, to = bin }
The compiler lists the store access a program needs. The reorder desk asks to read part and follow its feed, and nothing else, so a grant that matches it cannot write a single part record.
u16 schemaVersion = 1
chest[] uncovered = [
{
text kind = "read"
text name = "part"
text scope = "a"
},
{
text kind = "feed"
text name = "part"
text scope = "a"
}
]
Uses
Review questions
Which questions must answer fast, and which declared index serves each one
Which consumers need every change, and where each one keeps its feed position
Which writes can collide, and what the program does when one is refused