Keys
One table has one primary key.
Where this fits
Start here. Columns are the other fields.
One table has one primary key. A unique is a separate constraint.
One column
.primaryKey() marks that column. Update omits it. Insert still accepts the value, unless the column is generated or guarded.
import { table, t } from "okmodel/pg";
export const notes = table(
"notes",
{
id: t.uuid().primaryKey(),
title: t.text(),
},
);Not a composite
A second .primaryKey() is not a composite key. Use several columns.
Several columns
primaryKey lists the columns, in column order.
import { table, t } from "okmodel/pg";
export const memberships = table(
"memberships",
{
organizationId: t.uuid(),
userId: t.uuid(),
},
{ primaryKey: ["organizationId", "userId"] },
);Unique
A unique is its own constraint. The primary key stays separate.
| Form | Result |
|---|---|
.unique() | One column. |
unique: { name: [fields] } | Those fields together, in that order. name is the key you choose. |
.unique({ global: "reason" }) | On a tenant table, the tenant key is left out. |
column
.unique() is the constraint for one field.
import { table, t } from "okmodel/pg";
export const users = table("users", {
id: t.identity(),
email: t.text().unique(),
});several
The table option names the constraint and lists the fields, in column order.
import { table, t } from "okmodel/pg";
export const logins = table(
"logins",
{
id: t.identity(),
email: t.text(),
provider: t.text(),
},
{ unique: { login: ["email", "provider"] } },
);global
On a tenant table a unique includes the tenant key. { global: "reason" } leaves that key out. { global: true, reason } is the same opt-out.
import { schema, table, t } from "okmodel/pg";
import { columnTenancy } from "okmodel/tenancy";
const users = table("users", {
id: t.identity(),
email: t.text().unique({ global: "one login email" }),
});
export const app = schema({
tables: [users],
tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
});OKM1060
An empty reason is unique() global needs a reason. { global: true } with no reason is Unique users.email sets global and needs a reason.
id()
Needs PostgreSQL 18 or higher.
t.id() is a uuid primary key. The schema id default is what a bare call means. A column option wins. OKID is t.id({ default: okid() }), stored as text.
| Call | What fills it | Insert |
|---|---|---|
t.id() | Database gen_random_uuid(), unless the schema default says otherwise. | Omitted. |
t.id({ default: "uuidv7" }) | Database uuidv7(). Needs Postgres 18. | Omitted. |
t.id({ default: "uuidv4" }) | Database gen_random_uuid(), the same as a bare call. | Omitted. |
t.id({ default: uuidv7 }) | The client generator. Nothing is stored in the catalog. | Omitted. |
t.id({ default: "none" }) | Nothing. | Required. Still omitted from update. |
t.identity() | generated always as identity. A bigint. | Omitted. |
database
A bare t.id() stores gen_random_uuid(). Random ids are fine for most tables. A large write-heavy table can use t.id({ default: "uuidv7" }): the ids are time-ordered and the index stays local. That function needs Postgres 18 and minPostgres: 18.
import { table, t } from "okmodel/pg";
export const notes = table("notes", {
id: t.id(),
title: t.text(),
});uuidv4
"uuidv4" stores gen_random_uuid().
import { table, t } from "okmodel/pg";
export const notes = table("notes", {
id: t.id({ default: "uuidv4" }),
title: t.text(),
});generator
A function from okmodel/ids runs on insert. The catalog stores no default.
import { uuidv7 } from "okmodel/ids";
import { table, t } from "okmodel/pg";
export const notes = table("notes", {
id: t.id({ default: uuidv7 }),
title: t.text(),
});none
"none" takes the id on insert. Update still omits it.
import { table, t } from "okmodel/pg";
export const notes = table("notes", {
id: t.id({ default: "none" }),
title: t.text(),
});identity
t.identity() is a generated bigint, not a uuid.
import { table, t } from "okmodel/pg";
export const notes = table("notes", {
id: t.identity(),
title: t.text(),
});OKM1060
A default other than uuidv7, uuidv4, none, or a client generator is refused.