Schema
Tables, rows, and the indexes queries are allowed to use.
A schema declares your tables and their indexes. It is handed to the tenant object, and the reactive engine reads it to decide which subscriptions a write affects.
import { defineSchema, defineTable, schema } from "@highx/core";
const noteRow = schema((input: unknown): Record<string, unknown> => {
if (typeof input !== "object" || input === null) throw new Error("Expected a note");
return { ...input };
});
export const databaseSchema = defineSchema({
notes: defineTable("notes", noteRow, [
{ name: "byCreatedAt", fields: ["createdAt"] },
{ name: "byColor", fields: ["color", "createdAt"] },
]),
});Every row gets an id automatically. Rows are validated on the way in and on
the way out — a row read back from SQLite is untrusted input like any other.
Collaborative fields
A column several people change at once is declared with a fourth argument, and merges by the algebra it names rather than by optimistic concurrency:
notes: defineTable("notes", noteRow, [
{ name: "byUpdatedAt", fields: ["updatedAt"] },
], {
collaborative: { body: { algebra: "text", maxBytes: 1_000_000 } },
})Per field, because one row usually wants both answers — here the body merges character by character while the title and colour keep optimistic concurrency. The algebra is named with a string; what that name means is registered by the application, which is what keeps merge logic out of every schema's dependency tree.
A collaborative field may not also be indexed. An index holds a value the query engine compares; a document reconstructed from an op log is not that, and refusing beats an index that silently never matches.
Indexes are a contract
An index lists fields in order, and a query may constrain them left to right:
Index ["color", "createdAt"] | Allowed |
|---|---|
withIndex("byColor", { color }) | yes |
withIndex("byColor", { color }).range({ gte: t }) | yes — bounds createdAt |
withIndex("byColor", { createdAt }) | no — skips a leading field |
An index does two jobs: it makes the read fast, and it makes the invalidation
precise. A query that pins color is only woken by writes touching that colour.
Declaring one is therefore closer to declaring a subscription channel than to
tuning a database. → Reactivity
Index ordering has exactly one definition in the codebase: the comparison used at query time and the byte encoding rows are keyed by must agree, and an exhaustive test says so.
Choosing indexes
- One per query shape you intend to subscribe to.
- Put the field you filter by first, and the field you order by after it.
- A table with no index for a query means a full scan, which records a whole-table dependency and wakes on every write to that table.
Changing a schema
Adding a table or an index is a code change. Changing the shape of existing rows is a migration — applied per tenant, when that tenant next wakes.