Skip to content

Drizzle ​

@typesql/drizzle converts your Drizzle schema module into a TypeSQL schema and runs queries on the driver client under your Drizzle database. The conversion happens only at the type level: there is no code generation and no database connection.

sh
pnpm add @typesql/core @typesql/drizzle drizzle-orm

Pass your schema module as it is. Tables, views, enums and schemas are converted; relations and any other exports are ignored:

ts
// schema.ts: your Drizzle schema
import { relations, sql } from "drizzle-orm";
import { integer, pgEnum, pgSchema, pgTable, pgView, serial, text, timestamp, uuid } from "drizzle-orm/pg-core";

export const mood = pgEnum("mood", ["happy", "sad"]);

export const users = pgTable("users", {
  id: serial().primaryKey(),
  email: text().notNull(),
  displayName: text("display_name"),
  mood: mood(),
});

export const posts = pgTable("posts", {
  id: serial().primaryKey(),
  authorId: integer("author_id").notNull().references(() => users.id),
  title: text().notNull(),
  slug: text().notNull().$defaultFn(() => crypto.randomUUID()),
  createdAt: timestamp("created_at").notNull().defaultNow(),
});

export const auth = pgSchema("auth");
export const sessions = auth.table("sessions", {
  id: uuid().primaryKey().defaultRandom(),
  userId: integer("user_id").notNull(),
});

export const happyUsers = pgView("happy_users").as((qb) =>
  qb.select({ id: users.id, email: users.email }).from(users).where(sql`${users.mood} = 'happy'`),
);

export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts) }));
ts
import { createSql } from "@typesql/drizzle";
import type * as schema from "./orm-drizzle-schema.js";

const sql = createSql<typeof schema>(db); // db: your Drizzle database (or a transaction)

const q = sql(
  `SELECT u.email, u.display_name, p.title
   FROM users u JOIN posts p ON p.author_id = u.id
   WHERE u.mood = $1`,
  "happy",
);
// Query<{ email: string; display_name: string | null; title: string }, ["happy" | "sad"]>

sql("SELECT h.email FROM happy_users h JOIN auth.sessions s ON s.user_id = h.id"); // views, pgSchema
sql("INSERT INTO posts (author_id, title, slug) VALUES ($1, $2, $3) RETURNING id, created_at", 1, "Hello", "hello");
// id and created_at have SQL defaults; slug's $defaultFn runs in Drizzle, so raw SQL must set it

SQL uses the database names (display_name), not the TypeScript keys (displayName).

DrizzleTypeSQL
pgTable("users", …)table users, keyed by the SQL name
.notNull(), .primaryKey(), serial, identityNOT NULL
.default(), .defaultNow(), .defaultRandom(), serial, identity, generatedAlwaysAs()has a default
.$defaultFn(), .$default()no default: these run in Drizzle, not in Postgres
pgEnum exports$types, under the export name
pgSchema("auth").table("sessions", …)auth.sessions
pgView, pgMaterializedViewread-only views

The Postgres type comes from the column kind (integer → int4, doublePrecision → float8, uuid → uuid, .array() → [], …). Rows are typed by the type map, not by Drizzle's mode: TypeSQL queries go straight to the driver, so a bigint({ mode: "number" }) column is int8, which node-postgres returns as a string.

Types Drizzle does not record ​

Drizzle's types do not record withTimezone, so timestamp() columns are timestamp and time() columns are time. Pin a column with pgType, which returns the builder unchanged at runtime and also works for customType columns:

ts
import { pgType } from "@typesql/drizzle";

export const events = pgTable("events", {
  at: pgType(timestamp("at", { withTimezone: true }).notNull(), "timestamptz"),
});

Or set the default for all columns. The same options take Drizzle's casing setting:

ts
createSql<typeof schema, DefaultTypeMap, {
  casing: "snake_case"; // drizzle(client, { casing: "snake_case" })
  timestamp: "timestamptz"; // type of timestamp() columns not pinned with pgType
}>(db);

Enums are typed by finding the exported pgEnum with the same values. Name the export after the SQL type (export const mood = pgEnum("mood", …)) so casts like 'happy'::mood resolve. If an enum isn't exported, its columns are text.

Running queries ​

createSql<typeof schema>(db) runs the queries on the driver client under db: the Pool, Client or postgres.js sql you passed to drizzle(), or inside db.transaction(fn) the transaction's connection. Rows are what the driver returns; Drizzle's column mappers are not involved. sql.transaction(fn) runs in db.transaction(fn). Pass the driver's type map as the second type argument:

ts
import type { PgTypeMap } from "@typesql/pg";

const pgSql = createSql<typeof schema, PgTypeMap>(db); // node-postgres' exact types
const rows = await pgSql("SELECT id, title FROM posts WHERE author_id = $1", 1).execute();
// { id: number; title: string }[]

await pgSql.transaction(async (tx) => {
  await tx("UPDATE posts SET title = upper(title) WHERE author_id = $1", 1).execute();
});

// inside a Drizzle transaction, bind a sql to it to mix ORM and SQL queries
await db.transaction(async (tx) => {
  await createSql<typeof schema, PgTypeMap>(tx)("DELETE FROM posts WHERE author_id = $1", 1).execute();
});

Only node-postgres and postgres.js databases are supported. drizzle-orm/postgres-js turns off postgres.js' date parsers on the client it is given, so on that driver date, timestamp, timestamptz and interval come back as strings: use a type map that says so. Queries do not go through Drizzle's logger.

Limits ​

  • Generated-always columns count as having a default. Postgres rejects an INSERT that sets one, but TypeSQL doesn't.
  • $onUpdate columns look like they have a default, so INSERT doesn't require them.
  • .$type<T>() is ignored; rows use the type map (jsonb().$type<T>() is unknown).
  • text({ enum: [...] }) is plain text: in Drizzle, those values are only a TypeScript constraint.