@prisma-next/sql-runtime 0.3.0-dev.33 → 0.3.0-dev.36

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