# Models, repositories and migrations

A `Model` is a [Schema](https://effect.plants.sh/schema/) that describes one row of a SQL table while
generating the variants you actually need at runtime — a `select` variant for
reading, an `insert` variant that omits database-generated columns, and an
`update` variant for partial writes (plus JSON variants for an HTTP API).
`SqlModel.makeRepository` turns a model into a ready-made repository with
`insert`, `update`, `findById`, and `delete`, so the common CRUD operations are
type-safe and decoded without hand-writing each query.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

// Branded id keeps Person ids from being confused with other numeric ids.
export const PersonId = Schema.Number.pipe(Schema.brand("PersonId"))

export class Person extends Model.Class<Person>("Person")({
  // FieldExcept(["insert"]): the column is present in the select and update
  // variants but omitted from insert, because the database assigns the id.
  id: PersonId.pipe(Model.FieldExcept(["insert"])),
  name: Schema.String,
  email: Schema.String,
  // Timestamp fields filled with the current time on insert / update.
  createdAt: Model.DateTimeInsertFromDate,
  updatedAt: Model.DateTimeUpdateFromDate
}) {}

// `Person`         — the select schema (full row)
// `Person.insert`  — input for INSERT (no id, no timestamps to supply)
// `Person.update`  — input for UPDATE
// `Person.json`    — variant for serializing over an API
```

The field helpers encode intent: `Model.FieldExcept(["insert"])` keeps a column
in the select and update variants while omitting it from insert (ideal for a
database-assigned, but still updatable, id), `Model.DateTimeInsertFromDate` /
`Model.DateTimeUpdateFromDate` populate timestamps on write, and
`Model.Sensitive` hides values from JSON output. `Model.GeneratedByDb` exists for
read-only generated columns — it contributes only `select` and `json` variants,
so it is not suitable for an id you also pass to `update`. Each variant is a real
Schema, so models compose with `SqlSchema`, validation, and the rest of the
ecosystem.
**Import path:** `Model` lives in the unstable schema package: ``. The repository and migrator helpers live in
`effect/unstable/sql`.

## Model variants

A model is built from six variants. The three **database** variants
(`VariantsDatabase = "select" | "insert" | "update"`) describe the shapes you
encode to and decode from SQL, and the three **JSON** variants (`VariantsJson =
"json" | "jsonCreate" | "jsonUpdate"`) describe the shapes you expose over an
HTTP API. The default variant — used when you reference the model directly — is
`select`.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

export const GroupId = Schema.Number.pipe(Schema.brand("GroupId"))

export class Group extends Model.Class<Group>("Group")({
  id: Model.GeneratedByDb(GroupId),
  name: Schema.String,
  createdAt: Model.DateTimeInsertFromDate,
  updatedAt: Model.DateTimeUpdateFromDate
}) {}

Group          // select  — the full row (default variant)
Group.insert   // insert  — no id (GeneratedByDb), no timestamps to supply
Group.update   // update  — no createdAt (insert-only)
Group.json     // json    — read shape for an API
Group.jsonCreate // jsonCreate — create payload
Group.jsonUpdate // jsonUpdate — update payload
```

Every variant is itself a `Schema`, so you can decode/encode with it directly or
promote it into a `Schema.Class` to attach derived data:

```ts
import { Schema } from "effect"

// Turn the json variant into a class with extra computed members.
class GroupJson extends Schema.Class<GroupJson>("GroupJson")(Group.json) {
  get upperName() {
    return this.name.toUpperCase()
  }
}
```

How a field maps onto variants is decided by the helper you use. A **plain
schema** (e.g. `name: Schema.String`) is included in *all* variants. A
`Field`-based helper opts the property into *only* the variants it declares — so
`Model.GeneratedByDb` (select + json) disappears from insert and update, and
`Model.Sensitive` (database variants only) disappears from every JSON variant.

## Generating a repository

`SqlModel.makeRepository` reads the `SqlClient` from context and returns the CRUD
operations bound to a table. Wrap it in a [service](https://effect.plants.sh/services-and-layers/) so the
rest of your app depends on the repository, not on raw SQL.

```ts
import { Context, Effect, Layer } from "effect"
import { SqlClient, SqlModel } from "effect/unstable/sql"
import { Person, PersonId } from "./Person.js"

// makeRepository returns an Effect whose success value is the record of CRUD
// operations and whose only requirement is SqlClient. `Effect.Success` reads
// that shape back out so the service interface stays in sync.
const makePeopleRepository = SqlModel.makeRepository(Person, {
  tableName: "people",
  spanPrefix: "People", // names the tracing spans for each operation
  idColumn: "id"
})

export class People extends Context.Service<People, Effect.Success<typeof makePeopleRepository>>()(
  "app/People"
) {
  // Layer.effect runs makePeopleRepository, so this layer still needs a
  // SqlClient layer to be provided alongside it.
  static readonly layer: Layer.Layer<People, never, SqlClient.SqlClient> =
    Layer.effect(People, makePeopleRepository)
}

const program = Effect.gen(function*() {
  const people = yield* People

  // insert encodes Person.insert, runs INSERT … RETURNING *, and decodes the
  // row back into a full `Person`.
  const created = yield* people.insert(
    Person.insert.make({ name: "Ada", email: "ada@example.com" })
  )

  // findById fails with NoSuchElementError when the row is absent.
  const found = yield* people.findById(PersonId.make(created.id))

  yield* people.update(
    Person.update.make({ id: found.id, name: "Ada Lovelace", email: found.email })
  )
})
```
**The repository is just an Effect:** `SqlModel.makeRepository` returns an `Effect` whose success value is the record
of operations and whose only requirement is `SqlClient`. Wrapping it in a
`Context.Service` with `Layer.effect` (as above) is the idiomatic way to expose
it; see [Services & Layers](https://effect.plants.sh/services-and-layers/) for the full pattern.

### Repository operations

`makeRepository(Model, { tableName, spanPrefix, idColumn, softDeleteColumn? })`
returns a record of six operations. Returned rows are decoded with the full model
schema; insert/update inputs are encoded with the model's dedicated input
schemas. Each operation is wrapped in a tracing span prefixed with `spanPrefix`.

#### `insert`

Encodes `Model.insert`, runs `INSERT … RETURNING *` (or an insert + follow-up
select on MySQL), and decodes the new row into a full model value.

```ts
const created = yield* repo.insert(
  Person.insert.make({ name: "Ada", email: "ada@example.com" })
)
// => Person (with the DB-assigned id and timestamps)
```

#### `insertVoid`

Like `insert` but discards the result — no `RETURNING`, no decode. Use it when
you do not need the inserted row back.

```ts
yield* repo.insertVoid(Person.insert.make({ name: "Ada", email: "ada@example.com" }))
// => void
```

#### `update`

Encodes `Model.update`, runs `UPDATE … WHERE id = ? RETURNING *`, and decodes the
updated row. The id is read from the `idColumn` field of the update payload.

```ts
const updated = yield* repo.update(
  Person.update.make({ id, name: "Ada Lovelace", email })
)
// => Person (the updated row)
```

#### `updateVoid`

Like `update` but discards the returned row.

```ts
yield* repo.updateVoid(Person.update.make({ id, name: "Ada L.", email }))
// => void
```

#### `findById`

Selects the row whose `idColumn` equals the supplied id and decodes it. Fails
with `NoSuchElementError` when no row matches.

```ts
const person = yield* repo.findById(PersonId.make(1))
// => Person  |  fails NoSuchElementError if absent
```

#### `delete`

Removes the row by id. With a `softDeleteColumn` set, it instead sets that column
to `CURRENT_TIMESTAMP` rather than deleting.

```ts
yield* repo.delete(PersonId.make(1))
// => void
```

### Soft deletes

Pass `softDeleteColumn` to mark rows as deleted instead of removing them. Reads
and updates then only see rows where that column `IS NULL`, and `delete` sets it
to `CURRENT_TIMESTAMP`.

```ts
const makeRepo = SqlModel.makeRepository(Person, {
  tableName: "people",
  spanPrefix: "People",
  idColumn: "id",
  softDeleteColumn: "deletedAt" // delete sets deletedAt = CURRENT_TIMESTAMP
})
// findById / update / delete all add  ...AND deletedAt IS NULL
```
**RETURNING vs MySQL:** Dialects with `RETURNING` support (PostgreSQL, SQLite) get changed rows back
directly. MySQL has no `RETURNING`, so the helpers run a follow-up `SELECT` —
which means generated ids, defaults, and trigger-updated values must be
observable from that query for the decode to succeed.

### Batched repository resolvers

When the same lookups happen across concurrent fibers (e.g. resolving fields in a
GraphQL/HTTP API), `SqlModel.makeResolvers` returns the operations as
[`RequestResolver`s](https://effect.plants.sh/batching/) that coalesce concurrent requests into a single
batched query. It requires `SqlClient | Scope`.

```ts
import { Effect } from "effect"
import { SqlModel, SqlResolver } from "effect/unstable/sql"
import { Person, PersonId } from "./Person.js"

const program = Effect.gen(function*() {
  // makeResolvers returns { insert, insertVoid, findById, delete }
  const resolvers = yield* SqlModel.makeResolvers(Person, {
    tableName: "people",
    spanPrefix: "People",
    idColumn: "id"
  })

  // `SqlResolver.request` wraps the payload in a `SqlRequest` and runs it
  // through the resolver. Two concurrent findById requests coalesce into one
  // `select * from people where id in (1, 2)`.
  const [a, b] = yield* Effect.all(
    [
      SqlResolver.request(PersonId.make(1), resolvers.findById),
      SqlResolver.request(PersonId.make(2), resolvers.findById)
    ],
    { batching: true }
  )
})
```

The resolver record contains `insert` (batched insert returning rows),
`insertVoid` (batched insert, no rows), `findById` (batched `WHERE id IN (...)`,
failing `NoSuchElementError` per missing id), and `delete` (batched delete, or
soft delete when `softDeleteColumn` is set). For the lower-level query and
schema-binding primitives these resolvers build on, see [Queries](https://effect.plants.sh/sql/queries/);
for the request/resolver model itself, see [Batching](https://effect.plants.sh/batching/).

## Migrations

A migrator runs numbered migration effects exactly once. It ensures a migrations
table exists (default `effect_sql_migrations`), reads the highest applied id, and
runs every pending migration in order inside a transaction, recording each id so
it never runs twice. Each migration is an `Effect` that uses the ambient
`SqlClient`.

1. **Write migration files** named `<id>_<name>.ts`, each default-exporting an
   `Effect` that creates or alters tables. (`.js`, `.mjs`, and `.mts` extensions
   are also recognized.)

   ```ts
   // migrations/0001_create_people.ts
   import { Effect } from "effect"
   import { SqlClient } from "effect/unstable/sql"

   // The default export is the migration Effect. It runs with the SqlClient
   // already in context, inside the migrator's transaction.
   export default Effect.gen(function*() {
     const sql = yield* SqlClient.SqlClient
     yield* sql`
       CREATE TABLE people (
         id SERIAL PRIMARY KEY,
         name TEXT NOT NULL,
         email TEXT NOT NULL UNIQUE,
         created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
         updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
       )
     `
   })
   ```

2. **Provide a migrator layer.** The driver's `Migrator` re-exports the loaders
   and adds a `layer` that runs pending migrations during layer construction.

   ```ts
   import { Config, Layer } from "effect"
   import { PgClient, PgMigrator } from "@effect/sql-pg"
   import { NodeServices } from "@effect/platform-node"

   const PgLive = PgClient.layerConfig({
     url: Config.redacted("DATABASE_URL")
   })

   // `fromFileSystem` discovers <id>_<name>.ts files at runtime. Other loaders:
   // `fromGlob` (bundler import map) and `fromRecord` (in-memory, great for tests).
   const MigratorLive = PgMigrator.layer({
     loader: PgMigrator.fromFileSystem("migrations"),
     // Optional: dump the schema after successful migrations.
     schemaDirectory: "migrations"
   }).pipe(
     Layer.provide(PgLive),
     // fromFileSystem + schema dump need FileSystem, Path, and a process spawner,
     // which NodeServices.layer provides together.
     Layer.provide(NodeServices.layer)
   )
   ```

Because `MigratorLive` runs migrations as a side effect of building its layer,
provide it once at application startup (typically merged with your other layers)
and pending migrations apply before the rest of the program starts. Migration
ids must be unique; only ids greater than the latest recorded id are considered
pending, so editing an already-applied migration will not re-run it — add a new
one instead.

### Migrator API

The core lives in `effect/unstable/sql`'s `Migrator` module; each driver package
(`@effect/sql-pg`, `@effect/sql-sqlite-node`, …) wires it to a dialect and adds a
`layer`. The migrator returns the list of applied `[id, name]` pairs.

#### `Migrator.make`

The constructor factory. `make({ dumpSchema? })` returns a function taking
`MigratorOptions` — `{ loader, schemaDirectory?, table? }` — and yields an
`Effect` that applies pending migrations. `table` defaults to
`effect_sql_migrations`. Drivers call this for you; use it directly only when
building a custom migrator.

```ts
import { Migrator } from "effect/unstable/sql"

// A migrator with no schema-dump hook.
const migrate = Migrator.make({})({
  loader: Migrator.fromRecord({}),
  table: "my_migrations"
})
// => Effect<ReadonlyArray<[id, name]>, MigrationError | SqlError, SqlClient>
```

#### `Migrator.fromFileSystem`

Loader that reads a directory via `FileSystem`, importing files matching
`<id>_<name>.(js|ts|mjs|mts)`, sorted by id. Returns `Loader<FileSystem>`.

```ts
import { Migrator } from "effect/unstable/sql"

const loader = Migrator.fromFileSystem("migrations")
// => Loader<FileSystem>  (needs a FileSystem layer, e.g. NodeServices.layer)
```

#### `Migrator.fromGlob`

Loader built from a bundler glob record of dynamic `import` functions (keys are
file paths), parsed and sorted by id. No `FileSystem` requirement — the bundler
inlines the imports.

```ts
import { Migrator } from "effect/unstable/sql"

const loader = Migrator.fromGlob(
  import.meta.glob("./migrations/*.ts") as Record<string, () => Promise<any>>
)
// => Loader
```

#### `Migrator.fromBabelGlob`

Loader for Babel-style glob records, where keys look like `_0001_initTs`. The
values are the already-imported modules (not const loader = Migrator.fromBabelGlob(babelGeneratedMigrations)
// => Loader
```

#### `Migrator.fromRecord`

In-memory loader: a record keyed by `<id>_<name>` whose values are migration
Effects. Ideal for tests — no filesystem, no bundler.

```ts
const loader = Migrator.fromRecord({
  "0001_init": Effect.gen(function*() {
    const sql = yield* SqlClient.SqlClient
    yield* sql`CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT)`
  })
})
// => Loader  (pair with an in-memory SQLite layer for fast isolated runs)
```

#### Types

- `MigratorOptions<R>` — `{ loader: Loader<R>; schemaDirectory?: string; table?: string }`.
- `Loader<R>` — `Effect<ReadonlyArray<ResolvedMigration>, MigrationError, R>`.
- `ResolvedMigration` — the tuple `[id: number, name: string, load: Effect<…, …, SqlClient>]`.
- `Migration` — recorded metadata `{ id: number; name: string; createdAt: Date }`.

#### `Migrator.MigrationError`

A `Data.TaggedError("MigrationError")` raised while loading, validating, locking,
or running migrations. Its `kind` is one of:

```ts
// "BadState"    — migrations table in an inconsistent state
// "ImportError" — a file had no default export, or it was not an Effect
// "Failed"      — a migration Effect threw / a directory could not be read
// "Duplicates"  — two migrations share the same id
// "Locked"      — another runner holds the migration lock
```
**Loaders for tests:** For unit tests, `Migrator.fromRecord({ "0001_init": initEffect })` supplies
migrations directly in memory, with no filesystem access — pair it with an
in-memory SQLite layer (`@effect/sql-sqlite-node`) for fast, isolated runs. See
[Testing](https://effect.plants.sh/testing/).

On PostgreSQL the migrations table is explicitly `LOCK`ed (`ACCESS EXCLUSIVE`)
during a run, so concurrent deployers coordinate safely and a blocked runner logs
`Migrations already running` rather than failing. Other dialects rely on the
migrations table's primary-key constraint: a duplicate insert from a second
runner surfaces as a `MigrationError` with `kind: "Locked"`.

For the query-building primitives these models and migrations are built on, see
[Queries](https://effect.plants.sh/sql/queries/); for the section overview, see [SQL](https://effect.plants.sh/sql/).
## Field helpers reference

Every helper below is exported from `effect/unstable/schema`'s `Model` module.
Each one declares which variants a property contributes to.

### `Model.Field`

Builds a variant field from a record of per-variant schemas. This is the
primitive every other helper is built on — supply only the variants you want the
property to appear in.

```ts
// Present in select + json, absent from insert/update/jsonCreate/jsonUpdate.
const id = Model.Field({
  select: Schema.Number,
  json: Schema.Number
})
// => a VariantSchema.Field usable as a model field
```

### `Model.FieldExcept(variants)`

Applies a single schema to every variant *except* the listed ones. Ideal for a
database-assigned id that you still update by.

```ts
// Present everywhere except insert (the DB assigns it on insert).
const id = Schema.Number.pipe(Model.FieldExcept(["insert"]))
// => select/update/json/jsonCreate/jsonUpdate have id; insert does not
```

### `Model.FieldOnly(variants)`

The inverse of `FieldExcept`: applies a schema *only* to the listed variants.

```ts
// Only appears in the json read variant.
const etag = Schema.String.pipe(Model.FieldOnly(["json"]))
// => json has etag; all other variants omit it
```

### `Model.FieldOption`

Makes a field optional in *all* variants. Database variants accept `null` and
decode to `Option`; JSON variants additionally allow the key to be missing.

```ts
const bio = Model.FieldOption(Schema.String)
// db variants:   Option.none() <- null;  Option.some("...") <- "..."
// json variants: also tolerate a missing key, decoding to Option.none()
```

### `Model.fieldEvolve`

Transforms the schemas inside an existing field (or plain schema) per variant —
the building block `FieldOption` itself uses.

```ts
// Make only the insert variant of a field nullable.
const evolve = Model.fieldEvolve({
  insert: (s: Schema.String) => Schema.NullOr(s)
})
// => a function that rewrites the named variants of a field
```

### `Model.fields`

Returns the raw per-variant field definitions stored on a model or variant
struct — useful for reflection or for re-composing fields.

```ts
const defs = Model.fields(Group)
// => { id, name, createdAt, updatedAt } field configs
```

### `Model.extract`

Pulls a single generated variant schema out of a model or variant struct as a
standalone `Schema.Struct`.

```ts
// The insert struct schema, equivalent to Group.insert.
const insertSchema = Model.extract(Group, "insert")
// => Schema.Struct of the insert variant
```

### `Model.Struct`

Builds a variant struct from field definitions *without* the `Class` machinery —
when you want the variants but not a class.

```ts
const Point = Model.Struct({
  x: Schema.Number,
  y: Schema.Number
})
Point.insert // => Schema.Struct
```

### `Model.Union`

Creates a union over the default and generated variants of several variant
structs.

```ts
const Shape = Model.Union(Point, Group)
// => a variant union; Shape.insert is a union of each member's insert variant
```

### `Model.Override`

Marks a value as an explicit override for a field that otherwise carries an
overrideable default (e.g. the timestamp helpers). Use it inside a constructor
call to supply the value yourself instead of letting the default fire.

```ts
// Bypass the "now" default and pin an exact timestamp.
Group.insert.make({
  name: "Engineering",
  createdAt: Model.Override(new Date("2024-01-01T00:00:00Z")) as any
})
```

### `Model.GeneratedByDb`

A column the **database** generates (serial id, default). Contributes to
`select` and `json` only, so it is absent from insert *and* update.

```ts
const id = Model.GeneratedByDb(Schema.Number)
// select: id present | insert: absent | update: absent | json: present
```
**Not for an id you update by:** `GeneratedByDb` omits the column from the `update` variant. If you need the id in
`update` (the usual case for `makeRepository`'s `idColumn`), use
`Schema.…pipe(Model.FieldExcept(["insert"]))` instead, which keeps it in
`select` and `update` while omitting it from `insert`.

### `Model.GeneratedByApp`

A value the **application** generates before insert (e.g. a client-side UUID).
Contributes to `select`, `insert`, `update`, and `json`, but is omitted from the
JSON create/update payloads.

```ts
const id = Model.GeneratedByApp(Schema.String)
// select/insert/update/json: present | jsonCreate/jsonUpdate: absent
```

### `Model.Sensitive`

A value present in the database variants but hidden from *all* JSON variants —
for password hashes, tokens, etc.

```ts
const passwordHash = Model.Sensitive(Schema.String)
// select/insert/update: present | json/jsonCreate/jsonUpdate: absent
```

### `Model.optionalOption`

A standalone schema (not a field) for an optional key whose encoded value may be
missing or `null` and whose decoded value is an `Option`. `FieldOption` uses this
for its JSON variants.

```ts
const nickname = Model.optionalOption(Schema.String)
// decode: missing/null -> Option.none(); "x" -> Option.some("x")
// encode: Option.none() -> omitted/null
```

## Date and time helpers

These helpers store `DateTime.Utc` values and differ in their encoded
representation (string / JS `Date` / milliseconds) and in whether they auto-fill
on insert, on update, or not at all.

### `Model.Date`

A `DateTime.Utc` *date-only* value encoded as a `YYYY-MM-DD` string. The time
component is removed on decode.

```ts
const birthday = Model.Date
// encode: DateTime.Utc -> "2024-05-30"  | decode: "2024-05-30" -> DateTime.Utc
```

### `Model.DateWithNow`

Like `Model.Date`, but overrideable with a constructor default of *today* (time
removed).

```ts
const day = Model.DateWithNow
// constructor default => DateTime.removeTime(DateTime.now)
```

### `Model.DateTimeWithNow`

An overrideable `DateTime.Utc` encoded as an ISO string, defaulting to the
current time.

```ts
const ts = Model.DateTimeWithNow
// default => DateTime.now ; encoded as "2024-05-30T12:00:00.000Z"
```

### `Model.DateTimeFromDateWithNow`

As above, but encoded as a JavaScript `Date` (for drivers that pass `Date`
objects), defaulting to the current time.

```ts
const ts = Model.DateTimeFromDateWithNow
// encoded representation: JS Date ; default => DateTime.now
```

### `Model.DateTimeFromNumberWithNow`

As above, but encoded as a number of milliseconds since the epoch, defaulting to
the current time.

```ts
const ts = Model.DateTimeFromNumberWithNow
// encoded representation: number (ms) ; default => DateTime.now
```

### `Model.DateTimeInsert` / `…FromDate` / `…FromNumber`

Auto-timestamp **insert** fields: defaulted to the current time on insert,
selectable, and *omitted from updates*. The three variants differ only in the
database encoding (string / `Date` / number); JSON encodes as a string for the
first two and a number for `FromNumber`.

```ts
Model.DateTimeInsert           // db: string  | insert default = now | no update
Model.DateTimeInsertFromDate   // db: JS Date | insert default = now | no update
Model.DateTimeInsertFromNumber // db: number  | insert default = now | no update
```

### `Model.DateTimeUpdate` / `…FromDate` / `…FromNumber`

Auto-timestamp **update** fields: defaulted to the current time on *both* inserts
and updates, and selectable. Same encoding split as the insert helpers.

```ts
Model.DateTimeUpdate           // db: string  | default = now on insert + update
Model.DateTimeUpdateFromDate   // db: JS Date | default = now on insert + update
Model.DateTimeUpdateFromNumber // db: number  | default = now on insert + update
```

## Column representation helpers

### `Model.JsonFromString`

Stores a structured value as **text** in the database (parsed/serialized via
JSON) while exposing the object schema directly in the JSON variants.

```ts
const Prefs = Schema.Struct({ theme: Schema.String, beta: Schema.Boolean })
const preferences = Model.JsonFromString(Prefs)
// db variants:   '{"theme":"dark","beta":true}'  <-> { theme, beta }
// json variants: { theme, beta } used directly (no string wrapping)
```

### `Model.BooleanSqlite`

A boolean stored as SQLite's `0 | 1` bit in the database variants but exposed as
a real `boolean` in the JSON variants.

```ts
const active = Model.BooleanSqlite
// db: 1 <-> true, 0 <-> false  | json: true / false directly
```

### `Model.Uint8Array`

A schema for binary `Uint8Array` values backed by an `ArrayBuffer` — the building
block for the binary UUID helpers below.

```ts
const bytes = Model.Uint8Array
// => Schema.instanceOf<Uint8Array<ArrayBuffer>>
```

## UUID helpers

These build `insert` fields that generate a UUID by default while keeping the
column selectable and updatable. Pass a *branded* schema so ids stay distinct.

### `Model.UuidV4Insert` / `Model.UuidV4WithGenerate`

A branded **string** UUID v4. `UuidV4Insert` is the field (default-generated on
insert); `UuidV4WithGenerate` adds the constructor default to an existing branded
string schema.

```ts
const UserId = Schema.String.pipe(Schema.brand("UserId"))
const id = Model.UuidV4Insert(UserId)
// select/update/json: branded string | insert: auto-generated UUID v4
```

### `Model.UuidV7Insert` / `Model.UuidV7WithGenerate`

A branded **string** UUID v7 (time-ordered, sortable) generated from the current
`Clock` on insert.

```ts
const EventId = Schema.String.pipe(Schema.brand("EventId"))
const id = Model.UuidV7Insert(EventId)
// insert: auto-generated UUID v7 (uses Clock for the timestamp)
```

### `Model.UuidV4BytesInsert` / `Model.UuidV4BytesWithGenerate`

A branded **binary** (`Uint8Array`) UUID v4 — compact 16-byte storage.
`UuidV4BytesInsert` is the field; `UuidV4BytesWithGenerate` adds the generating
default to a branded `Uint8Array` schema.

```ts
const KeyId = Model.Uint8Array.pipe(Schema.brand("KeyId"))
const id = Model.UuidV4BytesInsert(KeyId)
// insert: auto-generated 16-byte UUID v4 | stored as binary
```