tsql
.md

Effect

@bms/tsql/effect is an optional Effect adapter. It imports effect as a peer dependency, so applications use their installed Effect version.

Install both packages in the application:

pnpm add @bms/tsql effect

Promise connections

Use connect from the Effect subpath with the same SqlConnection shape as core tsql.

import { Effect } from "effect";
import { sqliteDialect, type SqlConnection } from "@bms/tsql";
import { connect } from "@bms/tsql/effect";
import { schema } from "./schema";

const connection: SqlConnection = {
	execute: async (sql, params) => {
		return { rows: [] };
	},
};

const db = connect({
	schema,
	dialect: sqliteDialect(),
	connection,
});

const program = db.run(
	db.from("users").select(({ users }) => ({
		id: users.id,
		email: users.email,
	})),
);

const users = await Effect.runPromise(program);

db.run(query) returns Effect.Effect<TResult, TSqlError>. It reuses the query object's compile and result-mapping logic, including dialect value decoding.

Effect-native connections

If your driver is already wrapped in Effect, use connectFromEffect.

import { Effect } from "effect";
import { sqliteDialect } from "@bms/tsql";
import { connectFromEffect, type EffectSqlConnection } from "@bms/tsql/effect";
import { schema } from "./schema";

type DriverError = { readonly _tag: "DriverError"; readonly cause: unknown };

const connection: EffectSqlConnection<DriverError> = {
	execute: (sql, params) =>
		Effect.tryPromise({
			try: () => driver.execute(sql, params),
			catch: (cause): DriverError => ({ _tag: "DriverError", cause }),
		}),
};

const db = yield* connectFromEffect({
	schema,
	dialect: sqliteDialect(),
	connection,
});

db.run(...) keeps the driver error inside TSqlError.reason.

Transactions

Use db.transaction to let Effect handle commit and rollback.

const createUser = db.transaction((tx) =>
	tx.run(
		tx.insert("users").values({
			email: "alice@example.com",
			name: "Alice",
			createdAt: new Date(),
		}),
	),
);

If the callback succeeds, transaction commits. If the callback fails, defects, or is interrupted, it rolls back. The transaction callback receives EffectTransaction, which omits manual commit, rollback, begin, and migrate from its public type.

Migrations

Use migrateEffect in Effect bootstraps.

await Effect.runPromise(db.migrateEffect());

Standalone helpers

You can also wrap a core Database without using the Effect-aware connect.

import { execute, migrate, transaction } from "@bms/tsql/effect";

const rows = yield* execute(db.from("users").select(({ users }) => ({ id: users.id })));
yield* migrate(db);
yield* transaction(db, (tx) => tx.run(tx.delete("users").where(({ users }) => users.id.eq(1, db.dialect))));

Errors

Failures use TSqlError.

class TSqlError<TReason = unknown> extends Error {
	readonly _tag: "TSqlError";
	readonly operation: "execute" | "begin" | "commit" | "rollback" | "migrate";
	readonly query: CompiledQuery | undefined;
	readonly reason: TReason;
}

For Effect-native connections, reason includes the driver error type plus possible Error values from query compilation or row mapping.