Skip to content

Drizzle / Prisma / TypeORM / Sequelize Compatibility

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 closed 36 distinct ORM-shaped wire bugs found by running a real Drizzle + postgres-js app against it. Drizzle, Prisma, TypeORM, and Sequelize now run unchanged — same connection string, same migrations, same generated SQL.


Prerequisites

  • HeliosDB Nano, current release (heliosdb-nano --version)
  • 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 depends on wire-level behaviour that was fixed for Drizzle:

Drizzle patternBug ID
serial("id").primaryKey() round-trip via extended protocolB1, B4, B28
GENERATED ALWAYS AS IDENTITY (alternative to SERIAL)B2
INSERT INTO... VALUES (default, $1, default) RETURNING *B3, B27
EXTRACT(EPOCH FROM created_at) in analytics queriesB5
CREATE SEQUENCE / nextval() / currval() / setval()B7, B8
DO $$... $$ blocks (plain SQL only)B9, B21
Dollar-quoted string literals $$text$$B10
Multi-statement simple queries (;-separated)B11
pg_catalog.pg_type connect-time introspection over extended QB12, B19
pg_tables / information_schema WHERE filteringB13, B20
gen_random_uuid()B15
Flush (H / 0x48) frontend messageB22
Scalar subquery in UPDATE... SET (correlated + uncorrelated)B23
DEFAULT <expr> evaluated on omitted columnsB24
INSERT INTO t DEFAULT VALUES syntaxB25
NOT NULL enforcement on every INSERT pathB26
Timestamp wire format (YYYY-MM-DD HH:MM:SS.ffffff)B30
Stale result_cache after INSERT… RETURNINGB29
UPDATE/DELETE WHERE "t"."col" = $1 qualified refB31
timestamp >= '2026-04-23T00:00:00.000Z' ISO string compareB32
LIMIT $1 OFFSET $2 parameterisedB33
UPDATE SET ts_col = $1 auto-cast string→timestampB34
GROUP BY with mixed qualifier styles + DATE keysB35
Quoted FK references (REFERENCES "users"("id"))B36

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 Nano 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 of them are covered.


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 connectOlder releases: pg_catalog only on simple-Q pathUpgrade to the current release
column "t.col" not found on UPDATE/DELETE with qualified WHEREOlder releases: DML evaluator schema lacked source_table_nameUpgrade to the current release
INSERT-then-SELECT returns [] for the same canonical queryOlder releases: stale result_cache after execute_plan_with_paramsUpgrade to the current release
Cannot compare Timestamp(...) and String(...) in analyticsOlder releases: implicit ISO-string coercion missingUpgrade to the current release
LIMIT/OFFSET must be a number on paginated readsOlder releases: placeholder + quoted-string sentinel issuesUpgrade to the current release
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 existOlder releases: quoted identifiers stored verbatim in FK constraintUpgrade to the current release

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.