1import { LibsqlClient } from "@effect/sql-libsql"
2import { assert, describe, it, layer } from "@effect/vitest"
3import { Effect, Exit, Layer, Option } from "effect"
4import * as Reactivity from "effect/reactivity/Reactivity"
5import { LibsqlContainer } from "./util.ts"
6
7const Migrations = Layer.effectDiscard(
8 LibsqlClient.LibsqlClient.pipe(
9 Effect.andThen((sql) =>
10 Effect.acquireRelease(
11 sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`,
12 () => sql`DROP TABLE test;`.pipe(Effect.ignore)
13 )
14 )
15 )
16)
17
18describe("Client", () => {
19 it.effect("exposes the active transaction service for each client", () =>
20 Effect.gen(function*() {
21 const a = yield* LibsqlClient.make({ url: ":memory:" })
22 const b = yield* LibsqlClient.make({ url: ":memory:" })
23 const active = Effect.all([
24 Effect.map(Effect.serviceOption(a.transactionService), Option.isSome),
25 Effect.map(Effect.serviceOption(b.transactionService), Option.isSome)
26 ])
27
28 assert.deepStrictEqual(yield* active, [false, false])
29 yield* a.withTransaction(Effect.gen(function*() {
30 assert.deepStrictEqual(yield* active, [true, false])
31 assert.deepStrictEqual(yield* b.withTransaction(active), [true, true])
32 assert.deepStrictEqual(yield* active, [true, false])
33 }))
34 assert.deepStrictEqual(yield* active, [false, false])
35 }).pipe(Effect.provide(Reactivity.layer)))
36
37 it.effect("releases completed nested savepoints", () =>
38 Effect.gen(function*() {
39 const sql = yield* LibsqlClient.make({ url: ":memory:" })
40 yield* sql`CREATE TABLE savepoint_release (value INTEGER)`
41 yield* sql.withTransaction(Effect.gen(function*() {
42 for (const rollback of [false, true]) {
43 yield* sql.withTransaction(
44 sql`INSERT INTO savepoint_release VALUES (1)`.pipe(
45 Effect.andThen(rollback ? Effect.fail("rollback") : Effect.void)
46 )
47 ).pipe(Effect.ignore)
48 const error = yield* sql`RELEASE SAVEPOINT effect_sql_1`.unprepared.pipe(Effect.flip)
49 assert.strictEqual(error._tag, "SqlError")
50 }
51 }))
52 assert.deepStrictEqual(yield* sql`SELECT value FROM savepoint_release`, [{ value: 1 }])
53 const error = yield* sql.withTransaction(
54 sql.withTransaction(sql`INSERT INTO savepoint_release VALUES (2)`).pipe(
55 Effect.andThen(Effect.fail("outer rollback"))
56 )
57 ).pipe(Effect.flip)
58 assert.strictEqual(error, "outer rollback")
59 assert.deepStrictEqual(yield* sql`SELECT value FROM savepoint_release`, [{ value: 1 }])
60 }).pipe(Effect.provide(Reactivity.layer)))
61
62 it.effect("keeps transactions isolated between clients", () =>
63 Effect.gen(function*() {
64 const a = yield* LibsqlClient.make({ url: ":memory:" })
65 const b = yield* LibsqlClient.make({ url: ":memory:" })
66
67 yield* a`CREATE TABLE marker (owner TEXT NOT NULL)`
68 yield* b`CREATE TABLE marker (owner TEXT NOT NULL)`
69 yield* a`INSERT INTO marker VALUES ('A')`
70 yield* b`INSERT INTO marker VALUES ('B')`
71
72 const rows = yield* a.withTransaction(b`SELECT owner FROM marker`)
73 assert.deepStrictEqual(rows, [{ owner: "B" }])
74 }).pipe(
75 Effect.scoped,
76 Effect.provide(Reactivity.layer)
77 ))
78
79 it.effect("releases transaction serialization after begin fails", () => {
80 let transactionCalls = 0
81 const transaction = {
82 execute: () => Promise.resolve({ rows: [] }),
83 commit: () => Promise.resolve(),
84 rollback: () => Promise.resolve()
85 }
86 const liveClient = {
87 execute: () => Promise.resolve({ rows: [] }),
88 transaction: () => {
89 transactionCalls++
90 return transactionCalls === 1
91 ? Promise.reject(new Error("transient begin failure"))
92 : Promise.resolve(transaction)
93 }
94 }
95
96 return Effect.gen(function*() {
97 const client = yield* LibsqlClient.make({ liveClient: liveClient as any })
98 const first = yield* Effect.exit(client.withTransaction(Effect.void))
99 assert.isTrue(Exit.isFailure(first))
100
101 yield* Effect.forkChild(client.withTransaction(Effect.void))
102 yield* Effect.yieldNow
103
104 assert.strictEqual(transactionCalls, 2)
105 }).pipe(
106 Effect.scoped,
107 Effect.provide(Reactivity.layer)
108 )
109 })
110
111 // Each test recreates the same tables.
112 layer(LibsqlContainer.layerClient, { timeout: "30 seconds", concurrent: false })("shared tables", (it) => {
113 it.effect("should work", () =>
114 Effect.gen(function*() {
115 const sql = yield* LibsqlClient.LibsqlClient
116 let response = yield* sql`INSERT INTO test (name) VALUES ('hello')`
117 assert.deepStrictEqual(response, [])
118 response = yield* sql`SELECT * FROM test`
119 assert.deepStrictEqual(response, [{ id: 1, name: "hello" }])
120 response = yield* sql`SELECT * FROM test`
121 assert.deepStrictEqual(yield* sql`select * from test`.values, [
122 [1, "hello"]
123 ])
124 }).pipe(Effect.provide(Migrations)))
125
126 it.effect("should work with raw", () =>
127 Effect.gen(function*() {
128 const sql = yield* LibsqlClient.LibsqlClient
129 let response: any
130 response = yield* sql`CREATE TABLE test2 (id INTEGER PRIMARY KEY, name TEXT)`.raw
131 yield* Effect.addFinalizer(() => sql`DROP TABLE test2;`.pipe(Effect.ignore))
132 assert.deepStrictEqual(response.toJSON(), {
133 columnTypes: [],
134 columns: [],
135 lastInsertRowid: null,
136 rows: [],
137 rowsAffected: 0
138 })
139 response = yield* sql`INSERT INTO test (name) VALUES ('hello')`.raw
140 assert.deepStrictEqual(response.toJSON(), {
141 columnTypes: [],
142 columns: [],
143 lastInsertRowid: "1",
144 rows: [],
145 rowsAffected: 1
146 })
147 response = yield* sql`SELECT * FROM test`.raw
148 assert.deepStrictEqual(response.toJSON(), {
149 columnTypes: ["INTEGER", "TEXT"],
150 columns: ["id", "name"],
151 lastInsertRowid: null,
152 rows: [[1, "hello"]],
153 rowsAffected: 0
154 })
155 }).pipe(Effect.provide(Migrations)))
156
157 it.effect("withTransaction", () =>
158 Effect.gen(function*() {
159 const sql = yield* LibsqlClient.LibsqlClient
160 yield* sql.withTransaction(sql`INSERT INTO test (name) VALUES ('hello')`)
161 const rows = yield* sql`SELECT * FROM test`
162 assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }])
163 }).pipe(Effect.provide(Migrations)))
164
165 it.effect("withTransaction rollback", () =>
166 Effect.gen(function*() {
167 const sql = yield* LibsqlClient.LibsqlClient
168 yield* sql`INSERT INTO test (name) VALUES ('hello')`.pipe(
169 Effect.andThen(Effect.fail("boom")),
170 sql.withTransaction,
171 Effect.ignore
172 )
173 const rows = yield* sql`SELECT * FROM test`
174 assert.deepStrictEqual(rows, [])
175 }).pipe(Effect.provide(Migrations)))
176
177 it.effect("withTransaction nested", () =>
178 Effect.gen(function*() {
179 const sql = yield* LibsqlClient.LibsqlClient
180 const stmt = sql`INSERT INTO test (name) VALUES ('hello')`
181
182 yield* stmt.pipe(Effect.andThen(() => stmt.pipe(sql.withTransaction)), sql.withTransaction)
183 const rows = yield* sql<{ total_rows: number }>`select count(*) as total_rows FROM test`
184 assert.deepStrictEqual(rows.at(0)?.total_rows, 2)
185 }).pipe(Effect.provide(Migrations)))
186
187 it.effect("withTransaction nested rollback", () =>
188 Effect.gen(function*() {
189 const sql = yield* LibsqlClient.LibsqlClient
190 const stmt = sql`INSERT INTO test (name) VALUES ('hello')`
191
192 yield* stmt.pipe(
193 Effect.andThen(() => stmt.pipe(Effect.andThen(Effect.fail("boom")), sql.withTransaction, Effect.ignore)),
194 sql.withTransaction
195 )
196 const rows = yield* sql<{ total_rows: number }>`select count(*) as total_rows FROM test`
197 assert.deepStrictEqual(rows.at(0)?.total_rows, 1)
198 }).pipe(Effect.provide(Migrations)))
199 })
200})