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