Keys
One table has one primary key. Identity is the id default and the generators.
Keys identify a row. The id default is what a bare t.id() means, and a column option wins. Generators replace that function when you connect, once for each missing field.
Keys
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. |
t.integerIdentity() | generated always as identity. An integer. | 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. t.integerIdentity() is the same column as an integer.
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.
Id default
defaults has one field, id. It replaces a bare t.id(). A column that passes default is left alone. "none" is that column option. It is not a schema value.
The id is omitted from insert for a database default and for a client generator. "none" is required on insert and still omitted from update.
omitted
Omitted and {} store gen_random_uuid(). "uuidv4" stores the same function. It is built in from Postgres 13, so a bare t.id() works on every supported major. Random ids are fine for most tables. A large write-heavy table can pick uuidv7: the ids are time-ordered and the index stays local. That function needs Postgres 18.
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.id(),
title: t.text(),
});
export default schema({
tables: [notes],
});uuidv7
"uuidv7" stores uuidv7(). Keep the previous default with defaults: { id: "uuidv7" }.
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.id(),
title: t.text(),
});
export default schema({
tables: [notes],
defaults: { id: "uuidv7" },
});OKM1812
uuidv7() needs PostgreSQL 18 or higher. A minPostgres floor below 18 is uuidv7() needs PostgreSQL 18 or higher. minPostgres: 18. An omitted floor does not name a version, so an explicit uuidv7() stays until apply.
Default uuidv4
"uuidv4" stores gen_random_uuid(), the same expression a bare t.id() stores. That function is built in from Postgres 13.
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.id(),
title: t.text(),
});
export default schema({
tables: [notes],
defaults: { id: "uuidv4" },
});client
A generator from okmodel/ids runs on insert. The catalog stores no default. Changing an OKID prefix, length, or sortable flag does not change the catalog hash. The SQL type does: okid() is text, and uuidv4 or uuidv7 stay uuid.
uuid
uuidv4 and uuidv7 stay uuid. The client uuidv7 mints a time-ordered id, so a server older than Postgres 18 can still use one.
import { uuidv7 } from "okmodel/ids";
import { schema, table, t } from "okmodel/pg";
const sessions = table("sessions", {
id: t.id(),
name: t.text(),
});
export default schema({
tables: [sessions],
defaults: { id: uuidv7 },
});okid
okid() is text with collation C. Calling it returns the generator. It does not mint the id here. Length, prefix, and sortable are on OKID.
import { okid } from "okmodel/ids";
import { schema, table, t } from "okmodel/pg";
const users = table("users", {
id: t.id(),
name: t.text(),
});
export default schema({
tables: [users],
defaults: { id: okid({ prefix: "usr_" }) },
});Column option
A column option wins. A bare t.id() takes the schema default. t.id({ default: "uuidv4" }) stays gen_random_uuid() and stays uuid.
import { okid } from "okmodel/ids";
import { schema, table, t } from "okmodel/pg";
const users = table("users", {
id: t.id(),
name: t.text(),
});
const sessions = table("sessions", {
id: t.id({ default: "uuidv4" }),
name: t.text(),
});
export default schema({
tables: [users, sessions],
defaults: { id: okid({ prefix: "usr_" }) },
});OKM1060
Anything other than an object is schema() defaults must be an object. Another key is schema() defaults.createdAt is not a schema default. Accepted names: id. Another value, including "none", is schema() defaults.id must be "uuidv7", "uuidv4", or a client generator.
Id generators
generators replaces a built-in client generator by name. The names are uuidv4, uuidv7, and okid. Each replacement returns a string. A generator runs when insert omits the field.
| Key | Insert |
|---|---|
| omitted | Calls the function stored on the column. |
uuidv4 | Calls the function you passed, for a uuidv4 generator. |
uuidv7 | Calls the function you passed, for a uuidv7 generator. |
okid | Calls the function you passed, for an okid generator. |
The function takes no arguments. One function covers every column of that name. A bare t.id() that took that generator from the schema id default uses it too. Each missing field calls the function once.
Generator uuidv7
Insert stores this string when id is omitted.
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { uuidv7 } from "okmodel/ids";
const notes = table("notes", {
id: t.uuid().default(uuidv7),
title: t.text(),
});
const app = schema({ tables: [notes] });
export async function add(url: string, title: string) {
await using db = connect(url, {
schema: app,
generators: {
uuidv7: () => "00000000-0000-7000-8000-000000000000",
},
});
return await db.notes.insert({ title });
}Left alone
A function you wrote and passed to .default() keeps its own call. A bare t.id() stores gen_random_uuid(). t.id({ default: "uuidv7" }) stores uuidv7(), which needs Postgres 18. Pick that for a large write-heavy table: the ids are time-ordered and the index stays local. Random ids are fine elsewhere.
| Next | You use it for |
|---|---|
| References | A column that points at a row in another table. |