1import { assert, describe, it } from "@effect/vitest"
2import { Effect, Option, References, Stream } from "effect"
3import * as Reactivity from "effect/reactivity/Reactivity"
4import * as SqlClient from "effect/sql/SqlClient"
5import type { Connection } from "effect/sql/SqlConnection"
6import * as Statement from "effect/sql/Statement"
7
8describe("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
110const 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}