# Traits

`traits` is a list. Schema traits apply first. A table's own list applies after them. An omitted list and `[]` add nothing. Import `okmodel/traits` to load the columns. A schema that never imports a trait does not carry that code.

`timestamps()`, `archivable()`, `versioned()`, and `sortable()` are the built-ins. A trait you write uses the same list. The builders are on [traits](/docs/define/table/traits). `archivable()` and `sortable()` rewrite uniques after tenancy has widened them.

### omitted

No list, or `[]`, leaves each table's own columns.

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

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

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

### list

Each entry is a trait. `timestamps()` adds `createdAt` and `updatedAt`. Both are `timestamptz not null default now()`, guarded, and sealed. Insert leaves them to the default. Update sets `updatedAt` and leaves `createdAt`.

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

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  traits: [timestamps()], // [!code focus]
});
```

#### trigger

`{ enforce: "trigger" }` also writes `updatedAt` in the database. The catalog gains `okm_touch_<column>` and one before-update trigger per table, named `<table>_touch`. `<column>` and `<table>` are the SQL names. With [casing](/docs/define/schema/casing) `"snake"`, the function is `okm_touch_updated_at`.

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

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  traits: [timestamps({ enforce: "trigger" })], // [!code focus]
});
```

<Callout type="error" title="OKM1060">
Another key is `timestamps() option extra is not supported. Accepted options: enforce.` Another value is `timestamps() enforce database must be trigger.`
</Callout>

### opt out

`omitDefaults` is a reason. It drops the schema list and keeps traits declared on that table. An empty reason, or one that is only spaces, is refused.

```ts title="opt-out.ts"
import { schema, table, t } from "okmodel/pg";
import { timestamps } from "okmodel/traits";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const sessions = table(
  "sessions",
  { id: t.identity(), token: t.text() },
  { omitDefaults: "sessions are not stamped" }, // [!code focus]
);

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

<Callout type="error" title="OKM1060">
A value that is not a list is `schema() traits must be a list of traits.` The same on a table is `Table notes traits must be a list of traits.` An empty reason is `Table sessions omitDefaults needs a reason.`
</Callout>

### together

Schema traits apply to `notes`. `tasks` adds `sortable` after them, so it also has `position`. `sessions` opts out, so it has neither `createdAt` nor `label`.

```ts title="together.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { sortable, timestamps, trait } from "okmodel/traits";

const labeled = trait("labeled", {
  fields: { label: t.text().default("note") },
});

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const tasks = table(
  "tasks",
  { id: t.identity(), title: t.text(), laneId: t.text() },
  { traits: [sortable("laneId")] }, // [!code focus]
);

const sessions = table(
  "sessions",
  { id: t.identity(), token: t.text() },
  { omitDefaults: "sessions are not stamped" },
);

const app = schema({
  tables: [notes, tasks, sessions],
  traits: [timestamps(), labeled], // [!code focus]
});

export async function open(url: string, title: string, token: string) {
  await using db = connect(url, { schema: app });
  const note = await db.notes.insert({ title });
  const session = await db.sessions.insert({ token });
  return { createdAt: note.createdAt, token: session.token };
}
```

<Callout type="error" title="OKM1012">
A column the table already has is `Trait labeled adds notes.label, and notes already declares it.` A column another trait already added is `Trait labeled adds notes.label, and trait timestamps already adds it.`
</Callout>

### Create your own

`timestamps()`, `archivable()`, `versioned()`, and `sortable()` are the built-ins above. A trait you write is `trait(name, { fields })` from `okmodel/traits`, the same function those built-ins call. Put it on this list and it is copied onto every table that does not opt out. The field options are on [traits](/docs/define/table/traits).

#### Simple

One text column with a default. Insert omits it, and the insert is awaited: `await using` closes the client when the function returns, so a returned promise would close it before the row is written. `okm generate` adds `"label" text not null default 'note'` on `notes`.

```ts title="own.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { trait } from "okmodel/traits";

const labeled = trait("labeled", {
  fields: { label: t.text().default("note") }, // [!code focus]
});

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export const app = schema({
  tables: [notes],
  traits: [labeled], // [!code focus]
});

export async function add(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return await db.notes.insert({ title }); // [!code focus]
}
```

#### Advanced

`stamped` seals `label`, so input cannot set it, and touches `labeledAt`, so an update sets that column to `now()`. `timestamps()` is the same list, applied first, so the table also has `createdAt` and `updatedAt`.

```ts title="own-advanced.ts"
import { schema, table, t } from "okmodel/pg";
import { timestamps, trait } from "okmodel/traits";

const stamped = trait("stamped", {
  fields: {
    label: t.text().default("note"), // [!code focus]
    labeledAt: t.timestamptz().defaultSql("now()"), // [!code focus]
  },
  touch: ["labeledAt"], // [!code focus]
  sealed: ["label"], // [!code focus]
});

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  traits: [timestamps(), stamped], // [!code focus]
});
```

<Callout type="error" title="OKM1012">
A column the table already declares is `Trait stamped adds notes.label, and notes already declares it.`
</Callout>
