> Complete content index: https://sirannon.sondelali.com/llms.txt

# Code generation

> Emit typed operation references from the registry your server is built from, so a client calls each read and write by name with the argument and row types the server serves.

URL: https://sirannon.sondelali.com/docs/code-generation
Section: Networked access
Version: 0.2 (@delali/sirannon-db@0.2.2, latest)

Writing an `operationRef` by hand works until someone renames an argument, and then it points at a name the server no longer serves. The `sirannon-codegen` binary reads the [registry](/docs/registered-operations) your server is built from and writes the references your client calls it through, so the names, the arguments, and the columns come from the definitions the server runs. It imports a module rather than talking to a server, so it runs in continuous integration with nothing else up.

## Run the generator

Installing the package installs the binary, so `pnpm exec` reaches it in any workspace that depends on `@delali/sirannon-db`.

```bash
pnpm exec sirannon-codegen --registry ./src/operations.ts --out ./src/generated/operations.ts
```

Flags come in pairs, and passing one without a value prints the usage text and exits non-zero.

| Flag | Required | What it does |
| --- | --- | --- |
| `--registry` | Yes | The module holding the registry. The generator imports it, so the runtime has to be able to load that file. |
| `--out` | Yes | Where to write the generated TypeScript. Missing directories are created. |
| `--manifest` | No | Also write the manifest as JSON, which is useful for diffing a registry between deploys. |
| `--export` | No | Read a named export other than `operations`. Without it the generator reads `operations`, then a default export. |
| `--package` | No | Import `OperationRef` from a package name other than `@delali/sirannon-db`, for a workspace that re-exports it. |

The generator imports the registry module as it stands, so how you invoke it depends on what your runtime can load. A recent Node strips TypeScript types on its own, and the command above then reads a `.ts` registry with no extra setup. Put a loader in front of it when your runtime does not, or when the registry uses TypeScript syntax that cannot be erased, such as an `enum` or a parameter property.

```bash
pnpm exec tsx node_modules/@delali/sirannon-db/dist/codegen/cli.mjs --registry ./src/operations.ts --out ./src/generated/operations.ts
```

## What it emits

Running the generator against the two reads and one write from the [registered operations guide](/docs/registered-operations) writes this file.

```ts title="src/generated/operations.ts"

export const registryDigest = "40c2fd904f25bbc10e88bf0ad329c1a073b4201042b02399da7e7c8c4d12009a"

export interface OrdersMyOrdersRow {
  id: unknown
  total: unknown
  status: unknown
}

export interface OrdersOrdersByStatusRow {
  id: unknown
  total: unknown
  status: unknown
}

export const orders = {
  reads: {
    myOrders: { name: "myOrders" } as OperationRef<Record<string, never>, OrdersMyOrdersRow>,
    ordersByStatus: { name: "ordersByStatus" } as OperationRef<{ status: unknown }, OrdersOrdersByStatusRow>,
  },
  writes: {
    placeOrder: { name: "placeOrder" } as OperationRef<{ total: unknown }, never>,
  },
}
```

Four things are worth reading off that output:

- One exported constant per database, named after the database identifier, holding a `reads` map and a `writes` map. Import it and every operation is a property rather than a string.
- One row interface per read that declared `columns`, named from the database and the operation. A read that declared none, and takes arguments, leaves its row shape open instead.
- `myOrders` takes `Record<string, never>`, because every value it needs comes from `fromIdentity`. The call site passes `{}` and TypeScript rejects anything else.
- Each property is typed `unknown`. The registry declares which columns exist, not what SQLite stores in them, so the generator will not invent a type it cannot prove. Narrow at the edge of your application, the same way you would narrow a parsed JSON payload.

`registryDigest` records the digest the registry had at the moment you generated the file. Compare it against the `registry.digest` in `GET /capabilities` to catch a client built against a registry the server no longer runs, and let a [live query](/docs/live-queries) echo it so the server refuses a stale subscription with `REGISTRY_MISMATCH`.

## Call through the generated file

The references replace every hand-written `operationRef`, and the row types follow the read.

```ts title="client.ts"

const client = new SirannonClient('http://localhost:9876', {
  transport: 'websocket',
  headers: { Authorization: 'Bearer session-amara' },
})

const db = client.database('orders')

const pending = await db.query(orders.reads.ordersByStatus, { status: 'pending' })
const mine = await db.query(orders.reads.myOrders, {})

await db.execute(orders.writes.placeOrder, { total: 4999 })
```

After a rename in the registry and a regeneration, every call site still passing the old name fails to compile. That is what generating the references buys you over writing them by hand.

## Catch a stale generated file in CI

The generator needs no running server, so a check that regenerates and compares is cheap:

```bash
pnpm exec sirannon-codegen --registry ./src/operations.ts --out ./src/generated/operations.ts
git diff --exit-code src/generated/operations.ts
```

A non-empty diff means someone changed the registry without regenerating, which is exactly the change that would otherwise reach production as a runtime `UNKNOWN_QUERY` or `MISSING_ARGUMENT`. Committing the generated file also keeps the digest visible in review, so a rename shows up as a contract change rather than a line of SQL.

Two names that generate the same identifier stop the run with an error naming both, rather than emitting a file that declares one of them twice. Rename one of the operations to clear it.
