File size: 6,033 Bytes
9697ede | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | 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 })
}
})
|