Skip to content

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 npm or pnpm
  • About 30 minutes for the full walk-through

1. Start the Server

Terminal window
heliosdb-nano start --memory --auth trust

Trust 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:

Terminal window
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

Terminal window
npm install drizzle-orm postgres
npm install -D drizzle-kit

Schema (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 shape
const [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/OFFSET
const 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 patternBug IDFixed in
serial("id").primaryKey() round-trip via extended protocolB1, B4, B283.14.0 / 3.14.4
GENERATED ALWAYS AS IDENTITY (alternative to SERIAL)B23.14.0
INSERT INTO ... VALUES (default, $1, default) RETURNING *B3, B273.14.0 / 3.14.4
EXTRACT(EPOCH FROM created_at) in analytics queriesB53.14.0
CREATE SEQUENCE / nextval() / currval() / setval()B7, B83.14.0
DO $$ ... $$ blocks (plain SQL only)B9, B213.14.0 / 3.14.1
Dollar-quoted string literals $$text$$B103.14.0
Multi-statement simple queries (;-separated)B113.14.0
pg_catalog.pg_type connect-time introspection over extended QB12, B193.14.0 / 3.14.1
pg_tables / information_schema WHERE filteringB13, B203.14.0 / 3.14.1
gen_random_uuid()B153.14.0
Flush (H / 0x48) frontend messageB223.14.2
Scalar subquery in UPDATE ... SET (correlated + uncorrelated)B233.14.2
DEFAULT <expr> evaluated on omitted columnsB243.14.3
INSERT INTO t DEFAULT VALUES syntaxB253.14.3
NOT NULL enforcement on every INSERT pathB263.14.3
Timestamp wire format (YYYY-MM-DD HH:MM:SS.ffffff)B303.14.5
Stale result_cache after INSERT … RETURNINGB293.14.6
UPDATE/DELETE WHERE "t"."col" = $1 qualified refB313.14.7
timestamp >= '2026-04-23T00:00:00.000Z' ISO string compareB323.14.7
LIMIT $1 OFFSET $2 parameterisedB333.14.8
UPDATE SET ts_col = $1 auto-cast string→timestampB343.14.8
GROUP BY with mixed qualifier styles + DATE keysB353.14.9
Quoted FK references (REFERENCES "users"("id"))B363.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

Terminal window
npm install prisma @prisma/client
npx prisma init

schema.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])
}
Terminal window
DATABASE_URL="postgres://postgres@localhost:5432/postgres" npx prisma migrate dev --name init

Prisma’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

Terminal window
npm install typeorm pg reflect-metadata

entities/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

Terminal window
npm install sequelize pg

Use

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_id
b2_generated_always_as_identity
b3_default_keyword_in_values
b4_returning_field_count
b5_extract_epoch_from_timestamp
b7_create_sequence
b8_nextval_currval_setval
b9_do_block_plain_sql
b10_dollar_quoted_strings
b11_multi_statement_simple_query
...
b35_mixed_qualifier_group_by
b36_fk_insert_with_quoted_references

If 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

SymptomCauseFix
Table 'pg_catalog.pg_type' does not exist on connectPre-3.14.1 — pg_catalog only on simple-Q pathUpgrade to v3.14.1+
column "t.col" not found on UPDATE/DELETE with qualified WHEREPre-3.14.7 — DML evaluator schema lacked source_table_nameUpgrade to v3.14.7+
INSERT-then-SELECT returns [] for the same canonical queryPre-3.14.6 — stale result_cache after execute_plan_with_paramsUpgrade to v3.14.6+
Cannot compare Timestamp(...) and String(...) in analyticsPre-3.14.7 — implicit ISO-string coercion missingUpgrade to v3.14.7+
LIMIT/OFFSET must be a number on paginated readsPre-3.14.8 — placeholder + quoted-string sentinel issuesUpgrade to v3.14.8+
DO $$ DECLARE ... in a migration silently no-opsPL/pgSQL control flow not supportedSee docs/compatibility/plpgsql.md for rewrite recipes
FK violation on REFERENCES "users"("id") says Table '"users"' does not existPre-3.14.10 — quoted identifiers stored verbatim in FK constraintUpgrade 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.md in the source tree for the full bug-by-bug history.