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