1import { PgliteClient } from "@effect/sql-pglite"
2import { assert, describe, layer } from "@effect/vitest"
3import * as Pglite from "@electric-sql/pglite"
4import { Effect, Exit, Layer, Queue, Scope } from "effect"
5import { SqlClient } from "effect/sql/SqlClient"
6
7const ClientLayer = PgliteClient.layer()
8
9const Migrations = Layer.effectDiscard(
10 Effect.gen(function*() {
11 const sql = yield* SqlClient
12 yield* sql`DROP TABLE test`.pipe(Effect.ignore)
13 yield* sql`CREATE TABLE test (id SERIAL PRIMARY KEY, name TEXT)`
14 })
15)
16
17// Avoid overlapping PGlite startup with SQL assertions in other tests.
18describe("PgliteClient", { concurrent: false }, () => {
19 layer(ClientLayer, { timeout: "30 seconds" })((it) => {
20 it.effect("basic insert/select", () =>
21 Effect.gen(function*() {
22 const sql = yield* PgliteClient.PgliteClient
23 yield* sql`INSERT INTO test (name) VALUES ('hello')`
24 const rows = yield* sql<{ id: number; name: string }>`SELECT * FROM test`
25 assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }])
26 const values = yield* sql`SELECT * FROM test`.values
27 assert.deepStrictEqual(values, [[1, "hello"]])
28 }).pipe(Effect.provide(Migrations)))
29
30 it.effect("insert helper", () =>
31 Effect.gen(function*() {
32 const sql = yield* PgliteClient.PgliteClient
33 const [query, params] = sql`INSERT INTO people ${sql.insert({ name: "Tim", age: 10 })}`.compile()
34 assert.strictEqual(query, `INSERT INTO people ("name","age") VALUES ($1,$2)`)
35 assert.deepStrictEqual(params, ["Tim", 10])
36 }))
37
38 it.effect("update helper", () =>
39 Effect.gen(function*() {
40 const sql = yield* PgliteClient.PgliteClient
41 const [query, params] = sql`UPDATE people SET ${sql.update({ name: "Tim" })}`.compile()
42 assert.strictEqual(query, `UPDATE people SET "name" = $1`)
43 assert.deepStrictEqual(params, ["Tim"])
44 }))
45
46 it.effect("updateValues helper", () =>
47 Effect.gen(function*() {
48 const sql = yield* PgliteClient.PgliteClient
49 const [query, params] = sql`UPDATE people SET name = data.name FROM ${
50 sql.updateValues([{ name: "Tim" }, { name: "John" }], "data")
51 }`.compile()
52 assert.strictEqual(
53 query,
54 `UPDATE people SET name = data.name FROM (values ($1),($2)) AS data("name")`
55 )
56 assert.deepStrictEqual(params, ["Tim", "John"])
57 }))
58
59 it.effect("in helper", () =>
60 Effect.gen(function*() {
61 const sql = yield* PgliteClient.PgliteClient
62 const [query, params] = sql`SELECT * FROM ${sql("people")} WHERE id IN ${sql.in([1, 2, "x"])}`.compile()
63 assert.strictEqual(query, `SELECT * FROM "people" WHERE id IN ($1,$2,$3)`)
64 assert.deepStrictEqual(params, [1, 2, "x"])
65 }))
66
67 it.effect("and helper", () =>
68 Effect.gen(function*() {
69 const sql = yield* PgliteClient.PgliteClient
70 const now = new Date()
71 const [query, params] = sql`SELECT * FROM ${sql("people")} WHERE ${
72 sql.and([sql.in("name", ["Tim", "John"]), sql`created_at < ${now}`])
73 }`.compile()
74 assert.strictEqual(query, `SELECT * FROM "people" WHERE ("name" IN ($1,$2) AND created_at < $3)`)
75 assert.deepStrictEqual(params, ["Tim", "John", now])
76 }))
77
78 it.effect("identifier transform", () =>
79 Effect.gen(function*() {
80 const sql = yield* PgliteClient.PgliteClient
81 const compiler = PgliteClient.makeCompiler((s) => s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`))
82 const [query] = compiler.compile(sql`SELECT * FROM ${sql("peopleTest")}`, false)
83 assert.strictEqual(query, `SELECT * FROM "people_test"`)
84 }))
85
86 it.effect("json fragment", () =>
87 Effect.gen(function*() {
88 const sql = yield* PgliteClient.PgliteClient
89 const rows = yield* sql<{ json: unknown }>`SELECT ${sql.json({ a: 1 })}::jsonb AS json`
90 assert.deepStrictEqual(rows[0].json, { a: 1 })
91 }))
92
93 it.effect("listen + notify", () =>
94 Effect.gen(function*() {
95 const sql = yield* PgliteClient.PgliteClient
96 const notifications = yield* sql.listen("ch1")
97
98 yield* sql.notify("ch1", "hello")
99 assert.strictEqual(yield* Queue.take(notifications), "hello")
100
101 yield* sql.notify("ch1", "")
102 assert.strictEqual(yield* Queue.take(notifications), "")
103 }), { timeout: 15_000 })
104
105 it.effect("listen shuts down with its scope", () =>
106 Effect.gen(function*() {
107 const sql = yield* PgliteClient.PgliteClient
108 const scope = yield* Scope.make()
109 const notifications = yield* sql.listen("ch1").pipe(Scope.provide(scope))
110
111 yield* Scope.close(scope, Exit.void)
112
113 assert.isTrue(Exit.hasInterrupts(yield* Queue.take(notifications).pipe(Effect.exit)))
114 }), { timeout: 15_000 })
115
116 it.effect("provider extras", () =>
117 Effect.gen(function*() {
118 const sql = yield* PgliteClient.PgliteClient
119 const dump = yield* sql.dumpDataDir("none")
120 assert.isAbove((dump as Blob).size, 0)
121 }))
122 })
123
124 describe("fromClient", () => {
125 layer(
126 PgliteClient.layerFrom(Effect.gen(function*() {
127 const pg = yield* Effect.acquireRelease(
128 Effect.sync(() => new Pglite.PGlite()),
129 (pg) => Effect.promise(() => pg.close())
130 )
131 // Charge startup to the layer hook timeout, not the first query's test timeout.
132 yield* Effect.promise(() => pg.waitReady)
133 return yield* PgliteClient.fromClient({ liveClient: pg })
134 })),
135 { timeout: "30 seconds" }
136 )((it) => {
137 it.effect("works", () =>
138 Effect.gen(function*() {
139 const sql = yield* PgliteClient.PgliteClient
140 const rows = yield* sql<{ value: number }>`SELECT 1 AS value`
141 assert.deepStrictEqual(rows, [{ value: 1 }])
142 }))
143 })
144
145 layer(
146 Layer.effectDiscard(Effect.gen(function*() {
147 const sql = yield* PgliteClient.PgliteClient
148 yield* sql`CREATE TYPE mood AS ENUM ('sad', 'happy')`
149 yield* sql`CREATE TABLE test_moods (id SERIAL PRIMARY KEY, name TEXT, moods mood[])`
150 yield* sql.refreshArrayTypes
151 })).pipe(
152 Layer.provideMerge(ClientLayer)
153 ),
154 { timeout: "30 seconds" }
155 )((it) => {
156 it.effect("refreshArrayTypes", () =>
157 Effect.gen(function*() {
158 const sql = yield* PgliteClient.PgliteClient
159 yield* sql`INSERT INTO test_moods (name, moods) VALUES (${"test2"}, ${["sad", "happy"]})`
160 const rows = yield* sql<{ moods: ReadonlyArray<string> }>`SELECT moods FROM test_moods`
161 assert.deepStrictEqual(rows, [{ moods: ["sad", "happy"] }])
162 }))
163 })
164 })
165})