@prisma-next/sql-runtime 0.3.0-dev.7 → 0.3.0-dev.70

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 (166) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +137 -26
  3. package/dist/exports-BhZqJPVb.mjs +771 -0
  4. package/dist/exports-BhZqJPVb.mjs.map +1 -0
  5. package/dist/index-D59jqEKF.d.mts +159 -0
  6. package/dist/index-D59jqEKF.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 +212 -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 +15 -2
  16. package/src/codecs/json-schema-validation.ts +61 -0
  17. package/src/exports/index.ts +14 -6
  18. package/src/lower-sql-plan.ts +8 -8
  19. package/src/plugins/lints.ts +204 -0
  20. package/src/sql-context.ts +385 -98
  21. package/src/sql-family-adapter.ts +9 -5
  22. package/src/sql-marker.ts +2 -2
  23. package/src/sql-runtime.ts +131 -31
  24. package/test/async-iterable-result.test.ts +42 -34
  25. package/test/context.types.test-d.ts +68 -0
  26. package/test/execution-stack.test.ts +166 -0
  27. package/test/json-schema-validation.test.ts +653 -0
  28. package/test/lints.test.ts +330 -0
  29. package/test/parameterized-types.test.ts +539 -0
  30. package/test/sql-context.test.ts +292 -117
  31. package/test/sql-family-adapter.test.ts +7 -6
  32. package/test/sql-runtime.test.ts +218 -30
  33. package/test/utils.ts +80 -51
  34. package/dist/accelerate-EEKAFGN3-P6A6XJWJ.js +0 -137863
  35. package/dist/accelerate-EEKAFGN3-P6A6XJWJ.js.map +0 -1
  36. package/dist/amcheck-24VY6X5V.js +0 -13
  37. package/dist/amcheck-24VY6X5V.js.map +0 -1
  38. package/dist/bloom-VS74NLHT.js +0 -13
  39. package/dist/bloom-VS74NLHT.js.map +0 -1
  40. package/dist/btree_gin-WBC4EAAI.js +0 -13
  41. package/dist/btree_gin-WBC4EAAI.js.map +0 -1
  42. package/dist/btree_gist-UNC6QD3M.js +0 -13
  43. package/dist/btree_gist-UNC6QD3M.js.map +0 -1
  44. package/dist/chunk-3KTOEDFX.js +0 -49
  45. package/dist/chunk-3KTOEDFX.js.map +0 -1
  46. package/dist/chunk-47DZBRQC.js +0 -1280
  47. package/dist/chunk-47DZBRQC.js.map +0 -1
  48. package/dist/chunk-52N6AFZM.js +0 -133
  49. package/dist/chunk-52N6AFZM.js.map +0 -1
  50. package/dist/chunk-7D4SUZUM.js +0 -38
  51. package/dist/chunk-7D4SUZUM.js.map +0 -1
  52. package/dist/chunk-C6I3V3DM.js +0 -455
  53. package/dist/chunk-C6I3V3DM.js.map +0 -1
  54. package/dist/chunk-ECWIHLAT.js +0 -37
  55. package/dist/chunk-ECWIHLAT.js.map +0 -1
  56. package/dist/chunk-EI626SDC.js +0 -105
  57. package/dist/chunk-EI626SDC.js.map +0 -1
  58. package/dist/chunk-UKKOYUGL.js +0 -578
  59. package/dist/chunk-UKKOYUGL.js.map +0 -1
  60. package/dist/chunk-XPLNMXQV.js +0 -1537
  61. package/dist/chunk-XPLNMXQV.js.map +0 -1
  62. package/dist/citext-T7MXGUY7.js +0 -13
  63. package/dist/citext-T7MXGUY7.js.map +0 -1
  64. package/dist/client-5FENX6AW.js +0 -299
  65. package/dist/client-5FENX6AW.js.map +0 -1
  66. package/dist/cube-TFDQBZCI.js +0 -13
  67. package/dist/cube-TFDQBZCI.js.map +0 -1
  68. package/dist/dict_int-AEUOPGWP.js +0 -13
  69. package/dist/dict_int-AEUOPGWP.js.map +0 -1
  70. package/dist/dict_xsyn-DAAYX3FL.js +0 -13
  71. package/dist/dict_xsyn-DAAYX3FL.js.map +0 -1
  72. package/dist/dist-AQ3LWXOX.js +0 -570
  73. package/dist/dist-AQ3LWXOX.js.map +0 -1
  74. package/dist/dist-LBVX6BJW.js +0 -189
  75. package/dist/dist-LBVX6BJW.js.map +0 -1
  76. package/dist/dist-WLKUVDN2.js +0 -5127
  77. package/dist/dist-WLKUVDN2.js.map +0 -1
  78. package/dist/earthdistance-KIGTF4LE.js +0 -13
  79. package/dist/earthdistance-KIGTF4LE.js.map +0 -1
  80. package/dist/file_fdw-5N55UP6I.js +0 -13
  81. package/dist/file_fdw-5N55UP6I.js.map +0 -1
  82. package/dist/fuzzystrmatch-KN3YWBFP.js +0 -13
  83. package/dist/fuzzystrmatch-KN3YWBFP.js.map +0 -1
  84. package/dist/hstore-YX726NKN.js +0 -13
  85. package/dist/hstore-YX726NKN.js.map +0 -1
  86. package/dist/http-exception-FZY2H4OF.js +0 -8
  87. package/dist/http-exception-FZY2H4OF.js.map +0 -1
  88. package/dist/index.js +0 -30
  89. package/dist/index.js.map +0 -1
  90. package/dist/intarray-NKVXNO2D.js +0 -13
  91. package/dist/intarray-NKVXNO2D.js.map +0 -1
  92. package/dist/isn-FTEMJGEV.js +0 -13
  93. package/dist/isn-FTEMJGEV.js.map +0 -1
  94. package/dist/lo-DB7L4NGI.js +0 -13
  95. package/dist/lo-DB7L4NGI.js.map +0 -1
  96. package/dist/logger-WQ7SHNDD.js +0 -68
  97. package/dist/logger-WQ7SHNDD.js.map +0 -1
  98. package/dist/ltree-Z32TZT6W.js +0 -13
  99. package/dist/ltree-Z32TZT6W.js.map +0 -1
  100. package/dist/nodefs-NM46ACH7.js +0 -31
  101. package/dist/nodefs-NM46ACH7.js.map +0 -1
  102. package/dist/opfs-ahp-NJO33LVZ.js +0 -332
  103. package/dist/opfs-ahp-NJO33LVZ.js.map +0 -1
  104. package/dist/pageinspect-YP3IZR4X.js +0 -13
  105. package/dist/pageinspect-YP3IZR4X.js.map +0 -1
  106. package/dist/pg_buffercache-7TD5J2FB.js +0 -13
  107. package/dist/pg_buffercache-7TD5J2FB.js.map +0 -1
  108. package/dist/pg_dump-SG4KYBUB.js +0 -2492
  109. package/dist/pg_dump-SG4KYBUB.js.map +0 -1
  110. package/dist/pg_freespacemap-DZDNCPZK.js +0 -13
  111. package/dist/pg_freespacemap-DZDNCPZK.js.map +0 -1
  112. package/dist/pg_surgery-J2MUEWEP.js +0 -13
  113. package/dist/pg_surgery-J2MUEWEP.js.map +0 -1
  114. package/dist/pg_trgm-7VNQOYS6.js +0 -13
  115. package/dist/pg_trgm-7VNQOYS6.js.map +0 -1
  116. package/dist/pg_visibility-TTSIPHFL.js +0 -13
  117. package/dist/pg_visibility-TTSIPHFL.js.map +0 -1
  118. package/dist/pg_walinspect-KPFHSHRJ.js +0 -13
  119. package/dist/pg_walinspect-KPFHSHRJ.js.map +0 -1
  120. package/dist/proxy-signals-GUDAMDHV.js +0 -39
  121. package/dist/proxy-signals-GUDAMDHV.js.map +0 -1
  122. package/dist/seg-IYVDLE4O.js +0 -13
  123. package/dist/seg-IYVDLE4O.js.map +0 -1
  124. package/dist/src/codecs/decoding.d.ts +0 -4
  125. package/dist/src/codecs/decoding.d.ts.map +0 -1
  126. package/dist/src/codecs/encoding.d.ts +0 -5
  127. package/dist/src/codecs/encoding.d.ts.map +0 -1
  128. package/dist/src/codecs/validation.d.ts +0 -6
  129. package/dist/src/codecs/validation.d.ts.map +0 -1
  130. package/dist/src/exports/index.d.ts +0 -11
  131. package/dist/src/exports/index.d.ts.map +0 -1
  132. package/dist/src/index.d.ts +0 -2
  133. package/dist/src/index.d.ts.map +0 -1
  134. package/dist/src/lower-sql-plan.d.ts +0 -15
  135. package/dist/src/lower-sql-plan.d.ts.map +0 -1
  136. package/dist/src/sql-context.d.ts +0 -65
  137. package/dist/src/sql-context.d.ts.map +0 -1
  138. package/dist/src/sql-family-adapter.d.ts +0 -10
  139. package/dist/src/sql-family-adapter.d.ts.map +0 -1
  140. package/dist/src/sql-marker.d.ts +0 -22
  141. package/dist/src/sql-marker.d.ts.map +0 -1
  142. package/dist/src/sql-runtime.d.ts +0 -25
  143. package/dist/src/sql-runtime.d.ts.map +0 -1
  144. package/dist/tablefunc-EF4RCS7S.js +0 -13
  145. package/dist/tablefunc-EF4RCS7S.js.map +0 -1
  146. package/dist/tcn-3VT5BQYW.js +0 -13
  147. package/dist/tcn-3VT5BQYW.js.map +0 -1
  148. package/dist/test/utils.d.ts +0 -59
  149. package/dist/test/utils.d.ts.map +0 -1
  150. package/dist/test/utils.js +0 -24634
  151. package/dist/test/utils.js.map +0 -1
  152. package/dist/tiny-CW6F4GX6.js +0 -10
  153. package/dist/tiny-CW6F4GX6.js.map +0 -1
  154. package/dist/tsm_system_rows-ES7KNUQH.js +0 -13
  155. package/dist/tsm_system_rows-ES7KNUQH.js.map +0 -1
  156. package/dist/tsm_system_time-76WEIMBG.js +0 -13
  157. package/dist/tsm_system_time-76WEIMBG.js.map +0 -1
  158. package/dist/unaccent-7RYF3R64.js +0 -13
  159. package/dist/unaccent-7RYF3R64.js.map +0 -1
  160. package/dist/utility-Q5A254LJ-J4HTKZPT.js +0 -347
  161. package/dist/utility-Q5A254LJ-J4HTKZPT.js.map +0 -1
  162. package/dist/uuid_ossp-4ETE4FPE.js +0 -13
  163. package/dist/uuid_ossp-4ETE4FPE.js.map +0 -1
  164. package/dist/vector-74GPNV7V.js +0 -13
  165. package/dist/vector-74GPNV7V.js.map +0 -1
  166. 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
+ });