@prisma-next/sql-runtime 0.3.0-dev.10 → 0.3.0-dev.113

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 (169) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +141 -24
  3. package/dist/exports-BKjZvwMh.mjs +971 -0
  4. package/dist/exports-BKjZvwMh.mjs.map +1 -0
  5. package/dist/index-eHiENgIB.d.mts +182 -0
  6. package/dist/index-eHiENgIB.d.mts.map +1 -0
  7. package/dist/index.d.mts +2 -0
  8. package/dist/index.mjs +3 -0
  9. package/dist/test/utils.d.mts +82 -0
  10. package/dist/test/utils.d.mts.map +1 -0
  11. package/dist/test/utils.mjs +221 -0
  12. package/dist/test/utils.mjs.map +1 -0
  13. package/package.json +32 -25
  14. package/src/codecs/decoding.ts +84 -3
  15. package/src/codecs/encoding.ts +5 -15
  16. package/src/codecs/json-schema-validation.ts +61 -0
  17. package/src/exports/index.ts +19 -7
  18. package/src/lower-sql-plan.ts +8 -8
  19. package/src/plugins/budgets.ts +375 -0
  20. package/src/plugins/lints.ts +211 -0
  21. package/src/sql-context.ts +448 -98
  22. package/src/sql-family-adapter.ts +9 -5
  23. package/src/sql-marker.ts +2 -2
  24. package/src/sql-runtime.ts +126 -30
  25. package/test/async-iterable-result.test.ts +43 -35
  26. package/test/budgets.test.ts +481 -0
  27. package/test/context.types.test-d.ts +68 -0
  28. package/test/execution-stack.test.ts +166 -0
  29. package/test/json-schema-validation.test.ts +575 -0
  30. package/test/lints.test.ts +161 -0
  31. package/test/mutation-default-generators.test.ts +256 -0
  32. package/test/parameterized-types.test.ts +536 -0
  33. package/test/sql-context.test.ts +293 -121
  34. package/test/sql-family-adapter.test.ts +8 -10
  35. package/test/sql-runtime.test.ts +219 -34
  36. package/test/utils.ts +90 -51
  37. package/dist/accelerate-EEKAFGN3-P6A6XJWJ.js +0 -137863
  38. package/dist/accelerate-EEKAFGN3-P6A6XJWJ.js.map +0 -1
  39. package/dist/amcheck-24VY6X5V.js +0 -13
  40. package/dist/amcheck-24VY6X5V.js.map +0 -1
  41. package/dist/bloom-VS74NLHT.js +0 -13
  42. package/dist/bloom-VS74NLHT.js.map +0 -1
  43. package/dist/btree_gin-WBC4EAAI.js +0 -13
  44. package/dist/btree_gin-WBC4EAAI.js.map +0 -1
  45. package/dist/btree_gist-UNC6QD3M.js +0 -13
  46. package/dist/btree_gist-UNC6QD3M.js.map +0 -1
  47. package/dist/chunk-3KTOEDFX.js +0 -49
  48. package/dist/chunk-3KTOEDFX.js.map +0 -1
  49. package/dist/chunk-47DZBRQC.js +0 -1280
  50. package/dist/chunk-47DZBRQC.js.map +0 -1
  51. package/dist/chunk-52N6AFZM.js +0 -133
  52. package/dist/chunk-52N6AFZM.js.map +0 -1
  53. package/dist/chunk-7D4SUZUM.js +0 -38
  54. package/dist/chunk-7D4SUZUM.js.map +0 -1
  55. package/dist/chunk-C6I3V3DM.js +0 -455
  56. package/dist/chunk-C6I3V3DM.js.map +0 -1
  57. package/dist/chunk-ECWIHLAT.js +0 -37
  58. package/dist/chunk-ECWIHLAT.js.map +0 -1
  59. package/dist/chunk-EI626SDC.js +0 -105
  60. package/dist/chunk-EI626SDC.js.map +0 -1
  61. package/dist/chunk-UKKOYUGL.js +0 -578
  62. package/dist/chunk-UKKOYUGL.js.map +0 -1
  63. package/dist/chunk-XPLNMXQV.js +0 -1537
  64. package/dist/chunk-XPLNMXQV.js.map +0 -1
  65. package/dist/citext-T7MXGUY7.js +0 -13
  66. package/dist/citext-T7MXGUY7.js.map +0 -1
  67. package/dist/client-5FENX6AW.js +0 -299
  68. package/dist/client-5FENX6AW.js.map +0 -1
  69. package/dist/cube-TFDQBZCI.js +0 -13
  70. package/dist/cube-TFDQBZCI.js.map +0 -1
  71. package/dist/dict_int-AEUOPGWP.js +0 -13
  72. package/dist/dict_int-AEUOPGWP.js.map +0 -1
  73. package/dist/dict_xsyn-DAAYX3FL.js +0 -13
  74. package/dist/dict_xsyn-DAAYX3FL.js.map +0 -1
  75. package/dist/dist-AQ3LWXOX.js +0 -570
  76. package/dist/dist-AQ3LWXOX.js.map +0 -1
  77. package/dist/dist-LBVX6BJW.js +0 -189
  78. package/dist/dist-LBVX6BJW.js.map +0 -1
  79. package/dist/dist-WLKUVDN2.js +0 -5127
  80. package/dist/dist-WLKUVDN2.js.map +0 -1
  81. package/dist/earthdistance-KIGTF4LE.js +0 -13
  82. package/dist/earthdistance-KIGTF4LE.js.map +0 -1
  83. package/dist/file_fdw-5N55UP6I.js +0 -13
  84. package/dist/file_fdw-5N55UP6I.js.map +0 -1
  85. package/dist/fuzzystrmatch-KN3YWBFP.js +0 -13
  86. package/dist/fuzzystrmatch-KN3YWBFP.js.map +0 -1
  87. package/dist/hstore-YX726NKN.js +0 -13
  88. package/dist/hstore-YX726NKN.js.map +0 -1
  89. package/dist/http-exception-FZY2H4OF.js +0 -8
  90. package/dist/http-exception-FZY2H4OF.js.map +0 -1
  91. package/dist/index.js +0 -30
  92. package/dist/index.js.map +0 -1
  93. package/dist/intarray-NKVXNO2D.js +0 -13
  94. package/dist/intarray-NKVXNO2D.js.map +0 -1
  95. package/dist/isn-FTEMJGEV.js +0 -13
  96. package/dist/isn-FTEMJGEV.js.map +0 -1
  97. package/dist/lo-DB7L4NGI.js +0 -13
  98. package/dist/lo-DB7L4NGI.js.map +0 -1
  99. package/dist/logger-WQ7SHNDD.js +0 -68
  100. package/dist/logger-WQ7SHNDD.js.map +0 -1
  101. package/dist/ltree-Z32TZT6W.js +0 -13
  102. package/dist/ltree-Z32TZT6W.js.map +0 -1
  103. package/dist/nodefs-NM46ACH7.js +0 -31
  104. package/dist/nodefs-NM46ACH7.js.map +0 -1
  105. package/dist/opfs-ahp-NJO33LVZ.js +0 -332
  106. package/dist/opfs-ahp-NJO33LVZ.js.map +0 -1
  107. package/dist/pageinspect-YP3IZR4X.js +0 -13
  108. package/dist/pageinspect-YP3IZR4X.js.map +0 -1
  109. package/dist/pg_buffercache-7TD5J2FB.js +0 -13
  110. package/dist/pg_buffercache-7TD5J2FB.js.map +0 -1
  111. package/dist/pg_dump-SG4KYBUB.js +0 -2492
  112. package/dist/pg_dump-SG4KYBUB.js.map +0 -1
  113. package/dist/pg_freespacemap-DZDNCPZK.js +0 -13
  114. package/dist/pg_freespacemap-DZDNCPZK.js.map +0 -1
  115. package/dist/pg_surgery-J2MUEWEP.js +0 -13
  116. package/dist/pg_surgery-J2MUEWEP.js.map +0 -1
  117. package/dist/pg_trgm-7VNQOYS6.js +0 -13
  118. package/dist/pg_trgm-7VNQOYS6.js.map +0 -1
  119. package/dist/pg_visibility-TTSIPHFL.js +0 -13
  120. package/dist/pg_visibility-TTSIPHFL.js.map +0 -1
  121. package/dist/pg_walinspect-KPFHSHRJ.js +0 -13
  122. package/dist/pg_walinspect-KPFHSHRJ.js.map +0 -1
  123. package/dist/proxy-signals-GUDAMDHV.js +0 -39
  124. package/dist/proxy-signals-GUDAMDHV.js.map +0 -1
  125. package/dist/seg-IYVDLE4O.js +0 -13
  126. package/dist/seg-IYVDLE4O.js.map +0 -1
  127. package/dist/src/codecs/decoding.d.ts +0 -4
  128. package/dist/src/codecs/decoding.d.ts.map +0 -1
  129. package/dist/src/codecs/encoding.d.ts +0 -5
  130. package/dist/src/codecs/encoding.d.ts.map +0 -1
  131. package/dist/src/codecs/validation.d.ts +0 -6
  132. package/dist/src/codecs/validation.d.ts.map +0 -1
  133. package/dist/src/exports/index.d.ts +0 -11
  134. package/dist/src/exports/index.d.ts.map +0 -1
  135. package/dist/src/index.d.ts +0 -2
  136. package/dist/src/index.d.ts.map +0 -1
  137. package/dist/src/lower-sql-plan.d.ts +0 -15
  138. package/dist/src/lower-sql-plan.d.ts.map +0 -1
  139. package/dist/src/sql-context.d.ts +0 -65
  140. package/dist/src/sql-context.d.ts.map +0 -1
  141. package/dist/src/sql-family-adapter.d.ts +0 -10
  142. package/dist/src/sql-family-adapter.d.ts.map +0 -1
  143. package/dist/src/sql-marker.d.ts +0 -22
  144. package/dist/src/sql-marker.d.ts.map +0 -1
  145. package/dist/src/sql-runtime.d.ts +0 -25
  146. package/dist/src/sql-runtime.d.ts.map +0 -1
  147. package/dist/tablefunc-EF4RCS7S.js +0 -13
  148. package/dist/tablefunc-EF4RCS7S.js.map +0 -1
  149. package/dist/tcn-3VT5BQYW.js +0 -13
  150. package/dist/tcn-3VT5BQYW.js.map +0 -1
  151. package/dist/test/utils.d.ts +0 -59
  152. package/dist/test/utils.d.ts.map +0 -1
  153. package/dist/test/utils.js +0 -24634
  154. package/dist/test/utils.js.map +0 -1
  155. package/dist/tiny-CW6F4GX6.js +0 -10
  156. package/dist/tiny-CW6F4GX6.js.map +0 -1
  157. package/dist/tsm_system_rows-ES7KNUQH.js +0 -13
  158. package/dist/tsm_system_rows-ES7KNUQH.js.map +0 -1
  159. package/dist/tsm_system_time-76WEIMBG.js +0 -13
  160. package/dist/tsm_system_time-76WEIMBG.js.map +0 -1
  161. package/dist/unaccent-7RYF3R64.js +0 -13
  162. package/dist/unaccent-7RYF3R64.js.map +0 -1
  163. package/dist/utility-Q5A254LJ-J4HTKZPT.js +0 -347
  164. package/dist/utility-Q5A254LJ-J4HTKZPT.js.map +0 -1
  165. package/dist/uuid_ossp-4ETE4FPE.js +0 -13
  166. package/dist/uuid_ossp-4ETE4FPE.js.map +0 -1
  167. package/dist/vector-74GPNV7V.js +0 -13
  168. package/dist/vector-74GPNV7V.js.map +0 -1
  169. package/src/index.ts +0 -1
@@ -0,0 +1,575 @@
1
+ import type { ExecutionPlan, ParamDescriptor } from '@prisma-next/contract/types';
2
+ import { coreHash } from '@prisma-next/contract/types';
3
+ import type { SqlContract, SqlStorage, StorageTypeInstance } from '@prisma-next/sql-contract/types';
4
+ import type { CodecRegistry } from '@prisma-next/sql-relational-core/ast';
5
+ import { codec, createCodecRegistry } from '@prisma-next/sql-relational-core/ast';
6
+ import type {
7
+ JsonSchemaValidateFn,
8
+ JsonSchemaValidatorRegistry,
9
+ } from '@prisma-next/sql-relational-core/query-lane-context';
10
+ import { ifDefined } from '@prisma-next/utils/defined';
11
+ import { type as arktype } from 'arktype';
12
+ import { describe, expect, it } from 'vitest';
13
+ import { decodeRow } from '../src/codecs/decoding';
14
+ import { encodeParam, encodeParams } from '../src/codecs/encoding';
15
+ import type {
16
+ RuntimeParameterizedCodecDescriptor,
17
+ SqlRuntimeExtensionDescriptor,
18
+ } from '../src/sql-context';
19
+ import { createStubAdapter, createTestContext } from './utils';
20
+
21
+ // =============================================================================
22
+ // Shared test helpers
23
+ // =============================================================================
24
+
25
+ function createStubValidator(schema: Record<string, unknown>): JsonSchemaValidateFn {
26
+ return (value: unknown) => {
27
+ if (schema['type'] === 'object' && typeof value === 'object' && value !== null) {
28
+ const required = (schema['required'] ?? []) as string[];
29
+ const obj = value as Record<string, unknown>;
30
+ for (const prop of required) {
31
+ if (!(prop in obj)) {
32
+ return {
33
+ valid: false,
34
+ errors: [
35
+ {
36
+ path: '/',
37
+ message: `must have required property '${prop}'`,
38
+ keyword: 'required',
39
+ },
40
+ ],
41
+ };
42
+ }
43
+ }
44
+ return { valid: true };
45
+ }
46
+ if (schema['type'] === 'object' && (typeof value !== 'object' || value === null)) {
47
+ return {
48
+ valid: false,
49
+ errors: [{ path: '/', message: 'must be object', keyword: 'type' }],
50
+ };
51
+ }
52
+ return { valid: true };
53
+ };
54
+ }
55
+
56
+ const metadataSchema: Record<string, unknown> = {
57
+ type: 'object',
58
+ properties: { name: { type: 'string' } },
59
+ required: ['name'],
60
+ };
61
+
62
+ const userMetadataSchema: Record<string, unknown> = {
63
+ type: 'object',
64
+ properties: { userName: { type: 'string' } },
65
+ required: ['userName'],
66
+ };
67
+
68
+ const postMetadataSchema: Record<string, unknown> = {
69
+ type: 'object',
70
+ properties: { postTitle: { type: 'string' } },
71
+ required: ['postTitle'],
72
+ };
73
+
74
+ function createMetadataValidatorRegistry(): JsonSchemaValidatorRegistry {
75
+ const validators = new Map<string, JsonSchemaValidateFn>();
76
+ validators.set('user.metadata', createStubValidator(metadataSchema));
77
+ return { get: (key) => validators.get(key), size: validators.size };
78
+ }
79
+
80
+ function createJoinMetadataValidatorRegistry(): JsonSchemaValidatorRegistry {
81
+ const validators = new Map<string, JsonSchemaValidateFn>();
82
+ validators.set('user.metadata', createStubValidator(userMetadataSchema));
83
+ validators.set('post.metadata', createStubValidator(postMetadataSchema));
84
+ return { get: (key) => validators.get(key), size: validators.size };
85
+ }
86
+
87
+ function createTestCodecRegistry(): CodecRegistry {
88
+ const registry = createCodecRegistry();
89
+ registry.register(
90
+ codec({
91
+ typeId: 'pg/jsonb@1',
92
+ targetTypes: ['jsonb'],
93
+ encode: (v: unknown) => JSON.stringify(v),
94
+ decode: (w: string) => (typeof w === 'string' ? JSON.parse(w) : w),
95
+ }),
96
+ );
97
+ registry.register(
98
+ codec({
99
+ typeId: 'pg/json@1',
100
+ targetTypes: ['json'],
101
+ encode: (v: unknown) => JSON.stringify(v),
102
+ decode: (w: string) => (typeof w === 'string' ? JSON.parse(w) : w),
103
+ }),
104
+ );
105
+ registry.register(
106
+ codec({
107
+ typeId: 'pg/int4@1',
108
+ targetTypes: ['int4'],
109
+ encode: (v: number) => v,
110
+ decode: (w: number) => w,
111
+ }),
112
+ );
113
+ return registry;
114
+ }
115
+
116
+ function createJsonSchemaContract(
117
+ options?: Partial<{
118
+ types: Record<string, StorageTypeInstance>;
119
+ tableColumns: Record<
120
+ string,
121
+ {
122
+ nativeType: string;
123
+ codecId: string;
124
+ nullable: boolean;
125
+ typeParams?: Record<string, unknown>;
126
+ typeRef?: string;
127
+ }
128
+ >;
129
+ }>,
130
+ ): SqlContract<SqlStorage> {
131
+ return {
132
+ schemaVersion: '1',
133
+ targetFamily: 'sql',
134
+ target: 'postgres',
135
+ storageHash: coreHash('sha256:test'),
136
+ models: {},
137
+ relations: {},
138
+ storage: {
139
+ tables: {
140
+ user: {
141
+ columns: options?.tableColumns ?? {
142
+ id: { nativeType: 'int4', codecId: 'pg/int4@1', nullable: false },
143
+ metadata: {
144
+ nativeType: 'jsonb',
145
+ codecId: 'pg/jsonb@1',
146
+ nullable: true,
147
+ typeParams: { schema: metadataSchema },
148
+ },
149
+ },
150
+ primaryKey: { columns: ['id'] },
151
+ uniques: [],
152
+ indexes: [],
153
+ foreignKeys: [],
154
+ },
155
+ },
156
+ ...ifDefined('types', options?.types),
157
+ },
158
+ extensionPacks: {},
159
+ capabilities: {},
160
+ meta: {},
161
+ sources: {},
162
+ mappings: {},
163
+ };
164
+ }
165
+
166
+ const jsonTypeParamsSchema = arktype({
167
+ schema: 'object',
168
+ 'type?': 'string',
169
+ });
170
+
171
+ function createJsonbExtensionDescriptor(): SqlRuntimeExtensionDescriptor<'postgres'> {
172
+ const parameterizedCodecs: RuntimeParameterizedCodecDescriptor[] = [
173
+ {
174
+ codecId: 'pg/json@1',
175
+ paramsSchema: jsonTypeParamsSchema,
176
+ init: (params: Record<string, unknown>) => ({
177
+ validate: createStubValidator(params['schema'] as Record<string, unknown>),
178
+ }),
179
+ },
180
+ {
181
+ codecId: 'pg/jsonb@1',
182
+ paramsSchema: jsonTypeParamsSchema,
183
+ init: (params: Record<string, unknown>) => ({
184
+ validate: createStubValidator(params['schema'] as Record<string, unknown>),
185
+ }),
186
+ },
187
+ ];
188
+
189
+ const registry = createCodecRegistry();
190
+ registry.register(
191
+ codec({
192
+ typeId: 'pg/json@1',
193
+ targetTypes: ['json'],
194
+ encode: (v: unknown) => JSON.stringify(v),
195
+ decode: (w: string) => (typeof w === 'string' ? JSON.parse(w) : w),
196
+ }),
197
+ );
198
+ registry.register(
199
+ codec({
200
+ typeId: 'pg/jsonb@1',
201
+ targetTypes: ['jsonb'],
202
+ encode: (v: unknown) => JSON.stringify(v),
203
+ decode: (w: string) => (typeof w === 'string' ? JSON.parse(w) : w),
204
+ }),
205
+ );
206
+
207
+ return {
208
+ kind: 'extension' as const,
209
+ id: 'json-validation',
210
+ version: '0.0.1',
211
+ familyId: 'sql' as const,
212
+ targetId: 'postgres' as const,
213
+ codecs: () => registry,
214
+ operationSignatures: () => [],
215
+ parameterizedCodecs: () => parameterizedCodecs,
216
+ create() {
217
+ return { familyId: 'sql' as const, targetId: 'postgres' as const };
218
+ },
219
+ };
220
+ }
221
+
222
+ function createTestPlan(overrides?: Partial<ExecutionPlan>): ExecutionPlan {
223
+ return {
224
+ sql: 'SELECT 1',
225
+ params: [],
226
+ meta: {
227
+ target: 'postgres',
228
+ storageHash: 'sha256:test',
229
+ lane: 'dsl',
230
+ paramDescriptors: [],
231
+ },
232
+ ...overrides,
233
+ };
234
+ }
235
+
236
+ // =============================================================================
237
+ // Tests: Validator Registry via createExecutionContext
238
+ // =============================================================================
239
+
240
+ describe('JSON Schema validator registry', () => {
241
+ describe('context creation', () => {
242
+ it('builds validator registry for contract with JSON columns that have schemas', () => {
243
+ const contract = createJsonSchemaContract();
244
+ const context = createTestContext(contract, createStubAdapter(), {
245
+ extensionPacks: [createJsonbExtensionDescriptor()],
246
+ });
247
+
248
+ expect(context.jsonSchemaValidators).toBeDefined();
249
+ expect(context.jsonSchemaValidators!.size).toBe(1);
250
+ expect(context.jsonSchemaValidators!.get('user.metadata')).toBeDefined();
251
+ });
252
+
253
+ it('omits validator registry when no JSON columns have schemas', () => {
254
+ const contract = createJsonSchemaContract({
255
+ tableColumns: {
256
+ id: { nativeType: 'int4', codecId: 'pg/int4@1', nullable: false },
257
+ },
258
+ });
259
+ const context = createTestContext(contract, createStubAdapter(), {
260
+ extensionPacks: [createJsonbExtensionDescriptor()],
261
+ });
262
+
263
+ expect(context.jsonSchemaValidators).toBeUndefined();
264
+ });
265
+
266
+ it('builds validators for columns with typeRef', () => {
267
+ const contract = createJsonSchemaContract({
268
+ types: {
269
+ ProfileJson: {
270
+ codecId: 'pg/jsonb@1',
271
+ nativeType: 'jsonb',
272
+ typeParams: {
273
+ schema: {
274
+ type: 'object',
275
+ properties: { displayName: { type: 'string' } },
276
+ required: ['displayName'],
277
+ },
278
+ },
279
+ },
280
+ },
281
+ tableColumns: {
282
+ id: { nativeType: 'int4', codecId: 'pg/int4@1', nullable: false },
283
+ profile: {
284
+ nativeType: 'jsonb',
285
+ codecId: 'pg/jsonb@1',
286
+ nullable: true,
287
+ typeRef: 'ProfileJson',
288
+ },
289
+ },
290
+ });
291
+ const context = createTestContext(contract, createStubAdapter(), {
292
+ extensionPacks: [createJsonbExtensionDescriptor()],
293
+ });
294
+
295
+ expect(context.jsonSchemaValidators).toBeDefined();
296
+ expect(context.jsonSchemaValidators!.get('user.profile')).toBeDefined();
297
+ });
298
+
299
+ it('omits validator registry when no init hooks are defined', () => {
300
+ const registry = createCodecRegistry();
301
+ registry.register(
302
+ codec({
303
+ typeId: 'pg/jsonb@1',
304
+ targetTypes: ['jsonb'],
305
+ encode: (v: unknown) => JSON.stringify(v),
306
+ decode: (w: string) => (typeof w === 'string' ? JSON.parse(w) : w),
307
+ }),
308
+ );
309
+
310
+ const noInitExtension: SqlRuntimeExtensionDescriptor<'postgres'> = {
311
+ kind: 'extension' as const,
312
+ id: 'json-no-init',
313
+ version: '0.0.1',
314
+ familyId: 'sql' as const,
315
+ targetId: 'postgres' as const,
316
+ codecs: () => registry,
317
+ operationSignatures: () => [],
318
+ parameterizedCodecs: () => [{ codecId: 'pg/jsonb@1', paramsSchema: jsonTypeParamsSchema }],
319
+ create() {
320
+ return { familyId: 'sql' as const, targetId: 'postgres' as const };
321
+ },
322
+ };
323
+
324
+ const contract = createJsonSchemaContract();
325
+ const context = createTestContext(contract, createStubAdapter(), {
326
+ extensionPacks: [noInitExtension],
327
+ });
328
+
329
+ expect(context.jsonSchemaValidators).toBeUndefined();
330
+ });
331
+ });
332
+ });
333
+
334
+ // =============================================================================
335
+ // Tests: Encoding validation
336
+ // =============================================================================
337
+
338
+ describe('JSON Schema encoding validation', () => {
339
+ const codecRegistry = createTestCodecRegistry();
340
+
341
+ it('encodes JSON values via codec', () => {
342
+ const plan = createTestPlan({
343
+ params: [{ name: 'Alice' }],
344
+ meta: {
345
+ target: 'postgres',
346
+ storageHash: 'sha256:test',
347
+ lane: 'dsl',
348
+ paramDescriptors: [
349
+ {
350
+ codecId: 'pg/jsonb@1',
351
+ source: 'dsl' as const,
352
+ },
353
+ ],
354
+ },
355
+ });
356
+
357
+ const result = encodeParams(plan, codecRegistry);
358
+ expect(result[0]).toBe('{"name":"Alice"}');
359
+ });
360
+
361
+ it('returns null for null values', () => {
362
+ const descriptor: ParamDescriptor = {
363
+ codecId: 'pg/jsonb@1',
364
+ source: 'dsl',
365
+ };
366
+
367
+ const result = encodeParam(null, descriptor, 0, codecRegistry);
368
+ expect(result).toBeNull();
369
+ });
370
+
371
+ it('encodes when descriptor has name', () => {
372
+ const descriptor: ParamDescriptor = {
373
+ name: 'metadata',
374
+ codecId: 'pg/jsonb@1',
375
+ source: 'dsl',
376
+ };
377
+
378
+ const result = encodeParam({ age: 30 }, descriptor, 0, codecRegistry);
379
+ expect(result).toBe('{"age":30}');
380
+ });
381
+ });
382
+
383
+ // =============================================================================
384
+ // Tests: Decoding validation
385
+ // =============================================================================
386
+
387
+ describe('JSON Schema decoding validation', () => {
388
+ const codecRegistry = createTestCodecRegistry();
389
+
390
+ it('passes valid decoded JSON values', () => {
391
+ const plan = createTestPlan({
392
+ meta: {
393
+ target: 'postgres',
394
+ storageHash: 'sha256:test',
395
+ lane: 'dsl',
396
+ paramDescriptors: [],
397
+ projectionTypes: { metadata: 'pg/jsonb@1' },
398
+ refs: { columns: [{ table: 'user', column: 'metadata' }] },
399
+ },
400
+ });
401
+
402
+ const row = { metadata: '{"name":"Alice"}' };
403
+ const result = decodeRow(row, plan, codecRegistry, createMetadataValidatorRegistry());
404
+ expect(result['metadata']).toEqual({ name: 'Alice' });
405
+ });
406
+
407
+ it('throws RUNTIME.JSON_SCHEMA_VALIDATION_FAILED for invalid decoded values', () => {
408
+ const plan = createTestPlan({
409
+ meta: {
410
+ target: 'postgres',
411
+ storageHash: 'sha256:test',
412
+ lane: 'dsl',
413
+ paramDescriptors: [],
414
+ projectionTypes: { metadata: 'pg/jsonb@1' },
415
+ refs: { columns: [{ table: 'user', column: 'metadata' }] },
416
+ },
417
+ });
418
+
419
+ const row = { metadata: '{"age":30}' };
420
+ expect(() => decodeRow(row, plan, codecRegistry, createMetadataValidatorRegistry())).toThrow(
421
+ expect.objectContaining({
422
+ code: 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED',
423
+ category: 'RUNTIME',
424
+ severity: 'error',
425
+ details: expect.objectContaining({
426
+ table: 'user',
427
+ column: 'metadata',
428
+ direction: 'decode',
429
+ codecId: 'pg/jsonb@1',
430
+ }),
431
+ }),
432
+ );
433
+ });
434
+
435
+ it('validates aliased projection columns using projection mapping', () => {
436
+ const plan = createTestPlan({
437
+ meta: {
438
+ target: 'postgres',
439
+ storageHash: 'sha256:test',
440
+ lane: 'dsl',
441
+ paramDescriptors: [],
442
+ projection: { userMeta: 'user.metadata' },
443
+ projectionTypes: { userMeta: 'pg/jsonb@1' },
444
+ refs: { columns: [{ table: 'user', column: 'metadata' }] },
445
+ },
446
+ });
447
+
448
+ const row = { userMeta: '{"age":30}' };
449
+ expect(() => decodeRow(row, plan, codecRegistry, createMetadataValidatorRegistry())).toThrow(
450
+ expect.objectContaining({
451
+ code: 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED',
452
+ details: expect.objectContaining({
453
+ table: 'user',
454
+ column: 'metadata',
455
+ direction: 'decode',
456
+ }),
457
+ }),
458
+ );
459
+ });
460
+
461
+ it('resolves join aliases with duplicate column names using projection mapping', () => {
462
+ const plan = createTestPlan({
463
+ meta: {
464
+ target: 'postgres',
465
+ storageHash: 'sha256:test',
466
+ lane: 'dsl',
467
+ paramDescriptors: [],
468
+ projection: {
469
+ userMeta: 'user.metadata',
470
+ postMeta: 'post.metadata',
471
+ },
472
+ projectionTypes: {
473
+ userMeta: 'pg/jsonb@1',
474
+ postMeta: 'pg/jsonb@1',
475
+ },
476
+ refs: {
477
+ columns: [
478
+ { table: 'user', column: 'metadata' },
479
+ { table: 'post', column: 'metadata' },
480
+ ],
481
+ },
482
+ },
483
+ });
484
+
485
+ const row = {
486
+ userMeta: '{"userName":"Alice"}',
487
+ postMeta: '{"userName":"Alice"}',
488
+ };
489
+ expect(() =>
490
+ decodeRow(row, plan, codecRegistry, createJoinMetadataValidatorRegistry()),
491
+ ).toThrow(
492
+ expect.objectContaining({
493
+ code: 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED',
494
+ details: expect.objectContaining({
495
+ table: 'post',
496
+ column: 'metadata',
497
+ direction: 'decode',
498
+ }),
499
+ }),
500
+ );
501
+ });
502
+
503
+ it('skips validation when column ref cannot be resolved', () => {
504
+ const plan = createTestPlan({
505
+ meta: {
506
+ target: 'postgres',
507
+ storageHash: 'sha256:test',
508
+ lane: 'dsl',
509
+ paramDescriptors: [],
510
+ projectionTypes: { data: 'pg/jsonb@1' },
511
+ },
512
+ });
513
+
514
+ const row = { data: '{"bad":"data"}' };
515
+ const result = decodeRow(row, plan, codecRegistry, createMetadataValidatorRegistry());
516
+ expect(result['data']).toEqual({ bad: 'data' });
517
+ });
518
+
519
+ it('skips validation for null wire values', () => {
520
+ const plan = createTestPlan({
521
+ meta: {
522
+ target: 'postgres',
523
+ storageHash: 'sha256:test',
524
+ lane: 'dsl',
525
+ paramDescriptors: [],
526
+ projectionTypes: { metadata: 'pg/jsonb@1' },
527
+ refs: { columns: [{ table: 'user', column: 'metadata' }] },
528
+ },
529
+ });
530
+
531
+ const row = { metadata: null };
532
+ const result = decodeRow(row, plan, codecRegistry, createMetadataValidatorRegistry());
533
+ expect(result['metadata']).toBeNull();
534
+ });
535
+
536
+ it('skips validation when no registry is provided', () => {
537
+ const plan = createTestPlan({
538
+ meta: {
539
+ target: 'postgres',
540
+ storageHash: 'sha256:test',
541
+ lane: 'dsl',
542
+ paramDescriptors: [],
543
+ projectionTypes: { metadata: 'pg/jsonb@1' },
544
+ refs: { columns: [{ table: 'user', column: 'metadata' }] },
545
+ },
546
+ });
547
+
548
+ const row = { metadata: '{"bad":"data"}' };
549
+ const result = decodeRow(row, plan, codecRegistry);
550
+ expect(result['metadata']).toEqual({ bad: 'data' });
551
+ });
552
+
553
+ it('decodes non-JSON columns without validation', () => {
554
+ const plan = createTestPlan({
555
+ meta: {
556
+ target: 'postgres',
557
+ storageHash: 'sha256:test',
558
+ lane: 'dsl',
559
+ paramDescriptors: [],
560
+ projectionTypes: { id: 'pg/int4@1', metadata: 'pg/jsonb@1' },
561
+ refs: {
562
+ columns: [
563
+ { table: 'user', column: 'id' },
564
+ { table: 'user', column: 'metadata' },
565
+ ],
566
+ },
567
+ },
568
+ });
569
+
570
+ const row = { id: 42, metadata: '{"name":"Alice"}' };
571
+ const result = decodeRow(row, plan, codecRegistry, createMetadataValidatorRegistry());
572
+ expect(result['id']).toBe(42);
573
+ expect(result['metadata']).toEqual({ name: 'Alice' });
574
+ });
575
+ });