Microsoft SQL Server
The @effect/sql-mssql package provides Microsoft SQL Server support for Effect SQL.
Installation
bash
npm install @effect/sql @effect/sql-mssqlBasic Setup
typescript
import { Effect } from "effect"
import { SqlClient } from "@effect/sql"
import { MssqlClient } from "@effect/sql-mssql"
const DatabaseLive = MssqlClient.layer({
server: "localhost",
port: 1433,
database: "myapp",
username: "sa",
password: Redacted.make("secret"),
trustServerCertificate: true // For development
})
const program = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient
const users = yield* sql`SELECT * FROM users`
return users
})
Effect.runPromise(program.pipe(Effect.provide(DatabaseLive)))Configuration Options
typescript
import { Redacted, Duration } from "effect"
const DatabaseLive = MssqlClient.layer({
// Connection
server: "localhost",
port: 1433,
database: "myapp",
username: "sa",
password: Redacted.make("secret"),
// Or instance name instead of port
instanceName: "SQLEXPRESS",
// SSL/TLS
trustServerCertificate: true,
encrypt: true,
// Pool settings
maxConnections: 10,
minConnections: 2,
connectTimeout: Duration.seconds(15),
requestTimeout: Duration.seconds(30),
// Name transformations
transformQueryNames: String.camelToSnake,
transformResultNames: String.snakeToCamel
})SQL Server-Specific SQL
Identity Columns
typescript
yield* sql`
CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(255) NOT NULL,
email NVARCHAR(255) NOT NULL UNIQUE
)
`OUTPUT Clause (RETURNING equivalent)
typescript
// Get inserted row
const [user] = yield* sql`
INSERT INTO users (name, email)
OUTPUT INSERTED.*
VALUES (${"Alice"}, ${"alice@example.com"})
`
// Get updated rows
const updated = yield* sql`
UPDATE users
SET name = ${"Alice Smith"}
OUTPUT INSERTED.*
WHERE id = ${1}
`
// Get deleted rows
const deleted = yield* sql`
DELETE FROM users
OUTPUT DELETED.*
WHERE id = ${1}
`MERGE (Upsert)
typescript
yield* sql`
MERGE users AS target
USING (SELECT ${"alice@example.com"} AS email, ${"Alice"} AS name) AS source
ON target.email = source.email
WHEN MATCHED THEN
UPDATE SET name = source.name
WHEN NOT MATCHED THEN
INSERT (email, name) VALUES (source.email, source.name);
`Pagination with OFFSET-FETCH
typescript
const page = 2
const pageSize = 20
const users = yield* sql`
SELECT * FROM users
ORDER BY id
OFFSET ${(page - 1) * pageSize} ROWS
FETCH NEXT ${pageSize} ROWS ONLY
`Common Table Expressions (CTEs)
typescript
const hierarchyResults = yield* sql`
WITH EmployeeHierarchy AS (
SELECT id, name, manager_id, 0 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, eh.level + 1
FROM employees e
INNER JOIN EmployeeHierarchy eh ON e.manager_id = eh.id
)
SELECT * FROM EmployeeHierarchy
`Working with SQL Server Types
NVARCHAR for Unicode
typescript
yield* sql`
CREATE TABLE posts (
id INT IDENTITY PRIMARY KEY,
title NVARCHAR(255) NOT NULL, -- Unicode support
content NVARCHAR(MAX) -- Unlimited length
)
`DATETIME2
typescript
yield* sql`
CREATE TABLE events (
id INT IDENTITY PRIMARY KEY,
name NVARCHAR(100),
created_at DATETIME2 DEFAULT GETDATE()
)
`
yield* sql`INSERT INTO events ${sql.insert({ name: "login", createdAt: new Date() })}`BIT (Boolean)
typescript
yield* sql`
CREATE TABLE settings (
id INT IDENTITY PRIMARY KEY,
is_active BIT DEFAULT 1
)
`
// Insert boolean value
yield* sql`INSERT INTO settings ${sql.insert({ isActive: true })}`UNIQUEIDENTIFIER (UUID)
typescript
yield* sql`
CREATE TABLE sessions (
id UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY,
user_id INT NOT NULL
)
`Transactions
Basic Transaction
typescript
const sql = yield* SqlClient.SqlClient
yield* sql.withTransaction(
Effect.gen(function* () {
yield* sql`INSERT INTO orders (user_id, total) VALUES (${1}, ${100})`
yield* sql`UPDATE accounts SET balance = balance - ${100} WHERE user_id = ${1}`
})
)Transaction Isolation Levels
typescript
yield* sql.withTransaction(
Effect.gen(function* () {
yield* sql`SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`
// Your queries
})
)Error Handling
typescript
import { SqlError } from "@effect/sql"
program.pipe(
Effect.catchTag("SqlError", (error) => {
const mssqlError = error.cause as { number?: number }
switch (mssqlError.number) {
case 2627: // Unique constraint violation
return Effect.fail(new DuplicateKeyError())
case 547: // Foreign key violation
return Effect.fail(new ForeignKeyError())
case 515: // Cannot insert NULL
return Effect.fail(new RequiredFieldError())
default:
return Effect.fail(error)
}
})
)Common SQL Server error numbers:
2627- Unique constraint violation2601- Duplicate key (unique index)547- Foreign key violation515- Cannot insert NULL208- Invalid object name207- Invalid column name
Stored Procedures
typescript
// Execute a stored procedure
const results = yield* sql`EXEC GetUserById @UserId = ${1}`
// With output parameter (using raw query)
const sql = yield* SqlClient.SqlClient
const result = yield* sql.unsafe(`
DECLARE @TotalCount INT
EXEC GetUsersWithCount @TotalCount = @TotalCount OUTPUT
SELECT @TotalCount AS total_count
`)Migrations
typescript
import { MssqlMigrator } from "@effect/sql-mssql"
import { Migrator } from "@effect/sql"
const MigratorLive = MssqlMigrator.layer({
loader: Migrator.fromGlob(import.meta.glob("./migrations/*.ts"))
})Example migration:
typescript
// migrations/001_initial.ts
import { SqlClient } from "@effect/sql"
import { Effect } from "effect"
export default Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient
yield* sql`
CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(255) NOT NULL,
email NVARCHAR(255) NOT NULL UNIQUE,
created_at DATETIME2 DEFAULT GETDATE()
)
`
})Dialect-Specific Code
typescript
const result = yield* sql.onDialect({
mssql: () => sql`SELECT GETDATE()`,
pg: () => sql`SELECT NOW()`,
mysql: () => sql`SELECT NOW()`,
sqlite: () => sql`SELECT datetime('now')`,
clickhouse: () => sql`SELECT now()`
})Next Steps
- Migrations - Database schema management
- Transactions - Advanced transaction patterns