tsql
.md

Schema & Migrations

defineDatabase is the schema entrypoint.

import { defineDatabase } from "@bms/tsql";

export const schema = defineDatabase((db) =>
	db
		.migration("001_create_users", (migration) =>
			migration
				.createTable("users", (t) => ({
					id: t.integer().primaryKey().autoIncrement(),
					email: t.text().notNull(),
					createdAt: t.datetime().notNull(),
				}))
				.createIndex("idx_users_email", "users", ["email"]),
		)
		.migration("002_create_posts", (migration) =>
			migration.createTable("posts", (t) => ({
				id: t.integer().primaryKey().autoIncrement(),
				authorId: t.integer().notNull().references("users.id"),
				title: t.text().notNull(),
				body: t.text(),
			})),
		)
		.migration("003_normalize_user_emails", (migration) =>
			migration
				.update("users")
				.set(({ users }) => ({
					email: users.email.trim().lower(),
				}))
				.execute(),
		),
);

DatabaseDefinitionBuilder.migration(name, run)

Schema methods in migration callback

createTable options

createTable("users", buildColumns, { strict: true })

strict requires a dialect with supportsStrictTables = true.

Column types and modifiers

Column builders:

t.datetime() maps to Date in TypeScript query/mutation types. The active dialect controls runtime conversion at the driver boundary (for example SQLite stores datetime as text and parses ISO values back into Date).

set type

t.set<T>() maps to Set<T> in TypeScript.

const schema = defineDatabase((db) =>
	db.migration("001_create_groups", (migration) =>
		migration.createTable("groups", (t) => ({
			id: t.integer().primaryKey(),
			memberIds: t.set<number>().notNull(),
		})),
	),
);

await db.insert("groups").values({
	id: 1,
	memberIds: new Set([1, 2, 3]),
}).execute();

For SQLite, sets are stored as JSON array text and parsed back to Set when reading typed select results.

Modifiers:

What this creates

defineDatabase(...) returns a Schema<TShape> object with:

No SQL runs during definition. Execution happens only through db.migrate().