sqlite.test.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import { mkdir, mkdtemp, rm } from "node:fs/promises"
  2. import { tmpdir } from "node:os"
  3. import { join } from "node:path"
  4. import { Database } from "bun:sqlite"
  5. import { expect, test } from "bun:test"
  6. import { SqliteClient } from "@effect/sql-sqlite-bun"
  7. import { eq, sql } from "drizzle-orm"
  8. import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
  9. import { Effect } from "effect"
  10. import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
  11. import { isSqlError } from "effect/unstable/sql/SqlError"
  12. import { EffectDrizzleSqlite } from "../src"
  13. const users = sqliteTable("users", {
  14. id: integer().primaryKey({ autoIncrement: true }),
  15. name: text().notNull(),
  16. })
  17. const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
  18. Effect.runPromise(
  19. effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped),
  20. )
  21. const makeDb = Effect.gen(function* () {
  22. const db = yield* EffectDrizzleSqlite.makeWithDefaults()
  23. yield* db.run(sql`create table users (id integer primary key autoincrement, name text not null)`)
  24. return db
  25. })
  26. const createMigrationsFolder = async () => {
  27. const migrationsFolder = await mkdtemp(join(tmpdir(), "effect-drizzle-sqlite-"))
  28. await mkdir(join(migrationsFolder, "20240101000000_create_migrated_users"), { recursive: true })
  29. await Bun.write(
  30. join(migrationsFolder, "20240101000000_create_migrated_users", "migration.sql"),
  31. "create table migrated_users (id integer primary key autoincrement, name text not null);",
  32. )
  33. return migrationsFolder
  34. }
  35. test("selects rows through Effect-yieldable query builders", async () => {
  36. await run(
  37. Effect.gen(function* () {
  38. const db = yield* makeDb
  39. yield* db.insert(users).values({ name: "Ada" })
  40. expect(yield* db.select().from(users)).toEqual([{ id: 1, name: "Ada" }])
  41. expect(yield* db.select({ id: users.id }).from(users).where(eq(users.name, "Ada")).get()).toEqual({ id: 1 })
  42. }),
  43. )
  44. })
  45. test("commits successful transactions", async () => {
  46. await run(
  47. Effect.gen(function* () {
  48. const db = yield* makeDb
  49. yield* db.transaction((tx) => tx.insert(users).values({ name: "Grace" }), { behavior: "immediate" })
  50. expect(yield* db.select().from(users)).toEqual([{ id: 1, name: "Grace" }])
  51. }),
  52. )
  53. })
  54. test("rolls back failed transactions", async () => {
  55. await run(
  56. Effect.gen(function* () {
  57. const db = yield* makeDb
  58. yield* db
  59. .transaction((tx) =>
  60. tx
  61. .insert(users)
  62. .values({ name: "Linus" })
  63. .pipe(Effect.andThen(Effect.fail("boom"))),
  64. )
  65. .pipe(Effect.ignore)
  66. expect(yield* db.select().from(users)).toEqual([])
  67. }),
  68. )
  69. })
  70. test("rolls back explicit transaction rollback", async () => {
  71. await run(
  72. Effect.gen(function* () {
  73. const db = yield* makeDb
  74. yield* db
  75. .transaction((tx) =>
  76. tx
  77. .insert(users)
  78. .values({ name: "Barbara" })
  79. .pipe(Effect.andThen(Effect.fail(tx.rollback()))),
  80. )
  81. .pipe(Effect.ignore)
  82. expect(yield* db.select().from(users)).toEqual([])
  83. }),
  84. )
  85. })
  86. test("preserves failed transaction begin errors", async () => {
  87. const dir = await mkdtemp(join(tmpdir(), "effect-drizzle-sqlite-"))
  88. const filename = join(dir, "locked.db")
  89. const holder = new Database(filename)
  90. try {
  91. holder.run("create table users (id integer primary key autoincrement, name text not null)")
  92. holder.run("pragma busy_timeout = 0")
  93. holder.run("begin immediate")
  94. await Effect.runPromise(
  95. Effect.gen(function* () {
  96. const db = yield* EffectDrizzleSqlite.makeWithDefaults()
  97. yield* db.run(sql`pragma busy_timeout = 0`)
  98. const error = yield* db
  99. .transaction((tx) => tx.insert(users).values({ name: "Blocked" }), { behavior: "immediate" })
  100. .pipe(Effect.flip)
  101. if (!isSqlError(error)) throw new Error("Expected SqlError")
  102. expect(error.reason._tag).toBe("LockTimeoutError")
  103. expect(error.reason.cause instanceof Error ? error.reason.cause.message : "").toContain("database is locked")
  104. }).pipe(Effect.provide(SqliteClient.layer({ filename, disableWAL: true })), Effect.scoped),
  105. )
  106. } finally {
  107. if (holder.inTransaction) holder.run("rollback")
  108. holder.close()
  109. await rm(dir, { recursive: true, force: true })
  110. }
  111. })
  112. test("supports returning and rejects empty update sets", async () => {
  113. await run(
  114. Effect.gen(function* () {
  115. const db = yield* makeDb
  116. const inserted = yield* db.insert(users).values({ name: "Ada" }).returning({ id: users.id, name: users.name })
  117. expect(inserted).toEqual([{ id: 1, name: "Ada" }])
  118. const updated = yield* db.update(users).set({ name: "Grace" }).where(eq(users.id, 1)).returning()
  119. expect(updated).toEqual([{ id: 1, name: "Grace" }])
  120. const deleted = yield* db.delete(users).where(eq(users.id, 1)).returning({ id: users.id })
  121. expect(deleted).toEqual([{ id: 1 }])
  122. expect(() => db.update(users).set({ name: undefined })).toThrow("No values to set")
  123. }),
  124. )
  125. })
  126. test("runs migrations once and records migration metadata", async () => {
  127. const migrationsFolder = await createMigrationsFolder()
  128. try {
  129. await run(
  130. Effect.gen(function* () {
  131. const db = yield* EffectDrizzleSqlite.makeWithDefaults()
  132. yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder })
  133. yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder })
  134. yield* db.run(sql`insert into migrated_users (name) values ('Margaret')`)
  135. expect(yield* db.all<{ name: string }>(sql`select name from migrated_users`)).toEqual([{ name: "Margaret" }])
  136. expect(yield* db.all<{ name: string | null }>(sql`select name from __drizzle_migrations`)).toEqual([
  137. { name: "20240101000000_create_migrated_users" },
  138. ])
  139. }),
  140. )
  141. } finally {
  142. await rm(migrationsFolder, { recursive: true, force: true })
  143. }
  144. })