Appearance
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-ormPass 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 itSQL uses the database names (display_name), not the TypeScript keys (displayName).
| Drizzle | TypeSQL |
|---|---|
pgTable("users", …) | table users, keyed by the SQL name |
.notNull(), .primaryKey(), serial, identity | NOT 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, pgMaterializedView | read-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
INSERTthat sets one, but TypeSQL doesn't. $onUpdatecolumns look like they have a default, soINSERTdoesn't require them..$type<T>()is ignored; rows use the type map (jsonb().$type<T>()isunknown).text({ enum: [...] })is plaintext: in Drizzle, those values are only a TypeScript constraint.