| 1 | import { PgliteClient } from "@effect/sql-pglite" |
| 2 | import { assert, describe, layer } from "@effect/vitest" |
| 3 | import * as Pglite from "@electric-sql/pglite" |
| 4 | import { Effect, Exit, Layer, Queue, Scope } from "effect" |
| 5 | import { SqlClient } from "effect/sql/SqlClient" |
| 6 | |
| 7 | const ClientLayer = PgliteClient.layer() |
| 8 | |
| 9 | const 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. |
| 18 | describe("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 | }) |