Skip to content

Drizzle adapter

@smonn/ids/drizzle provides Drizzle custom column types bound to a codec. It requires drizzle-orm as an optional peer dependency.

Terminal window
pnpm add drizzle-orm
import { pgTable } from "drizzle-orm/pg-core";
import { idColumn } from "@smonn/ids/drizzle";
import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");
export const users = pgTable("users", {
id: idColumn(usr).primaryKey(),
});
// users.id is typed as Id<"usr"> end-to-end
import { mysqlTable } from "drizzle-orm/mysql-core";
import { idColumnMysql } from "@smonn/ids/drizzle";
import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");
export const users = mysqlTable("users", {
id: idColumnMysql(usr).primaryKey(),
});
// users.id is typed as Id<"usr"> end-to-end
import { sqliteTable } from "drizzle-orm/sqlite-core";
import { idColumnSqlite } from "@smonn/ids/drizzle";
import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");
export const users = sqliteTable("users", {
id: idColumnSqlite(usr).primaryKey(),
});
// users.id is typed as Id<"usr"> end-to-end

All three column builders (idColumn, idColumnMysql, idColumnSqlite) work with any codec variant — any codec that exposes safeParse satisfies the required interface (Timestamp, Opaque Timestamp, Reverse Timestamp, Signed Timestamp, Digest, and Wrapped key codecs all qualify).

  • Write path: values are validated via codec.safeParse before being passed to the driver. A cast-smuggled or otherwise invalid string throws IdsError("invalid_id") at write time. Passing null or undefined also throws — use nullableIdColumn for nullable columns.
  • Read path: values are normalised via codec.safeParse() rather than the strict is(). Data at rest should already be canonical (ADR-0003), but safeParse is a safe boundary for stale non-canonical values. An unrecognised value throws at read time so corrupt data surfaces immediately.
  • Column type: dataType() returns "text" by default; pass { columnType: "..." } as the second argument to idColumn to override (e.g. idColumn(usr, { columnType: "varchar(30)" })).

Auto-generating IDs on insert — generatedIdColumn family

Section titled “Auto-generating IDs on insert — generatedIdColumn family”

The generatedIdColumn, generatedIdColumnMysql, and generatedIdColumnSqlite column builders wire .$defaultFn(() => codec.generate()) so inserts that omit the ID field receive a freshly generated Id<Brand> automatically — no per-call-site id needed:

import { pgTable } from "drizzle-orm/pg-core";
import { generatedIdColumn } from "@smonn/ids/drizzle";
import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");
export const users = pgTable("users", {
id: generatedIdColumn(usr).primaryKey(),
});
// id is auto-filled on insert — await db.insert(users).values({ name: "Alice" }) works

For MySQL and SQLite use generatedIdColumnMysql and generatedIdColumnSqlite respectively — they behave identically but target those dialects’ column types.

These builders require IdGeneratingCodec — a codec that exposes a synchronous generate(). Only the Timestamp codec and Reverse Timestamp codec qualify; Opaque, Signed, Wrapped, and Digest codecs are a compile-time error.

nullableIdColumn(codec) is a PostgreSQL-only variant that normalises null and undefined driver values to null rather than throwing. Use it for optional foreign keys and LEFT JOIN results.

import { pgTable } from "drizzle-orm/pg-core";
import { nullableIdColumn } from "@smonn/ids/drizzle";
import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");
// default: text column
export const posts = pgTable("posts", {
authorId: nullableIdColumn(usr),
});
// posts.authorId is Id<"usr"> | null end-to-end
// explicit char column — matches existing DDL
export const comments = pgTable("comments", {
authorId: nullableIdColumn(usr, { columnType: "char(26)" }),
});
  • Read path: null and undefined driver values are returned as null. Non-null values go through codec.safeParse() and throw IdsError("invalid_id") if the stored value does not parse as a valid Id<Brand>.
  • Write path: toDriver normalises null and undefined to null; non-null values are validated via codec.safeParse and an invalid string throws IdsError("invalid_id") at write time.
  • Column type: dataType() returns "text" by default; pass { columnType: "..." } as the second argument to nullableIdColumn to override (e.g. nullableIdColumn(usr, { columnType: "char(26)" })).

The read path throws IdsError with code "invalid_id" when the stored value does not parse as a valid Id<Brand>. The underlying ParseError is attached as err.cause. Catch and narrow using isIdsError:

import { idColumn, isIdsError } from "@smonn/ids/drizzle";
try {
// query that triggers a read through idColumn / idColumnMysql / idColumnSqlite
} catch (err) {
if (isIdsError(err) && err.code === "invalid_id") {
// err.cause is the ParseError returned by safeParse
}
}

IdsError, isIdsError, and IdsErrorCode are re-exported from @smonn/ids/drizzle — no separate import from "@smonn/ids" is needed. For the full list of IdsErrorCode values, see the error-code reference.