effect-sql-db
Effect patterns for SQL databases (Drizzle/Kysely/Prisma): services, transactions, retries, streaming, and observability.
When & Why to Use This Skill
This Claude skill provides standardized Effect patterns for SQL database integration using popular ORMs and query builders like Drizzle, Kysely, and Prisma. It enables developers to build highly resilient database layers by implementing type-safe error handling, automated retry logic for deadlocks, transactional integrity, and comprehensive observability for performance monitoring within the Effect ecosystem.
Use Cases
- 1. Resilient Database Service Architecture: Wrap SQL clients into Effect Services to ensure type-safe error management and clean dependency injection across the application.
- 2. Fault-Tolerant Transactions: Implement complex database transactions that automatically handle transient failures, such as MySQL deadlocks, using customizable exponential backoff retry policies.
- 3. Database Performance Monitoring: Integrate telemetry spans and threshold-based slow-query logging to identify bottlenecks and maintain database health in production.
- 4. High-Performance Data Streaming: Process massive result sets efficiently using Effect Streams to handle row-by-row iteration with controlled concurrency and minimal memory overhead.
- 5. Type-Safe Query Helpers: Utilize reusable patterns for dynamic SQL generation, such as JSON extraction and conditional updates, while maintaining strict type safety.
| name | effect-sql-db |
|---|---|
| description | Effect patterns for SQL databases (Drizzle/Kysely/Prisma): services, transactions, retries, streaming, and observability. |
| allowed-tools | Read, Grep, Glob, Edit, Write |
SQL & Databases
When to use
- You are integrating a SQL client (Drizzle, Kysely, Prisma, mysql2/pg) with Effect
- You want typed errors, retries, transactions, and observability
Provide DB config via Layer
import { Config, Effect, Layer } from "effect"
class DbConfig extends Effect.Service<DbConfig>()("DbConfig", {
sync: () => ({
url: process.env.DATABASE_URL ?? ""
})
}){}
// Provide once at app boundary
export const DbConfigLive = DbConfig.Default
Wrap a client in a Service (typed errors)
import { Effect } from "effect"
class DatabaseError extends Data.TaggedError("DatabaseError")<{ cause: unknown }>{}
export class Database extends Effect.Service<Database>()("Database", {
effect: Effect.gen(function* () {
// drizzle() / new PrismaClient() / kysely instance
const client = makeDbClient() // your factory (validated url)
return {
// use-pattern ensures client is passed from a single place
use: <A>(cb: (c: typeof client) => Promise<A>) =>
Effect.tryPromise({ try: () => cb(client), catch: (cause) => new DatabaseError({ cause }) })
}
})
}){}
Real-world snippet: Drizzle wrapper with use(cb)
// Pattern adapted from Cap
export class Database extends Effect.Service<Database>()("Database", {
effect: Effect.gen(function* () {
return {
use: <T>(cb: (_: DbClient) => Promise<T>) =>
Effect.tryPromise({ try: () => cb(db()), catch: (cause) => new DatabaseError({ cause }) })
}
})
}){}
Transactions with retry-on-deadlock
// Generic transactional helper with selective retry
const isDeadlock = (e: unknown) => typeof e === "object" && e !== null && (e as any).code === "1213" // MySQL
export const transactional = <A>(eff: Effect.Effect<A, DatabaseError, Database>) =>
Effect.gen(function* () {
const db = yield* Database
return yield* Effect.tryPromise({
try: () => db.use((c) => c.transaction(async (tx) => await Effect.runPromise(eff))),
catch: (cause) => new DatabaseError({ cause })
})
}).pipe(
Effect.retry({ schedule: Schedule.exponential("100 millis"), times: 3, while: isDeadlock })
)
Query helpers (Drizzle examples)
// Fallback to existing column if value is undefined
export const updateIfDefined = <T>(v: T | undefined, col: AnyMySqlColumn) =>
sql`COALESCE(${v === undefined ? sql`NULL` : v}, ${col})`
// JSON_EXTRACT helper that returns string | undefined
export function jsonExtractString(column: MySqlColumn<any>, field: string) {
return sql<string | undefined>`JSON_UNQUOTE(JSON_EXTRACT(${column}, ${`$.${field}`}))`
}
Streaming large result sets
// If your driver supports async iteration over rows
const rowsAsync = client.executeStream("SELECT * FROM events") // placeholder
const stream = Stream.fromAsyncIterable(rowsAsync).pipe(
Stream.mapEffect(processRow, { concurrency: 16 })
)
yield* Stream.runDrain(stream)
Observability: spans and slow query logging
const withSqlSpan = <A, E, R>(name: string, eff: Effect.Effect<A, E, R>) =>
eff.pipe(Effect.withSpan(name, { attributes: { "db.system": "mysql" } }))
const logSlow = <A, E, R>(thresholdMs: number, eff: Effect.Effect<A, E, R>) =>
Effect.timed(eff).pipe(
Effect.tap(([d]) => Duration.toMillis(d) > thresholdMs ? Effect.logWarning(`slow query: ${Duration.toMillis(d)}ms`) : Effect.void),
Effect.map(([_, a]) => a)
)
Testing patterns
// Provide an in-memory or test DB client via Layer
export const DatabaseTest = Layer.succeed(Database, {
use: <A>(cb: (c: any) => Promise<A>) => Effect.tryPromise({ try: () => cb(makeTestDb()), catch: (cause) => new DatabaseError({ cause }) })
})
// Or fully mock repository methods at the service boundary
Guidance
- Centralize DB usage in a service; callers never touch the client directly
- Map low-level driver errors to a single DatabaseError; consider domain mapping at repository layer
- Use transactions for multi-row invariants; add retry policy for deadlocks/timeouts
- Always add timeouts for long-running operations; log slow queries
- Keep queries in repositories; route/handler stays thin
Pitfalls
- Leaking raw client/connection across layers → hard to test and observe
- No retry on deadlocks/timeouts → transient failures bubble to users
- Building SQL strings manually → prefer query builder/typed ORM where possible
Cross-links
- Resources & Scope for wrapping external clients and spans
- Errors & Retries for selective retry policies
- Config & Schema for validated DB configuration
Local Source Reference
CRITICAL: Search local Effect source before implementing
The full Effect source code is available at docs/effect-source/. Always search the actual implementation before writing Effect code.
Key Source Files
- SqlClient:
docs/effect-source/sql/src/SqlClient.ts - SqlConnection:
docs/effect-source/sql/src/SqlConnection.ts - SqlResolver:
docs/effect-source/sql/src/SqlResolver.ts - Drizzle integration:
docs/effect-source/sql-drizzle/src/ - SQLite Node:
docs/effect-source/sql-sqlite-node/src/
Example Searches
# Find SqlClient patterns
grep -F "SqlClient" docs/effect-source/sql/src/SqlClient.ts
# Study transaction patterns
grep -rF "withTransaction" docs/effect-source/sql/src/
# Find Drizzle integration
grep -rF "export" docs/effect-source/sql-drizzle/src/
# Look at SQL test examples
grep -rF "SqlClient." docs/effect-source/sql/test/
Workflow
- Identify the SQL API you need (e.g., SqlClient, transactions)
- Search
docs/effect-source/sql/src/for the implementation - Study the types and transaction patterns
- Look at test files for usage examples
- Write your code based on real implementations
Real source code > documentation > assumptions
References
- Agent Skills overview: https://www.anthropic.com/news/skills
- Skills guide: https://docs.claude.com/en/docs/claude-code/skills
- EffectPatterns: https://github.com/PaulJPhilp/EffectPatterns