Skip to content

SqlClient

The SqlClient is the primary interface for interacting with your database. It's provided as an Effect service, meaning you access it through the Effect context system.

Accessing the Client

The client is accessed using SqlClient.SqlClient:

typescript
import { Effect } from "effect"
import { SqlClient } from "@effect/sql"

const program = Effect.gen(function* () {
  // Yield the client from context
  const sql = yield* SqlClient.SqlClient
  
  // Now use it to run queries
  const users = yield* sql`SELECT * FROM users`
})

The actual implementation comes from a database-specific Layer:

typescript
import { PgClient } from "@effect/sql-pg"

const program = /* ... */

// Provide the PostgreSQL implementation
Effect.runPromise(
  program.pipe(Effect.provide(PgClient.layer({ /* config */ })))
)

This design has several benefits:

  • Testability - Easily swap implementations for testing
  • Flexibility - Change databases without changing business logic
  • Composability - The client is available anywhere in your Effect code

The Tagged Template Interface

The client is callable as a tagged template literal:

typescript
const sql = yield* SqlClient.SqlClient

// Basic query
const users = yield* sql`SELECT * FROM users`

// With parameters
const name = "Alice"
const filteredUsers = yield* sql`SELECT * FROM users WHERE name = ${name}`

How Parameters Work

When you interpolate values with ${}, they become parameterized query values:

typescript
const id = 5
const name = "Alice"

const users = yield* sql`
  SELECT * FROM users 
  WHERE id = ${id} AND name = ${name}
`

// Compiles to:
// PostgreSQL: SELECT * FROM users WHERE id = $1 AND name = $2
// MySQL:      SELECT * FROM users WHERE id = ? AND name = ?
// SQLite:     SELECT * FROM users WHERE id = ? AND name = ?
// Parameters: [5, "Alice"]

This prevents SQL injection because values are never interpolated into the SQL string—they're passed separately to the database driver.

Type Annotations

Specify the expected result type using a generic:

typescript
interface User {
  id: number
  name: string
  email: string
}

// Results are typed as User[]
const users = yield* sql<User>`SELECT id, name, email FROM users`

Note that this is a declaration, not a runtime check. The database might return different data. For runtime validation, use Schema integration.

Client Methods

Beyond the template literal interface, the client provides several methods:

sql.unsafe

Execute raw SQL strings (use with caution):

typescript
// When you need to construct SQL dynamically
const tableName = "users" // Must be trusted!
const users = yield* sql.unsafe(`SELECT * FROM ${tableName}`)

WARNING

Only use unsafe with trusted input. Never interpolate user input directly.

sql.insert

Generate INSERT clause values:

typescript
// Single row
yield* sql`INSERT INTO users ${sql.insert({ name: "Alice", email: "alice@example.com" })}`
// → INSERT INTO users ("name","email") VALUES ($1,$2)

// Multiple rows
yield* sql`INSERT INTO users ${sql.insert([
  { name: "Alice", email: "alice@example.com" },
  { name: "Bob", email: "bob@example.com" }
])}`
// → INSERT INTO users ("name","email") VALUES ($1,$2),($3,$4)

With RETURNING clause:

typescript
const [newUser] = yield* sql`
  INSERT INTO users ${sql.insert({ name: "Alice" }).returning("*")}
`

sql.update

Generate SET clause for UPDATE statements:

typescript
// Update a single record
yield* sql`
  UPDATE users 
  SET ${sql.update({ name: "Alice Smith", email: "alice.smith@example.com" })}
  WHERE id = ${1}
`
// → UPDATE users SET "name" = $1, "email" = $2 WHERE id = $3

// Omit specific fields from the update
yield* sql`
  UPDATE users 
  SET ${sql.update({ id: 1, name: "Alice" }, ["id"])}
  WHERE id = ${1}
`
// → UPDATE users SET "name" = $1 WHERE id = $2
// (id is omitted from the SET clause)

sql.updateValues (Batch Updates)

Update multiple rows efficiently (PostgreSQL, MySQL, MSSQL):

typescript
yield* sql`
  UPDATE users 
  SET name = data.name 
  FROM ${sql.updateValues([{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }], "data")}
  WHERE users.id = data.id
`

INFO

updateValues is not supported in SQLite. Use a transaction with individual updates instead.

sql.in

Generate IN clause:

typescript
const ids = [1, 2, 3]

// Just the values
yield* sql`SELECT * FROM users WHERE id IN ${sql.in(ids)}`
// → SELECT * FROM users WHERE id IN ($1,$2,$3)

// With column name (handles empty arrays)
yield* sql`SELECT * FROM users WHERE ${sql.in("id", ids)}`
// → SELECT * FROM users WHERE "id" IN ($1,$2,$3)

// Empty array produces a false condition
yield* sql`SELECT * FROM users WHERE ${sql.in("id", [])}`
// → SELECT * FROM users WHERE 1=0

sql.and / sql.or

Combine conditions:

typescript
const conditions = [
  sql`name = ${"Alice"}`,
  sql`age > ${18}`,
  sql`active = ${true}`
]

yield* sql`SELECT * FROM users WHERE ${sql.and(conditions)}`
// → SELECT * FROM users WHERE (name = $1 AND age > $2 AND active = $3)

yield* sql`SELECT * FROM users WHERE ${sql.or(conditions)}`
// → SELECT * FROM users WHERE (name = $1 OR age > $2 OR active = $3)

sql.csv

Generate comma-separated values:

typescript
const columns = ["id", "name", "email"]

yield* sql`SELECT ${sql.csv(columns)} FROM users`
// → SELECT "id","name","email" FROM users

// With prefix (useful for ORDER BY)
yield* sql`SELECT * FROM users ORDER BY ${sql.csv("ORDER BY", ["name", "created_at"])}`
// → SELECT * FROM users ORDER BY "name","created_at"

sql.literal

Insert raw SQL (no escaping):

typescript
const orderBy = sql.literal("created_at DESC")
yield* sql`SELECT * FROM users ORDER BY ${orderBy}`

WARNING

Only use literal with trusted input.

Identifier Escaping

Pass a string directly to sql() to create an escaped identifier:

typescript
const tableName = "users"
const columnName = "name"

yield* sql`SELECT ${sql(columnName)} FROM ${sql(tableName)}`
// → SELECT "name" FROM "users"

Dialect-Specific Code

Use onDialect or onDialectOrElse for database-specific SQL:

typescript
const result = sql.onDialect({
  pg: () => sql`SELECT NOW()`,
  mysql: () => sql`SELECT NOW()`,
  sqlite: () => sql`SELECT datetime('now')`,
  mssql: () => sql`SELECT GETDATE()`,
  clickhouse: () => sql`SELECT now()`
})

// Or with a fallback
const result = sql.onDialectOrElse({
  pg: () => sql`SELECT NOW()`,
  orElse: () => sql`SELECT CURRENT_TIMESTAMP`
})

Database-Specific Clients

Each database adapter provides its own client type with additional methods:

typescript
import { PgClient } from "@effect/sql-pg"

const program = Effect.gen(function* () {
  // Get the PostgreSQL-specific client
  const pg = yield* PgClient.PgClient
  
  // PostgreSQL-specific: LISTEN/NOTIFY
  const notifications = pg.listen("my_channel")
  
  // PostgreSQL-specific: JSON helper
  yield* pg`INSERT INTO data ${pg.insert({ config: pg.json({ key: "value" }) })}`
})

See the database-specific documentation for details:

Client Options

Transform Functions

Automatically transform column and parameter names:

typescript
import { String } from "effect"
import { PgClient } from "@effect/sql-pg"

const DatabaseLive = PgClient.layer({
  database: "myapp",
  // Transform camelCase to snake_case for queries
  transformQueryNames: String.camelToSnake,
  // Transform snake_case to camelCase for results  
  transformResultNames: String.snakeToCamel
})

This allows you to use camelCase in TypeScript while your database uses snake_case:

typescript
// TypeScript uses camelCase
yield* sql`SELECT * FROM users WHERE ${sql.in("createdAt", dates)}`
// Query uses snake_case: WHERE "created_at" IN (...)

// Results come back as camelCase
const users = yield* sql<{ createdAt: Date }>`SELECT created_at FROM users`
// users[0].createdAt (not users[0].created_at)

withoutTransforms

Temporarily disable transformations:

typescript
const sql = yield* SqlClient.SqlClient
const rawSql = sql.withoutTransforms()

// This query won't have transformations applied
const result = yield* rawSql`SELECT * FROM users`

Connection Management

Reserving Connections

For operations that need a dedicated connection:

typescript
Effect.scoped(
  Effect.gen(function* () {
    const sql = yield* SqlClient.SqlClient
    const connection = yield* sql.reserve
    
    // Use the connection directly
    // Connection is released when the scope closes
  })
)

Transactions

Wrap effects in transactions:

typescript
const sql = yield* SqlClient.SqlClient

yield* sql.withTransaction(
  Effect.gen(function* () {
    yield* sql`INSERT INTO users ...`
    yield* sql`INSERT INTO profiles ...`
  })
)

See Transactions for details.

Error Handling

SQL operations can fail with SqlError:

typescript
import { SqlError } from "@effect/sql"

const program = Effect.gen(function* () {
  const sql = yield* SqlClient.SqlClient
  const users = yield* sql`SELECT * FROM users`
  return users
})

// Handle SQL errors
program.pipe(
  Effect.catchTag("SqlError", (error) => {
    console.error("Database error:", error.message)
    console.error("Cause:", error.cause) // Original driver error
    return Effect.succeed([])
  })
)

See Error Handling for more details.

Next Steps

Released under the MIT License.