# 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.

```ts
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

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.

```ts
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](#sql-constructor-helpers)
below.
**literal vs. interpolation:** Interpolating a value (`${value}`) always binds a parameter and is safe.
`sql.literal` and `sql.unsafe` bypass escaping and must only receive SQL you
control — never untrusted input.

## 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.

```ts
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](https://effect.plants.sh/sql/resolvers/).

## Streaming large result sets

Loading millions of rows into an array is wasteful. Every statement exposes a
`.stream` property — a [Stream](https://effect.plants.sh/streaming/) that pulls rows from the cursor
incrementally, so memory stays bounded regardless of result size.

```ts
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

`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.

```ts
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"))
      }
    })
  )
})
```
**Joining a transaction:** A query only joins the surrounding transaction when it is run through the *same*
`SqlClient` service. A query is tracked automatically because the transaction
connection is threaded through context — you do not pass it around manually.
Top-level transactions acquire the configured transaction connection and issue
the dialect's `BEGIN`/`COMMIT`/`ROLLBACK`; nested transactions reuse that same
connection and issue savepoint SQL instead.

## The `SqlClient` service

`SqlClient.SqlClient` is the [Context.Service](https://effect.plants.sh/context/) tag you yield to get
the client. The client extends the `sql` constructor (so it is callable) and
adds the members below.

```ts
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`

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.

```ts
const safeClient = sql.safe // => the SqlClient
```

### `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.

```ts
const raw = sql.withoutTransforms()
// raw`SELECT first_name FROM users` => rows keep `first_name`, not `firstName`
```

### `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.

```ts
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)`

Runs `effect` inside a transaction, adding `SqlError` to its error channel.
Nested calls become savepoints on the same connection. (Demonstrated in
[Transactions](#transactions) above.)

```ts
sql.withTransaction(effect) // => Effect<A, E | SqlError, R>
```

### `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)`

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`.

```ts
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)`

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.

```ts
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`

A `Context.Reference<boolean>` (default `false`) that integrations read to opt in
to safe (BigInt-style) integer handling for large numeric columns.

```ts
import { Effect } from "effect"
import { SqlClient } from "effect/unstable/sql"

const withSafeInts = Effect.provideService(myProgram, SqlClient.SafeIntegers, true)
```

## `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.

### `.raw`

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.).

```ts
const result = yield* sql`UPDATE users SET active = ${false}`.raw
// => driver-specific result object (e.g. { rowCount: 3, command: "UPDATE" })
```

### `.withoutTransform`

Executes the query with row/identifier transforms disabled, returning
`Effect<ReadonlyArray<A>, SqlError>` with untransformed column names.

```ts
const rows = yield* sql`SELECT first_name FROM users`.withoutTransform
// => [{ first_name: "Ada" }] — not transformed to `firstName`
```

### `.stream`

A `Stream<A, SqlError>` that pulls rows incrementally from a cursor. (See
[Streaming](#streaming-large-result-sets).)

```ts
sql`SELECT id FROM events`.stream // => Stream<{ id: number }, SqlError>
```

### `.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.

```ts
const rows = yield* sql`SELECT id, name FROM users`.values
// => [[1, "Ada"], [2, "Linus"]]
```

### `.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.

```ts
yield* sql`CREATE TABLE users (id INTEGER PRIMARY KEY)`.unprepared
// => [] (executed without preparing)
```

### `.compile(withoutTransform?)`

Compiles the statement to `[sql, params]` without executing it, using the active
dialect. Pass `true` to compile with transforms disabled.

```ts
const [text, params] = sql`SELECT * FROM users WHERE id = ${42}`.compile()
// => ["SELECT * FROM users WHERE id = $1", [42]]  (pg dialect)
```

## `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)

The primary form. Interpolated values become bound parameters; nested fragments
and helpers are spliced in. Returns `Statement<A>`.

```ts
sql<{ readonly id: number }>`SELECT id FROM users WHERE id = ${1}`.compile()
// => ["SELECT id FROM users WHERE id = $1", [1]]
```

### `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.

```ts
sql`SELECT * FROM ${sql("user accounts")}`.compile()
// => ['SELECT * FROM "user accounts"', []]
```

### `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.

```ts
sql.unsafe("SELECT * FROM users WHERE id = $1", [7]).compile()
// => ["SELECT * FROM users WHERE id = $1", [7]]
```

### `sql.literal(sql)`

Returns a `Fragment` containing trusted SQL text inlined verbatim (no
placeholder, no escaping). Compose it into a larger statement.

```ts
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)`

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`.

```ts
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[])`

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.

```ts
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?)`

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.

```ts
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 object
sql`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)`

Builds a `RecordUpdateHelper` for updating **multiple** rows from a values list
under a table alias (typically used with `UPDATE ... FROM (VALUES ...)`).

```ts
sql.updateValues([{ id: 1, name: "Ada" }, { id: 2, name: "Linus" }], "data")
// => RecordUpdateHelper compiled into a (VALUES ...) AS data(...) clause
```
**Not supported in SQLite:** `sql.updateValues` relies on a dialect's multi-row `UPDATE ... FROM (VALUES)`
support and is **not supported by the SQLite compiler**.

### `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.

```ts
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)`

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.

```ts
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?)`

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.

```ts
const joinPlus = sql.join(" + ", false, "0")
sql`SELECT ${joinPlus(["a", "b", "c"])}`.compile()
// => ["SELECT a + b + c", []]
```

### `sql.onDialect({...})`

Selects a value by the client's active dialect. All five branches
(`sqlite`, `pg`, `mysql`, `mssql`, `clickhouse`) are required.

```ts
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, ...})`

Like `onDialect`, but `orElse` supplies the default and every dialect branch is
optional.

```ts
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

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.

```ts
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,name
Statement.fragment([Statement.literal("NOW()")]) // => Fragment from raw segments

Statement.isFragment(Statement.csv(["x"])) // => true
Statement.isCustom("json")(someSegment) // => guard for Custom segments of kind "json"
```

### `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.

```ts
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

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.

```ts
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

Several reasons are transient (connection drops, deadlocks, lock waits). Rather
than enumerate tags, branch on `isRetryable` and pair it with
[Scheduling](https://effect.plants.sh/scheduling/).

```ts
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

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:

```ts
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`

Runtime guards for boundaries that receive unknown values (e.g. catching a
`Cause` defect or interop with non-Effect code).

```ts
import { SqlError } from "effect/unstable/sql"

SqlError.isSqlError(value) // => value is SqlError
SqlError.isSqlErrorReason(value) // => value is SqlErrorReason
```

### `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`.

```ts
import { Schema } from "effect"
import { SqlError } from "effect/unstable/sql"

const decode = Schema.decodeUnknownSync(SqlError.SqlErrorReason)
// decode(json) => one of ConnectionError | UniqueViolation | ...
```

### `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.

```ts
import { SqlError } from "effect/unstable/sql"

const reason = SqlError.classifySqliteError(
  { code: "SQLITE_BUSY" },
  { operation: "execute" }
)
// => LockTimeoutError (isRetryable: true)
```

### `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](https://effect.plants.sh/sql/resolvers/), not a
`SqlError` reason.

```ts
import { SqlError } from "effect/unstable/sql"

new SqlError.ResultLengthMismatch({ expected: 3, actual: 2 }).message
// => "Expected 3 results but got 2"
```

## 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` — the `Context.Service` tag for a low-level
  connection. Its methods (`execute`, `executeRaw`, `executeStream`,
  `executeValues`, `executeUnprepared`) take `(sql, params, transformRows?)`.
- `SqlConnection.Acquirer` — the type `Effect<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 default `Statement` element type.

```ts
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 }]
})
```
**Transactions live at the client layer:** Mixing a manually reserved/raw `Connection` with transactional `SqlClient`
queries can bypass the atomic boundary, since transaction coordination is tracked
on the client, not the bare connection.

Next, see [Resolvers](https://effect.plants.sh/sql/resolvers/) for batching and schema-mapped queries,
or [Models and migrations](https://effect.plants.sh/sql/models-and-migrations/) to generate repositories
from a `Model` and manage your database schema over time.