effect-qb 0.15.0 → 0.17.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 (88) hide show
  1. package/dist/mysql.js +1957 -595
  2. package/dist/postgres/metadata.js +2507 -182
  3. package/dist/postgres.js +9587 -8201
  4. package/dist/sqlite.js +8360 -0
  5. package/package.json +7 -2
  6. package/src/internal/column-state.ts +7 -0
  7. package/src/internal/column.ts +22 -0
  8. package/src/internal/derived-table.ts +29 -3
  9. package/src/internal/dialect.ts +14 -1
  10. package/src/internal/dsl-mutation-runtime.ts +173 -4
  11. package/src/internal/dsl-plan-runtime.ts +165 -20
  12. package/src/internal/dsl-query-runtime.ts +60 -6
  13. package/src/internal/dsl-transaction-ddl-runtime.ts +72 -2
  14. package/src/internal/executor.ts +62 -13
  15. package/src/internal/expression-ast.ts +3 -2
  16. package/src/internal/grouping-key.ts +141 -1
  17. package/src/internal/implication-runtime.ts +2 -1
  18. package/src/internal/json/types.ts +155 -40
  19. package/src/internal/predicate/analysis.ts +103 -1
  20. package/src/internal/predicate/atom.ts +7 -0
  21. package/src/internal/predicate/context.ts +170 -17
  22. package/src/internal/predicate/key.ts +64 -2
  23. package/src/internal/predicate/normalize.ts +115 -34
  24. package/src/internal/predicate/runtime.ts +144 -13
  25. package/src/internal/query.ts +563 -103
  26. package/src/internal/renderer.ts +39 -2
  27. package/src/internal/runtime/driver-value-mapping.ts +244 -0
  28. package/src/internal/runtime/normalize.ts +62 -38
  29. package/src/internal/runtime/schema.ts +5 -3
  30. package/src/internal/runtime/value.ts +153 -30
  31. package/src/internal/scalar.ts +11 -0
  32. package/src/internal/table-options.ts +108 -1
  33. package/src/internal/table.ts +87 -29
  34. package/src/mysql/column.ts +19 -2
  35. package/src/mysql/datatypes/index.ts +21 -0
  36. package/src/mysql/errors/catalog.ts +5 -5
  37. package/src/mysql/errors/normalize.ts +2 -2
  38. package/src/mysql/executor.ts +20 -5
  39. package/src/mysql/internal/dialect.ts +12 -6
  40. package/src/mysql/internal/dsl.ts +995 -263
  41. package/src/mysql/internal/renderer.ts +13 -3
  42. package/src/mysql/internal/sql-expression-renderer.ts +530 -128
  43. package/src/mysql/query.ts +9 -2
  44. package/src/mysql/renderer.ts +7 -2
  45. package/src/mysql/table.ts +38 -12
  46. package/src/postgres/cast.ts +22 -7
  47. package/src/postgres/column.ts +5 -2
  48. package/src/postgres/errors/normalize.ts +2 -2
  49. package/src/postgres/executor.ts +68 -10
  50. package/src/postgres/function/core.ts +19 -1
  51. package/src/postgres/internal/dialect.ts +12 -6
  52. package/src/postgres/internal/dsl.ts +958 -288
  53. package/src/postgres/internal/renderer.ts +13 -3
  54. package/src/postgres/internal/schema-ddl.ts +2 -1
  55. package/src/postgres/internal/schema-model.ts +6 -3
  56. package/src/postgres/internal/sql-expression-renderer.ts +477 -96
  57. package/src/postgres/json.ts +57 -17
  58. package/src/postgres/query.ts +9 -2
  59. package/src/postgres/renderer.ts +7 -2
  60. package/src/postgres/schema-management.ts +91 -4
  61. package/src/postgres/schema.ts +1 -1
  62. package/src/postgres/table.ts +189 -53
  63. package/src/postgres/type.ts +4 -0
  64. package/src/sqlite/column.ts +128 -0
  65. package/src/sqlite/datatypes/index.ts +79 -0
  66. package/src/sqlite/datatypes/spec.ts +98 -0
  67. package/src/sqlite/errors/catalog.ts +103 -0
  68. package/src/sqlite/errors/fields.ts +19 -0
  69. package/src/sqlite/errors/index.ts +19 -0
  70. package/src/sqlite/errors/normalize.ts +229 -0
  71. package/src/sqlite/errors/requirements.ts +71 -0
  72. package/src/sqlite/errors/types.ts +29 -0
  73. package/src/sqlite/executor.ts +227 -0
  74. package/src/sqlite/function/aggregate.ts +2 -0
  75. package/src/sqlite/function/core.ts +2 -0
  76. package/src/sqlite/function/index.ts +19 -0
  77. package/src/sqlite/function/string.ts +2 -0
  78. package/src/sqlite/function/temporal.ts +100 -0
  79. package/src/sqlite/function/window.ts +2 -0
  80. package/src/sqlite/internal/dialect.ts +37 -0
  81. package/src/sqlite/internal/dsl.ts +6926 -0
  82. package/src/sqlite/internal/renderer.ts +47 -0
  83. package/src/sqlite/internal/sql-expression-renderer.ts +1821 -0
  84. package/src/sqlite/json.ts +2 -0
  85. package/src/sqlite/query.ts +196 -0
  86. package/src/sqlite/renderer.ts +24 -0
  87. package/src/sqlite/table.ts +183 -0
  88. package/src/sqlite.ts +22 -0
@@ -1,5 +1,7 @@
1
1
  import * as Query from "./query.js"
2
- import { type Projection, validateProjections } from "./projections.js"
2
+ import type * as Expression from "./scalar.js"
3
+ import { flattenSelection, type Projection, validateProjections } from "./projections.js"
4
+ import * as Plan from "./row-set.js"
3
5
 
4
6
  /** Symbol used to attach rendered-query phantom row metadata. */
5
7
  export const TypeId: unique symbol = Symbol.for("effect-qb/Renderer")
@@ -21,6 +23,7 @@ export interface RenderedQuery<Row, Dialect extends string = string> {
21
23
  readonly params: readonly unknown[]
22
24
  readonly dialect: Dialect
23
25
  readonly projections: readonly Projection[]
26
+ readonly valueMappings?: Expression.DriverValueMappings
24
27
  readonly [TypeId]: {
25
28
  readonly row: Row
26
29
  readonly dialect: Dialect
@@ -42,7 +45,7 @@ export interface Renderer<Dialect extends string = string> {
42
45
  readonly dialect: Dialect
43
46
  render<PlanValue extends Query.Plan.Any>(
44
47
  plan: Query.DialectCompatiblePlan<PlanValue, Dialect>
45
- ): RenderedQuery<any, Dialect>
48
+ ): RenderedQuery<Query.ResultRow<PlanValue>, Dialect>
46
49
  }
47
50
 
48
51
  type CustomRender<Dialect extends string> = <PlanValue extends Query.Plan.Any>(
@@ -51,6 +54,30 @@ type CustomRender<Dialect extends string> = <PlanValue extends Query.Plan.Any>(
51
54
  readonly sql: string
52
55
  readonly params?: readonly unknown[]
53
56
  readonly projections?: readonly Projection[]
57
+ readonly valueMappings?: Expression.DriverValueMappings
58
+ }
59
+
60
+ const projectionPathKey = (path: readonly string[]): string => JSON.stringify(path)
61
+
62
+ const formatProjectionPath = (path: readonly string[]): string => path.join(".")
63
+
64
+ const validateProjectionPathsMatchSelection = (
65
+ plan: Query.Plan.Any,
66
+ projections: readonly Projection[]
67
+ ): void => {
68
+ const expected = flattenSelection(Query.getAst(plan).select as Record<string, unknown>)
69
+ const expectedPaths = new Set(expected.map((projection) => projectionPathKey(projection.path)))
70
+ const actualPaths = new Set(projections.map((projection) => projectionPathKey(projection.path)))
71
+ for (const projection of projections) {
72
+ if (!expectedPaths.has(projectionPathKey(projection.path))) {
73
+ throw new Error(`Projection path ${formatProjectionPath(projection.path)} does not exist in the query selection`)
74
+ }
75
+ }
76
+ for (const projection of expected) {
77
+ if (!actualPaths.has(projectionPathKey(projection.path))) {
78
+ throw new Error(`Projection path ${formatProjectionPath(projection.path)} is missing from rendered projections`)
79
+ }
80
+ }
54
81
  }
55
82
 
56
83
  /**
@@ -70,13 +97,23 @@ export function make<Dialect extends string>(
70
97
  return {
71
98
  dialect,
72
99
  render(plan) {
100
+ const required = Query.currentRequiredList(plan[Plan.TypeId].required)
101
+ if (required.length > 0) {
102
+ throw new Error(`query references sources that are not yet in scope: ${required.join(", ")}`)
103
+ }
104
+ const planDialect = plan[Plan.TypeId].dialect
105
+ if (planDialect !== dialect) {
106
+ throw new Error("effect-qb: plan dialect is not compatible with the target renderer or executor")
107
+ }
73
108
  const rendered = render(plan)
74
109
  const projections = rendered.projections ?? []
75
110
  validateProjections(projections)
111
+ validateProjectionPathsMatchSelection(plan as Query.Plan.Any, projections)
76
112
  return {
77
113
  sql: rendered.sql,
78
114
  params: rendered.params ?? [],
79
115
  projections,
116
+ valueMappings: rendered.valueMappings,
80
117
  dialect,
81
118
  [TypeId]: {
82
119
  row: undefined as any,
@@ -0,0 +1,244 @@
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 isJsonDbType = (dbType: Expression.DbType.Any | undefined): boolean => {
88
+ if (dbType === undefined) {
89
+ return false
90
+ }
91
+ if ("base" in dbType) {
92
+ return isJsonDbType(dbType.base)
93
+ }
94
+ if (!("variant" in dbType)) {
95
+ return false
96
+ }
97
+ const variant = dbType.variant as string
98
+ return variant === "json" || variant === "jsonb"
99
+ }
100
+
101
+ const schemaAccepts = (
102
+ schema: Schema.Schema.Any | undefined,
103
+ value: unknown
104
+ ): boolean =>
105
+ schema !== undefined && (Schema.is(schema) as (candidate: unknown) => boolean)(value)
106
+
107
+ const encodeWithSchema = (
108
+ schema: Schema.Schema.Any | undefined,
109
+ value: unknown
110
+ ): { readonly value: unknown; readonly encoded: boolean } => {
111
+ if (schema === undefined) {
112
+ return { value, encoded: false }
113
+ }
114
+ if (!(Schema.is(schema) as (value: unknown) => boolean)(value)) {
115
+ return { value, encoded: false }
116
+ }
117
+ return {
118
+ value: (Schema.encodeUnknownSync as any)(schema)(value),
119
+ encoded: true
120
+ }
121
+ }
122
+
123
+ const normalizeJsonDriverString = (
124
+ value: string,
125
+ context: DriverValueContext
126
+ ): unknown | undefined => {
127
+ if (!isJsonDbType(context.dbType) || context.runtimeSchema === undefined) {
128
+ return undefined
129
+ }
130
+ try {
131
+ const parsed = JSON.parse(value)
132
+ if (value.trimStart().startsWith("\"") && schemaAccepts(context.runtimeSchema, parsed)) {
133
+ return parsed
134
+ }
135
+ if (schemaAccepts(context.runtimeSchema, value) && !schemaAccepts(context.runtimeSchema, parsed)) {
136
+ return value
137
+ }
138
+ } catch (error) {
139
+ if (error instanceof SyntaxError && schemaAccepts(context.runtimeSchema, value)) {
140
+ return value
141
+ }
142
+ if (!(error instanceof SyntaxError)) {
143
+ throw error
144
+ }
145
+ }
146
+ return undefined
147
+ }
148
+
149
+ export const toDriverValue = (
150
+ value: unknown,
151
+ context: DriverValueContext
152
+ ): unknown => {
153
+ if (value === null) {
154
+ return null
155
+ }
156
+ if (value instanceof Date && Number.isNaN(value.getTime())) {
157
+ throw new Error("Expected a valid Date value")
158
+ }
159
+ const dbType = context.dbType
160
+ const encoded = encodeWithSchema(context.runtimeSchema, value)
161
+ let current = encoded.value
162
+ const custom = findMapping(context, "toDriver")
163
+ if (custom !== undefined && dbType !== undefined) {
164
+ return custom(current, dbType)
165
+ }
166
+ if (encoded.encoded && typeof current === "string" && isJsonDbType(dbType)) {
167
+ return current
168
+ }
169
+ return dbType === undefined || !encoded.encoded
170
+ ? current
171
+ : normalizeDbValue(dbType, current)
172
+ }
173
+
174
+ export const fromDriverValue = (
175
+ value: unknown,
176
+ context: DriverValueContext
177
+ ): unknown => {
178
+ if (value === null) {
179
+ return null
180
+ }
181
+ const dbType = context.dbType
182
+ const custom = findMapping(context, "fromDriver")
183
+ if (custom !== undefined && dbType !== undefined) {
184
+ return custom(value, dbType)
185
+ }
186
+ if (typeof value === "string") {
187
+ const normalizedJsonString = normalizeJsonDriverString(value, context)
188
+ if (normalizedJsonString !== undefined) {
189
+ return normalizedJsonString
190
+ }
191
+ }
192
+ return dbType === undefined
193
+ ? value
194
+ : normalizeDbValue(dbType, value)
195
+ }
196
+
197
+ const textCast = (sql: string): string => `(${sql})::text`
198
+
199
+ const postgresJsonSql = (
200
+ sql: string,
201
+ dbType: Expression.DbType.Any
202
+ ): string => {
203
+ const runtimeTag = runtimeTagOfDbType(dbType)
204
+ switch (runtimeTag) {
205
+ case "bigintString":
206
+ case "decimalString":
207
+ case "localDate":
208
+ case "localTime":
209
+ case "offsetTime":
210
+ case "localDateTime":
211
+ case "instant":
212
+ case "year":
213
+ return textCast(sql)
214
+ case "bytes":
215
+ return `encode(${sql}, 'base64')`
216
+ default:
217
+ return sql
218
+ }
219
+ }
220
+
221
+ export const renderSelectSql = (
222
+ sql: string,
223
+ context: DriverValueContext
224
+ ): string => {
225
+ const dbType = context.dbType
226
+ const custom = findMapping(context, "selectSql")
227
+ return custom !== undefined && dbType !== undefined
228
+ ? custom(sql, dbType)
229
+ : sql
230
+ }
231
+
232
+ export const renderJsonSelectSql = (
233
+ sql: string,
234
+ context: DriverValueContext
235
+ ): string => {
236
+ const dbType = context.dbType
237
+ const custom = findMapping(context, "jsonSelectSql")
238
+ if (custom !== undefined && dbType !== undefined) {
239
+ return custom(sql, dbType)
240
+ }
241
+ return context.dialect === "postgres" && dbType !== undefined
242
+ ? postgresJsonSql(sql, dbType)
243
+ : sql
244
+ }
@@ -1,13 +1,30 @@
1
1
  import type * as Expression from "../scalar.js"
2
2
  import type { RuntimeTag } from "../datatypes/shape.js"
3
+ import {
4
+ canonicalizeBigIntString,
5
+ canonicalizeDecimalString,
6
+ isValidInstantString,
7
+ isValidLocalDateString,
8
+ isValidLocalDateTimeString,
9
+ isValidLocalTimeString,
10
+ isValidOffsetTimeString
11
+ } from "./value.js"
3
12
 
4
13
  const isRecord = (value: unknown): value is Record<string, unknown> =>
5
14
  typeof value === "object" && value !== null && !Array.isArray(value)
6
15
 
16
+ const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
17
+ if (!isRecord(value)) {
18
+ return false
19
+ }
20
+ const prototype = Object.getPrototypeOf(value)
21
+ return prototype === Object.prototype || prototype === null
22
+ }
23
+
7
24
  const pad = (value: number, width = 2): string => value.toString().padStart(width, "0")
8
25
 
9
26
  const formatLocalDate = (value: Date): string =>
10
- `${value.getUTCFullYear()}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())}`
27
+ `${pad(value.getUTCFullYear(), 4)}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())}`
11
28
 
12
29
  const formatLocalTime = (value: Date): string => {
13
30
  const milliseconds = value.getUTCMilliseconds()
@@ -34,12 +51,17 @@ const expectString = (value: unknown, label: string): string => {
34
51
  throw new Error(`Expected ${label} as string`)
35
52
  }
36
53
 
54
+ const finiteNumberStringPattern = /^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:[eE][+-]?\d+)?$/
55
+
37
56
  const normalizeNumber = (value: unknown): number => {
38
57
  if (typeof value === "number" && Number.isFinite(value)) {
39
58
  return value
40
59
  }
41
- if (typeof value === "string" && value.trim() !== "") {
42
- const parsed = Number(value)
60
+ if (typeof value === "string") {
61
+ const trimmed = value.trim()
62
+ const parsed = finiteNumberStringPattern.test(trimmed)
63
+ ? Number(trimmed)
64
+ : Number.NaN
43
65
  if (Number.isFinite(parsed)) {
44
66
  return parsed
45
67
  }
@@ -81,27 +103,12 @@ const normalizeBigIntString = (value: unknown): string => {
81
103
  if (typeof value === "number" && Number.isSafeInteger(value)) {
82
104
  return BigInt(value).toString()
83
105
  }
84
- if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
85
- return BigInt(value.trim()).toString()
106
+ if (typeof value === "string") {
107
+ return canonicalizeBigIntString(value)
86
108
  }
87
109
  throw new Error("Expected an integer-like bigint value")
88
110
  }
89
111
 
90
- const canonicalizeDecimalString = (input: string): string => {
91
- const trimmed = input.trim()
92
- const match = /^([+-]?)(\d+)(?:\.(\d+))?$/.exec(trimmed)
93
- if (match === null) {
94
- throw new Error("Expected a decimal string")
95
- }
96
- const sign = match[1] === "-" ? "-" : ""
97
- const integer = match[2]!.replace(/^0+(?=\d)/, "") || "0"
98
- const fraction = (match[3] ?? "").replace(/0+$/, "")
99
- if (fraction.length === 0) {
100
- return `${sign}${integer}`
101
- }
102
- return `${sign}${integer}.${fraction}`
103
- }
104
-
105
112
  const normalizeDecimalString = (value: unknown): string => {
106
113
  if (typeof value === "string") {
107
114
  return canonicalizeDecimalString(value)
@@ -121,12 +128,15 @@ const normalizeLocalDate = (value: unknown): string => {
121
128
  return formatLocalDate(value)
122
129
  }
123
130
  const raw = expectString(value, "local date").trim()
124
- if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
131
+ if (isValidLocalDateString(raw)) {
125
132
  return raw
126
133
  }
127
- const parsed = new Date(raw)
128
- if (!Number.isNaN(parsed.getTime())) {
129
- return formatLocalDate(parsed)
134
+ const canonicalInstant = raw.replace(" ", "T").replace(/z$/, "Z")
135
+ if (isValidInstantString(canonicalInstant)) {
136
+ const parsed = new Date(canonicalInstant)
137
+ if (!Number.isNaN(parsed.getTime())) {
138
+ return formatLocalDate(parsed)
139
+ }
130
140
  }
131
141
  throw new Error("Expected a local-date value")
132
142
  }
@@ -136,7 +146,7 @@ const normalizeLocalTime = (value: unknown): string => {
136
146
  return formatLocalTime(value)
137
147
  }
138
148
  const raw = expectString(value, "local time").trim()
139
- if (/^\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(raw)) {
149
+ if (isValidLocalTimeString(raw)) {
140
150
  return raw
141
151
  }
142
152
  throw new Error("Expected a local-time value")
@@ -147,7 +157,7 @@ const normalizeOffsetTime = (value: unknown): string => {
147
157
  return `${formatLocalTime(value)}Z`
148
158
  }
149
159
  const raw = expectString(value, "offset time").trim()
150
- if (/^\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(raw)) {
160
+ if (isValidOffsetTimeString(raw)) {
151
161
  return raw
152
162
  }
153
163
  throw new Error("Expected an offset-time value")
@@ -158,11 +168,13 @@ const normalizeLocalDateTime = (value: unknown): string => {
158
168
  return formatLocalDateTime(value)
159
169
  }
160
170
  const raw = expectString(value, "local datetime").trim()
161
- if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(raw)) {
162
- return raw.replace(" ", "T")
171
+ const canonicalLocalDateTime = raw.replace(" ", "T")
172
+ if (isValidLocalDateTimeString(canonicalLocalDateTime)) {
173
+ return canonicalLocalDateTime
163
174
  }
164
- if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(raw)) {
165
- const parsed = new Date(raw)
175
+ const canonicalInstant = raw.replace(" ", "T").replace(/z$/, "Z")
176
+ if (isValidInstantString(canonicalInstant)) {
177
+ const parsed = new Date(canonicalInstant)
166
178
  if (!Number.isNaN(parsed.getTime())) {
167
179
  return formatLocalDateTime(parsed)
168
180
  }
@@ -178,7 +190,11 @@ const normalizeInstant = (value: unknown): string => {
178
190
  if (!/[zZ]|[+-]\d{2}:\d{2}$/.test(raw)) {
179
191
  throw new Error("Instant values require a timezone offset")
180
192
  }
181
- const parsed = new Date(raw)
193
+ const canonicalInstant = raw.replace(" ", "T").replace(/z$/, "Z")
194
+ if (!isValidInstantString(canonicalInstant)) {
195
+ throw new Error("Expected an ISO instant value")
196
+ }
197
+ const parsed = new Date(canonicalInstant)
182
198
  if (Number.isNaN(parsed.getTime())) {
183
199
  throw new Error("Expected an ISO instant value")
184
200
  }
@@ -209,20 +225,21 @@ const normalizeBytes = (value: unknown): Uint8Array => {
209
225
  throw new Error("Expected a byte array value")
210
226
  }
211
227
 
212
- const isJsonValue = (value: unknown): boolean => {
228
+ export const isJsonValue = (value: unknown): boolean => {
213
229
  if (value === null) {
214
230
  return true
215
231
  }
216
232
  switch (typeof value) {
217
233
  case "string":
218
- case "number":
219
234
  case "boolean":
220
235
  return true
236
+ case "number":
237
+ return Number.isFinite(value)
221
238
  case "object":
222
239
  if (Array.isArray(value)) {
223
240
  return value.every(isJsonValue)
224
241
  }
225
- return isRecord(value) && Object.values(value).every(isJsonValue)
242
+ return isPlainRecord(value) && Object.values(value).every(isJsonValue)
226
243
  default:
227
244
  return false
228
245
  }
@@ -230,11 +247,18 @@ const isJsonValue = (value: unknown): boolean => {
230
247
 
231
248
  const normalizeJson = (value: unknown): unknown => {
232
249
  if (typeof value === "string") {
233
- const parsed = JSON.parse(value)
234
- if (isJsonValue(parsed)) {
235
- return parsed
250
+ try {
251
+ const parsed = JSON.parse(value)
252
+ if (isJsonValue(parsed)) {
253
+ return parsed
254
+ }
255
+ throw new Error("Parsed JSON value is not a valid JSON runtime")
256
+ } catch (error) {
257
+ if (error instanceof SyntaxError) {
258
+ return value
259
+ }
260
+ throw error
236
261
  }
237
- throw new Error("Parsed JSON value is not a valid JSON runtime")
238
262
  }
239
263
  if (isJsonValue(value)) {
240
264
  return value
@@ -38,12 +38,14 @@ const schemaCache = new WeakMap<Expression.Any, RuntimeSchema | undefined>()
38
38
  const isRecord = (value: unknown): value is Record<string, unknown> =>
39
39
  typeof value === "object" && value !== null && !Array.isArray(value)
40
40
 
41
+ const FiniteNumberSchema = Schema.Number.pipe(Schema.finite())
42
+
41
43
  const runtimeSchemaForTag = (tag: RuntimeTag): RuntimeSchema | undefined => {
42
44
  switch (tag) {
43
45
  case "string":
44
46
  return Schema.String
45
47
  case "number":
46
- return Schema.Number
48
+ return FiniteNumberSchema
47
49
  case "bigintString":
48
50
  return BigIntStringSchema
49
51
  case "boolean":
@@ -388,7 +390,7 @@ const deriveRuntimeSchema = (
388
390
  return Schema.String
389
391
  case "count":
390
392
  case "jsonLength":
391
- return Schema.Number
393
+ return FiniteNumberSchema
392
394
  case "max":
393
395
  case "min":
394
396
  return expressionRuntimeSchema(ast.value, context)
@@ -404,7 +406,7 @@ const deriveRuntimeSchema = (
404
406
  case "window":
405
407
  return ast.function === "over" && ast.value !== undefined
406
408
  ? expressionRuntimeSchema(ast.value, context)
407
- : Schema.Number
409
+ : FiniteNumberSchema
408
410
  case "jsonGet":
409
411
  case "jsonPath":
410
412
  case "jsonAccess":