effect-qb 0.17.0 → 0.20.0

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.
Files changed (137) hide show
  1. package/README.md +11 -1
  2. package/dist/index.js +9376 -0
  3. package/dist/mysql.js +4092 -2671
  4. package/dist/postgres/metadata.js +2589 -1402
  5. package/dist/postgres.js +3953 -3583
  6. package/dist/sqlite.js +5527 -4088
  7. package/dist/standard.js +9330 -0
  8. package/package.json +9 -4
  9. package/src/casing.ts +71 -0
  10. package/src/index.ts +2 -0
  11. package/src/internal/casing.ts +89 -0
  12. package/src/internal/coercion/rules.ts +13 -1
  13. package/src/internal/column-state.d.ts +3 -3
  14. package/src/internal/column-state.ts +24 -18
  15. package/src/internal/column.ts +52 -15
  16. package/src/internal/datatypes/define.ts +7 -1
  17. package/src/internal/datatypes/enrich.ts +23 -0
  18. package/src/internal/datatypes/lookup.ts +81 -25
  19. package/src/internal/datatypes/matrix.ts +903 -0
  20. package/src/internal/datatypes/shape.ts +2 -0
  21. package/src/internal/derived-table.ts +4 -36
  22. package/src/{mysql/internal/sql-expression-renderer.ts → internal/dialect-renderers/mysql.ts} +552 -361
  23. package/src/{postgres/internal/sql-expression-renderer.ts → internal/dialect-renderers/postgres.ts} +657 -400
  24. package/src/{sqlite/internal/sql-expression-renderer.ts → internal/dialect-renderers/sqlite.ts} +505 -347
  25. package/src/internal/dialect.ts +36 -1
  26. package/src/internal/dsl-mutation-runtime.ts +12 -162
  27. package/src/internal/dsl-plan-runtime.ts +10 -138
  28. package/src/internal/dsl-query-runtime.ts +5 -79
  29. package/src/internal/dsl-transaction-ddl-runtime.ts +41 -65
  30. package/src/internal/executor.ts +66 -49
  31. package/src/internal/grouping-key.ts +87 -20
  32. package/src/internal/implication-runtime.ts +1 -1
  33. package/src/internal/json/path-access.ts +351 -0
  34. package/src/internal/predicate/runtime.ts +3 -0
  35. package/src/internal/query.d.ts +39 -12
  36. package/src/internal/query.ts +65 -26
  37. package/src/internal/renderer.ts +26 -14
  38. package/src/internal/runtime/driver-value-mapping.ts +3 -3
  39. package/src/internal/runtime/normalize.ts +12 -5
  40. package/src/internal/runtime/schema.ts +28 -38
  41. package/src/internal/runtime/value.ts +20 -23
  42. package/src/internal/scalar.d.ts +1 -1
  43. package/src/internal/scalar.ts +8 -2
  44. package/src/internal/schema-derivation.d.ts +12 -61
  45. package/src/internal/schema-derivation.ts +95 -43
  46. package/src/internal/schema-expression.ts +2 -2
  47. package/src/internal/sql-expression-renderer.ts +19 -0
  48. package/src/internal/standard-dsl.ts +6978 -0
  49. package/src/internal/table-options.ts +126 -66
  50. package/src/internal/table.ts +819 -237
  51. package/src/mysql/column-extension.ts +3 -0
  52. package/src/mysql/column.ts +14 -15
  53. package/src/mysql/datatypes/index.ts +4 -2
  54. package/src/mysql/datatypes/spec.ts +6 -176
  55. package/src/mysql/errors/normalize.ts +0 -1
  56. package/src/mysql/executor.ts +11 -11
  57. package/src/mysql/function/temporal.ts +1 -1
  58. package/src/mysql/internal/dialect.ts +9 -4
  59. package/src/mysql/internal/dsl.ts +334 -170
  60. package/src/mysql/internal/renderer.ts +6 -2
  61. package/src/mysql/json.ts +7 -2
  62. package/src/mysql/query-extension.ts +16 -0
  63. package/src/mysql/renderer.ts +37 -3
  64. package/src/mysql/type.ts +60 -0
  65. package/src/mysql.ts +7 -13
  66. package/src/postgres/check.ts +1 -0
  67. package/src/postgres/column-extension.ts +28 -0
  68. package/src/postgres/column.ts +13 -19
  69. package/src/postgres/datatypes/index.d.ts +2 -1
  70. package/src/postgres/datatypes/index.ts +4 -2
  71. package/src/postgres/datatypes/spec.ts +6 -260
  72. package/src/postgres/errors/normalize.ts +0 -1
  73. package/src/postgres/executor.ts +11 -11
  74. package/src/postgres/foreign-key.ts +24 -0
  75. package/src/postgres/function/core.ts +1 -3
  76. package/src/postgres/function/index.ts +1 -17
  77. package/src/postgres/function/temporal.ts +1 -1
  78. package/src/postgres/index.ts +1 -0
  79. package/src/postgres/internal/dialect.ts +9 -4
  80. package/src/postgres/internal/dsl.ts +328 -179
  81. package/src/postgres/internal/renderer.ts +6 -2
  82. package/src/postgres/internal/schema-ddl.ts +22 -10
  83. package/src/postgres/internal/schema-model.ts +238 -7
  84. package/src/postgres/json-extension.ts +7 -0
  85. package/src/postgres/json.ts +762 -173
  86. package/src/postgres/jsonb.ts +37 -0
  87. package/src/postgres/primary-key.ts +24 -0
  88. package/src/postgres/query-extension.ts +2 -0
  89. package/src/postgres/renderer.ts +37 -3
  90. package/src/postgres/schema-management.ts +13 -13
  91. package/src/postgres/schema.ts +106 -15
  92. package/src/postgres/table.ts +205 -538
  93. package/src/postgres/type.ts +93 -10
  94. package/src/postgres/unique.ts +32 -0
  95. package/src/postgres.ts +20 -16
  96. package/src/sqlite/column-extension.ts +3 -0
  97. package/src/sqlite/column.ts +14 -15
  98. package/src/sqlite/datatypes/index.ts +4 -2
  99. package/src/sqlite/datatypes/spec.ts +6 -94
  100. package/src/sqlite/errors/normalize.ts +0 -1
  101. package/src/sqlite/executor.ts +11 -11
  102. package/src/sqlite/function/temporal.ts +1 -1
  103. package/src/sqlite/internal/dialect.ts +9 -4
  104. package/src/sqlite/internal/dsl.ts +307 -159
  105. package/src/sqlite/internal/renderer.ts +6 -2
  106. package/src/sqlite/json.ts +8 -2
  107. package/src/sqlite/query-extension.ts +2 -0
  108. package/src/sqlite/renderer.ts +37 -3
  109. package/src/sqlite/type.ts +40 -0
  110. package/src/sqlite.ts +7 -13
  111. package/src/standard/cast.ts +113 -0
  112. package/src/standard/check.ts +17 -0
  113. package/src/standard/column.ts +163 -0
  114. package/src/standard/datatypes/index.ts +83 -0
  115. package/src/standard/datatypes/spec.ts +12 -0
  116. package/src/standard/dialect.ts +40 -0
  117. package/src/standard/foreign-key.ts +37 -0
  118. package/src/standard/function/aggregate.ts +2 -0
  119. package/src/standard/function/core.ts +2 -0
  120. package/src/standard/function/index.ts +18 -0
  121. package/src/standard/function/string.ts +2 -0
  122. package/src/standard/function/temporal.ts +78 -0
  123. package/src/standard/function/window.ts +2 -0
  124. package/src/standard/index.ts +17 -0
  125. package/src/standard/internal/renderer.ts +45 -0
  126. package/src/standard/json.ts +883 -0
  127. package/src/standard/primary-key.ts +17 -0
  128. package/src/standard/query.ts +152 -0
  129. package/src/standard/renderer.ts +49 -0
  130. package/src/standard/table.ts +151 -0
  131. package/src/standard/unique.ts +17 -0
  132. package/src/standard.ts +32 -0
  133. package/src/internal/aggregation-validation.ts +0 -57
  134. package/src/internal/table.d.ts +0 -173
  135. package/src/mysql/table.ts +0 -183
  136. package/src/postgres/cast.ts +0 -45
  137. package/src/sqlite/table.ts +0 -183
@@ -1,5 +1,4 @@
1
1
  import * as Plan from "./row-set.js"
2
- import * as Table from "./table.js"
3
2
 
4
3
  type DslTransactionDdlRuntimeContext = {
5
4
  readonly profile: {
@@ -11,16 +10,13 @@ type DslTransactionDdlRuntimeContext = {
11
10
  readonly defaultIndexName: (tableName: string, columns: readonly string[], unique: boolean) => string
12
11
  }
13
12
 
14
- const allowedIsolationLevels = new Set(["read committed", "repeatable read", "serializable"])
15
-
16
- export const renderTransactionIsolationLevel = (isolationLevel: unknown): string => {
13
+ export const renderTransactionIsolationLevel = (
14
+ isolationLevel: unknown
15
+ ): string => {
17
16
  if (isolationLevel === undefined) {
18
17
  return ""
19
18
  }
20
- if (typeof isolationLevel !== "string" || !allowedIsolationLevels.has(isolationLevel)) {
21
- throw new Error("Unsupported transaction isolation level")
22
- }
23
- return `isolation level ${isolationLevel}`
19
+ return `isolation level ${isolationLevel as string}`
24
20
  }
25
21
 
26
22
  export const expectDdlClauseKind = <
@@ -28,53 +24,29 @@ export const expectDdlClauseKind = <
28
24
  Kind extends Ddl["kind"]
29
25
  >(
30
26
  ddl: Ddl | undefined,
31
- kind: Kind
32
- ): Extract<Ddl, { readonly kind: Kind }> => {
33
- if (ddl === undefined || ddl.kind !== kind) {
34
- throw new Error("Unsupported DDL statement kind")
35
- }
36
- return ddl as Extract<Ddl, { readonly kind: Kind }>
37
- }
27
+ _kind: Kind
28
+ ): Extract<Ddl, { readonly kind: Kind }> =>
29
+ ddl as Extract<Ddl, { readonly kind: Kind }>
38
30
 
39
31
  export const expectTruncateClause = <
40
32
  Truncate extends { readonly kind: string }
41
33
  >(
42
34
  truncate: Truncate | undefined
43
- ): Extract<Truncate, { readonly kind: "truncate" }> => {
44
- if (truncate === undefined || truncate.kind !== "truncate") {
45
- throw new Error("Unsupported truncate statement kind")
46
- }
47
- return truncate as Extract<Truncate, { readonly kind: "truncate" }>
48
- }
35
+ ): Extract<Truncate, { readonly kind: "truncate" }> =>
36
+ truncate as Extract<Truncate, { readonly kind: "truncate" }>
49
37
 
50
- const validateIsolationLevel = (isolationLevel: unknown): void => {
51
- renderTransactionIsolationLevel(isolationLevel)
52
- }
53
-
54
- export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContext) => {
55
- const isRecord = (value: unknown): value is Record<PropertyKey, unknown> =>
56
- typeof value === "object" && value !== null
57
-
58
- const assertTableTarget = (target: unknown, apiName: string): void => {
59
- if (!isRecord(target) || !(Table.TypeId in target) || !(Plan.TypeId in target)) {
60
- throw new Error(`${apiName}(...) requires a table target`)
61
- }
62
- }
38
+ export const normalizeStatementFlag = (value: unknown): boolean =>
39
+ (value as boolean | undefined) ?? false
63
40
 
64
- const validateIndexColumns = (target: any, columns: readonly string[]): void => {
65
- const fields = target[Table.TypeId]?.fields as Record<string, unknown> | undefined
66
- if (fields === undefined) {
67
- return
68
- }
69
- for (const columnName of columns) {
70
- if (!(columnName in fields)) {
71
- throw new Error(`effect-qb: unknown index column '${columnName}'`)
72
- }
73
- }
74
- }
41
+ export const normalizeStatementIdentifier = (
42
+ _apiName: string,
43
+ _identifierName: string,
44
+ value: unknown
45
+ ): string =>
46
+ value as string
75
47
 
48
+ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContext) => {
76
49
  const transaction = (options: { readonly isolationLevel?: any; readonly readOnly?: boolean } = {}) => {
77
- validateIsolationLevel(options.isolationLevel)
78
50
  return ctx.makePlan({
79
51
  selection: {},
80
52
  required: [],
@@ -134,8 +106,8 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
134
106
  orderBy: []
135
107
  }, undefined, "transaction", "rollback")
136
108
 
137
- const savepoint = (name: string) =>
138
- ctx.makePlan({
109
+ const savepoint = (name: string) => {
110
+ return ctx.makePlan({
139
111
  selection: {},
140
112
  required: [],
141
113
  available: {},
@@ -153,9 +125,10 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
153
125
  groupBy: [],
154
126
  orderBy: []
155
127
  }, undefined, "transaction", "savepoint")
128
+ }
156
129
 
157
- const rollbackTo = (name: string) =>
158
- ctx.makePlan({
130
+ const rollbackTo = (name: string) => {
131
+ return ctx.makePlan({
159
132
  selection: {},
160
133
  required: [],
161
134
  available: {},
@@ -173,9 +146,10 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
173
146
  groupBy: [],
174
147
  orderBy: []
175
148
  }, undefined, "transaction", "rollbackTo")
149
+ }
176
150
 
177
- const releaseSavepoint = (name: string) =>
178
- ctx.makePlan({
151
+ const releaseSavepoint = (name: string) => {
152
+ return ctx.makePlan({
179
153
  selection: {},
180
154
  required: [],
181
155
  available: {},
@@ -193,9 +167,10 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
193
167
  groupBy: [],
194
168
  orderBy: []
195
169
  }, undefined, "transaction", "releaseSavepoint")
170
+ }
196
171
 
197
172
  const createTable = (target: any, options: { readonly ifNotExists?: boolean } = {}) => {
198
- assertTableTarget(target, "createTable")
173
+ const ifNotExists = normalizeStatementFlag(options.ifNotExists)
199
174
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
200
175
  return ctx.makePlan({
201
176
  selection: {},
@@ -213,7 +188,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
213
188
  },
214
189
  ddl: {
215
190
  kind: "createTable",
216
- ifNotExists: options.ifNotExists ?? false
191
+ ifNotExists
217
192
  },
218
193
  where: [],
219
194
  having: [],
@@ -224,7 +199,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
224
199
  }
225
200
 
226
201
  const dropTable = (target: any, options: { readonly ifExists?: boolean } = {}) => {
227
- assertTableTarget(target, "dropTable")
202
+ const ifExists = normalizeStatementFlag(options.ifExists)
228
203
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
229
204
  return ctx.makePlan({
230
205
  selection: {},
@@ -242,7 +217,7 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
242
217
  },
243
218
  ddl: {
244
219
  kind: "dropTable",
245
- ifExists: options.ifExists ?? false
220
+ ifExists
246
221
  },
247
222
  where: [],
248
223
  having: [],
@@ -253,9 +228,10 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
253
228
  }
254
229
 
255
230
  const createIndex = (target: any, columns: string | readonly string[], options: { readonly name?: string; readonly unique?: boolean; readonly ifNotExists?: boolean } = {}) => {
256
- assertTableTarget(target, "createIndex")
257
231
  const normalizedColumns = ctx.normalizeColumnList(columns)
258
- validateIndexColumns(target, normalizedColumns)
232
+ const unique = normalizeStatementFlag(options.unique)
233
+ const ifNotExists = normalizeStatementFlag(options.ifNotExists)
234
+ const name = options.name
259
235
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
260
236
  return ctx.makePlan({
261
237
  selection: {},
@@ -273,10 +249,10 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
273
249
  },
274
250
  ddl: {
275
251
  kind: "createIndex",
276
- name: options.name ?? ctx.defaultIndexName(sourceBaseName, normalizedColumns, options.unique ?? false),
252
+ name: name ?? ctx.defaultIndexName(sourceBaseName, normalizedColumns, unique),
277
253
  columns: normalizedColumns,
278
- unique: options.unique ?? false,
279
- ifNotExists: options.ifNotExists ?? false
254
+ unique,
255
+ ifNotExists
280
256
  },
281
257
  where: [],
282
258
  having: [],
@@ -287,9 +263,9 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
287
263
  }
288
264
 
289
265
  const dropIndex = (target: any, columns: string | readonly string[], options: { readonly name?: string; readonly ifExists?: boolean } = {}) => {
290
- assertTableTarget(target, "dropIndex")
291
266
  const normalizedColumns = ctx.normalizeColumnList(columns)
292
- validateIndexColumns(target, normalizedColumns)
267
+ const ifExists = normalizeStatementFlag(options.ifExists)
268
+ const name = options.name
293
269
  const { sourceName, sourceBaseName } = ctx.targetSourceDetails(target)
294
270
  return ctx.makePlan({
295
271
  selection: {},
@@ -307,8 +283,8 @@ export const makeDslTransactionDdlRuntime = (ctx: DslTransactionDdlRuntimeContex
307
283
  },
308
284
  ddl: {
309
285
  kind: "dropIndex",
310
- name: options.name ?? ctx.defaultIndexName(sourceBaseName, normalizedColumns, false),
311
- ifExists: options.ifExists ?? false
286
+ name: name ?? ctx.defaultIndexName(sourceBaseName, normalizedColumns, false),
287
+ ifExists
312
288
  },
313
289
  where: [],
314
290
  having: [],
@@ -1,9 +1,10 @@
1
1
  import * as Chunk from "effect/Chunk"
2
2
  import * as Effect from "effect/Effect"
3
+ import * as Exit from "effect/Exit"
3
4
  import * as Option from "effect/Option"
4
5
  import * as Schema from "effect/Schema"
5
- import * as SqlClient from "@effect/sql/SqlClient"
6
- import * as SqlError from "@effect/sql/SqlError"
6
+ import * as SqlClient from "effect/unstable/sql/SqlClient"
7
+ import * as SqlError from "effect/unstable/sql/SqlError"
7
8
  import * as Stream from "effect/Stream"
8
9
 
9
10
  import * as Expression from "./scalar.js"
@@ -44,6 +45,10 @@ export interface RowDecodeError {
44
45
  readonly normalized?: unknown
45
46
  readonly stage: "normalize" | "schema"
46
47
  readonly cause: unknown
48
+ readonly schemaError?: {
49
+ readonly message: string
50
+ readonly issue: unknown
51
+ }
47
52
  }
48
53
 
49
54
  /**
@@ -183,26 +188,35 @@ const makeRowDecodeError = (
183
188
  stage: RowDecodeError["stage"],
184
189
  cause: unknown,
185
190
  normalized?: unknown
186
- ): RowDecodeError => ({
187
- _tag: "RowDecodeError",
188
- message: stage === "normalize"
189
- ? `Failed to normalize projection '${projection.alias}'`
190
- : `Failed to decode projection '${projection.alias}' against its runtime schema`,
191
- dialect: rendered.dialect,
192
- query: {
193
- sql: rendered.sql,
194
- params: rendered.params
195
- },
196
- projection: {
197
- alias: projection.alias,
198
- path: projection.path
199
- },
200
- dbType: expression[Expression.TypeId].dbType,
201
- raw,
202
- normalized,
203
- stage,
204
- cause
205
- })
191
+ ): RowDecodeError => {
192
+ const schemaError = Schema.isSchemaError(cause)
193
+ ? {
194
+ message: cause.message,
195
+ issue: cause.issue
196
+ }
197
+ : undefined
198
+ return {
199
+ _tag: "RowDecodeError",
200
+ message: stage === "normalize"
201
+ ? `Failed to normalize projection '${projection.alias}'`
202
+ : `Failed to decode projection '${projection.alias}' against its runtime schema`,
203
+ dialect: rendered.dialect,
204
+ query: {
205
+ sql: rendered.sql,
206
+ params: rendered.params
207
+ },
208
+ projection: {
209
+ alias: projection.alias,
210
+ path: projection.path
211
+ },
212
+ dbType: expression[Expression.TypeId].dbType,
213
+ raw,
214
+ normalized,
215
+ stage,
216
+ cause,
217
+ ...(schemaError === undefined ? {} : { schemaError })
218
+ }
219
+ }
206
220
 
207
221
  const hasOptionalSourceDependency = (
208
222
  expression: Expression.Any,
@@ -249,7 +263,7 @@ const dbTypeAllowsTopLevelJsonNull = (
249
263
  }
250
264
 
251
265
  const schemaAcceptsNull = (
252
- schema: Schema.Schema.Any | undefined
266
+ schema: Schema.Top | undefined
253
267
  ): boolean =>
254
268
  schema !== undefined && (Schema.is(schema) as (value: unknown) => boolean)(null)
255
269
 
@@ -325,15 +339,20 @@ const decodeProjectionValue = (
325
339
  return normalized
326
340
  }
327
341
 
328
- if ((Schema.is(schema as Schema.Schema.Any) as (value: unknown) => boolean)(normalized)) {
342
+ if ((Schema.is(schema as Schema.Top) as (value: unknown) => boolean)(normalized)) {
329
343
  return normalized
330
344
  }
331
345
 
332
- try {
333
- return (Schema.decodeUnknownSync as any)(schema)(normalized)
334
- } catch (cause) {
335
- throw makeRowDecodeError(rendered, projection, expression, raw, "schema", cause, normalized)
346
+ const decoded = (Schema.decodeUnknownExit as any)(schema)(normalized)
347
+ if (Exit.isSuccess(decoded)) {
348
+ return decoded.value
336
349
  }
350
+
351
+ const cause = Option.match(Exit.findErrorOption(decoded), {
352
+ onNone: () => decoded.cause,
353
+ onSome: (schemaError) => schemaError
354
+ })
355
+ throw makeRowDecodeError(rendered, projection, expression, raw, "schema", cause, normalized)
337
356
  }
338
357
 
339
358
  export const makeRowDecoder = (
@@ -390,7 +409,7 @@ export const decodeChunk = (
390
409
  } = {}
391
410
  ): Chunk.Chunk<any> => {
392
411
  const decodeRow = makeRowDecoder(rendered, plan, options)
393
- return Chunk.unsafeFromArray(Chunk.toReadonlyArray(rows).map((row) => decodeRow(row)))
412
+ return Chunk.fromIterable(Chunk.toReadonlyArray(rows).map((row) => decodeRow(row)))
394
413
  }
395
414
 
396
415
  export const decodeRows = (
@@ -509,33 +528,37 @@ export const fromDriver = <
509
528
  renderer: Renderer.Renderer<Dialect>,
510
529
  sqlDriver: Driver<Dialect, Error, Context>
511
530
  ): Executor<Dialect, Error, Context> => {
512
- const executor = {
531
+ const executor: Executor<Dialect, Error, Context> = {
513
532
  dialect: renderer.dialect,
514
- execute(plan: any) {
533
+ execute<PlanValue extends Query.Plan.Any>(
534
+ plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
535
+ ) {
515
536
  const rendered = renderer.render(plan) as Renderer.RenderedQuery<any, Dialect>
516
537
  return Effect.map(
517
538
  sqlDriver.execute(rendered),
518
539
  (rows) => remapRows<any>(rendered, rows)
519
- )
540
+ ) as Effect.Effect<Query.ResultRows<PlanValue>, Error, Context>
520
541
  },
521
- stream(plan: any) {
542
+ stream<PlanValue extends Query.Plan.Any>(
543
+ plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
544
+ ) {
522
545
  const rendered = renderer.render(plan) as Renderer.RenderedQuery<any, Dialect>
523
- return Stream.mapChunks(
546
+ return Stream.mapArray(
524
547
  sqlDriver.stream(rendered),
525
- (rows) => Chunk.unsafeFromArray(remapRows<any>(rendered, Chunk.toReadonlyArray(rows)))
526
- )
548
+ (rows) => remapRows<any>(rendered, rows) as never
549
+ ) as Stream.Stream<Query.ResultRow<PlanValue>, Error, Context>
527
550
  }
528
551
  }
529
- return executor as unknown as Executor<Dialect, Error, Context>
552
+ return executor
530
553
  }
531
554
 
532
555
  export const streamFromSqlClient = <Dialect extends string>(
533
556
  query: Renderer.RenderedQuery<any, Dialect>
534
557
  ): Stream.Stream<FlatRow, SqlError.SqlError, SqlClient.SqlClient> =>
535
- Stream.unwrapScoped(
558
+ Stream.unwrap(
536
559
  Effect.flatMap(SqlClient.SqlClient, (sql) =>
537
560
  Effect.flatMap(
538
- Effect.serviceOption(SqlClient.TransactionConnection),
561
+ Effect.serviceOption(sql.transactionService),
539
562
  Option.match({
540
563
  onNone: () => sql.reserve,
541
564
  onSome: ([connection]) => Effect.succeed(connection)
@@ -556,19 +579,13 @@ export const fromSqlClient = <Dialect extends string>(
556
579
  stream: (query) => streamFromSqlClient(query)
557
580
  }))
558
581
 
559
- /** Runs an effect within the ambient `@effect/sql` transaction service. */
560
- export const withTransaction = <A, E, R>(
561
- effect: Effect.Effect<A, E, R>
562
- ): Effect.Effect<A, E | SqlError.SqlError, R | SqlClient.SqlClient> =>
563
- Effect.flatMap(SqlClient.SqlClient, (sql) => sql.withTransaction(effect))
564
-
565
582
  /**
566
- * Runs an effect in a nested transaction scope.
583
+ * Runs an effect within the ambient `effect/unstable/sql` transaction service.
567
584
  *
568
- * When the ambient `@effect/sql` client is already inside a transaction, the
569
- * underlying client implementation uses a savepoint.
585
+ * Nested calls rely on the underlying client transaction implementation for
586
+ * savepoint behavior.
570
587
  */
571
- export const withSavepoint = <A, E, R>(
588
+ export const withTransaction = <A, E, R>(
572
589
  effect: Effect.Effect<A, E, R>
573
590
  ): Effect.Effect<A, E | SqlError.SqlError, R | SqlClient.SqlClient> =>
574
591
  Effect.flatMap(SqlClient.SqlClient, (sql) => sql.withTransaction(effect))
@@ -21,7 +21,9 @@ const subqueryPlanGroupingKey = (plan: unknown): string => {
21
21
 
22
22
  const literalGroupingKey = (value: unknown): string => {
23
23
  if (value instanceof Date) {
24
- return `date:${value.toISOString()}`
24
+ return Number.isNaN(value.getTime())
25
+ ? "date:invalid"
26
+ : `date:${value.toISOString()}`
25
27
  }
26
28
  if (value === null) {
27
29
  return "null"
@@ -44,6 +46,30 @@ const isExpression = (value: unknown): value is Expression.Any =>
44
46
  const expressionGroupingKey = (value: unknown): string =>
45
47
  isExpression(value) ? groupingKeyOfExpression(value) : "missing"
46
48
 
49
+ const requiredExpressionGroupingKey = (
50
+ _functionName: string,
51
+ value: unknown
52
+ ): string => groupingKeyOfExpression(value as Expression.Any)
53
+
54
+ const requiredBinaryExpressionGroupingKey = (
55
+ _functionName: string,
56
+ left: unknown,
57
+ right: unknown
58
+ ): string => `${groupingKeyOfExpression(left as Expression.Any)},${groupingKeyOfExpression(right as Expression.Any)}`
59
+
60
+ const functionCallArgsGroupingKey = (args: unknown): string =>
61
+ (args as readonly Expression.Any[]).map(groupingKeyOfExpression).join(",")
62
+
63
+ const variadicGroupingKey = (
64
+ _functionName: string,
65
+ values: unknown
66
+ ): string => (values as readonly Expression.Any[]).map(groupingKeyOfExpression).join(",")
67
+
68
+ const castTargetGroupingKey = (target: unknown): string => {
69
+ const typed = target as { readonly dialect?: string; readonly kind?: string } | null | undefined
70
+ return `${typed?.dialect}:${typed?.kind}`
71
+ }
72
+
47
73
  const escapeGroupingText = (value: string): string =>
48
74
  value
49
75
  .replace(/\\/g, "\\\\")
@@ -52,6 +78,30 @@ const escapeGroupingText = (value: string): string =>
52
78
  .replace(/=/g, "\\=")
53
79
  .replace(/>/g, "\\>")
54
80
 
81
+ const functionCallNameGroupingKey = (name: unknown): string =>
82
+ escapeGroupingText(name as string)
83
+
84
+ const quantifiedComparisonGroupingName = (
85
+ kind: "comparisonAny" | "comparisonAll"
86
+ ): "compareAny" | "compareAll" =>
87
+ kind === "comparisonAny" ? "compareAny" : "compareAll"
88
+
89
+ const caseGroupingKey = (
90
+ branches: unknown,
91
+ fallback: unknown
92
+ ): string => {
93
+ const typedBranches = branches as readonly ExpressionAst.CaseBranchNode[]
94
+ return `case(${typedBranches.map((branch) =>
95
+ `when:${groupingKeyOfExpression(branch.when)}=>${groupingKeyOfExpression(branch.then)}`).join("|")};else:${groupingKeyOfExpression(fallback as Expression.Any)})`
96
+ }
97
+
98
+ const collationGroupingKey = (collation: unknown): string => {
99
+ if (Array.isArray(collation)) {
100
+ return collation.map((part) => escapeGroupingText(String(part))).join(",")
101
+ }
102
+ return escapeGroupingText(String(collation))
103
+ }
104
+
55
105
  const jsonSegmentGroupingKey = (segment: unknown): string => {
56
106
  if (segment !== null && typeof segment === "object" && "kind" in segment) {
57
107
  switch ((segment as { readonly kind: string }).kind) {
@@ -78,8 +128,15 @@ const jsonSegmentGroupingKey = (segment: unknown): string => {
78
128
  return "unknown"
79
129
  }
80
130
 
81
- const jsonPathGroupingKey = (segments: readonly unknown[] | undefined): string =>
82
- (segments ?? []).map(jsonSegmentGroupingKey).join(",")
131
+ const jsonPathGroupingKey = (segments: unknown): string => {
132
+ if (segments === undefined) {
133
+ return ""
134
+ }
135
+ if (!Array.isArray(segments)) {
136
+ return "unknown"
137
+ }
138
+ return segments.map(jsonSegmentGroupingKey).join(",")
139
+ }
83
140
 
84
141
  const isJsonPath = (value: unknown): value is JsonPath.Path =>
85
142
  value !== null && typeof value === "object" && JsonPath.TypeId in value
@@ -97,9 +154,20 @@ const jsonOpaquePathGroupingKey = (value: unknown): string => {
97
154
  return "jsonpath:unknown"
98
155
  }
99
156
 
100
- const jsonEntryGroupingKey = (
101
- entry: { readonly key: string; readonly value: Expression.Any }
102
- ): string => `${escapeGroupingText(entry.key)}=>${groupingKeyOfExpression(entry.value)}`
157
+ const jsonKeysGroupingKey = (keys: unknown): string => {
158
+ if (!Array.isArray(keys) || keys.length === 0) {
159
+ return ""
160
+ }
161
+ return keys.map((key) => escapeGroupingText(String(key))).join(",")
162
+ }
163
+
164
+ const jsonBuildObjectGroupingKey = (entries: unknown): string => {
165
+ return (entries as readonly { readonly key: string; readonly value: Expression.Any }[]).map((entry) =>
166
+ `${escapeGroupingText(entry.key)}=>${groupingKeyOfExpression(entry.value)}`).join("|")
167
+ }
168
+
169
+ const jsonBuildArrayGroupingKey = (values: unknown): string =>
170
+ (values as readonly Expression.Any[]).map(groupingKeyOfExpression).join(",")
103
171
 
104
172
  export const groupingKeyOfExpression = (expression: Expression.Any): string => {
105
173
  const ast = (expression as Expression.Any & {
@@ -111,11 +179,11 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
111
179
  case "literal":
112
180
  return `literal:${literalGroupingKey(ast.value)}`
113
181
  case "cast":
114
- return `cast(${groupingKeyOfExpression(ast.value)} as ${ast.target.dialect}:${ast.target.kind})`
182
+ return `cast(${requiredExpressionGroupingKey("cast", ast.value)} as ${castTargetGroupingKey(ast.target)})`
115
183
  case "collate":
116
- return `collate(${groupingKeyOfExpression(ast.value)},${ast.collation.map(escapeGroupingText).join(",")})`
184
+ return `collate(${requiredExpressionGroupingKey("collate", ast.value)},${collationGroupingKey(ast.collation)})`
117
185
  case "function":
118
- return `function(${escapeGroupingText(ast.name)},${ast.args.map(groupingKeyOfExpression).join(",")})`
186
+ return `function(${functionCallNameGroupingKey(ast.name)},${functionCallArgsGroupingKey(ast.args)})`
119
187
  case "isNull":
120
188
  case "isNotNull":
121
189
  case "not":
@@ -124,7 +192,7 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
124
192
  case "count":
125
193
  case "max":
126
194
  case "min":
127
- return `${ast.kind}(${groupingKeyOfExpression(ast.value)})`
195
+ return `${ast.kind}(${requiredExpressionGroupingKey(ast.kind, ast.value)})`
128
196
  case "eq":
129
197
  case "neq":
130
198
  case "lt":
@@ -142,7 +210,7 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
142
210
  case "contains":
143
211
  case "containedBy":
144
212
  case "overlaps":
145
- return `${ast.kind}(${groupingKeyOfExpression(ast.left)},${groupingKeyOfExpression(ast.right)})`
213
+ return `${ast.kind}(${requiredBinaryExpressionGroupingKey(ast.kind, ast.left, ast.right)})`
146
214
  case "and":
147
215
  case "or":
148
216
  case "coalesce":
@@ -150,19 +218,18 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
150
218
  case "in":
151
219
  case "notIn":
152
220
  case "between":
153
- return `${ast.kind}(${ast.values.map(groupingKeyOfExpression).join(",")})`
221
+ return `${ast.kind}(${variadicGroupingKey(ast.kind, ast.values)})`
154
222
  case "case":
155
- return `case(${ast.branches.map((branch: ExpressionAst.CaseBranchNode) =>
156
- `when:${groupingKeyOfExpression(branch.when)}=>${groupingKeyOfExpression(branch.then)}`).join("|")};else:${groupingKeyOfExpression(ast.else)})`
223
+ return caseGroupingKey(ast.branches, ast.else)
157
224
  case "exists":
158
225
  return `exists(${subqueryPlanGroupingKey(ast.plan)})`
159
226
  case "scalarSubquery":
160
227
  return `scalarSubquery(${subqueryPlanGroupingKey(ast.plan)})`
161
228
  case "inSubquery":
162
- return `inSubquery(${groupingKeyOfExpression(ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
229
+ return `inSubquery(${requiredExpressionGroupingKey("inSubquery", ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
163
230
  case "comparisonAny":
164
231
  case "comparisonAll":
165
- return `${ast.kind}(${ast.operator},${groupingKeyOfExpression(ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
232
+ return `${ast.kind}(${ast.operator},${requiredExpressionGroupingKey(quantifiedComparisonGroupingName(ast.kind), ast.left)},${subqueryPlanGroupingKey(ast.plan)})`
166
233
  case "jsonGet":
167
234
  case "jsonPath":
168
235
  case "jsonAccess":
@@ -176,7 +243,7 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
176
243
  case "jsonKeyExists":
177
244
  case "jsonHasAnyKeys":
178
245
  case "jsonHasAllKeys":
179
- return `json(${ast.kind},${expressionGroupingKey(ast.base)},${(ast.keys ?? []).map(escapeGroupingText).join(",")})`
246
+ return `json(${ast.kind},${expressionGroupingKey(ast.base)},${jsonKeysGroupingKey(ast.keys)})`
180
247
  case "jsonConcat":
181
248
  case "jsonMerge":
182
249
  return `json(${ast.kind},${expressionGroupingKey(ast.left)},${expressionGroupingKey(ast.right)},)`
@@ -192,9 +259,9 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
192
259
  case "jsonPathMatch":
193
260
  return `json(${ast.kind},${expressionGroupingKey(ast.base)},${jsonOpaquePathGroupingKey(ast.query)})`
194
261
  case "jsonBuildObject":
195
- return `json(${ast.kind},${(ast.entries ?? []).map(jsonEntryGroupingKey).join("|")})`
262
+ return `json(${ast.kind},${jsonBuildObjectGroupingKey(ast.entries)})`
196
263
  case "jsonBuildArray":
197
- return `json(${ast.kind},${(ast.values ?? []).map(groupingKeyOfExpression).join(",")})`
264
+ return `json(${ast.kind},${jsonBuildArrayGroupingKey(ast.values)})`
198
265
  case "jsonToJson":
199
266
  case "jsonToJsonb":
200
267
  case "jsonTypeOf":
@@ -203,7 +270,7 @@ export const groupingKeyOfExpression = (expression: Expression.Any): string => {
203
270
  case "jsonStripNulls":
204
271
  return `json(${ast.kind},${expressionGroupingKey(ast.value)})`
205
272
  default:
206
- throw new Error("Unsupported expression for grouping key generation")
273
+ return `unknown:${typeof ast === "object" && ast !== null && "kind" in ast ? String((ast as { readonly kind: unknown }).kind) : "ast"}`
207
274
  }
208
275
  }
209
276
 
@@ -41,7 +41,7 @@ const collectPresenceWitnesses = (
41
41
  return
42
42
  }
43
43
  if (Expression.TypeId in selection && ExpressionAst.TypeId in selection) {
44
- const expression = selection as unknown as AstBackedExpression
44
+ const expression = selection as AstBackedExpression
45
45
  const ast = expression[ExpressionAst.TypeId]
46
46
  if (ast.kind === "column" && expression[Expression.TypeId].nullability === "never") {
47
47
  output.add(columnPredicateKey(ast.tableName, ast.columnName))