| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { Effect, Option, References, Stream } from "effect" |
| 3 | import * as Reactivity from "effect/reactivity/Reactivity" |
| 4 | import * as SqlClient from "effect/sql/SqlClient" |
| 5 | import type { Connection } from "effect/sql/SqlConnection" |
| 6 | import * as Statement from "effect/sql/Statement" |
| 7 | |
| 8 | describe("Statement", () => { |
| 9 | it("defaultTransforms ignores inherited properties", () => { |
| 10 | const row = Object.create({ inherited: 1 }) |
| 11 | row.own = 2 |
| 12 | |
| 13 | const nested = Statement.defaultTransforms((key) => key.toUpperCase()) |
| 14 | const flat = Statement.defaultTransforms((key) => key.toUpperCase(), false) |
| 15 | |
| 16 | assert.deepStrictEqual(nested.object(row), { OWN: 2 }) |
| 17 | assert.deepStrictEqual(nested.array([row]), [{ OWN: 2 }]) |
| 18 | assert.deepStrictEqual(nested.array([[row]]), [[{ OWN: 2 }]]) |
| 19 | assert.deepStrictEqual(flat.array([row]), [{ OWN: 2 }]) |
| 20 | }) |
| 21 | |
| 22 | it("compiles one fragment independently for each compiler", () => { |
| 23 | const postgres = Statement.makeCompiler({ |
| 24 | dialect: "pg", |
| 25 | placeholder: (index) => `$${index}`, |
| 26 | onIdentifier: Statement.defaultEscape("\""), |
| 27 | onRecordUpdate: () => ["", []], |
| 28 | onCustom: () => ["", []] |
| 29 | }) |
| 30 | const sqlite = Statement.makeCompilerSqlite() |
| 31 | const fragment = Statement.fragment([ |
| 32 | Statement.identifier("value"), |
| 33 | Statement.parameter(1) |
| 34 | ]) |
| 35 | |
| 36 | assert.deepStrictEqual(postgres.compile(fragment, false), ["\"value\"$1", [1]]) |
| 37 | assert.deepStrictEqual(sqlite.compile(fragment, false), ["\"value\"?", [1]]) |
| 38 | }) |
| 39 | |
| 40 | it("renumbers a cached returning fragment", () => { |
| 41 | const sql = Statement.make( |
| 42 | Effect.void as any, |
| 43 | Statement.makeCompiler({ |
| 44 | dialect: "pg", |
| 45 | placeholder: (index) => `$${index}`, |
| 46 | onIdentifier: Statement.defaultEscape("\""), |
| 47 | onRecordUpdate: () => ["", []], |
| 48 | onCustom: () => ["", []] |
| 49 | }), |
| 50 | [], |
| 51 | undefined |
| 52 | ) |
| 53 | const returning = sql`${"label"} AS label` |
| 54 | |
| 55 | assert.deepStrictEqual(returning.compile(), ["$1 AS label", ["label"]]) |
| 56 | assert.deepStrictEqual( |
| 57 | sql`INSERT INTO people ${sql.insert({ name: "Ada" }).returning(returning)}, ${"extra"} AS extra`.compile(), |
| 58 | ["INSERT INTO people (\"name\") VALUES ($1) RETURNING $2 AS label, $3 AS extra", ["Ada", "label", "extra"]] |
| 59 | ) |
| 60 | }) |
| 61 | |
| 62 | it.effect("reads propagation overrides when executing existing statements and streams", () => |
| 63 | Effect.gen(function*() { |
| 64 | let expected = "none" |
| 65 | const sql = yield* makeClient(Effect.map(Effect.option(Effect.currentSpan), (span) => { |
| 66 | assert.strictEqual(Option.isSome(span) ? span.value.name : "none", expected) |
| 67 | })) |
| 68 | const query = sql`select 1` |
| 69 | const run = Effect.andThen(query, Stream.runDrain(query.stream)) |
| 70 | |
| 71 | yield* run |
| 72 | expected = "sql.execute" |
| 73 | yield* Effect.provideService(run, Statement.SpanPropagationEnabled, true) |
| 74 | expected = "none" |
| 75 | yield* run.pipe( |
| 76 | Effect.provideService(Statement.SpanPropagationEnabled, false), |
| 77 | Effect.provideService(Statement.SpanPropagationEnabled, true) |
| 78 | ) |
| 79 | }).pipe(Effect.provide(Reactivity.layer))) |
| 80 | |
| 81 | it.effect("parents acquisition, execution, and pulls without adding stack frames", () => |
| 82 | Effect.gen(function*() { |
| 83 | const frame = yield* Effect.service(References.CurrentStackFrame) |
| 84 | const observe = Effect.gen(function*() { |
| 85 | assert.strictEqual((yield* Effect.orDie(Effect.currentSpan)).name, "sql.execute") |
| 86 | assert.strictEqual(yield* Effect.service(References.CurrentStackFrame), frame) |
| 87 | }) |
| 88 | const acquired = yield* makeClient(observe) |
| 89 | const borrowed = yield* makeClient(observe, true) |
| 90 | |
| 91 | yield* Effect.gen(function*() { |
| 92 | yield* acquired`select 1` |
| 93 | yield* borrowed`select 1` |
| 94 | yield* Stream.runDrain(borrowed`select 1`.stream) |
| 95 | }).pipe(Effect.provideService(Statement.SpanPropagationEnabled, true)) |
| 96 | }).pipe(Effect.provide(Reactivity.layer))) |
| 97 | |
| 98 | it.effect("skips propagation when tracing is disabled", () => |
| 99 | Effect.gen(function*() { |
| 100 | const sql = yield* makeClient(Effect.map(Effect.option(Effect.currentSpan), (span) => { |
| 101 | assert.isTrue(Option.isNone(span)) |
| 102 | })) |
| 103 | yield* Effect.andThen(sql`select 1`, Stream.runDrain(sql`select 1`.stream)).pipe( |
| 104 | Effect.provideService(Statement.SpanPropagationEnabled, true), |
| 105 | Effect.provideService(References.TracerEnabled, false) |
| 106 | ) |
| 107 | }).pipe(Effect.provide(Reactivity.layer))) |
| 108 | }) |
| 109 | |
| 110 | const makeClient = (observe: Effect.Effect<void>, borrow = false) => { |
| 111 | const execute = Effect.as(observe, []) |
| 112 | const connection: Connection = { |
| 113 | execute: () => execute, |
| 114 | executeRaw: () => execute, |
| 115 | executeValues: () => execute, |
| 116 | executeUnprepared: () => execute, |
| 117 | executeValuesUnprepared: () => execute, |
| 118 | executeStream: () => Stream.fromEffect(execute) |
| 119 | } |
| 120 | return SqlClient.make({ |
| 121 | acquirer: Effect.as(observe, connection), |
| 122 | borrower: borrow ? (f) => Effect.andThen(observe, f(connection)) : undefined, |
| 123 | compiler: Statement.makeCompilerSqlite(), |
| 124 | spanAttributes: [] |
| 125 | }) |
| 126 | } |