1import type { D1Result } from "@cloudflare/workers-types"
2import { D1Client } from "@effect/sql-d1"
3import { assert, describe, it } from "@effect/vitest"
4import { Cause, Effect } from "effect"
5import * as Reactivity from "effect/reactivity/Reactivity"
6import { Statement } from "effect/sql"
7import { D1Miniflare } from "./utils.ts"
8
9describe("Client", () => {
10 it.effect("raw preserves the native result envelope", () =>
11 Effect.gen(function*() {
12 const sql = yield* D1Client.D1Client
13 yield* sql`CREATE TABLE raw_test (id INTEGER PRIMARY KEY, name TEXT)`
14 const raw = (yield* sql`INSERT INTO raw_test (name) VALUES (${"effect"})`.raw) as D1Result
15 assert.strictEqual(raw.success, true)
16 assert.deepStrictEqual(raw.results, [])
17 assert.strictEqual(raw.meta.changes, 1)
18 assert.strictEqual(raw.meta.last_row_id, 1)
19 }).pipe(Effect.provide(D1Miniflare.layerClient)))
20
21 it.effect("classifies native errors without stable sqlite codes as UnknownError", () =>
22 Effect.gen(function*() {
23 const failingDb = {
24 prepare: () => ({
25 bind: () => ({
26 all: async () => {
27 throw new Error("boom")
28 }
29 })
30 })
31 } as any
32
33 const client = yield* D1Client.make({ db: failingDb })
34 const error = yield* Effect.flip(client`SELECT 1`)
35 assert.strictEqual(error.reason._tag, "UnknownError")
36 }).pipe(
37 Effect.scoped,
38 Effect.provide(Reactivity.layer)
39 ))
40
41 it.effect("should handle queries without transactions", () =>
42 Effect.gen(function*() {
43 const sql = yield* D1Client.D1Client
44 yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
45 yield* sql`INSERT INTO test (name) VALUES ('hello')`
46 let rows = yield* sql`SELECT * FROM test`
47 assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }])
48 yield* sql`INSERT INTO test (name) VALUES ('world')`
49 rows = yield* sql`SELECT * FROM test`
50 assert.deepStrictEqual(rows, [
51 { id: 1, name: "hello" },
52 { id: 2, name: "world" }
53 ])
54 }).pipe(Effect.provide(D1Miniflare.layerClient)))
55
56 it.effect("should handle queries with params without transactions", () =>
57 Effect.gen(function*() {
58 const sql = yield* D1Client.D1Client
59 yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
60 yield* sql`INSERT INTO test ${sql.insert({ name: "hello" })}`
61 const rows = yield* sql`SELECT * FROM test WHERE name = ${"hello"}`
62 assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }])
63 }).pipe(Effect.provide(D1Miniflare.layerClient)))
64
65 it.effect("should execute statements in a batch", () =>
66 Effect.gen(function*() {
67 const sql = yield* D1Client.D1Client
68 yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
69
70 const results: readonly [
71 ReadonlyArray<{ id: number; name: string }>,
72 ReadonlyArray<{ id: number; name: string }>,
73 ReadonlyArray<{ count: number }>
74 ] = yield* sql.batch(
75 [
76 sql<{ id: number; name: string }>`INSERT INTO test (name) VALUES (${"hello"}) RETURNING *`,
77 sql<{ id: number; name: string }>`INSERT INTO test (name) VALUES (${"world"}) RETURNING *`,
78 sql<{ count: number }>`SELECT COUNT(*) AS count FROM test`
79 ] as const
80 )
81
82 assert.deepStrictEqual(results, [
83 [{ id: 1, name: "hello" }],
84 [{ id: 2, name: "world" }],
85 [{ count: 2 }]
86 ])
87 }).pipe(Effect.provide(D1Miniflare.layerClient)))
88
89 it.effect("should apply result transforms to batch results", () =>
90 Effect.gen(function*() {
91 const sql = yield* D1Client.D1Client
92 yield* sql`CREATE TABLE test (first_name TEXT, "firstName" TEXT)`
93 yield* sql`INSERT INTO test (first_name, "firstName") VALUES ('John', 'Jane')`
94
95 yield* Effect.gen(function*() {
96 const transformed = yield* D1Client.make({
97 db: sql.config.db,
98 transformQueryNames: (name) => name === "firstName" ? "first_name" : name,
99 transformResultNames: (name) => name === "first_name" ? "firstName" : name
100 })
101 const [rows] = yield* transformed.batch([
102 transformed<{ firstName: string }>`SELECT ${transformed("firstName")} FROM test`
103 ])
104 assert.deepStrictEqual(rows, [{ firstName: "John" }])
105
106 const withoutTransforms = transformed.withoutTransforms()
107 const [rawRows] = yield* withoutTransforms.batch([
108 withoutTransforms<{ firstName: string }>`SELECT ${withoutTransforms("firstName")} FROM test`
109 ])
110 assert.deepStrictEqual(rawRows, [{ firstName: "Jane" }])
111
112 const [rawRowsFromTransformedBatch] = yield* transformed.batch([
113 withoutTransforms<{ first_name: string }>`SELECT ${withoutTransforms("first_name")} FROM test`
114 ])
115 assert.deepStrictEqual(rawRowsFromTransformedBatch, [{ first_name: "John" }])
116
117 const [transformedRowsFromRawBatch] = yield* withoutTransforms.batch([
118 transformed<{ firstName: string }>`SELECT ${transformed("firstName")} FROM test`
119 ])
120 assert.deepStrictEqual(transformedRowsFromRawBatch, [{ firstName: "John" }])
121 }).pipe(
122 Effect.scoped,
123 Effect.provide(Reactivity.layer)
124 )
125 }).pipe(Effect.provide(D1Miniflare.layerClient)))
126
127 it.effect("should disable query-only transforms", () =>
128 Effect.gen(function*() {
129 const sql = yield* D1Client.D1Client
130 yield* sql`CREATE TABLE test (first_name TEXT, "firstName" TEXT)`
131 yield* sql`INSERT INTO test (first_name, "firstName") VALUES ('John', 'Jane')`
132
133 yield* Effect.gen(function*() {
134 const transformed = yield* D1Client.make({
135 db: sql.config.db,
136 transformQueryNames: (name) => name === "firstName" ? "first_name" : name
137 })
138 const [transformedRows] = yield* transformed.batch([
139 transformed<{ first_name: string }>`SELECT ${transformed("firstName")} FROM test`
140 ])
141 assert.deepStrictEqual(transformedRows, [{ first_name: "John" }])
142
143 const withoutTransforms = transformed.withoutTransforms()
144 const [rawRows] = yield* withoutTransforms.batch([
145 withoutTransforms<{ firstName: string }>`SELECT ${withoutTransforms("firstName")} FROM test`
146 ])
147 assert.deepStrictEqual(rawRows, [{ firstName: "Jane" }])
148 }).pipe(
149 Effect.scoped,
150 Effect.provide(Reactivity.layer)
151 )
152 }).pipe(Effect.provide(D1Miniflare.layerClient)))
153
154 it.effect("should roll back a failed batch", () =>
155 Effect.gen(function*() {
156 const sql = yield* D1Client.D1Client
157 yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT UNIQUE)`
158
159 const error = yield* sql.batch([
160 sql`INSERT INTO test (name) VALUES (${"duplicate"})`,
161 sql`INSERT INTO test (name) VALUES (${"duplicate"})`
162 ]).pipe(Effect.flip)
163
164 assert.strictEqual(error.reason._tag, "UnknownError")
165 assert.strictEqual(error.reason.operation, "execute")
166 const rows = yield* sql`SELECT * FROM test`
167 assert.deepStrictEqual(rows, [])
168 }).pipe(Effect.provide(D1Miniflare.layerClient)))
169
170 it.effect("should support an empty batch", () =>
171 Effect.gen(function*() {
172 const sql = yield* D1Client.D1Client
173 const results: readonly [] = yield* sql.batch([])
174 assert.deepStrictEqual(results, [])
175 }).pipe(Effect.provide(D1Miniflare.layerClient)))
176
177 it.effect("should apply statement transformers in a batch", () =>
178 Effect.gen(function*() {
179 const sql = yield* D1Client.D1Client
180 const queries: Array<string> = []
181
182 const results = yield* sql.batch([
183 sql`SELECT ${1} AS value`,
184 sql`SELECT ${2} AS value`
185 ]).pipe(
186 Effect.provideService(Statement.CurrentTransformer, (statement, sql) =>
187 Effect.sync(() => {
188 queries.push(statement.compile()[0])
189 }).pipe(Effect.as(sql`SELECT ${3} AS value`)))
190 )
191
192 assert.deepStrictEqual(queries, ["SELECT ? AS value", "SELECT ? AS value"])
193 assert.deepStrictEqual(results, [[{ value: 3 }], [{ value: 3 }]])
194 }).pipe(Effect.provide(D1Miniflare.layerClient)))
195
196 it.effect("should defect on transactions", () =>
197 Effect.gen(function*() {
198 const sql = yield* D1Client.D1Client
199 yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
200 const res = yield* sql`INSERT INTO test ${sql.insert({ name: "hello" })}`.pipe(
201 sql.withTransaction,
202 Effect.sandbox,
203 Effect.flip
204 )
205 const rows = yield* sql`SELECT * FROM test`
206 assert.deepStrictEqual(rows, [])
207 assert.equal(Cause.hasDies(res), true)
208 }).pipe(Effect.provide(D1Miniflare.layerClient)))
209
210 it.effect("should defect when batching in a transaction", () =>
211 Effect.gen(function*() {
212 const sql = yield* D1Client.D1Client
213 yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
214 const res = yield* sql.batch([
215 sql`INSERT INTO test ${sql.insert({ name: "hello" })}`
216 ]).pipe(
217 sql.withTransaction,
218 Effect.sandbox,
219 Effect.flip
220 )
221 const rows = yield* sql`SELECT * FROM test`
222 assert.deepStrictEqual(rows, [])
223 assert.equal(Cause.hasDies(res), true)
224 }).pipe(Effect.provide(D1Miniflare.layerClient)))
225})