Queries
The SqlClient service is a callable sql function. Tag a template literal to
build a query — interpolated values become bound parameters, so user input
is never spliced into the SQL string. The result is an Effect that yields an
array of rows and can fail with a SqlError. The same sql value also produces
quoted identifiers, fragments, and helpers for inserts, updates, and IN
clauses, so even dynamic queries stay parameterized.
import { Effect } from "effect"import { SqlClient } from "effect/unstable/sql"
const findActiveUsers = Effect.gen(function*() { const sql = yield* SqlClient.SqlClient
// Interpolated values (`true`, `10`) are sent as parameters, not text. The // type argument names the row shape for the returned array. const users = yield* sql<{ readonly id: number; readonly name: string }>` SELECT id, name FROM users WHERE active = ${true} LIMIT ${10} `
return users})A tagged sql template is a Statement<A>, and a Statement<A> is an
Effect<ReadonlyArray<A>, SqlError> — yielding it (or running it) executes the
query and returns the rows. The query composes with everything else: retry it,
add a span, race it, or run it under a transaction. users above is typed as
ReadonlyArray<{ id: number; name: string }>, but those are raw driver values —
to validate and transform them, decode with Schema (shown below).
Identifiers, fragments, and helpers
Section titled “Identifiers, fragments, and helpers”Calling sql with a string returns a safely-quoted identifier, useful when
a table or column name is dynamic. The constructor also exposes helpers that
build SQL fragments for common shapes without hand-writing placeholder lists.
import { Effect } from "effect"import { SqlClient } from "effect/unstable/sql"
const insertUser = Effect.fn("insertUser")(function*(name: string, email: string) { const sql = yield* SqlClient.SqlClient
// `sql.insert` builds the column list and VALUES tuple from an object. // `sql("users")` quotes the identifier for the active dialect. yield* sql` INSERT INTO ${sql("users")} ${sql.insert({ name, email })} `})
const usersByIds = Effect.fn("usersByIds")(function*(ids: ReadonlyArray<number>) { const sql = yield* SqlClient.SqlClient
// `sql.in` expands an array into a parameterized `IN (?, ?, …)` clause. return yield* sql<{ readonly id: number; readonly name: string }>` SELECT id, name FROM users WHERE ${sql.in("id", ids)} `})
const setUserName = Effect.fn("setUserName")(function*(id: number, name: string) { const sql = yield* SqlClient.SqlClient
// `sql.update` builds the SET clause from the object's keys; an optional // second arg lists columns to omit from SET. yield* sql` UPDATE ${sql("users")} SET ${sql.update({ name })} WHERE id = ${id} `})Other useful constructors: sql.and([...]) / sql.or([...]) chain WHERE
clauses, sql.csv(...) builds comma-separated lists for ORDER BY / GROUP BY,
sql.literal(str) inlines trusted SQL verbatim (no escaping — never pass user
input), and sql.unsafe(text, params) builds a fully custom statement. All of
these are enumerated with examples in the reference
below.
Decoding rows with Schema
Section titled “Decoding rows with Schema”Raw rows are untyped driver output. SqlSchema wraps a query’s execute
callback with a request schema (encoded on the way in) and a result schema
(decoded on the way out), so your code works with validated domain values and a
SchemaError surfaces any row that doesn’t match.
import { Effect, Schema } from "effect"import { SqlClient, SqlSchema } from "effect/unstable/sql"
const User = Schema.Struct({ id: Schema.Number, name: Schema.String, email: Schema.String})
const makeQueries = Effect.gen(function*() { const sql = yield* SqlClient.SqlClient
// findOne: encodes the request (here just an `id`), runs the query, then // decodes the first row. Fails with NoSuchElementError when there are none. const getUserById = SqlSchema.findOne({ Request: Schema.Number, Result: User, execute: (id) => sql`SELECT * FROM users WHERE id = ${id}` })
// findAll: decodes every row into a typed array (empty array when none). const searchUsers = SqlSchema.findAll({ Request: Schema.String, Result: User, execute: (term) => sql`SELECT * FROM users WHERE name ILIKE ${`%${term}%`}` })
return { getUserById, searchUsers } as const})SqlSchema offers findAll (zero or more rows), findNonEmpty (fails on an
empty result), findOne (first row, fails if absent), findOneOption (first
row as an Option), and void (encode the request, discard the result — for
writes). The execute callback receives the encoded request, so any Schema
transformations on the request shape apply before the value reaches SQL. To
batch and de-duplicate many such queries into a single round trip, see
Resolvers.
Streaming large result sets
Section titled “Streaming large result sets”Loading millions of rows into an array is wasteful. Every statement exposes a
.stream property — a Stream that pulls rows from the cursor
incrementally, so memory stays bounded regardless of result size.
import { Effect, Stream } from "effect"import { SqlClient } from "effect/unstable/sql"
const exportEvents = Effect.gen(function*() { const sql = yield* SqlClient.SqlClient
yield* sql<{ readonly id: number; readonly payload: string }>` SELECT id, payload FROM events ORDER BY id `.stream.pipe( // Process each row as it arrives without buffering the whole table. Stream.runForEach((event) => Effect.log(`event ${event.id}`)) )})Transactions
Section titled “Transactions”sql.withTransaction wraps any effect so every query it runs is part of one
transaction: the client issues BEGIN up front, COMMIT on success, and
ROLLBACK if the effect fails or is interrupted. Nested withTransaction calls
reuse the same connection and use savepoints, so composing transactional effects
is safe.
import { Effect } from "effect"import { SqlClient } from "effect/unstable/sql"
// Transfer between two accounts: both updates commit together, or neither does.const transfer = Effect.fn("transfer")(function*(from: number, to: number, amount: number) { const sql = yield* SqlClient.SqlClient
yield* sql.withTransaction( Effect.gen(function*() { yield* sql`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${from}` yield* sql`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${to}` // If anything below fails, both updates roll back automatically. const [account] = yield* sql<{ readonly balance: number }>` SELECT balance FROM accounts WHERE id = ${from} ` if (account.balance < 0) { return yield* Effect.fail(new Error("insufficient funds")) } }) )})The SqlClient service
Section titled “The SqlClient service”SqlClient.SqlClient is the Context.Service tag you yield to get
the client. The client extends the sql constructor (so it is callable) and
adds the members below.
import { Effect } from "effect"import { SqlClient } from "effect/unstable/sql"
const program = Effect.gen(function*() { const sql = yield* SqlClient.SqlClient // SqlClient extends the sql constructor return yield* sql<{ readonly n: number }>`SELECT 1 AS n`})sql.safe
Section titled “sql.safe”A copy of the client typed as itself, intended for tools like safe-ql that want a stable client reference. In practice it is the same client value.
const safeClient = sql.safe // => the SqlClientsql.withoutTransforms()
Section titled “sql.withoutTransforms()”Returns a copy of the client with row/identifier transforms disabled (for example, the camelCase ↔ snake_case mapping some integrations install). Use it when you need the raw column names and values.
const raw = sql.withoutTransforms()// raw`SELECT first_name FROM users` => rows keep `first_name`, not `firstName`sql.reserve
Section titled “sql.reserve”A scoped Effect<Connection, SqlError, Scope> that checks out a single
connection from the pool for lower-level work. The connection is released when
the Scope closes.
import { Effect } from "effect"
const withReserved = Effect.gen(function*() { const conn = yield* sql.reserve // requires a Scope in the environment yield* conn.executeUnprepared("SET statement_timeout = 5000", [], undefined)}).pipe(Effect.scoped)sql.withTransaction(effect)
Section titled “sql.withTransaction(effect)”Runs effect inside a transaction, adding SqlError to its error channel.
Nested calls become savepoints on the same connection. (Demonstrated in
Transactions above.)
sql.withTransaction(effect) // => Effect<A, E | SqlError, R>sql.transactionService
Section titled “sql.transactionService”The per-client Context.Service tag that holds the active transaction
connection and nesting depth (readonly [conn, depth]). It is what lets a query
detect that it is inside withTransaction. You rarely touch it directly; it is
exposed for advanced integrations that coordinate transactions across clients.
sql.reactive(keys, effect)
Section titled “sql.reactive(keys, effect)”Turns a query effect into a Stream that re-emits whenever one of the given
reactivity keys is invalidated (via the Reactivity service). keys is
either an array of values or a record of table -> ids.
import { Effect, Stream } from "effect"
const liveUsers = sql.reactive( ["users"], sql<{ readonly id: number }>`SELECT id FROM users`) // => Stream<ReadonlyArray<{ id: number }>, SqlError>
const drain = Stream.runForEach(liveUsers, (rows) => Effect.log(`${rows.length} users`))sql.reactiveMailbox(keys, effect)
Section titled “sql.reactiveMailbox(keys, effect)”Like reactive, but yields a scoped Queue.Dequeue you can pull from manually
instead of a Stream. Useful when you want full control over consumption.
import { Effect, Queue } from "effect"
const program = Effect.gen(function*() { const mailbox = yield* sql.reactiveMailbox(["users"], sql`SELECT id FROM users`) const rows = yield* Queue.take(mailbox) // => latest result set}).pipe(Effect.scoped)SqlClient.SafeIntegers
Section titled “SqlClient.SafeIntegers”A Context.Reference<boolean> (default false) that integrations read to opt in
to safe (BigInt-style) integer handling for large numeric columns.
import { Effect } from "effect"import { SqlClient } from "effect/unstable/sql"
const withSafeInts = Effect.provideService(myProgram, SqlClient.SafeIntegers, true)Statement methods and properties
Section titled “Statement methods and properties”A tagged template (and sql.unsafe) produces a Statement<A>. Besides being an
Effect<ReadonlyArray<A>, SqlError>, it carries these members.
Executes and returns the raw driver result (Effect<unknown, SqlError>),
bypassing row extraction — handy when you need the driver’s native result object
(affected-row counts, command tags, etc.).
const result = yield* sql`UPDATE users SET active = ${false}`.raw// => driver-specific result object (e.g. { rowCount: 3, command: "UPDATE" }).withoutTransform
Section titled “.withoutTransform”Executes the query with row/identifier transforms disabled, returning
Effect<ReadonlyArray<A>, SqlError> with untransformed column names.
const rows = yield* sql`SELECT first_name FROM users`.withoutTransform// => [{ first_name: "Ada" }] — not transformed to `firstName`.stream
Section titled “.stream”A Stream<A, SqlError> that pulls rows incrementally from a cursor. (See
Streaming.)
sql`SELECT id FROM events`.stream // => Stream<{ id: number }, SqlError>.values
Section titled “.values”Executes and returns rows as positional arrays instead of objects:
Effect<ReadonlyArray<ReadonlyArray<unknown>>, SqlError>. Useful for compact
exports where column order is known.
const rows = yield* sql`SELECT id, name FROM users`.values// => [[1, "Ada"], [2, "Linus"]].unprepared
Section titled “.unprepared”Executes via the driver’s unprepared path (no prepared-statement cache),
returning Effect<ReadonlyArray<A>, SqlError>. Use for one-off DDL or
statements drivers refuse to prepare.
yield* sql`CREATE TABLE users (id INTEGER PRIMARY KEY)`.unprepared// => [] (executed without preparing).compile(withoutTransform?)
Section titled “.compile(withoutTransform?)”Compiles the statement to [sql, params] without executing it, using the active
dialect. Pass true to compile with transforms disabled.
const [text, params] = sql`SELECT * FROM users WHERE id = ${42}`.compile()// => ["SELECT * FROM users WHERE id = $1", [42]] (pg dialect)sql constructor helpers
Section titled “sql constructor helpers”The callable sql value (the Constructor interface) builds statements,
identifiers, fragments, and helper segments. Below, .compile() is shown on a
wrapping statement to reveal the generated SQL where it clarifies behavior.
Dialect output varies; the examples assume the pg dialect ($1, "quoting").
sql\…“ (tagged template)
Section titled “sql\…“ (tagged template)”The primary form. Interpolated values become bound parameters; nested fragments
and helpers are spliced in. Returns Statement<A>.
sql<{ readonly id: number }>`SELECT id FROM users WHERE id = ${1}`.compile()// => ["SELECT id FROM users WHERE id = $1", [1]]sql(value: string) (identifier)
Section titled “sql(value: string) (identifier)”Calling sql with a plain string returns a dialect-escaped Identifier
segment — never an executable statement. Use it for dynamic table/column names.
sql`SELECT * FROM ${sql("user accounts")}`.compile()// => ['SELECT * FROM "user accounts"', []]sql.unsafe(sql, params?)
Section titled “sql.unsafe(sql, params?)”Builds a Statement from raw SQL text plus optional bind params. The text is
not escaped — only pass SQL you control.
sql.unsafe("SELECT * FROM users WHERE id = $1", [7]).compile()// => ["SELECT * FROM users WHERE id = $1", [7]]sql.literal(sql)
Section titled “sql.literal(sql)”Returns a Fragment containing trusted SQL text inlined verbatim (no
placeholder, no escaping). Compose it into a larger statement.
sql`SELECT * FROM users ${sql.literal("ORDER BY created_at DESC")}`.compile()// => ["SELECT * FROM users ORDER BY created_at DESC", []]sql.in(value) / sql.in(column, value)
Section titled “sql.in(value) / sql.in(column, value)”With one array argument, returns an ArrayHelper that compiles to a
parenthesized placeholder list. With (column, value), returns a full
column IN (...) Fragment; an empty array yields the always-false 1=0.
sql`SELECT * FROM users WHERE id ${sql.in([1, 2, 3])}`.compile()// => ["SELECT * FROM users WHERE id ($1,$2,$3)", [1, 2, 3]]
sql`SELECT * FROM users WHERE ${sql.in("id", [1, 2])}`.compile()// => ['SELECT * FROM users WHERE "id" IN ($1,$2)', [1, 2]]
sql`SELECT * FROM users WHERE ${sql.in("id", [])}`.compile()// => ["SELECT * FROM users WHERE 1=0", []]sql.insert(value | value[])
Section titled “sql.insert(value | value[])”Builds a RecordInsertHelper from one object or an array of objects, producing
the (columns) VALUES (...) clause. Multiple objects insert multiple rows.
Chain .returning(...) to append a RETURNING clause.
sql`INSERT INTO users ${sql.insert({ name: "Ada", age: 36 })}`.compile()// => ['INSERT INTO users ("name","age") VALUES ($1,$2)', ["Ada", 36]]
sql`INSERT INTO users ${sql.insert([{ name: "Ada" }, { name: "Linus" }])}`.compile()// => ['INSERT INTO users ("name") VALUES ($1),($2)', ["Ada", "Linus"]]
sql`INSERT INTO users ${sql.insert({ name: "Ada" }).returning("*")}`.compile()// => ['INSERT INTO users ("name") VALUES ($1) RETURNING *', ["Ada"]]sql.update(value, omit?)
Section titled “sql.update(value, omit?)”Builds a RecordUpdateHelperSingle — the col = $n, ... assignments for a
single-row update. The optional second argument lists keys to omit (e.g. the
primary key). Chain .returning(...) for a RETURNING clause.
sql`UPDATE users SET ${sql.update({ name: "Ada", age: 36 })} WHERE id = ${1}`.compile()// => ['UPDATE users SET "name" = $1, "age" = $2 WHERE id = $3', ["Ada", 36, 1]]
// omit `id` from the SET list while keeping it in the objectsql`UPDATE users SET ${sql.update({ id: 1, name: "Ada" }, ["id"])} WHERE id = ${1}`.compile()// => ['UPDATE users SET "name" = $1 WHERE id = $2', ["Ada", 1]]sql.updateValues(values, alias)
Section titled “sql.updateValues(values, alias)”Builds a RecordUpdateHelper for updating multiple rows from a values list
under a table alias (typically used with UPDATE ... FROM (VALUES ...)).
sql.updateValues([{ id: 1, name: "Ada" }, { id: 2, name: "Linus" }], "data")// => RecordUpdateHelper compiled into a (VALUES ...) AS data(...) clausesql.and(clauses) / sql.or(clauses)
Section titled “sql.and(clauses) / sql.or(clauses)”Join an array of string/fragment clauses with AND / OR, wrapping the group
in parentheses. An empty array produces 1=1 (always true), which is convenient
for optional filters.
sql`SELECT * FROM users WHERE ${sql.and(["active = true", sql`age > ${18}`])}`.compile()// => ["SELECT * FROM users WHERE (active = true AND age > $1)", [18]]
sql`SELECT * FROM users WHERE ${sql.and([])}`.compile()// => ["SELECT * FROM users WHERE 1=1", []]sql.csv(values) / sql.csv(prefix, values)
Section titled “sql.csv(values) / sql.csv(prefix, values)”Joins clauses with commas (no parentheses). The two-argument form prepends a
prefix like ORDER BY or GROUP BY. An empty list produces an empty fragment.
sql`SELECT * FROM users ${sql.csv("ORDER BY", ["name", "age DESC"])}`.compile()// => ["SELECT * FROM users ORDER BY name,age DESC", []]
sql`SELECT ${sql.csv(["id", "name"])} FROM users`.compile()// => ["SELECT id,name FROM users", []]sql.join(literal, addParens?, fallback?)
Section titled “sql.join(literal, addParens?, fallback?)”The lower-level combinator behind and/or/csv: returns a function that joins
clauses with the given separator. addParens (default true) wraps multiple
clauses in parentheses; fallback (default "") is used for an empty list.
const joinPlus = sql.join(" + ", false, "0")sql`SELECT ${joinPlus(["a", "b", "c"])}`.compile()// => ["SELECT a + b + c", []]sql.onDialect({...})
Section titled “sql.onDialect({...})”Selects a value by the client’s active dialect. All five branches
(sqlite, pg, mysql, mssql, clickhouse) are required.
const upsert = sql.onDialect({ sqlite: () => sql.literal("ON CONFLICT DO NOTHING"), pg: () => sql.literal("ON CONFLICT DO NOTHING"), mysql: () => sql.literal("ON DUPLICATE KEY UPDATE id = id"), mssql: () => sql.literal(""), clickhouse: () => sql.literal("")})sql.onDialectOrElse({orElse, ...})
Section titled “sql.onDialectOrElse({orElse, ...})”Like onDialect, but orElse supplies the default and every dialect branch is
optional.
const limit = sql.onDialectOrElse({ orElse: () => sql.literal("LIMIT 10"), mssql: () => sql.literal("TOP 10")})The Dialect union is "sqlite" | "pg" | "mysql" | "mssql" | "clickhouse".
Standalone Statement exports
Section titled “Standalone Statement exports”The Statement module exports dialect-agnostic fragment builders you can use
without a SqlClient — handy in helper code that assembles fragments before a
client is available. They mirror the sql.* helpers above.
import { Statement } from "effect/unstable/sql"
Statement.and(["a = 1", "b = 2"]) // => Fragment: (a = 1 AND b = 2)Statement.or(["a = 1", "b = 2"]) // => Fragment: (a = 1 OR b = 2)Statement.csv(["id", "name"]) // => Fragment: id,nameStatement.fragment([Statement.literal("NOW()")]) // => Fragment from raw segments
Statement.isFragment(Statement.csv(["x"])) // => trueStatement.isCustom("json")(someSegment) // => guard for Custom segments of kind "json"Statement.CurrentTransformer
Section titled “Statement.CurrentTransformer”A Context.Reference<Transformer | undefined> (default undefined). A
Transformer is a hook — (self, sql, fiber, span) => Effect<Statement> — that
can rewrite or wrap every statement just before execution (for query logging,
multi-tenant rewrites, etc.). Provide it via the environment to install it.
import { Effect } from "effect"import { Statement } from "effect/unstable/sql"
const logging = Effect.provideService( program, Statement.CurrentTransformer, (self, _sql, _fiber, _span) => Effect.as(Effect.log(self.compile()[0]), self))Errors
Section titled “Errors”Queries fail with SqlError from effect/unstable/sql. SqlError is a
Schema.TaggedErrorClass whose only field is a structured reason (a tagged
union). It derives message from the reason (reason.message || reason._tag),
sets cause to the reason, and delegates isRetryable to it. Match on
error.reason._tag to handle specific database conditions.
import { Effect } from "effect"import { SqlClient, SqlError } from "effect/unstable/sql"
const createUser = Effect.fn("createUser")( function*(email: string) { const sql = yield* SqlClient.SqlClient yield* sql`INSERT INTO users ${sql.insert({ email })}` }, // SqlError is the only error channel; branch on the structured reason. // Cross-cutting handling rides as a trailing Effect.fn argument, not `.pipe`. Effect.catchTag("SqlError", (error: SqlError.SqlError) => error.reason._tag === "UniqueViolation" ? Effect.fail(new Error("email already taken")) : Effect.fail(error) ))Retrying transient failures
Section titled “Retrying transient failures”Several reasons are transient (connection drops, deadlocks, lock waits). Rather
than enumerate tags, branch on isRetryable and pair it with
Scheduling.
import { Effect, Schedule } from "effect"import { SqlError } from "effect/unstable/sql"
const resilient = query.pipe( Effect.retry({ while: (error: SqlError.SqlError) => error.isRetryable, schedule: Schedule.exponential("10 millis").pipe(Schedule.both(Schedule.recurs(5))) }))Reason reference
Section titled “Reason reference”Every SqlError wraps exactly one SqlErrorReason. Each reason is a
Schema.TaggedErrorClass carrying cause, optional message, and optional
operation, plus an isRetryable flag. Adapters translate database-specific
codes into this shared vocabulary.
reason._tag | isRetryable | Meaning |
|---|---|---|
ConnectionError | true | Connection or open failure. |
AuthenticationError | false | Invalid credentials. |
AuthorizationError | false | Permission / authorization failure. |
SqlSyntaxError | false | Invalid SQL syntax. |
UniqueViolation | false | Unique constraint violation (also carries constraint: string). |
ConstraintError | false | Non-unique constraint violation. |
DeadlockError | true | Database deadlock. |
SerializationError | true | Transaction serialization / isolation conflict. |
LockTimeoutError | true | Timed out waiting on a lock. |
StatementTimeoutError | true | Statement / query timeout. |
UnknownError | false | Unclassified database failure. |
UniqueViolation is the canonical signal for a duplicate-key conflict and adds a
best-effort constraint field:
import { Effect } from "effect"import { SqlError } from "effect/unstable/sql"
const handled = query.pipe( Effect.catchTag("SqlError", (error: SqlError.SqlError) => { if (error.reason._tag === "UniqueViolation") { // error.reason.constraint => e.g. "users_email_key" (or "unknown") return Effect.fail(new Error(`duplicate: ${error.reason.constraint}`)) } return Effect.fail(error) }))SqlError.isSqlError / SqlError.isSqlErrorReason
Section titled “SqlError.isSqlError / SqlError.isSqlErrorReason”Runtime guards for boundaries that receive unknown values (e.g. catching a
Cause defect or interop with non-Effect code).
import { SqlError } from "effect/unstable/sql"
SqlError.isSqlError(value) // => value is SqlErrorSqlError.isSqlErrorReason(value) // => value is SqlErrorReasonSqlError.SqlErrorReason (schema)
Section titled “SqlError.SqlErrorReason (schema)”A Schema.Union of all eleven reason classes, used to encode/decode reasons
(for transporting errors across RPC, persisting them, etc.). The SqlError
class itself is also a schema since it extends Schema.TaggedErrorClass.
import { Schema } from "effect"import { SqlError } from "effect/unstable/sql"
const decode = Schema.decodeUnknownSync(SqlError.SqlErrorReason)// decode(json) => one of ConnectionError | UniqueViolation | ...SqlError.classifySqliteError
Section titled “SqlError.classifySqliteError”Maps a native SQLite error cause (by its code/errno) into the appropriate
SqlErrorReason. Driver integrations use it; application code rarely calls it
directly. For example a SQLITE_CONSTRAINT_UNIQUE becomes a UniqueViolation
with an extracted constraint name.
import { SqlError } from "effect/unstable/sql"
const reason = SqlError.classifySqliteError( { code: "SQLITE_BUSY" }, { operation: "execute" })// => LockTimeoutError (isRetryable: true)SqlError.ResultLengthMismatch
Section titled “SqlError.ResultLengthMismatch”A separate Schema.TaggedErrorClass (expected, actual) raised when an
ordered batched resolver gets a different number of result rows than requests.
It is the error you handle when working with Resolvers, not a
SqlError reason.
import { SqlError } from "effect/unstable/sql"
new SqlError.ResultLengthMismatch({ expected: 3, actual: 2 }).message// => "Expected 3 results but got 2"The Connection service (advanced)
Section titled “The Connection service (advanced)”Underneath SqlClient sits SqlConnection.Connection — a Context.Service that
executes already-compiled SQL with positional parameters. Most applications never
touch it; it exists for driver authors and for code that reserves a connection
via sql.reserve.
SqlConnection.Connection— theContext.Servicetag for a low-level connection. Its methods (execute,executeRaw,executeStream,executeValues,executeUnprepared) take(sql, params, transformRows?).SqlConnection.Acquirer— the typeEffect<Connection, SqlError, Scope>: a scoped effect that checks out a connection (pool checkout, transaction pinning) and releases it when the scope closes.SqlConnection.Row— the generic row shape{ readonly [column: string]: unknown }used as the defaultStatementelement type.
import { Effect } from "effect"import { SqlConnection } from "effect/unstable/sql"
const direct = Effect.gen(function*() { const conn = yield* SqlConnection.Connection return yield* conn.execute("SELECT 1 AS n", [], undefined) // => [{ n: 1 }]})Next, see Resolvers for batching and schema-mapped queries,
or Models and migrations to generate repositories
from a Model and manage your database schema over time.