Model
Define database tables with fields: scalar columns and relation associations
Creating a Model
Use s.model() with an object of field definitions (scalars and relations):
import { s } from "viborm";
const user = s.model({
id: s.string().id().ulid(),
email: s.string().unique(),
name: s.string(),
createdAt: s.dateTime().default(() => new Date()),
});
Table Name
By default, VibORM uses the variable name. Use .map() to set a custom table name:
const user = s
.model({
id: s.string().id(),
// ...
})
.map("users"); // SQL: CREATE TABLE "users" (...)
Chainable Methods
.map(tableName)
Sets the database table name.
.map("users")
.map("user_accounts")
.index(fields, options?)
Adds an index on one or more fields. Fields are always passed as an array:
// Single field index
.index(["email"])
// Composite index
.index(["lastName", "firstName"])
// With options
.index(["status", "createdAt"], {
name: "idx_status_date",
type: "btree", // see the table below
unique: false,
where: "status != 'deleted'" // Partial index (PostgreSQL, SQLite)
})
Not every type exists on every database. push and migrate refuse an index
whose type the target does not have, naming the index and listing what that
dialect supports — they never quietly build a different index:
type |
PostgreSQL | MySQL | SQLite |
|---|---|---|---|
btree (default) |
✅ | ✅ | ✅ |
hash |
✅ | ❌ (InnoDB has no user-defined HASH index) | ❌ |
gin, gist |
✅ | ❌ | ❌ |
fulltext, spatial |
❌ | ✅ (cannot be combined with unique) |
❌ |
where is the same story: PostgreSQL and SQLite build the partial index, and
MySQL — which has no partial index at all — refuses the declaration by name
rather than indexing the rows your schema excluded.
The foreign-key fields of a manyToOne relation are indexed for you: push
and migrate add an index over them on every database. Declare your own index
over the same fields — first in the list, so the index still serves them — when
you want more columns in it; the automatic index is then not added.
Composite indexes for ordered includes
An include that both orders and limits a to-many relation is the case
worth widening that index for. Each parent’s children are read by a subquery
that filters on the foreign key, sorts, and takes the first N:
await client.user.findMany({
include: {
posts: { orderBy: { createdAt: "desc" }, take: 5 },
},
});
With an index on the foreign key alone the database can find a user’s posts quickly but must then read all of them and sort before it can take five. An index over the foreign key and the order column, in that order, is already sorted within each parent, so the database walks it and stops at five:
const post = s.model({
id: s.string().id().ulid(),
title: s.string(),
createdAt: s.dateTime().now(),
authorId: s.string(),
author: s.manyToOne(() => user).fields("authorId").references("id"),
}).index(["authorId", "createdAt"]);
The foreign key comes first because it is the equality; the order column comes second because it is the sort. Reversing them serves neither. Declaring it also replaces the automatic foreign-key index rather than adding a second one, since its leading column is the same.
A relation you include without orderBy, or without take/skip, does not
need this — the automatic foreign-key index already answers it.
.id(fields, options?)
Defines a compound primary key (when multiple fields form the PK):
const membership = s.model({
orgId: s.string(),
userId: s.string(),
role: s.string(),
}).id(["orgId", "userId"]);
// With custom constraint name
.id(["orgId", "userId"], { name: "membership_pk" })
.unique(fields, options?)
Adds a compound unique constraint:
const user = s.model({
id: s.string().id(),
email: s.string(),
orgId: s.string(),
}).unique(["email", "orgId"]);
// With custom constraint name
.unique(["email", "orgId"], { name: "user_email_org_unique" })
.extends(fields)
Adds additional fields to an existing model:
const baseModel = s.model({
id: s.string().id(),
createdAt: s.dateTime().default(() => new Date()),
});
const user = baseModel.extends({
email: s.string().unique(),
name: s.string(),
});
Index Options
| Option | Type | Description |
|---|---|---|
name |
string |
Custom index name |
type |
"btree" | "hash" | "gin" | "gist" |
Index type (PostgreSQL) |
unique |
boolean |
Unique index |
where |
string |
Partial index condition |
Complete Example
const user = s
.model({
// Primary key with auto-generation
id: s.string().id().ulid(),
// Required fields
email: s.string().unique(),
passwordHash: s.string(),
orgId: s.string(),
// Optional fields
name: s.string().nullable(),
bio: s.string().nullable(),
// With defaults
role: s.enum(["USER", "ADMIN"]).default("USER"),
active: s.boolean().default(true),
createdAt: s.dateTime().default(() => new Date()),
updatedAt: s.dateTime().updatedAt(),
// Relations
posts: s.oneToMany(() => post),
profile: s.oneToOne(() => profile).optional(),
})
.index(["email"])
.index(["role", "active"])
.unique(["email", "orgId"]);