Skip to content

Drizzle reference

Source: src/Drizzle/Cloudflare.ts

Open a Drizzle database over the current Durable Object’s SQLite storage using the drizzle-orm/effect-sqlite-do integration (driven by @effect/sql-sqlite-do’s SqliteClient), applying migrations first when provided. Cloudflare.SqlMigrations captures a SQL directory during construction without importing .sql files. Each instance applies pending files on activation using Alchemy’s shared migration history.

Every query is an Effect with a typed error channel — drizzle’s EffectDrizzleQueryError (query + params + cause, wrapping the underlying effect-sql SqlError) — so failures are handled with Effect.catchTag instead of leaking as defects. Transactions add SqlError to the union. Opening the db itself never fails: a migration that cannot apply dies, since the instance is unusable without its schema.

Yield it in the object’s inner (instance) Effect — it runs when the instance activates, before any request reaches its methods:

schema.ts
import { defineRelations } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
});
export const posts = sqliteTable("posts", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: integer("user_id").notNull().references(() => users.id),
title: text("title").notNull(),
});
export const relations = defineRelations({ users, posts }, (t) => ({
users: { posts: t.many.posts() },
posts: { author: t.one.users({ from: t.posts.userId, to: t.users.id }) },
}));
import * as Drizzle from "alchemy/Drizzle/Cloudflare";
import { posts, relations, users } from "./schema.ts";
export class Users extends Cloudflare.DurableObject<Users>()(
"Users",
Effect.gen(function* () {
const migrations = yield* Cloudflare.SqlMigrations("./drizzle");
return Effect.gen(function* () {
const db = yield* Drizzle.DurableObject({ migrations, relations });
return {
addUser: (name: string) => db.insert(users).values({ name }),
listUsers: () => db.select().from(users),
listUsersWithPosts: () =>
db.query.users.findMany({ with: { posts: true } }),
// typed error handling per operation:
tryAddUser: (name: string) =>
db
.insert(users)
.values({ name })
.pipe(
Effect.catchTag("EffectDrizzleQueryError", () =>
Effect.succeed(undefined),
),
),
};
});
}),
) {}

Source: src/Drizzle/D1.ts

Open a Drizzle database over a Cloudflare D1 binding using the drizzle-orm/effect-d1 integration (which drives queries through @effect/sql-d1’s D1Client).

Accepts the client returned by Cloudflare.D1.QueryDatabase(db) — or its raw effect directly — and returns a chainable Proxy over EffectSQLiteD1Database (via proxyChain): every property read records a step, every call records args, and the chain is replayed against the resolved drizzle db when it’s finally yielded as an Effect. Callers don’t need a separate yield* conn step:

const d1 = yield* Cloudflare.D1.QueryDatabase(Db);
const db = yield* Drizzle.D1(d1, { relations });
fetch: Effect.gen(function* () {
const rows = yield* db.select().from(users);
});

The client build is deferred until the first query and memoized on the current execution’s Scope (via makeExecutionMemo), so the D1Client (and its prepared-statement cache) is built at most once per execution — a Worker fetch/queue/scheduled event, a Durable Object call, or a Workflow run — and reused across every query in that execution. Resolving the binding is likewise deferred, so deploy / plan-time invocations (where WorkerEnvironment isn’t provided) never touch D1.

The client is built against that same execution scope, so its finalizer fires when the scope closes — when the request / run settles, not when the Worker’s isolate-lifetime init completes. Wrapping queries in a nested Effect.scoped narrows both the memo and the client’s lifetime to that block: memo key and finalizer target are always the same scope object, so they cannot disagree.

Source: src/Drizzle/MySQL.ts

Open a Drizzle/MySQL database from a connection URL using the drizzle-orm/effect-mysql2 integration.

const conn = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
const db = yield* Drizzle.MySQL(conn.connectionString, { relations });
fetch: Effect.gen(function* () {
const rows = yield* db.select().from(users);
});

The pool opens on the first query of an execution, is reused for every query in it, and closes when the event settles (see makeExecutionMemo); plan/deploy never connect. Workers defaults (resolveMySQLConfig) are overridden via config.client:

const db = yield* Drizzle.MySQL(connectionString, {
relations,
client: { poolConfig: { ssl: { rejectUnauthorized: true } } },
});

Source: src/Drizzle/Postgres.ts

Open a Drizzle/Postgres database from a connection URL using the drizzle-orm/effect-postgres integration.

Returns a chainable Proxy over EffectPgDatabase (via proxyChain) — every property read records a step, every call records args, and the chain is replayed against the resolved drizzle db when it’s finally yielded as an Effect. Callers don’t need a separate yield* conn step:

const db = yield* Drizzle.Postgres(hd.connectionString);
fetch: Effect.gen(function* () {
const rows = yield* db.select().from(users);
});

The connect work is deferred until the first query and memoized on the current execution’s Scope (via makeExecutionMemo), so the pool is built at most once per execution — a Worker fetch/queue/scheduled event, a Durable Object call, a Workflow run, or a Lambda invocation — and reused across every query and task step in that execution. Yielding the connection string is likewise deferred, so deploy / plan-time invocations (where WorkerEnvironment isn’t provided) never trigger a real connection attempt.

The pool is built against that same execution scope, so its end finalizer fires when the scope closes — when the request / run settles, not when the Worker’s isolate-lifetime init completes. Wrapping queries in a nested Effect.scoped narrows both the memo and the pool’s lifetime to that block: memo key and finalizer target are always the same scope object, so they cannot disagree.

Source: src/Drizzle/Schema.ts

A Drizzle schema managed as an Alchemy resource.

Wraps drizzle-kit’s programmatic API (generateDrizzleJson / generateMigration) so migration SQL is regenerated as part of alchemy deploy whenever the source schema changes. The output directory is intended to be passed straight to a database resource’s migrations prop, giving you a single deploy-driven flow:

const schema = yield* Drizzle.Schema("app-schema", {
schema: "./src/schema.ts",
});
const branch = yield* Neon.Branch("app-branch", {
project,
migrations: schema,
});
const db = yield* Fly.Postgres("Db", {
region: "iad",
migrations: schema,
});

Drizzle.Schema runs first (because the database resource depends on its out output), regenerates pending migration files, and the database resource then applies them.

The resource is delete-safe: removing it from the stack does not wipe the migrations directory, since migration files are typically checked in and shared with other environments.