---
title: "Drizzle / Prisma / TypeORM / Sequelize Compatibility"
---

# 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

```bash
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:

```bash
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

```bash
npm install drizzle-orm postgres
npm install -D drizzle-kit
```

### Schema (`src/schema.ts`)

```typescript
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

```typescript
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 pattern | Bug ID |
|----------------|--------|
| `serial("id").primaryKey()` round-trip via extended protocol | B1, 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 queries | B5 |
| `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 Q | B12, B19 |
| `pg_tables` / `information_schema` `WHERE` filtering | B13, B20 |
| `gen_random_uuid()` | B15 |
| `Flush` (`H` / 0x48) frontend message | B22 |
| Scalar subquery in `UPDATE... SET` (correlated + uncorrelated) | B23 |
| `DEFAULT <expr>` evaluated on omitted columns | B24 |
| `INSERT INTO t DEFAULT VALUES` syntax | B25 |
| `NOT NULL` enforcement on every INSERT path | B26 |
| Timestamp wire format (`YYYY-MM-DD HH:MM:SS.ffffff`) | B30 |
| Stale `result_cache` after INSERT... RETURNING | B29 |
| `UPDATE/DELETE WHERE "t"."col" = $1` qualified ref | B31 |
| `timestamp >= '2026-04-23T00:00:00.000Z'` ISO string compare | B32 |
| `LIMIT $1 OFFSET $2` parameterised | B33 |
| `UPDATE SET ts_col = $1` auto-cast string→timestamp | B34 |
| `GROUP BY` with mixed qualifier styles + DATE keys | B35 |
| 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`:

```typescript
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

```bash
npm install prisma @prisma/client
npx prisma init
```

### `schema.prisma`

```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])
}
```

```bash
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

```typescript
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

```bash
npm install typeorm pg reflect-metadata
```

### `entities/User.ts`

```typescript
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

```typescript
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

```bash
npm install sequelize pg
```

### Use

```typescript
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

| Symptom | Cause | Fix |
|---------|-------|-----|
| `Table 'pg_catalog.pg_type' does not exist` on connect | Older releases: pg_catalog only on simple-Q path | Upgrade to the current release |
| `column "t.col" not found` on UPDATE/DELETE with qualified WHERE | Older releases: DML evaluator schema lacked `source_table_name` | Upgrade to the current release |
| INSERT-then-SELECT returns `[]` for the same canonical query | Older releases: stale `result_cache` after `execute_plan_with_params` | Upgrade to the current release |
| `Cannot compare Timestamp(...) and String(...)` in analytics | Older releases: implicit ISO-string coercion missing | Upgrade to the current release |
| `LIMIT/OFFSET must be a number` on paginated reads | Older releases: placeholder + quoted-string sentinel issues | Upgrade to the current release |
| `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` | Older releases: quoted identifiers stored verbatim in FK constraint | Upgrade to the current release |

---

## Where Next

- [BAAS_REST_API](https://www.heliosdb.com/docs/nano/guides/tutorials/baas_rest_api_tutorial/) — expose the same tables over HTTP without writing a server.
- [AUTH_AND_OAUTH](https://www.heliosdb.com/docs/nano/guides/tutorials/auth_and_oauth_tutorial/) — Argon2id + JWT for app-side users.
- [MYSQL_WIRE](https://www.heliosdb.com/docs/nano/guides/tutorials/mysql_wire_tutorial/) — for ORMs targeting MySQL (mysql2, Sequelize+mysql).
- [GETTING_STARTED_TUTORIAL](https://www.heliosdb.com/docs/nano/guides/tutorials/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.
