Drizzle / Prisma / TypeORM / Sequelize Compatibility
Drizzle / Prisma / TypeORM / Sequelize Compatibility
Available since: v3.10.0+ (initial Drizzle work) — wire-level parity completed v3.14.10 (2026-04-23)
Build: default — no feature flag required
Endpoints: PostgreSQL wire on 127.0.0.1:5432 (and Unix socket if --pg-socket-dir set)
UVP
TypeScript ORMs are demanding clients: they emit fully-quoted identifiers, every INSERT comes back with RETURNING *, dates are ISO 8601 strings, and LIMIT $1 OFFSET $2 is bound through the extended query protocol on every paginated read. Stock Postgres just works; most “alternative” databases don’t. HeliosDB Nano spent v3.10–v3.14 closing 36 distinct ORM-shaped wire bugs against a real Drizzle + postgres-js app (see BUGS_TIMETRACKER_DRIZZLE_COMPAT.md). Drizzle, Prisma, TypeORM, and Sequelize now run unchanged — same connection string, same migrations, same generated SQL.
Prerequisites
- HeliosDB Nano v3.14.10+ (
heliosdb-nano --version) — earlier versions miss specific Drizzle blockers - Node.js 18+ with
npmorpnpm - About 30 minutes for the full walk-through
1. Start the Server
heliosdb-nano start --memory --auth trustTrust auth keeps the connection setup short. For production set --auth scram-sha-256 --password ... and pass the credentials in the connection URL.
2. Discover What’s Supported — heliosdb_capability_report()
Before pointing an ORM at a fresh server, ask the server what it claims to support:
psql "postgresql://postgres@127.0.0.1:5432/postgres" \ -c "SELECT heliosdb_capability_report();"The function returns a human-readable summary of supported features vs. stock Postgres — SERIAL, GENERATED ALWAYS AS IDENTITY, EXTRACT(EPOCH FROM ...), gen_random_uuid(), nextval/currval/setval, DO $$ ... $$ (plain SQL only — no PL/pgSQL control flow), ON CONFLICT, RETURNING, dollar-quoted strings, multi-statement simple queries, and the version’s identifier-folding rules. Use it as a pre-flight check in CI to detect server downgrades before they bite a migration.
3. Drizzle ORM
Install
npm install drizzle-orm postgresnpm install -D drizzle-kitSchema (src/schema.ts)
import { pgTable, serial, text, timestamp, integer, boolean } from "drizzle-orm/pg-core"
export const users = pgTable("users", { id: serial("id").primaryKey(), email: text("email").notNull().unique(), password: text("password").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),})
export const posts = pgTable("posts", { id: serial("id").primaryKey(), authorId: integer("author_id").notNull().references(() => users.id), title: text("title").notNull(), body: text("body"), published: boolean("published").default(false).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),})Connect and use
import postgres from "postgres"import { drizzle } from "drizzle-orm/postgres-js"import { eq, and, desc } from "drizzle-orm"import * as schema from "./schema"
const sql = postgres("postgres://postgres@localhost:5432/postgres")const db = drizzle(sql, { schema })
// INSERT ... RETURNING — the canonical Drizzle shapeconst [alice] = await db.insert(schema.users) .values({ email: "alice@example.com", password: "hunter2!" }) .returning()
console.log(alice)// { id: 1, email: 'alice@example.com', password: 'hunter2!',// createdAt: 2026-04-26T10:00:00.000Z }
// SELECT with WHERE eq() — emits "WHERE users.email = $1"const [user] = await db.select() .from(schema.users) .where(eq(schema.users.email, "alice@example.com"))
// Pagination with parameterised LIMIT/OFFSETconst page = await db.select() .from(schema.posts) .where(eq(schema.posts.published, true)) .orderBy(desc(schema.posts.createdAt)) .limit(20).offset(40)Wire-level work that makes this run
Every line of the snippet above hit a bug in v3.13 or earlier. The fixes that landed in v3.14.x:
| Drizzle pattern | Bug ID | Fixed in |
|---|---|---|
serial("id").primaryKey() round-trip via extended protocol | B1, B4, B28 | 3.14.0 / 3.14.4 |
GENERATED ALWAYS AS IDENTITY (alternative to SERIAL) | B2 | 3.14.0 |
INSERT INTO ... VALUES (default, $1, default) RETURNING * | B3, B27 | 3.14.0 / 3.14.4 |
EXTRACT(EPOCH FROM created_at) in analytics queries | B5 | 3.14.0 |
CREATE SEQUENCE / nextval() / currval() / setval() | B7, B8 | 3.14.0 |
DO $$ ... $$ blocks (plain SQL only) | B9, B21 | 3.14.0 / 3.14.1 |
Dollar-quoted string literals $$text$$ | B10 | 3.14.0 |
Multi-statement simple queries (;-separated) | B11 | 3.14.0 |
pg_catalog.pg_type connect-time introspection over extended Q | B12, B19 | 3.14.0 / 3.14.1 |
pg_tables / information_schema WHERE filtering | B13, B20 | 3.14.0 / 3.14.1 |
gen_random_uuid() | B15 | 3.14.0 |
Flush (H / 0x48) frontend message | B22 | 3.14.2 |
Scalar subquery in UPDATE ... SET (correlated + uncorrelated) | B23 | 3.14.2 |
DEFAULT <expr> evaluated on omitted columns | B24 | 3.14.3 |
INSERT INTO t DEFAULT VALUES syntax | B25 | 3.14.3 |
NOT NULL enforcement on every INSERT path | B26 | 3.14.3 |
Timestamp wire format (YYYY-MM-DD HH:MM:SS.ffffff) | B30 | 3.14.5 |
Stale result_cache after INSERT … RETURNING | B29 | 3.14.6 |
UPDATE/DELETE WHERE "t"."col" = $1 qualified ref | B31 | 3.14.7 |
timestamp >= '2026-04-23T00:00:00.000Z' ISO string compare | B32 | 3.14.7 |
LIMIT $1 OFFSET $2 parameterised | B33 | 3.14.8 |
UPDATE SET ts_col = $1 auto-cast string→timestamp | B34 | 3.14.8 |
GROUP BY with mixed qualifier styles + DATE keys | B35 | 3.14.9 |
Quoted FK references (REFERENCES "users"("id")) | B36 | 3.14.10 |
If you’re on v3.14.10 or later, every one of these is exercised by tests/drizzle_compat_tests.rs on each release.
drizzle-kit migrations
drizzle-kit generate and drizzle-kit push work unchanged. The generated SQL uses CREATE TABLE, ALTER TABLE, and DROP TABLE only — features the v3.14 line covers. drizzle.config.ts:
import { defineConfig } from "drizzle-kit"export default defineConfig({ schema: "./src/schema.ts", out: "./drizzle", dialect: "postgresql", dbCredentials: { url: "postgres://postgres@localhost:5432/postgres" },})4. Prisma
Install and init
npm install prisma @prisma/clientnpx prisma initschema.prisma
datasource db { provider = "postgresql" url = env("DATABASE_URL")}
generator client { provider = "prisma-client-js"}
model User { id Int @id @default(autoincrement()) email String @unique password String createdAt DateTime @default(now()) posts Post[]}
model Post { id Int @id @default(autoincrement()) authorId Int title String body String? published Boolean @default(false) createdAt DateTime @default(now()) author User @relation(fields: [authorId], references: [id])}DATABASE_URL="postgres://postgres@localhost:5432/postgres" npx prisma migrate dev --name initPrisma’s introspection runs through pg_catalog over the extended protocol — covered by B12/B19. Migrations use CREATE TABLE with GENERATED ALWAYS AS IDENTITY (covered by B2) and FK references with quoted identifiers (covered by B36).
Use the client
import { PrismaClient } from "@prisma/client"const prisma = new PrismaClient()
const alice = await prisma.user.create({ data: { email: "alice@example.com", password: "hunter2!" },})
const posts = await prisma.post.findMany({ where: { published: true, author: { email: "alice@example.com" } }, orderBy: { createdAt: "desc" }, take: 20, skip: 0, include: { author: true },})take and skip map to LIMIT $1 OFFSET $2 — covered by B33.
5. TypeORM
Install
npm install typeorm pg reflect-metadataentities/User.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from "typeorm"
@Entity()export class User { @PrimaryGeneratedColumn() id!: number
@Column({ unique: true }) email!: string
@Column() password!: string
@CreateDateColumn() createdAt!: Date}Connect
import "reflect-metadata"import { DataSource } from "typeorm"import { User } from "./entities/User"
const ds = new DataSource({ type: "postgres", host: "localhost", port: 5432, username: "postgres", database: "postgres", synchronize: true, entities: [User],})
await ds.initialize()const repo = ds.getRepository(User)
const alice = repo.create({ email: "alice@example.com", password: "hunter2!" })await repo.save(alice)
const found = await repo.findOneBy({ email: "alice@example.com" })synchronize: true issues CREATE TABLE with SERIAL (B1) and UNIQUE constraints. Queries flow through the same extended-Q path as Drizzle, so the same fix list applies.
6. Sequelize
Install
npm install sequelize pgUse
import { Sequelize, DataTypes, Model } from "sequelize"
const sequelize = new Sequelize("postgres://postgres@localhost:5432/postgres", { dialect: "postgres", logging: false,})
class User extends Model {}User.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, email: { type: DataTypes.STRING, unique: true, allowNull: false }, password: { type: DataTypes.STRING, allowNull: false }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },}, { sequelize, modelName: "user", timestamps: false })
await sequelize.sync()
const alice = await User.create({ email: "alice@example.com", password: "hunter2!" })const found = await User.findOne({ where: { email: "alice@example.com" } })Sequelize’s pgsql dialect emits the same shapes Drizzle does — INSERT ... RETURNING *, qualified WHERE, parameterised LIMIT. All covered by the v3.14 series.
7. The Server-Side Test Suite
The proof that the bugs above stay fixed lives in tests/drizzle_compat_tests.rs. Every regression case is a test named b<N>_<description>:
b1_serial_returns_idb2_generated_always_as_identityb3_default_keyword_in_valuesb4_returning_field_countb5_extract_epoch_from_timestampb7_create_sequenceb8_nextval_currval_setvalb9_do_block_plain_sqlb10_dollar_quoted_stringsb11_multi_statement_simple_query...b35_mixed_qualifier_group_byb36_fk_insert_with_quoted_referencesIf you find a new ORM-shaped wire bug, file it with a Drizzle reproducer and the project will add a b<N>_* test alongside the fix.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Table 'pg_catalog.pg_type' does not exist on connect | Pre-3.14.1 — pg_catalog only on simple-Q path | Upgrade to v3.14.1+ |
column "t.col" not found on UPDATE/DELETE with qualified WHERE | Pre-3.14.7 — DML evaluator schema lacked source_table_name | Upgrade to v3.14.7+ |
INSERT-then-SELECT returns [] for the same canonical query | Pre-3.14.6 — stale result_cache after execute_plan_with_params | Upgrade to v3.14.6+ |
Cannot compare Timestamp(...) and String(...) in analytics | Pre-3.14.7 — implicit ISO-string coercion missing | Upgrade to v3.14.7+ |
LIMIT/OFFSET must be a number on paginated reads | Pre-3.14.8 — placeholder + quoted-string sentinel issues | Upgrade to v3.14.8+ |
DO $$ DECLARE ... in a migration silently no-ops | PL/pgSQL control flow not supported | See docs/compatibility/plpgsql.md for rewrite recipes |
FK violation on REFERENCES "users"("id") says Table '"users"' does not exist | Pre-3.14.10 — quoted identifiers stored verbatim in FK constraint | Upgrade to v3.14.10+ |
Where Next
- BAAS_REST_API — expose the same tables over HTTP without writing a server.
- AUTH_AND_OAUTH — Argon2id + JWT for app-side users.
- MYSQL_WIRE — for ORMs targeting MySQL (mysql2, Sequelize+mysql).
- GETTING_STARTED_TUTORIAL — first-time setup walkthrough.
- See also
BUGS_TIMETRACKER_DRIZZLE_COMPAT.mdin the source tree for the full bug-by-bug history.