# Your schema

Type your Postgres tables for the kysely plugin by declaring a schema on KyselyDatabase. Covers the column helpers, what an undeclared schema lets through, and keeping the schema in step with your migrations.

TypeScript can't read your tables out of Postgres while it compiles, so Kysely types every query against an interface you write by hand. To give the plugin that interface, declare `schema` on [`KyselyDatabase`](https://docs.seedcord.org/packages/plugin-kysely-postgres/latest/kysely-database). The plugin's [`connection`](https://docs.seedcord.org/packages/plugin-kysely-postgres/latest/kysely-postgres#connection) and every service get their table and column types from it.

```ts title="src/database.ts"
import type { ColumnType, Generated } from 'kysely';

export interface UsersTable {
    id: Generated<number>;
    discord_id: string;
    username: string;
    joined_at: ColumnType<Date, string | undefined, never>;
}

export interface AppDatabase {
    users: UsersTable;
}

declare module '@seedcord/plugin-kysely-postgres' {
    interface KyselyDatabase {
        schema: AppDatabase;
    }
}
```

TypeScript applies that `declare module` block from any file your tsconfig includes, so nothing needs to import `src/database.ts`.

`AppDatabase` takes one entry per table, keyed by the table's name in Postgres.

Most columns have one type whether you select, insert, or update them, like `username`. For the ones that don't, Kysely has two helpers. `Generated` is for a column Postgres fills in itself, like a serial or identity `id`. `ColumnType` takes three types, one each for select, insert, and update. Here's what `UsersTable` ends up allowing:

{/* prettier-ignore-start */}

| column      | select returns | insert takes              | update takes                                          |
| ----------- | -------------- | ------------------------- | ----------------------------------------------------- |
| `username`  | `string`       | `string`                  | `string`                                              |
| `id`        | `number`       | `number`, or leave it out | `number`                                              |
| `joined_at` | `Date`         | `string`, or leave it out | `never`, so TypeScript rejects an update that sets it |

{/* prettier-ignore-end */}

## Every table name compiles until you declare it

With `KyselyDatabase` left empty, [`KyselySchema`](https://docs.seedcord.org/packages/plugin-kysely-postgres/latest/kysely-schema) falls back to `Record<string, never>`. That widens [`KyselyTable`](https://docs.seedcord.org/packages/plugin-kysely-postgres/latest/kysely-table) to plain `string`.

```ts
import type { KyselyTable } from '@seedcord/plugin-kysely-postgres';

declare const table: KyselyTable;
```

A typo like `selectFrom('user')` then compiles. Postgres rejects the query because no `user` table exists, so you only find the typo when someone runs that command and it fails for them. A misspelled column name compiles the same way. Every column you select is typed `never`. TypeScript accepts a `never` value anywhere, so it can't report a column used as the wrong type either.

## What the declaration catches

Once you declare the schema, that typo fails to compile.

```ts
await this.core.db.connection
    .selectFrom('user')
    .selectAll()
    .execute();
```

`'user'` is missing its `s`. TypeScript reports it before the query can reach Postgres. With the name spelled `users`, the same query compiles.

```ts
const row = await this.core.db.connection
    .selectFrom('users')
    .select(['id', 'username'])
    .executeTakeFirst();

await this.reply(row ? row.username : 'No record.');
```

`row` has the shape of a `users` row, with `id` typed as `number` since the select unwraps its `Generated`. `executeTakeFirst()` returns `undefined` when no row matches, so the reply checks `row` before it reads `username`.

## Nothing checks this against your database

You write this interface by hand, separately from the migrations that create the tables. When a migration adds a column, your queries can't select it until you add it to this interface too. Every schema change is an edit in two places. If you misspell a column here, the query still compiles, though it fails when it runs.

[`kysely-codegen`](https://github.com/RobinBlomberg/kysely-codegen) saves you the second edit. It connects to your database, reads the tables your migrations created, and writes a file that exports an interface named `DB`.

```sh
pnpm add -D kysely-codegen
```

```sh
pnpm exec kysely-codegen --out-file src/db.d.ts
```

The command reads the connection string from `DATABASE_URL`, either in your environment or in a `.env` file. Point `schema` at the generated type, and delete the interfaces you wrote by hand.

```ts title="src/database.ts"
import type { DB } from './db';

declare module '@seedcord/plugin-kysely-postgres' {
    interface KyselyDatabase {
        schema: DB;
    }
}
```

The generated file only changes when you run the command again, so run it after every migration.

To keep each table's queries in one class, write [services](/plugins/kysely-services). [Migrations](/plugins/kysely-migrations) create the tables this interface describes.
