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 "./query.js"
2
+ import type * as Expression from "./scalar.js"
2
3
  import { type Projection, validateProjections } from "./projections.js"
3
4
 
4
5
  /** Symbol used to attach rendered-query phantom row metadata. */
@@ -21,6 +22,7 @@ export interface RenderedQuery<Row, Dialect extends string = string> {
21
22
  readonly params: readonly unknown[]
22
23
  readonly dialect: Dialect
23
24
  readonly projections: readonly Projection[]
25
+ readonly valueMappings?: Expression.DriverValueMappings
24
26
  readonly [TypeId]: {
25
27
  readonly row: Row
26
28
  readonly dialect: Dialect
@@ -51,6 +53,7 @@ type CustomRender<Dialect extends string> = <PlanValue extends Query.Plan.Any>(
51
53
  readonly sql: string
52
54
  readonly params?: readonly unknown[]
53
55
  readonly projections?: readonly Projection[]
56
+ readonly valueMappings?: Expression.DriverValueMappings
54
57
  }
55
58
 
56
59
  /**
@@ -77,6 +80,7 @@ export function make<Dialect extends string>(
77
80
  sql: rendered.sql,
78
81
  params: rendered.params ?? [],
79
82
  projections,
83
+ valueMappings: rendered.valueMappings,
80
84
  dialect,
81
85
  [TypeId]: {
82
86
  row: undefined as any,
@@ -0,0 +1,186 @@
1
+ import * as Schema from "effect/Schema"
2
+
3
+ import type * as Expression from "../scalar.js"
4
+ import { normalizeDbValue } from "./normalize.js"
5
+
6
+ export type DriverValueMapping = Expression.DriverValueMapping
7
+ export type DriverValueMappings = Expression.DriverValueMappings
8
+
9
+ export interface DriverValueContext {
10
+ readonly dialect?: string
11
+ readonly dbType?: Expression.DbType.Any
12
+ readonly runtimeSchema?: Schema.Schema.Any
13
+ readonly driverValueMapping?: DriverValueMapping
14
+ readonly valueMappings?: DriverValueMappings
15
+ }
16
+
17
+ type MappingKey =
18
+ | "fromDriver"
19
+ | "toDriver"
20
+ | "selectSql"
21
+ | "jsonSelectSql"
22
+
23
+ const runtimeTagOfDbType = (
24
+ dbType: Expression.DbType.Any | undefined
25
+ ): string | undefined => {
26
+ if (dbType === undefined) {
27
+ return undefined
28
+ }
29
+ if ("base" in dbType) {
30
+ return runtimeTagOfDbType(dbType.base)
31
+ }
32
+ if ("element" in dbType) {
33
+ return "array"
34
+ }
35
+ if ("fields" in dbType) {
36
+ return "record"
37
+ }
38
+ if ("variant" in dbType && dbType.variant === "json") {
39
+ return "json"
40
+ }
41
+ if ("variant" in dbType && (dbType.variant === "enum" || dbType.variant === "set")) {
42
+ return "string"
43
+ }
44
+ return dbType.runtime
45
+ }
46
+
47
+ const familyOfDbType = (
48
+ dbType: Expression.DbType.Any | undefined
49
+ ): string | undefined => {
50
+ if (dbType === undefined) {
51
+ return undefined
52
+ }
53
+ if ("base" in dbType) {
54
+ return familyOfDbType(dbType.base)
55
+ }
56
+ return dbType.family
57
+ }
58
+
59
+ const mappingCandidates = (
60
+ context: DriverValueContext
61
+ ): readonly (DriverValueMapping | undefined)[] => {
62
+ const dbType = context.dbType
63
+ const runtimeTag = runtimeTagOfDbType(dbType)
64
+ const family = familyOfDbType(dbType)
65
+ return [
66
+ context.driverValueMapping,
67
+ dbType?.driverValueMapping,
68
+ dbType === undefined ? undefined : context.valueMappings?.[dbType.kind],
69
+ family === undefined ? undefined : context.valueMappings?.[family],
70
+ runtimeTag === undefined ? undefined : context.valueMappings?.[runtimeTag]
71
+ ]
72
+ }
73
+
74
+ const findMapping = <Key extends MappingKey>(
75
+ context: DriverValueContext,
76
+ key: Key
77
+ ): NonNullable<DriverValueMapping[Key]> | undefined => {
78
+ for (const candidate of mappingCandidates(context)) {
79
+ const value = candidate?.[key]
80
+ if (value !== undefined) {
81
+ return value as NonNullable<DriverValueMapping[Key]>
82
+ }
83
+ }
84
+ return undefined
85
+ }
86
+
87
+ const encodeWithSchema = (
88
+ schema: Schema.Schema.Any | undefined,
89
+ value: unknown
90
+ ): { readonly value: unknown; readonly encoded: boolean } => {
91
+ if (schema === undefined) {
92
+ return { value, encoded: false }
93
+ }
94
+ if (!(Schema.is(schema) as (value: unknown) => boolean)(value)) {
95
+ return { value, encoded: false }
96
+ }
97
+ return {
98
+ value: (Schema.encodeUnknownSync as any)(schema)(value),
99
+ encoded: true
100
+ }
101
+ }
102
+
103
+ export const toDriverValue = (
104
+ value: unknown,
105
+ context: DriverValueContext
106
+ ): unknown => {
107
+ if (value === null) {
108
+ return null
109
+ }
110
+ const dbType = context.dbType
111
+ const encoded = encodeWithSchema(context.runtimeSchema, value)
112
+ let current = encoded.value
113
+ const custom = findMapping(context, "toDriver")
114
+ if (custom !== undefined && dbType !== undefined) {
115
+ return custom(current, dbType)
116
+ }
117
+ return dbType === undefined || !encoded.encoded
118
+ ? current
119
+ : normalizeDbValue(dbType, current)
120
+ }
121
+
122
+ export const fromDriverValue = (
123
+ value: unknown,
124
+ context: DriverValueContext
125
+ ): unknown => {
126
+ if (value === null) {
127
+ return null
128
+ }
129
+ const dbType = context.dbType
130
+ const custom = findMapping(context, "fromDriver")
131
+ if (custom !== undefined && dbType !== undefined) {
132
+ return custom(value, dbType)
133
+ }
134
+ return dbType === undefined
135
+ ? value
136
+ : normalizeDbValue(dbType, value)
137
+ }
138
+
139
+ const textCast = (sql: string): string => `(${sql})::text`
140
+
141
+ const postgresJsonSql = (
142
+ sql: string,
143
+ dbType: Expression.DbType.Any
144
+ ): string => {
145
+ const runtimeTag = runtimeTagOfDbType(dbType)
146
+ switch (runtimeTag) {
147
+ case "bigintString":
148
+ case "decimalString":
149
+ case "localDate":
150
+ case "localTime":
151
+ case "offsetTime":
152
+ case "localDateTime":
153
+ case "instant":
154
+ case "year":
155
+ return textCast(sql)
156
+ case "bytes":
157
+ return `encode(${sql}, 'base64')`
158
+ default:
159
+ return sql
160
+ }
161
+ }
162
+
163
+ export const renderSelectSql = (
164
+ sql: string,
165
+ context: DriverValueContext
166
+ ): string => {
167
+ const dbType = context.dbType
168
+ const custom = findMapping(context, "selectSql")
169
+ return custom !== undefined && dbType !== undefined
170
+ ? custom(sql, dbType)
171
+ : sql
172
+ }
173
+
174
+ export const renderJsonSelectSql = (
175
+ sql: string,
176
+ context: DriverValueContext
177
+ ): string => {
178
+ const dbType = context.dbType
179
+ const custom = findMapping(context, "jsonSelectSql")
180
+ if (custom !== undefined && dbType !== undefined) {
181
+ return custom(sql, dbType)
182
+ }
183
+ return context.dialect === "postgres" && dbType !== undefined
184
+ ? postgresJsonSql(sql, dbType)
185
+ : sql
186
+ }
@@ -51,6 +51,7 @@ export declare namespace DbType {
51
51
  readonly compareGroup?: string
52
52
  readonly castTargets?: readonly string[]
53
53
  readonly traits?: DatatypeTraits
54
+ readonly driverValueMapping?: DriverValueMapping
54
55
  }
55
56
 
56
57
  /** JSON-like database type. */
@@ -135,6 +136,15 @@ export declare namespace DbType {
135
136
  | Set<string, string>
136
137
  }
137
138
 
139
+ export interface DriverValueMapping {
140
+ readonly fromDriver?: (value: unknown, dbType: DbType.Any) => unknown
141
+ readonly toDriver?: (value: unknown, dbType: DbType.Any) => unknown
142
+ readonly selectSql?: (sql: string, dbType: DbType.Any) => string
143
+ readonly jsonSelectSql?: (sql: string, dbType: DbType.Any) => string
144
+ }
145
+
146
+ export type DriverValueMappings = Readonly<Record<string, DriverValueMapping | undefined>>
147
+
138
148
  /** Canonical static metadata stored on an expression. */
139
149
  export interface State<
140
150
  Runtime,
@@ -147,6 +157,7 @@ export interface State<
147
157
  readonly runtime: Runtime
148
158
  readonly dbType: Db
149
159
  readonly runtimeSchema?: Schema.Schema.Any
160
+ readonly driverValueMapping?: DriverValueMapping
150
161
  readonly nullability: Nullable
151
162
  readonly dialect: Dialect
152
163
  readonly kind: Kind
@@ -100,6 +100,7 @@ export const primaryKey = BaseColumn.primaryKey
100
100
  export const unique = BaseColumn.unique
101
101
  const default_ = BaseColumn.default_
102
102
  export const generated = BaseColumn.generated
103
+ export const driverValueMapping = BaseColumn.driverValueMapping
103
104
  export const references = BaseColumn.references
104
105
  export const schema = BaseColumn.schema
105
106
  export { default_ as default }
@@ -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 { renderMysqlPlan } from "./internal/renderer.js"
9
10
  import {
10
11
  narrowMysqlDriverErrorForReadQuery,
@@ -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 MySQL executors. */
33
35
  export type MysqlExecutorError = MysqlDriverError | RowDecodeError
@@ -101,7 +103,8 @@ const fromDriver = <
101
103
  >(
102
104
  renderer: Renderer,
103
105
  sqlDriver: Driver<Error, Context>,
104
- driverMode: CoreExecutor.DriverMode = "raw"
106
+ driverMode: CoreExecutor.DriverMode = "raw",
107
+ valueMappings?: Expression.DriverValueMappings
105
108
  ): QueryExecutor<Context> => ({
106
109
  dialect: "mysql",
107
110
  execute(plan) {
@@ -110,7 +113,7 @@ const fromDriver = <
110
113
  Effect.flatMap(
111
114
  sqlDriver.execute(rendered),
112
115
  (rows) => Effect.try({
113
- try: () => CoreExecutor.decodeRows(rendered, plan, rows, { driverMode }),
116
+ try: () => CoreExecutor.decodeRows(rendered, plan, rows, { driverMode, valueMappings }),
114
117
  catch: (error) => error as RowDecodeError
115
118
  })
116
119
  ),
@@ -131,7 +134,7 @@ const fromDriver = <
131
134
  Stream.mapChunksEffect(
132
135
  sqlDriver.stream(rendered),
133
136
  (rows) => Effect.try({
134
- try: () => CoreExecutor.decodeChunk(rendered, plan, rows, { driverMode }),
137
+ try: () => CoreExecutor.decodeChunk(rendered, plan, rows, { driverMode, valueMappings }),
135
138
  catch: (error) => error as RowDecodeError
136
139
  })
137
140
  ),
@@ -169,6 +172,7 @@ export function make(
169
172
  options: {
170
173
  readonly renderer?: Renderer
171
174
  readonly driverMode?: CoreExecutor.DriverMode
175
+ readonly valueMappings?: Expression.DriverValueMappings
172
176
  }
173
177
  ): QueryExecutor<SqlClient.SqlClient>
174
178
  export function make<Error = never, Context = never>(
@@ -176,15 +180,26 @@ export function make<Error = never, Context = never>(
176
180
  readonly renderer?: Renderer
177
181
  readonly driver: Driver<Error, Context>
178
182
  readonly driverMode?: CoreExecutor.DriverMode
183
+ readonly valueMappings?: Expression.DriverValueMappings
179
184
  }
180
185
  ): QueryExecutor<Context>
181
186
  export function make<Error = never, Context = never>(
182
187
  options: MakeOptions<Error, Context> = {}
183
188
  ): QueryExecutor<any> {
184
189
  if (options.driver) {
185
- return fromDriver(options.renderer ?? CoreRenderer.make("mysql", renderMysqlPlan), options.driver, options.driverMode)
190
+ return fromDriver(
191
+ options.renderer ?? CoreRenderer.make("mysql", (plan) => renderMysqlPlan(plan, { valueMappings: options.valueMappings })),
192
+ options.driver,
193
+ options.driverMode,
194
+ options.valueMappings
195
+ )
186
196
  }
187
- return fromDriver(options.renderer ?? CoreRenderer.make("mysql", renderMysqlPlan), sqlClientDriver(), options.driverMode)
197
+ return fromDriver(
198
+ options.renderer ?? CoreRenderer.make("mysql", (plan) => renderMysqlPlan(plan, { valueMappings: options.valueMappings })),
199
+ sqlClientDriver(),
200
+ options.driverMode,
201
+ options.valueMappings
202
+ )
188
203
  }
189
204
 
190
205
  /** Creates a MySQL-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: "mysql",
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 "?"
14
20
  }
15
21