# Keys

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

<Callout type="info" title="Where this fits">
Start here. [Columns](/docs/schema/columns) are the other fields.
</Callout>

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.

```ts title="notes.ts"
import { table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.uuid().primaryKey(), // [!code focus]
    title: t.text(),
  },
);
```

<Callout type="warning" title="Not a composite">
A second `.primaryKey()` is not a composite key. Use [several columns](#several-columns).
</Callout>

### Several columns

`primaryKey` lists the columns, in column order.

```ts title="memberships.ts"
import { table, t } from "okmodel/pg";

export const memberships = table(
  "memberships",
  {
    organizationId: t.uuid(),
    userId: t.uuid(),
  },
  { primaryKey: ["organizationId", "userId"] }, // [!code focus]
);
```

### 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.

```ts title="users.ts"
import { table, t } from "okmodel/pg";

export const users = table("users", {
  id: t.identity(),
  email: t.text().unique(), // [!code focus]
});
```

#### several

The table option names the constraint and lists the fields, in column order.

```ts title="logins.ts"
import { table, t } from "okmodel/pg";

export const logins = table(
  "logins",
  {
    id: t.identity(),
    email: t.text(),
    provider: t.text(),
  },
  { unique: { login: ["email", "provider"] } }, // [!code focus]
);
```

#### 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.

```ts title="global.ts"
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" }), // [!code focus]
});

export const app = schema({
  tables: [users],
  tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
});
```

<Callout type="error" title="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.`
</Callout>

### id()

Needs PostgreSQL 18 or higher.

`t.id()` is a uuid primary key. The schema [id default](/docs/schema/keys-and-identity#id-default) is what a bare call means. A column option wins. [OKID](/docs/schema/binary-json-id-columns#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`.

```ts title="id.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.id(), // [!code focus]
  title: t.text(),
});
```

#### uuidv4

`"uuidv4"` stores `gen_random_uuid()`.

```ts title="uuidv4.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.id({ default: "uuidv4" }), // [!code focus]
  title: t.text(),
});
```

#### generator

A function from `okmodel/ids` runs on insert. The catalog stores no default.

```ts title="generator.ts"
import { uuidv7 } from "okmodel/ids";
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.id({ default: uuidv7 }), // [!code focus]
  title: t.text(),
});
```

#### none

`"none"` takes the id on insert. Update still omits it.

```ts title="supplied.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.id({ default: "none" }), // [!code focus]
  title: t.text(),
});
```

#### identity

`t.identity()` is a generated bigint, not a uuid. `t.integerIdentity()` is the same column as an integer.

```ts title="identity.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(), // [!code focus]
  title: t.text(),
});
```

<Callout type="error" title="OKM1060">
A default other than `uuidv7`, `uuidv4`, `none`, or a client generator is refused.
</Callout>

## 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.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.id(), // [!code focus]
  title: t.text(),
});

export default schema({
  tables: [notes],
});
```

### uuidv7

`"uuidv7"` stores `uuidv7()`. Keep the previous default with `defaults: { id: "uuidv7" }`.

```ts title="schema.ts"
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" }, // [!code focus]
});
```

<Callout type="error" title="OKM1812">
`uuidv7()` needs PostgreSQL 18 or higher. A [minPostgres](/docs/schema/the-schema#minimum-postgres) 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.
</Callout>

### Default uuidv4

`"uuidv4"` stores `gen_random_uuid()`, the same expression a bare `t.id()` stores. That function is built in from Postgres 13.

```ts title="schema.ts"
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" }, // [!code focus]
});
```

### 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.

```ts title="schema.ts"
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 }, // [!code focus]
});
```

#### 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](/docs/schema/binary-json-id-columns#okid).

```ts title="schema.ts"
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_" }) }, // [!code focus]
});
```

### 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`.

```ts title="schema.ts"
import { okid } from "okmodel/ids";
import { schema, table, t } from "okmodel/pg";

const users = table("users", {
  id: t.id(), // [!code focus]
  name: t.text(),
});

const sessions = table("sessions", {
  id: t.id({ default: "uuidv4" }), // [!code focus]
  name: t.text(),
});

export default schema({
  tables: [users, sessions],
  defaults: { id: okid({ prefix: "usr_" }) },
});
```

<Callout type="warning" title="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.`
</Callout>

## 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](/docs/schema/defaults#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](/docs/schema/keys-and-identity#id-default) uses it too. Each missing field calls the function once.

### Generator uuidv7

Insert stores this string when `id` is omitted.

```ts title="db.ts"
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", // [!code focus]
    },
  });
  return await db.notes.insert({ title });
}
```

<Callout type="info" 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.
</Callout>

| Next | You use it for |
| --- | --- |
| [References](/docs/schema/references) | A column that points at a row in another table. |

Index: [/llms.txt](/llms.txt)
