Schemas
A schema describes two things: the shape of your rows and the indexes your code
can read through. Schemas are defined with defineTable and the v validator
library, and they also act as the source of truth for TypeScript types.
Indexes are part of the public data model. Selectors later name an index
explicitly with selectFrom(table, "indexName"), so the schema is where you
decide which access paths your application has.
Defining a table
Section titled “Defining a table”Every table has a name and a set of fields. A table must have a string id
field. HyperDB automatically creates a built-in unique hash index named byId
on id; declare the other indexes your selectors will read through.
import { defineTable, v, type ExtractSchema } from "@will-be-done/hyperdb";
export const tasksTable = defineTable("tasks", { id: v.string(), projectId: v.string(), title: v.string(), slug: v.string(), state: v.union(v.literal("todo"), v.literal("done")), orderToken: v.string(), note: v.optional(v.string()),}) .index("byProjectOrder", ["projectId", "orderToken"]) .index("byTitle", ["title"], { type: "hash" }) .index("bySlug", ["slug"], { type: "uniqhash" }) .index("byIds", ["id"]);
export type Task = ExtractSchema<typeof tasksTable>;ExtractSchema<typeof table> gives you the row type. Here, Task is:
type Task = { id: string; projectId: string; title: string; slug: string; state: "todo" | "done"; orderToken: string; note?: string;};Tables are plain descriptions. They don’t store data and aren’t bound to any
database, so you can import the same table into multiple DB instances. You make
a table usable on a database by calling loadTables.
byId and byIds are intentionally different in this example. The built-in
byId index is a uniqhash for exact id lookups. The explicit byIds index is
a B-tree over id, useful when you need ordered full-table scans, for example
with preloadTables on a HybridDB.
Validators
Section titled “Validators”Fields are described with validators from the v namespace. The full list of
value types lives in Data Types; here are the building
blocks:
v.string();v.number(); // finite numbers onlyv.bigint();v.boolean();v.null();v.literal("done"); // string | number | bigint | boolean | null literalsv.array(v.string());v.object({ x: v.number(), y: v.number() });v.record(v.string(), v.number());v.union(v.literal("todo"), v.literal("done"));v.optional(v.string());v.arrayBuffer();v.any();There are also helpers for deriving object validators:
v.partial(objectValidator): make every field optional.v.required(objectValidator, ["a", "b"]): make the listed optional fields required again.v.lazy(() => validator): defer evaluation, for recursive shapes.v.pass<T>(): accept any value as typeTwithout normalizing it.
Standalone validators and types
Section titled “Standalone validators and types”Validators are useful beyond tables, for example to type selector/action
arguments or intermediate data. Use Infer to get the TypeScript type of any
validator.
import { v, type Infer } from "@will-be-done/hyperdb";
const filterSchema = v.object({ projectId: v.string(), state: v.optional(v.union(v.literal("todo"), v.literal("done"))),});
type Filter = Infer<typeof filterSchema>;// { projectId: string; state?: "todo" | "done" }Tagged unions
Section titled “Tagged unions”defineTable also accepts a standalone object or union validator instead of a
field map. This is how you model a table whose rows are a tagged union of
several shapes:
const documentsTable = defineTable( "documents", v.union( v.object({ id: v.string(), type: v.literal("post"), title: v.string() }), v.object({ id: v.string(), type: v.literal("note"), body: v.string() }), ),).index("byPostTitle", ["title"]);Each variant must still include a string id. When you index a column, HyperDB
collects that field’s validator across every variant that declares it.
Indexes
Section titled “Indexes”Indexes are declared with .index(name, columns, options?). They are what make
queries fast, and they make query execution explicit: a selector chooses one
index, then builds equality or range bounds over that index. Each .index(...)
call returns a new table definition, so you can chain them.
defineTable("tasks", { /* fields */}) .index("byProjectOrder", ["projectId", "orderToken"]) // btree (default) .index("byTitle", ["title"], { type: "hash" }) // non-unique hash .index("bySlug", ["slug"], { type: "uniqhash" }) // unique hash .index("byIds", ["id"]); // btree full-table scanbtree(the default) supports equality and range queries, ordering, and composite (multi-column) keys.hashsupports equality lookups only and must have exactly one column.uniqhashsupports equality lookups only, must have exactly one column, and rejects duplicate values in every storage driver. The built-inbyIdindex is auniqhash.
Index columns must:
- exist in the table schema, and
- be indexable value types (
string, finitenumber,bigint,boolean,null,ArrayBuffer/typed-array, and compatible literals, unions, and optionals of those).
Invalid index definitions throw at defineTable time, so mistakes surface
immediately. For how composite indexes are queried, see
Indexes.
Choosing indexes
Section titled “Choosing indexes”Start from the reads your app needs:
- Exact lookup by id: use the built-in
byId. - Ordered list inside a parent: use a B-tree such as
["projectId", "orderToken"]. - Exact lookup by a non-unique field: use
hash. - Exact lookup by a unique field such as a slug: use
uniqhash. - Whole-table preload or ordered full scan: add a B-tree full-scan index such as
["id"].
For composite B-tree indexes, put equality columns first and the ordered or
range column last. A selector over ["projectId", "orderToken"] can read one
project in order without scanning every task.
Runtime validation
Section titled “Runtime validation”Validators give you TypeScript types and normalize values before they reach
storage. In development, pass runtimeRowsValidation: true to DB to validate
full rows on writes and on reads from the driver. That is useful when data may
come from older versions, imports, sync, or external storage.
The undefined rule
Section titled “The undefined rule”Stored values can never contain undefined. Optional object fields may be
omitted entirely, and { field: undefined } is normalized to “missing”. But
undefined is not allowed inside arrays or as a record value. See
Data Types for the details.
Type helpers reference
Section titled “Type helpers reference”| Helper | Purpose |
|---|---|
ExtractSchema<typeof table> | Row type of a table |
ExtractIndexes<typeof table> | Index definitions of a table |
Infer<typeof validator> | TypeScript type of any validator |
InferObject<fields> | TypeScript type of an object field map |