| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- import { mkdir, mkdtemp, rm } from "node:fs/promises"
- import { tmpdir } from "node:os"
- import { join } from "node:path"
- import { Database } from "bun:sqlite"
- import { expect, test } from "bun:test"
- import { SqliteClient } from "@effect/sql-sqlite-bun"
- import { eq, sql } from "drizzle-orm"
- import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
- import { Effect } from "effect"
- import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
- import { isSqlError } from "effect/unstable/sql/SqlError"
- import { EffectDrizzleSqlite } from "../src"
- const users = sqliteTable("users", {
- id: integer().primaryKey({ autoIncrement: true }),
- name: text().notNull(),
- })
- const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
- Effect.runPromise(
- effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped),
- )
- const makeDb = Effect.gen(function* () {
- const db = yield* EffectDrizzleSqlite.makeWithDefaults()
- yield* db.run(sql`create table users (id integer primary key autoincrement, name text not null)`)
- return db
- })
- const createMigrationsFolder = async () => {
- const migrationsFolder = await mkdtemp(join(tmpdir(), "effect-drizzle-sqlite-"))
- await mkdir(join(migrationsFolder, "20240101000000_create_migrated_users"), { recursive: true })
- await Bun.write(
- join(migrationsFolder, "20240101000000_create_migrated_users", "migration.sql"),
- "create table migrated_users (id integer primary key autoincrement, name text not null);",
- )
- return migrationsFolder
- }
- test("selects rows through Effect-yieldable query builders", async () => {
- await run(
- Effect.gen(function* () {
- const db = yield* makeDb
- yield* db.insert(users).values({ name: "Ada" })
- expect(yield* db.select().from(users)).toEqual([{ id: 1, name: "Ada" }])
- expect(yield* db.select({ id: users.id }).from(users).where(eq(users.name, "Ada")).get()).toEqual({ id: 1 })
- }),
- )
- })
- test("commits successful transactions", async () => {
- await run(
- Effect.gen(function* () {
- const db = yield* makeDb
- yield* db.transaction((tx) => tx.insert(users).values({ name: "Grace" }), { behavior: "immediate" })
- expect(yield* db.select().from(users)).toEqual([{ id: 1, name: "Grace" }])
- }),
- )
- })
- test("rolls back failed transactions", async () => {
- await run(
- Effect.gen(function* () {
- const db = yield* makeDb
- yield* db
- .transaction((tx) =>
- tx
- .insert(users)
- .values({ name: "Linus" })
- .pipe(Effect.andThen(Effect.fail("boom"))),
- )
- .pipe(Effect.ignore)
- expect(yield* db.select().from(users)).toEqual([])
- }),
- )
- })
- test("rolls back explicit transaction rollback", async () => {
- await run(
- Effect.gen(function* () {
- const db = yield* makeDb
- yield* db
- .transaction((tx) =>
- tx
- .insert(users)
- .values({ name: "Barbara" })
- .pipe(Effect.andThen(Effect.fail(tx.rollback()))),
- )
- .pipe(Effect.ignore)
- expect(yield* db.select().from(users)).toEqual([])
- }),
- )
- })
- test("preserves failed transaction begin errors", async () => {
- const dir = await mkdtemp(join(tmpdir(), "effect-drizzle-sqlite-"))
- const filename = join(dir, "locked.db")
- const holder = new Database(filename)
- try {
- holder.run("create table users (id integer primary key autoincrement, name text not null)")
- holder.run("pragma busy_timeout = 0")
- holder.run("begin immediate")
- await Effect.runPromise(
- Effect.gen(function* () {
- const db = yield* EffectDrizzleSqlite.makeWithDefaults()
- yield* db.run(sql`pragma busy_timeout = 0`)
- const error = yield* db
- .transaction((tx) => tx.insert(users).values({ name: "Blocked" }), { behavior: "immediate" })
- .pipe(Effect.flip)
- if (!isSqlError(error)) throw new Error("Expected SqlError")
- expect(error.reason._tag).toBe("LockTimeoutError")
- expect(error.reason.cause instanceof Error ? error.reason.cause.message : "").toContain("database is locked")
- }).pipe(Effect.provide(SqliteClient.layer({ filename, disableWAL: true })), Effect.scoped),
- )
- } finally {
- if (holder.inTransaction) holder.run("rollback")
- holder.close()
- await rm(dir, { recursive: true, force: true })
- }
- })
- test("supports returning and rejects empty update sets", async () => {
- await run(
- Effect.gen(function* () {
- const db = yield* makeDb
- const inserted = yield* db.insert(users).values({ name: "Ada" }).returning({ id: users.id, name: users.name })
- expect(inserted).toEqual([{ id: 1, name: "Ada" }])
- const updated = yield* db.update(users).set({ name: "Grace" }).where(eq(users.id, 1)).returning()
- expect(updated).toEqual([{ id: 1, name: "Grace" }])
- const deleted = yield* db.delete(users).where(eq(users.id, 1)).returning({ id: users.id })
- expect(deleted).toEqual([{ id: 1 }])
- expect(() => db.update(users).set({ name: undefined })).toThrow("No values to set")
- }),
- )
- })
- test("runs migrations once and records migration metadata", async () => {
- const migrationsFolder = await createMigrationsFolder()
- try {
- await run(
- Effect.gen(function* () {
- const db = yield* EffectDrizzleSqlite.makeWithDefaults()
- yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder })
- yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder })
- yield* db.run(sql`insert into migrated_users (name) values ('Margaret')`)
- expect(yield* db.all<{ name: string }>(sql`select name from migrated_users`)).toEqual([{ name: "Margaret" }])
- expect(yield* db.all<{ name: string | null }>(sql`select name from __drizzle_migrations`)).toEqual([
- { name: "20240101000000_create_migrated_users" },
- ])
- }),
- )
- } finally {
- await rm(migrationsFolder, { recursive: true, force: true })
- }
- })
|