effect-qb 0.15.0 → 0.16.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.
@@ -1,4 +1,5 @@
1
1
  import * as Query from "../../internal/query.js"
2
+ import type * as Expression from "../../internal/scalar.js"
2
3
  import { type RenderState } from "../../internal/dialect.js"
3
4
  import { mysqlDialect } from "./dialect.js"
4
5
  import { type Projection } from "../../internal/projections.js"
@@ -11,16 +12,23 @@ export interface MysqlRenderResult {
11
12
  readonly sql: string
12
13
  readonly params: readonly unknown[]
13
14
  readonly projections: readonly Projection[]
15
+ readonly valueMappings?: Expression.DriverValueMappings
16
+ }
17
+
18
+ export interface MysqlRenderOptions {
19
+ readonly valueMappings?: Expression.DriverValueMappings
14
20
  }
15
21
 
16
22
  /**
17
23
  * Renders the current query AST into MySQL-shaped SQL plus bind parameters.
18
24
  */
19
25
  export const renderMysqlPlan = <PlanValue extends Query.Plan.Any>(
20
- plan: Query.DialectCompatiblePlan<PlanValue, "mysql">
26
+ plan: Query.DialectCompatiblePlan<PlanValue, "mysql">,
27
+ options: MysqlRenderOptions = {}
21
28
  ): MysqlRenderResult => {
22
29
  const state: RenderState = {
23
30
  params: [],
31
+ valueMappings: options.valueMappings,
24
32
  ctes: [],
25
33
  cteNames: new Set<string>()
26
34
  }
@@ -32,6 +40,7 @@ export const renderMysqlPlan = <PlanValue extends Query.Plan.Any>(
32
40
  return {
33
41
  sql: rendered.sql,
34
42
  params: state.params,
35
- projections: rendered.projections
43
+ projections: rendered.projections,
44
+ valueMappings: state.valueMappings
36
45
  }
37
46
  }
@@ -5,6 +5,11 @@ import * as QueryAst from "../../internal/query-ast.js"
5
5
  import type { RenderState, SqlDialect } from "../../internal/dialect.js"
6
6
  import * as ExpressionAst from "../../internal/expression-ast.js"
7
7
  import * as JsonPath from "../../internal/json/path.js"
8
+ import {
9
+ renderJsonSelectSql,
10
+ renderSelectSql,
11
+ toDriverValue
12
+ } from "../../internal/runtime/driver-value-mapping.js"
8
13
  import { flattenSelection, type Projection } from "../../internal/projections.js"
9
14
  import { type SelectionValue, validateAggregationSelection } from "../../internal/aggregation-validation.js"
10
15
  import * as SchemaExpression from "../../internal/schema-expression.js"
@@ -313,6 +318,43 @@ const renderPostgresJsonValue = (
313
318
  : `cast(${rendered} as jsonb)`
314
319
  }
315
320
 
321
+ const expressionDriverContext = (
322
+ expression: Expression.Any,
323
+ state: RenderState,
324
+ dialect: SqlDialect
325
+ ) => ({
326
+ dialect: dialect.name,
327
+ valueMappings: state.valueMappings,
328
+ dbType: expression[Expression.TypeId].dbType,
329
+ runtimeSchema: expression[Expression.TypeId].runtimeSchema,
330
+ driverValueMapping: expression[Expression.TypeId].driverValueMapping
331
+ })
332
+
333
+ const renderJsonInputExpression = (
334
+ expression: Expression.Any,
335
+ state: RenderState,
336
+ dialect: SqlDialect
337
+ ): string =>
338
+ renderJsonSelectSql(
339
+ renderExpression(expression, state, dialect),
340
+ expressionDriverContext(expression, state, dialect)
341
+ )
342
+
343
+ const encodeArrayValues = (
344
+ values: readonly unknown[],
345
+ column: Table.AnyTable[typeof Table.TypeId]["fields"][string],
346
+ state: RenderState,
347
+ dialect: SqlDialect
348
+ ): readonly unknown[] =>
349
+ values.map((value) =>
350
+ toDriverValue(value, {
351
+ dialect: dialect.name,
352
+ valueMappings: state.valueMappings,
353
+ dbType: column.metadata.dbType,
354
+ runtimeSchema: column.schema,
355
+ driverValueMapping: column.metadata.driverValueMapping
356
+ }))
357
+
316
358
  const renderPostgresJsonKind = (
317
359
  value: Expression.Any
318
360
  ): "json" | "jsonb" => value[Expression.TypeId].dbType.kind === "jsonb" ? "jsonb" : "json"
@@ -474,7 +516,7 @@ const renderJsonExpression = (
474
516
  : []
475
517
  const renderedEntries = entries.flatMap((entry) => [
476
518
  dialect.renderLiteral(entry.key, state),
477
- renderExpression(entry.value, state, dialect)
519
+ renderJsonInputExpression(entry.value, state, dialect)
478
520
  ])
479
521
  if (dialect.name === "postgres") {
480
522
  return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_object(${renderedEntries.join(", ")})`
@@ -488,7 +530,7 @@ const renderJsonExpression = (
488
530
  const values = Array.isArray((ast as { readonly values?: readonly Expression.Any[] }).values)
489
531
  ? (ast as { readonly values: readonly Expression.Any[] }).values
490
532
  : []
491
- const renderedValues = values.map((value) => renderExpression(value, state, dialect)).join(", ")
533
+ const renderedValues = values.map((value) => renderJsonInputExpression(value, state, dialect)).join(", ")
492
534
  if (dialect.name === "postgres") {
493
535
  return `${postgresExpressionKind === "jsonb" ? "jsonb" : "json"}_build_array(${renderedValues})`
494
536
  }
@@ -502,7 +544,7 @@ const renderJsonExpression = (
502
544
  return undefined
503
545
  }
504
546
  if (dialect.name === "postgres") {
505
- return `to_json(${renderExpression(base, state, dialect)})`
547
+ return `to_json(${renderJsonInputExpression(base, state, dialect)})`
506
548
  }
507
549
  if (dialect.name === "mysql") {
508
550
  return `cast(${renderExpression(base, state, dialect)} as json)`
@@ -513,7 +555,7 @@ const renderJsonExpression = (
513
555
  return undefined
514
556
  }
515
557
  if (dialect.name === "postgres") {
516
- return `to_jsonb(${renderExpression(base, state, dialect)})`
558
+ return `to_jsonb(${renderJsonInputExpression(base, state, dialect)})`
517
559
  }
518
560
  if (dialect.name === "mysql") {
519
561
  return `cast(${renderExpression(base, state, dialect)} as json)`
@@ -767,7 +809,7 @@ const renderSelectionList = (
767
809
  const flattened = flattenSelection(selection)
768
810
  const projections = selectionProjections(selection)
769
811
  const sql = flattened.map(({ expression, alias }) =>
770
- `${renderExpression(expression, state, dialect)} as ${dialect.quoteIdentifier(alias)}`).join(", ")
812
+ `${renderSelectSql(renderExpression(expression, state, dialect), expressionDriverContext(expression, state, dialect))} as ${dialect.quoteIdentifier(alias)}`).join(", ")
771
813
  return {
772
814
  sql,
773
815
  projections
@@ -888,14 +930,18 @@ export const renderQueryAst = (
888
930
  const table = targetSource.source as Table.AnyTable
889
931
  const fields = table[Table.TypeId].fields
890
932
  const rendered = unnestSource.values.map((entry) =>
891
- `cast(${dialect.renderLiteral(entry.values, state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
933
+ `cast(${dialect.renderLiteral(encodeArrayValues(entry.values, fields[entry.columnName]!, state, dialect), state)} as ${renderCastType(dialect, fields[entry.columnName]!.metadata.dbType)}[])`
892
934
  ).join(", ")
893
935
  sql += ` (${columns}) select * from unnest(${rendered})`
894
936
  } else {
895
937
  const rowCount = unnestSource.values[0]?.values.length ?? 0
896
938
  const rows = Array.from({ length: rowCount }, (_, index) =>
897
939
  `(${unnestSource.values.map((entry) =>
898
- dialect.renderLiteral(entry.values[index], state)
940
+ dialect.renderLiteral(
941
+ entry.values[index],
942
+ state,
943
+ (targetSource.source as Table.AnyTable)[Table.TypeId].fields[entry.columnName]![Expression.TypeId]
944
+ )
899
945
  ).join(", ")})`
900
946
  ).join(", ")
901
947
  sql += ` (${columns}) values ${rows}`
@@ -1267,7 +1313,7 @@ export const renderExpression = (
1267
1313
  ? dialect.quoteIdentifier(ast.columnName)
1268
1314
  : `${dialect.quoteIdentifier(ast.tableName)}.${dialect.quoteIdentifier(ast.columnName)}`
1269
1315
  case "literal":
1270
- return dialect.renderLiteral(ast.value, state)
1316
+ return dialect.renderLiteral(ast.value, state, expression[Expression.TypeId])
1271
1317
  case "excluded":
1272
1318
  return dialect.name === "mysql"
1273
1319
  ? `values(${dialect.quoteIdentifier(ast.columnName)})`
@@ -1,4 +1,5 @@
1
1
  import * as CoreRenderer from "../internal/renderer.js"
2
+ import type * as Expression from "../internal/scalar.js"
2
3
  import { renderMysqlPlan } from "./internal/renderer.js"
3
4
 
4
5
  /** MySQL-specialized rendered query shape. */
@@ -8,12 +9,16 @@ export type RowOf<Value extends RenderedQuery<any>> = CoreRenderer.RowOf<Value>
8
9
  /** MySQL-specialized renderer contract. */
9
10
  export type Renderer = CoreRenderer.Renderer<"mysql">
10
11
 
12
+ export interface MakeOptions {
13
+ readonly valueMappings?: Expression.DriverValueMappings
14
+ }
15
+
11
16
  export { TypeId } from "../internal/renderer.js"
12
17
  export type { Projection } from "../internal/renderer.js"
13
18
 
14
19
  /** Creates the built-in MySQL renderer. */
15
- export const make = (): Renderer =>
16
- CoreRenderer.make("mysql", renderMysqlPlan)
20
+ export const make = (options: MakeOptions = {}): Renderer =>
21
+ CoreRenderer.make("mysql", (plan) => renderMysqlPlan(plan, options))
17
22
 
18
23
  /** Shared built-in MySQL renderer instance. */
19
24
  export const mysql = make()
@@ -1,26 +1,41 @@
1
1
  import type * as Expression from "../internal/scalar.js"
2
+ import type * as ExpressionAst from "../internal/expression-ast.js"
2
3
  import type { ExpressionInput } from "../internal/query.js"
3
4
  import { cast as postgresCast } from "./internal/dsl.js"
4
5
 
5
6
  type CastInput = ExpressionInput
6
7
  type CastTarget = Expression.DbType.Any
7
- type CastExpression<Target extends CastTarget> = Expression.Scalar<
8
+ type CastNullability<Value extends CastInput> = Value extends Expression.Any
9
+ ? Expression.NullabilityOf<Value>
10
+ : Value extends null ? "always" : "never"
11
+ type CastKind<Value extends CastInput> = Value extends Expression.Any
12
+ ? Expression.KindOf<Value>
13
+ : "scalar"
14
+ type CastDependencies<Value extends CastInput> = Value extends Expression.Any
15
+ ? Expression.DependenciesOf<Value>
16
+ : never
17
+ type CastExpression<
18
+ Value extends CastInput,
19
+ Target extends CastTarget
20
+ > = Expression.Scalar<
8
21
  Expression.RuntimeOfDbType<Target>,
9
22
  Target,
10
- Expression.Nullability,
23
+ CastNullability<Value>,
11
24
  Target["dialect"],
12
- Expression.ScalarKind,
13
- Expression.BindingId
14
- >
25
+ CastKind<Value>,
26
+ CastDependencies<Value>
27
+ > & {
28
+ readonly [ExpressionAst.TypeId]: ExpressionAst.CastNode<Value extends Expression.Any ? Value : Expression.Any, Target>
29
+ }
15
30
 
16
31
  const to: {
17
32
  <Value extends CastInput, Target extends CastTarget>(
18
33
  value: Value,
19
34
  target: Target
20
- ): CastExpression<Target>
35
+ ): CastExpression<Value, Target>
21
36
  <Target extends CastTarget>(
22
37
  target: Target
23
- ): <Value extends CastInput>(value: Value) => CastExpression<Target>
38
+ ): <Value extends CastInput>(value: Value) => CastExpression<Value, Target>
24
39
  } = ((...args: [CastInput, CastTarget] | [CastTarget]) =>
25
40
  args.length === 1
26
41
  ? ((value: CastInput) => postgresCast(value as never, args[0] as never))
@@ -165,6 +165,7 @@ export const unique = BaseColumn.unique
165
165
  const default_ = BaseColumn.default_
166
166
  export const generated = BaseColumn.generated
167
167
  export const ddlType = BaseColumn.ddlType
168
+ export const driverValueMapping = BaseColumn.driverValueMapping
168
169
  export const array = BaseColumn.array
169
170
  export const identityAlways = BaseColumn.identityAlways
170
171
  export const identityByDefault = BaseColumn.identityByDefault
@@ -5,6 +5,7 @@ import * as Stream from "effect/Stream"
5
5
  import * as CoreExecutor from "../internal/executor.js"
6
6
  import * as CoreQuery from "../internal/query.js"
7
7
  import * as CoreRenderer from "../internal/renderer.js"
8
+ import type * as Expression from "../internal/scalar.js"
8
9
  import { renderPostgresPlan } from "./internal/renderer.js"
9
10
  import {
10
11
  narrowPostgresDriverErrorForReadQuery,
@@ -28,6 +29,7 @@ export interface MakeOptions<Error = never, Context = never> {
28
29
  readonly renderer?: Renderer
29
30
  readonly driver?: Driver<Error, Context>
30
31
  readonly driverMode?: CoreExecutor.DriverMode
32
+ readonly valueMappings?: Expression.DriverValueMappings
31
33
  }
32
34
  /** Standard composed error shape for Postgres executors. */
33
35
  export type PostgresExecutorError = PostgresDriverError | RowDecodeError
@@ -67,7 +69,8 @@ const fromDriver = <
67
69
  >(
68
70
  renderer: Renderer,
69
71
  sqlDriver: Driver<Error, Context>,
70
- driverMode: CoreExecutor.DriverMode = "raw"
72
+ driverMode: CoreExecutor.DriverMode = "raw",
73
+ valueMappings?: Expression.DriverValueMappings
71
74
  ): QueryExecutor<Context> => ({
72
75
  dialect: "postgres",
73
76
  execute(plan) {
@@ -76,7 +79,7 @@ const fromDriver = <
76
79
  Effect.flatMap(
77
80
  sqlDriver.execute(rendered),
78
81
  (rows) => Effect.try({
79
- try: () => CoreExecutor.decodeRows(rendered, plan, rows, { driverMode }),
82
+ try: () => CoreExecutor.decodeRows(rendered, plan, rows, { driverMode, valueMappings }),
80
83
  catch: (error) => error as RowDecodeError
81
84
  })
82
85
  ),
@@ -97,7 +100,7 @@ const fromDriver = <
97
100
  Stream.mapChunksEffect(
98
101
  sqlDriver.stream(rendered),
99
102
  (rows) => Effect.try({
100
- try: () => CoreExecutor.decodeChunk(rendered, plan, rows, { driverMode }),
103
+ try: () => CoreExecutor.decodeChunk(rendered, plan, rows, { driverMode, valueMappings }),
101
104
  catch: (error) => error as RowDecodeError
102
105
  })
103
106
  ),
@@ -135,6 +138,7 @@ export function make(
135
138
  options: {
136
139
  readonly renderer?: Renderer
137
140
  readonly driverMode?: CoreExecutor.DriverMode
141
+ readonly valueMappings?: Expression.DriverValueMappings
138
142
  }
139
143
  ): QueryExecutor<SqlClient.SqlClient>
140
144
  export function make<Error = never, Context = never>(
@@ -142,15 +146,26 @@ export function make<Error = never, Context = never>(
142
146
  readonly renderer?: Renderer
143
147
  readonly driver: Driver<Error, Context>
144
148
  readonly driverMode?: CoreExecutor.DriverMode
149
+ readonly valueMappings?: Expression.DriverValueMappings
145
150
  }
146
151
  ): QueryExecutor<Context>
147
152
  export function make<Error = never, Context = never>(
148
153
  options: MakeOptions<Error, Context> = {}
149
154
  ): QueryExecutor<any> {
150
155
  if (options.driver) {
151
- return fromDriver(options.renderer ?? CoreRenderer.make("postgres", renderPostgresPlan), options.driver, options.driverMode)
156
+ return fromDriver(
157
+ options.renderer ?? CoreRenderer.make("postgres", (plan) => renderPostgresPlan(plan, { valueMappings: options.valueMappings })),
158
+ options.driver,
159
+ options.driverMode,
160
+ options.valueMappings
161
+ )
152
162
  }
153
- return fromDriver(options.renderer ?? CoreRenderer.make("postgres", renderPostgresPlan), sqlClientDriver(), options.driverMode)
163
+ return fromDriver(
164
+ options.renderer ?? CoreRenderer.make("postgres", (plan) => renderPostgresPlan(plan, { valueMappings: options.valueMappings })),
165
+ sqlClientDriver(),
166
+ options.driverMode,
167
+ options.valueMappings
168
+ )
154
169
  }
155
170
 
156
171
  /** Creates a Postgres-specialized executor from a typed implementation callback. */
@@ -1,15 +1,21 @@
1
- import type { RenderState, SqlDialect } from "../../internal/dialect.js"
1
+ import type { RenderState, RenderValueContext, SqlDialect } from "../../internal/dialect.js"
2
+ import { toDriverValue } from "../../internal/runtime/driver-value-mapping.js"
2
3
 
3
4
  const quoteIdentifier = (value: string): string => `"${value.replaceAll("\"", "\"\"")}"`
4
5
 
5
- const renderLiteral = (value: unknown, state: RenderState): string => {
6
- if (value === null) {
6
+ const renderLiteral = (value: unknown, state: RenderState, context: RenderValueContext = {}): string => {
7
+ const driverValue = toDriverValue(value, {
8
+ dialect: "postgres",
9
+ valueMappings: state.valueMappings,
10
+ ...context
11
+ })
12
+ if (driverValue === null) {
7
13
  return "null"
8
14
  }
9
- if (typeof value === "boolean") {
10
- return value ? "true" : "false"
15
+ if (typeof driverValue === "boolean") {
16
+ return driverValue ? "true" : "false"
11
17
  }
12
- state.params.push(value)
18
+ state.params.push(driverValue)
13
19
  return `$${state.params.length}`
14
20
  }
15
21