Cloudflare D1
The @effect/sql-d1 package provides Cloudflare D1 support for Effect SQL. D1 is Cloudflare's serverless SQLite database, designed for edge computing with Cloudflare Workers.
Installation
bash
npm install @effect/sql @effect/sql-d1Setup
Cloudflare Worker
typescript
// src/index.ts
import { Effect, Layer } from "effect"
import { SqlClient } from "@effect/sql"
import { D1Client } from "@effect/sql-d1"
interface Env {
DB: D1Database
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const DatabaseLive = D1Client.layer({
binding: env.DB
})
const program = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient
const users = yield* sql`SELECT * FROM users`
return new Response(JSON.stringify(users), {
headers: { "Content-Type": "application/json" }
})
})
return Effect.runPromise(program.pipe(Effect.provide(DatabaseLive)))
}
}wrangler.toml
toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"Configuration
typescript
import { D1Client } from "@effect/sql-d1"
const DatabaseLive = D1Client.layer({
// D1 binding from Cloudflare Workers environment
binding: env.DB,
// Name transformations
transformQueryNames: String.camelToSnake,
transformResultNames: String.snakeToCamel
})D1-Specific Considerations
SQLite Compatibility
D1 is based on SQLite, so it follows SQLite's syntax and limitations:
typescript
// Create table (SQLite syntax)
yield* sql`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`No Connection Pool
D1 doesn't use connection pooling—each request gets a fresh connection. This is handled automatically.
Batch Operations
D1 supports batching multiple statements:
typescript
yield* sql.withTransaction(
Effect.gen(function* () {
yield* sql`INSERT INTO users ${sql.insert({ name: "Alice" })}`
yield* sql`INSERT INTO profiles ${sql.insert({ userId: 1 })}`
})
)Row Limits
D1 has limits on result set sizes. Use pagination for large results:
typescript
const page = 1
const pageSize = 100
const users = yield* sql`
SELECT * FROM users
ORDER BY id
LIMIT ${pageSize}
OFFSET ${(page - 1) * pageSize}
`Migrations
Run migrations using Wrangler:
bash
wrangler d1 migrations apply my-databaseOr programmatically:
typescript
import { D1Migrator } from "@effect/sql-d1"
import { Migrator } from "@effect/sql"
const MigratorLive = D1Migrator.layer({
loader: Migrator.fromGlob(import.meta.glob("./migrations/*.ts"))
})
// Run during worker initialization
const program = Effect.gen(function* () {
const migrations = yield* Migrator.Migrator
yield* migrations
})Local Development
Use Wrangler's local D1:
bash
wrangler d1 execute my-database --local --command "SELECT * FROM users"Or in your test setup:
typescript
// Using miniflare for testing
import { Miniflare } from "miniflare"
const mf = new Miniflare({
d1Databases: ["DB"],
modules: true
})
const db = await mf.getD1Database("DB")Example: REST API
typescript
import { Effect, Schema } from "effect"
import { SqlClient, SqlSchema } from "@effect/sql"
import { D1Client } from "@effect/sql-d1"
// Schema
class User extends Schema.Class<User>("User")({
id: Schema.Number,
name: Schema.NonEmptyString,
email: Schema.NonEmptyString
}) {}
// Handlers
const listUsers = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient
return yield* sql<User>`SELECT * FROM users`
})
const createUser = (data: { name: string; email: string }) =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient
yield* sql`INSERT INTO users ${sql.insert(data)}`
const [user] = yield* sql<User>`SELECT * FROM users WHERE email = ${data.email}`
return user
})
// Worker
export default {
async fetch(request: Request, env: Env) {
const DatabaseLive = D1Client.layer({ binding: env.DB })
const url = new URL(request.url)
if (request.method === "GET" && url.pathname === "/users") {
return Effect.runPromise(
listUsers.pipe(
Effect.map((users) => Response.json(users)),
Effect.provide(DatabaseLive)
)
)
}
if (request.method === "POST" && url.pathname === "/users") {
const body = await request.json()
return Effect.runPromise(
createUser(body).pipe(
Effect.map((user) => Response.json(user, { status: 201 })),
Effect.provide(DatabaseLive)
)
)
}
return new Response("Not Found", { status: 404 })
}
}