Drizzle ORM
Drizzle ORM is a TypeScript ORM with a SQL-like query API and the schema migration tool
Drizzle Kit.
CedarDB works with Drizzle’s PostgreSQL dialect and the node-postgres (pg) driver, so you can define your schema,
manage it with migrations, and query it like a PostgreSQL database.
Create and change the schema with drizzle-kit generate and drizzle-kit migrate.
This page was tested with drizzle-orm 0.45.4, drizzle-kit 0.31.11 and pg 8.23.1.
This page is a self-contained example that you can follow from start to finish. It points out what is specific to CedarDB. For all features, see Drizzle’s documentation of schemas, migrations, and queries.
Prerequisites
- Node.js 20 or later.
- A running CedarDB server that accepts TCP connections (see Install CedarDB locally or CedarDB Cloud).
- A CedarDB superuser to create the application role and database.
Set up CedarDB
Connect to CedarDB as a superuser and create a role for your application that owns its database:
CREATE ROLE myuser WITH LOGIN PASSWORD 'CorrectHorseBatteryStaple';
CREATE DATABASE mydb OWNER myuser;As the database owner, Drizzle can create schemas, tables, and types without further privileges.
This works in every CedarDB edition.
Granting privileges with GRANT instead requires a CedarDB enterprise license.
Set up the project
Install Drizzle
Create a project and install Drizzle ORM, the pg driver, and Drizzle Kit:
mkdir hello-drizzle && cd hello-drizzle
npm init -y
npm install drizzle-orm pg dotenv
npm install --save-dev drizzle-kit tsx @types/pgConfigure the connection
Create a .env file with your CedarDB connection string:
DATABASE_URL=postgresql://myuser:CorrectHorseBatteryStaple@localhost:5432/mydbCreate a drizzle.config.ts in your project root:
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './src/db/schema.ts',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});Define the schema
Create a schema file at src/db/schema.ts:
import { boolean, index, integer, pgEnum, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core';
export const roleEnum = pgEnum('role', ['admin', 'member']);
export const usersTable = pgTable('users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
age: integer().notNull(),
email: varchar({ length: 255 }).notNull().unique(),
role: roleEnum().default('member').notNull(),
});
export const postsTable = pgTable('posts', {
id: serial().primaryKey(),
title: text().notNull(),
published: boolean().default(false).notNull(),
createdAt: timestamp().defaultNow().notNull(),
authorId: integer().notNull().references(() => usersTable.id, { onDelete: 'cascade' }),
}, (t) => [index('posts_author_idx').on(t.authorId)]);Create and change the schema
Drizzle Kit compares your schema file with its own snapshots in the drizzle folder and writes the difference as a
SQL migration.
Generate the first migration
npx drizzle-kit generateDrizzle Kit writes the migration to a file such as drizzle/0000_jazzy_bromley.sql:
CREATE TYPE "public"."role" AS ENUM('admin', 'member');--> statement-breakpoint
CREATE TABLE "posts" (
"id" serial PRIMARY KEY NOT NULL,
"title" text NOT NULL,
"published" boolean DEFAULT false NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"authorId" integer NOT NULL
);
--> statement-breakpoint
-- ...Review the SQL before you apply it and commit it together with your schema. Check that it only uses column types and features that CedarDB supports (see CedarDB limitations), and that it does not drop columns or tables you want to keep.
Apply the migration
npx drizzle-kit migrateDrizzle Kit runs all migrations that are not applied yet.
Change the schema
Edit src/db/schema.ts and, for example, add a column to users and remove one from posts:
export const usersTable = pgTable('users', {
// ...
phone: text(),
});
export const postsTable = pgTable('posts', {
id: serial().primaryKey(),
title: text().notNull(),
createdAt: timestamp().defaultNow().notNull(),
authorId: integer().notNull().references(() => usersTable.id, { onDelete: 'cascade' }),
}, (t) => [index('posts_author_idx').on(t.authorId)]);Then generate and apply a new migration:
npx drizzle-kit generate
npx drizzle-kit migrateThe migration contains statements such as ALTER TABLE "users" ADD COLUMN "phone" text and
ALTER TABLE "posts" DROP COLUMN "published".
CedarDB runs them in place, without recreating the tables.
Alternatively, if you deploy without Drizzle Kit, your application can apply the reviewed migration files from the
drizzle folder when it starts:
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
async function main() {
const db = drizzle(process.env.DATABASE_URL!);
await migrate(db, { migrationsFolder: './drizzle' });
await db.$client.end();
}
main();Query data
The Drizzle query API works with CedarDB, including relational queries and nested transactions:
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { and, avg, count, desc, eq, gt, like, relations } from 'drizzle-orm';
import { postsTable, usersTable } from './db/schema';
const usersRelations = relations(usersTable, ({ many }) => ({ posts: many(postsTable) }));
const postsRelations = relations(postsTable, ({ one }) => ({
author: one(usersTable, { fields: [postsTable.authorId], references: [usersTable.id] }),
}));
const db = drizzle(process.env.DATABASE_URL!, {
schema: { usersTable, postsTable, usersRelations, postsRelations },
});
async function main() {
// Start from empty tables, deletes cascade to the posts
await db.delete(usersTable);
// Batch insert with RETURNING
const [alice] = await db
.insert(usersTable)
.values([
{ name: 'Alice', age: 28, email: 'alice@example.com' },
{ name: 'Bob', age: 35, email: 'bob@example.com' },
{ name: 'Carol', age: 22, email: 'carol@example.com' },
])
.returning({ id: usersTable.id });
await db.insert(postsTable).values([
{ title: 'Hello CedarDB', authorId: alice.id },
{ title: 'Second post', authorId: alice.id },
]);
// Filtered select
const filtered = await db
.select()
.from(usersTable)
.where(and(gt(usersTable.age, 25), like(usersTable.name, '%o%')));
// Ordered select with LIMIT
const oldest = await db
.select({ name: usersTable.name, age: usersTable.age })
.from(usersTable)
.orderBy(desc(usersTable.age))
.limit(2);
// Aggregates
const [{ userCount, avgAge }] = await db
.select({ userCount: count(), avgAge: avg(usersTable.age) })
.from(usersTable);
// Relational query: users with their posts
const usersWithPosts = await db.query.usersTable.findMany({ with: { posts: true } });
// Upsert
await db
.insert(usersTable)
.values({ name: 'Bob', age: 36, email: 'bob@example.com' })
.onConflictDoUpdate({ target: usersTable.email, set: { age: 36 } });
// Transaction with a nested transaction (savepoint) that rolls back
await db.transaction(async (tx) => {
await tx.update(usersTable).set({ age: 29 }).where(eq(usersTable.id, alice.id));
try {
await tx.transaction(async (nested) => {
await nested.update(usersTable).set({ age: 99 }).where(eq(usersTable.id, alice.id));
nested.rollback();
});
} catch {
// Alice keeps age 29
}
});
// Delete, cascades to the posts
await db.delete(usersTable).where(eq(usersTable.email, 'alice@example.com'));
console.log({ filtered, oldest, userCount, avgAge, usersWithPosts });
await db.$client.end();
}
main();Run the script:
npx tsx src/index.tsCedarDB limitations
drizzle-kit pushworks only on an empty database, anddrizzle-kit pullgenerates an inaccurate schema. Both read the CedarDB schema, which Drizzle Kit does not fully recognize yet. Usedrizzle-kit generateanddrizzle-kit migrateinstead.numericcolumns support a precision of at most 38, for examplenumeric({ precision: 38, scale: 2 })(see Numeric).- Subqueries inside
insert().values()and renaming enum values are not supported. - The isolation level
serializableis not supported. CedarDB runs all transactions with snapshot isolation and accepts lower isolation levels for compatibility without changing behaviour (see Transactions).