effect-qb 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1294 -0
- package/dist/mysql.js +57575 -0
- package/dist/postgres.js +6303 -0
- package/package.json +42 -0
- package/src/internal/aggregation-validation.ts +57 -0
- package/src/internal/case-analysis.ts +50 -0
- package/src/internal/coercion-analysis.ts +30 -0
- package/src/internal/coercion-errors.ts +29 -0
- package/src/internal/coercion-kind.ts +32 -0
- package/src/internal/coercion-normalize.ts +7 -0
- package/src/internal/coercion-rules.ts +25 -0
- package/src/internal/column-state.ts +453 -0
- package/src/internal/column.ts +417 -0
- package/src/internal/datatypes/define.ts +44 -0
- package/src/internal/datatypes/lookup.ts +280 -0
- package/src/internal/datatypes/shape.ts +72 -0
- package/src/internal/derived-table.ts +149 -0
- package/src/internal/dialect.ts +30 -0
- package/src/internal/executor.ts +390 -0
- package/src/internal/expression-ast.ts +349 -0
- package/src/internal/expression.ts +325 -0
- package/src/internal/grouping-key.ts +82 -0
- package/src/internal/json/ast.ts +63 -0
- package/src/internal/json/errors.ts +13 -0
- package/src/internal/json/path.ts +227 -0
- package/src/internal/json/shape.ts +1 -0
- package/src/internal/json/types.ts +386 -0
- package/src/internal/mysql-dialect.ts +39 -0
- package/src/internal/mysql-renderer.ts +37 -0
- package/src/internal/plan.ts +64 -0
- package/src/internal/postgres-dialect.ts +34 -0
- package/src/internal/postgres-renderer.ts +40 -0
- package/src/internal/predicate-analysis.ts +71 -0
- package/src/internal/predicate-atom.ts +43 -0
- package/src/internal/predicate-branches.ts +40 -0
- package/src/internal/predicate-context.ts +279 -0
- package/src/internal/predicate-formula.ts +100 -0
- package/src/internal/predicate-key.ts +28 -0
- package/src/internal/predicate-nnf.ts +12 -0
- package/src/internal/predicate-normalize.ts +202 -0
- package/src/internal/projection-alias.ts +15 -0
- package/src/internal/projections.ts +101 -0
- package/src/internal/query-ast.ts +297 -0
- package/src/internal/query-factory.ts +6757 -0
- package/src/internal/query-requirements.ts +40 -0
- package/src/internal/query.ts +1590 -0
- package/src/internal/renderer.ts +102 -0
- package/src/internal/runtime-normalize.ts +344 -0
- package/src/internal/runtime-schema.ts +428 -0
- package/src/internal/runtime-value.ts +85 -0
- package/src/internal/schema-derivation.ts +131 -0
- package/src/internal/sql-expression-renderer.ts +1353 -0
- package/src/internal/table-options.ts +225 -0
- package/src/internal/table.ts +674 -0
- package/src/mysql/column.ts +30 -0
- package/src/mysql/datatypes/index.ts +6 -0
- package/src/mysql/datatypes/spec.ts +180 -0
- package/src/mysql/errors/catalog.ts +51662 -0
- package/src/mysql/errors/fields.ts +21 -0
- package/src/mysql/errors/index.ts +18 -0
- package/src/mysql/errors/normalize.ts +232 -0
- package/src/mysql/errors/requirements.ts +73 -0
- package/src/mysql/executor.ts +134 -0
- package/src/mysql/query.ts +189 -0
- package/src/mysql/renderer.ts +19 -0
- package/src/mysql/table.ts +157 -0
- package/src/mysql.ts +18 -0
- package/src/postgres/column.ts +20 -0
- package/src/postgres/datatypes/index.ts +8 -0
- package/src/postgres/datatypes/spec.ts +264 -0
- package/src/postgres/errors/catalog.ts +452 -0
- package/src/postgres/errors/fields.ts +48 -0
- package/src/postgres/errors/index.ts +4 -0
- package/src/postgres/errors/normalize.ts +209 -0
- package/src/postgres/errors/requirements.ts +65 -0
- package/src/postgres/errors/types.ts +38 -0
- package/src/postgres/executor.ts +131 -0
- package/src/postgres/query.ts +189 -0
- package/src/postgres/renderer.ts +29 -0
- package/src/postgres/table.ts +157 -0
- package/src/postgres.ts +18 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import * as SqlClient from "@effect/sql/SqlClient"
|
|
4
|
+
import * as SqlError from "@effect/sql/SqlError"
|
|
5
|
+
|
|
6
|
+
import * as Expression from "./expression.js"
|
|
7
|
+
import { normalizeDbValue } from "./runtime-normalize.js"
|
|
8
|
+
import { expressionRuntimeSchema } from "./runtime-schema.js"
|
|
9
|
+
import { flattenSelection } from "./projections.js"
|
|
10
|
+
import * as Query from "./query.js"
|
|
11
|
+
import * as QueryAst from "./query-ast.js"
|
|
12
|
+
import * as Renderer from "./renderer.js"
|
|
13
|
+
import * as Plan from "./plan.js"
|
|
14
|
+
|
|
15
|
+
/** Flat database row keyed by rendered projection aliases. */
|
|
16
|
+
export type FlatRow = Readonly<Record<string, unknown>>
|
|
17
|
+
export type DriverMode = "raw" | "normalized"
|
|
18
|
+
|
|
19
|
+
export interface RowDecodeError {
|
|
20
|
+
readonly _tag: "RowDecodeError"
|
|
21
|
+
readonly message: string
|
|
22
|
+
readonly dialect: string
|
|
23
|
+
readonly query?: {
|
|
24
|
+
readonly sql: string
|
|
25
|
+
readonly params: ReadonlyArray<unknown>
|
|
26
|
+
}
|
|
27
|
+
readonly projection: {
|
|
28
|
+
readonly alias: string
|
|
29
|
+
readonly path: readonly string[]
|
|
30
|
+
}
|
|
31
|
+
readonly dbType: Expression.DbType.Any
|
|
32
|
+
readonly raw: unknown
|
|
33
|
+
readonly normalized?: unknown
|
|
34
|
+
readonly stage: "normalize" | "schema"
|
|
35
|
+
readonly cause: unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Driver that executes already-rendered SQL.
|
|
40
|
+
*
|
|
41
|
+
* Drivers operate on rendered SQL plus projection metadata and return flat
|
|
42
|
+
* alias-keyed rows. Executors then normalize raw driver values into the
|
|
43
|
+
* canonical runtime contract, validate them against runtime schemas, and remap
|
|
44
|
+
* aliases back into the nested result shape.
|
|
45
|
+
*/
|
|
46
|
+
export interface Driver<
|
|
47
|
+
Dialect extends string = string,
|
|
48
|
+
Error = never,
|
|
49
|
+
Context = never
|
|
50
|
+
> {
|
|
51
|
+
readonly dialect: Dialect
|
|
52
|
+
execute<Row>(
|
|
53
|
+
query: Renderer.RenderedQuery<Row, Dialect>
|
|
54
|
+
): Effect.Effect<ReadonlyArray<FlatRow>, Error, Context>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Public execution contract.
|
|
59
|
+
*
|
|
60
|
+
* Executors only accept complete, dialect-compatible plans. Successful
|
|
61
|
+
* execution yields the compile-time query result contract after canonical
|
|
62
|
+
* scalar normalization plus runtime schema validation.
|
|
63
|
+
*/
|
|
64
|
+
export interface Executor<
|
|
65
|
+
Dialect extends string = string,
|
|
66
|
+
Error = never,
|
|
67
|
+
Context = never
|
|
68
|
+
> {
|
|
69
|
+
readonly dialect: Dialect
|
|
70
|
+
execute<PlanValue extends Query.QueryPlan<any, any, any, any, any, any, any, any, any>>(
|
|
71
|
+
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
72
|
+
): Effect.Effect<Query.ResultRows<PlanValue>, Error, Context>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const setPath = (
|
|
76
|
+
target: Record<string, unknown>,
|
|
77
|
+
path: readonly string[],
|
|
78
|
+
value: unknown
|
|
79
|
+
): void => {
|
|
80
|
+
let current = target
|
|
81
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
82
|
+
const key = path[index]!
|
|
83
|
+
const existing = current[key]
|
|
84
|
+
if (typeof existing === "object" && existing !== null && !Array.isArray(existing)) {
|
|
85
|
+
current = existing as Record<string, unknown>
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
const next: Record<string, unknown> = {}
|
|
89
|
+
current[key] = next
|
|
90
|
+
current = next
|
|
91
|
+
}
|
|
92
|
+
current[path[path.length - 1]!] = value
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const hasWriteStatement = (statement: QueryAst.QueryStatement): boolean =>
|
|
96
|
+
statement === "insert" ||
|
|
97
|
+
statement === "update" ||
|
|
98
|
+
statement === "delete" ||
|
|
99
|
+
statement === "truncate" ||
|
|
100
|
+
statement === "merge" ||
|
|
101
|
+
statement === "transaction" ||
|
|
102
|
+
statement === "commit" ||
|
|
103
|
+
statement === "rollback" ||
|
|
104
|
+
statement === "savepoint" ||
|
|
105
|
+
statement === "rollbackTo" ||
|
|
106
|
+
statement === "releaseSavepoint" ||
|
|
107
|
+
statement === "createTable" ||
|
|
108
|
+
statement === "createIndex" ||
|
|
109
|
+
statement === "dropIndex" ||
|
|
110
|
+
statement === "dropTable"
|
|
111
|
+
|
|
112
|
+
const hasWriteCapabilityInSource = (source: unknown): boolean =>
|
|
113
|
+
typeof source === "object" && source !== null && "plan" in source
|
|
114
|
+
? hasWriteCapability((source as { readonly plan: Query.QueryPlan<any, any, any, any, any, any, any, any, any, any> }).plan)
|
|
115
|
+
: false
|
|
116
|
+
|
|
117
|
+
export const hasWriteCapability = (
|
|
118
|
+
plan: Query.QueryPlan<any, any, any, any, any, any, any, any, any, any>
|
|
119
|
+
): boolean => {
|
|
120
|
+
const ast = Query.getAst(plan)
|
|
121
|
+
if (hasWriteStatement(ast.kind)) {
|
|
122
|
+
return true
|
|
123
|
+
}
|
|
124
|
+
if (ast.kind === "set") {
|
|
125
|
+
if (ast.setBase && hasWriteCapability((ast.setBase as Query.QueryPlan<any, any, any, any, any, any, any, any, any, any>))) {
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
if ((ast.setOperations ?? []).some((entry) => hasWriteCapability(entry.query as Query.QueryPlan<any, any, any, any, any, any, any, any, any, any>))) {
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (ast.from && hasWriteCapabilityInSource(ast.from.source)) {
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
if (ast.into && hasWriteCapabilityInSource(ast.into.source)) {
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
if (ast.target && hasWriteCapabilityInSource(ast.target.source)) {
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
if ((ast.joins ?? []).some((join) => hasWriteCapabilityInSource(join.source))) {
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const remapRows = <Row>(
|
|
148
|
+
query: Renderer.RenderedQuery<Row, any>,
|
|
149
|
+
rows: ReadonlyArray<FlatRow>
|
|
150
|
+
): ReadonlyArray<Row> =>
|
|
151
|
+
rows.map((row) => {
|
|
152
|
+
const decoded: Record<string, unknown> = {}
|
|
153
|
+
for (const projection of query.projections) {
|
|
154
|
+
if (projection.alias in row) {
|
|
155
|
+
setPath(decoded, projection.path, row[projection.alias])
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return decoded as Row
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const makeRowDecodeError = (
|
|
162
|
+
rendered: Renderer.RenderedQuery<any, any>,
|
|
163
|
+
projection: Renderer.RenderedQuery<any, any>["projections"][number],
|
|
164
|
+
expression: Expression.Any,
|
|
165
|
+
raw: unknown,
|
|
166
|
+
stage: RowDecodeError["stage"],
|
|
167
|
+
cause: unknown,
|
|
168
|
+
normalized?: unknown
|
|
169
|
+
): RowDecodeError => ({
|
|
170
|
+
_tag: "RowDecodeError",
|
|
171
|
+
message: stage === "normalize"
|
|
172
|
+
? `Failed to normalize projection '${projection.alias}'`
|
|
173
|
+
: `Failed to decode projection '${projection.alias}' against its runtime schema`,
|
|
174
|
+
dialect: rendered.dialect,
|
|
175
|
+
query: {
|
|
176
|
+
sql: rendered.sql,
|
|
177
|
+
params: rendered.params
|
|
178
|
+
},
|
|
179
|
+
projection: {
|
|
180
|
+
alias: projection.alias,
|
|
181
|
+
path: projection.path
|
|
182
|
+
},
|
|
183
|
+
dbType: expression[Expression.TypeId].dbType,
|
|
184
|
+
raw,
|
|
185
|
+
normalized,
|
|
186
|
+
stage,
|
|
187
|
+
cause
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const hasOptionalSourceDependency = (
|
|
191
|
+
expression: Expression.Any,
|
|
192
|
+
available: Readonly<Record<string, Plan.Source>>
|
|
193
|
+
): boolean => {
|
|
194
|
+
const state = expression[Expression.TypeId]
|
|
195
|
+
if (state.sourceNullability === "resolved") {
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
return Object.keys(state.dependencies).some((sourceName) => available[sourceName]?.mode === "optional")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const effectiveRuntimeNullability = (
|
|
202
|
+
expression: Expression.Any,
|
|
203
|
+
available: Readonly<Record<string, Plan.Source>>
|
|
204
|
+
): Expression.Nullability => {
|
|
205
|
+
const nullability = expression[Expression.TypeId].nullability
|
|
206
|
+
if (nullability === "always") {
|
|
207
|
+
return "always"
|
|
208
|
+
}
|
|
209
|
+
return hasOptionalSourceDependency(expression, available)
|
|
210
|
+
? "maybe"
|
|
211
|
+
: nullability
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const decodeProjectionValue = (
|
|
215
|
+
rendered: Renderer.RenderedQuery<any, any>,
|
|
216
|
+
projection: Renderer.RenderedQuery<any, any>["projections"][number],
|
|
217
|
+
expression: Expression.Any,
|
|
218
|
+
raw: unknown,
|
|
219
|
+
available: Readonly<Record<string, Plan.Source>>,
|
|
220
|
+
driverMode: DriverMode
|
|
221
|
+
): unknown => {
|
|
222
|
+
let normalized = raw
|
|
223
|
+
if (driverMode === "raw") {
|
|
224
|
+
try {
|
|
225
|
+
normalized = normalizeDbValue(expression[Expression.TypeId].dbType, raw)
|
|
226
|
+
} catch (cause) {
|
|
227
|
+
throw makeRowDecodeError(rendered, projection, expression, raw, "normalize", cause)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (normalized === null) {
|
|
232
|
+
if (effectiveRuntimeNullability(expression, available) === "never") {
|
|
233
|
+
throw makeRowDecodeError(
|
|
234
|
+
rendered,
|
|
235
|
+
projection,
|
|
236
|
+
expression,
|
|
237
|
+
raw,
|
|
238
|
+
"schema",
|
|
239
|
+
new Error("Received null for a non-null projection"),
|
|
240
|
+
normalized
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const schema = expressionRuntimeSchema(expression)
|
|
247
|
+
if (schema === undefined) {
|
|
248
|
+
return normalized
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if ((Schema.is(schema as Schema.Schema.Any) as (value: unknown) => boolean)(normalized)) {
|
|
252
|
+
return normalized
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
return (Schema.decodeUnknownSync as any)(schema)(normalized)
|
|
257
|
+
} catch (cause) {
|
|
258
|
+
throw makeRowDecodeError(rendered, projection, expression, raw, "schema", cause, normalized)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export const decodeRows = (
|
|
263
|
+
rendered: Renderer.RenderedQuery<any, any>,
|
|
264
|
+
plan: Query.QueryPlan<any, any, any, any, any, any, any, any, any, any>,
|
|
265
|
+
rows: ReadonlyArray<FlatRow>,
|
|
266
|
+
options: {
|
|
267
|
+
readonly driverMode?: DriverMode
|
|
268
|
+
} = {}
|
|
269
|
+
): ReadonlyArray<any> => {
|
|
270
|
+
const projections = flattenSelection(
|
|
271
|
+
Query.getAst(plan).select as Record<string, unknown>
|
|
272
|
+
)
|
|
273
|
+
const byAlias = new Map(
|
|
274
|
+
projections.map((projection) => [projection.alias, projection.expression] as const)
|
|
275
|
+
)
|
|
276
|
+
const driverMode = options.driverMode ?? "raw"
|
|
277
|
+
const available = plan[Plan.TypeId].available
|
|
278
|
+
return rows.map((row) => {
|
|
279
|
+
const decoded: Record<string, unknown> = {}
|
|
280
|
+
for (const projection of rendered.projections) {
|
|
281
|
+
if (!(projection.alias in row)) {
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
const expression = byAlias.get(projection.alias)
|
|
285
|
+
if (expression === undefined) {
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
setPath(
|
|
289
|
+
decoded,
|
|
290
|
+
projection.path,
|
|
291
|
+
decodeProjectionValue(rendered, projection, expression, row[projection.alias], available, driverMode)
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
return decoded
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Constructs an executor from a dialect and implementation callback.
|
|
300
|
+
*/
|
|
301
|
+
export const make = <
|
|
302
|
+
Dialect extends string,
|
|
303
|
+
Error = never,
|
|
304
|
+
Context = never
|
|
305
|
+
>(
|
|
306
|
+
dialect: Dialect,
|
|
307
|
+
execute: <PlanValue extends Query.QueryPlan<any, any, any, any, any, any, any, any, any>>(
|
|
308
|
+
plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
|
|
309
|
+
) => Effect.Effect<Query.ResultRows<PlanValue>, Error, Context>
|
|
310
|
+
): Executor<Dialect, Error, Context> => ({
|
|
311
|
+
dialect,
|
|
312
|
+
execute(plan) {
|
|
313
|
+
return (execute as any)(plan)
|
|
314
|
+
}
|
|
315
|
+
}) as Executor<Dialect, Error, Context>
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Constructs a driver from a dialect and execution callback.
|
|
319
|
+
*/
|
|
320
|
+
export const driver = <
|
|
321
|
+
Dialect extends string,
|
|
322
|
+
Error = never,
|
|
323
|
+
Context = never
|
|
324
|
+
>(
|
|
325
|
+
dialect: Dialect,
|
|
326
|
+
execute: <Row>(
|
|
327
|
+
query: Renderer.RenderedQuery<Row, Dialect>
|
|
328
|
+
) => Effect.Effect<ReadonlyArray<FlatRow>, Error, Context>
|
|
329
|
+
): Driver<Dialect, Error, Context> => ({
|
|
330
|
+
dialect,
|
|
331
|
+
execute(query) {
|
|
332
|
+
return execute(query)
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Creates an executor by composing a renderer with a rendered-query driver.
|
|
338
|
+
*
|
|
339
|
+
* This is the concrete render -> run -> remap pipeline:
|
|
340
|
+
* 1. render a complete query plan into SQL + params
|
|
341
|
+
* 2. execute that rendered query through the driver
|
|
342
|
+
* 3. remap flat alias-keyed rows back into nested objects
|
|
343
|
+
*/
|
|
344
|
+
export const fromDriver = <
|
|
345
|
+
Dialect extends string,
|
|
346
|
+
Error = never,
|
|
347
|
+
Context = never
|
|
348
|
+
>(
|
|
349
|
+
renderer: Renderer.Renderer<Dialect>,
|
|
350
|
+
sqlDriver: Driver<Dialect, Error, Context>
|
|
351
|
+
): Executor<Dialect, Error, Context> => {
|
|
352
|
+
const executor = {
|
|
353
|
+
dialect: renderer.dialect,
|
|
354
|
+
execute(plan: any) {
|
|
355
|
+
const rendered = renderer.render(plan) as Renderer.RenderedQuery<any, Dialect>
|
|
356
|
+
return Effect.map(
|
|
357
|
+
sqlDriver.execute(rendered),
|
|
358
|
+
(rows) => remapRows<any>(rendered, rows)
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return executor as unknown as Executor<Dialect, Error, Context>
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Creates an executor backed by `@effect/sql`'s `SqlClient`.
|
|
367
|
+
*/
|
|
368
|
+
export const fromSqlClient = <Dialect extends string>(
|
|
369
|
+
renderer: Renderer.Renderer<Dialect>
|
|
370
|
+
): Executor<Dialect, unknown, SqlClient.SqlClient> =>
|
|
371
|
+
fromDriver(renderer, driver(renderer.dialect, (query) =>
|
|
372
|
+
Effect.flatMap(SqlClient.SqlClient, (sql) =>
|
|
373
|
+
sql.unsafe<FlatRow>(query.sql, [...query.params]))))
|
|
374
|
+
|
|
375
|
+
/** Runs an effect within the ambient `@effect/sql` transaction service. */
|
|
376
|
+
export const withTransaction = <A, E, R>(
|
|
377
|
+
effect: Effect.Effect<A, E, R>
|
|
378
|
+
): Effect.Effect<A, E | SqlError.SqlError, R | SqlClient.SqlClient> =>
|
|
379
|
+
Effect.flatMap(SqlClient.SqlClient, (sql) => sql.withTransaction(effect))
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Runs an effect in a nested transaction scope.
|
|
383
|
+
*
|
|
384
|
+
* When the ambient `@effect/sql` client is already inside a transaction, the
|
|
385
|
+
* underlying client implementation uses a savepoint.
|
|
386
|
+
*/
|
|
387
|
+
export const withSavepoint = <A, E, R>(
|
|
388
|
+
effect: Effect.Effect<A, E, R>
|
|
389
|
+
): Effect.Effect<A, E | SqlError.SqlError, R | SqlClient.SqlClient> =>
|
|
390
|
+
Effect.flatMap(SqlClient.SqlClient, (sql) => sql.withTransaction(effect))
|