@prisma-next/sql-runtime 0.5.0-dev.7 → 0.5.0-dev.9

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 (42) hide show
  1. package/README.md +29 -21
  2. package/dist/{exports-BQZSVXXt.mjs → exports-BOHa3Emo.mjs} +481 -128
  3. package/dist/exports-BOHa3Emo.mjs.map +1 -0
  4. package/dist/{index-yb51L_1h.d.mts → index-CZmC2kD3.d.mts} +53 -16
  5. package/dist/index-CZmC2kD3.d.mts.map +1 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +1 -1
  8. package/dist/test/utils.d.mts +6 -5
  9. package/dist/test/utils.d.mts.map +1 -1
  10. package/dist/test/utils.mjs +7 -2
  11. package/dist/test/utils.mjs.map +1 -1
  12. package/package.json +12 -14
  13. package/src/codecs/decoding.ts +172 -116
  14. package/src/codecs/encoding.ts +59 -21
  15. package/src/exports/index.ts +10 -7
  16. package/src/fingerprint.ts +22 -0
  17. package/src/guardrails/raw.ts +214 -0
  18. package/src/lower-sql-plan.ts +3 -3
  19. package/src/marker.ts +82 -0
  20. package/src/middleware/before-compile-chain.ts +32 -1
  21. package/src/middleware/budgets.ts +14 -11
  22. package/src/middleware/lints.ts +3 -3
  23. package/src/middleware/sql-middleware.ts +6 -5
  24. package/src/runtime-spi.ts +43 -0
  25. package/src/sql-family-adapter.ts +3 -2
  26. package/src/sql-marker.ts +1 -1
  27. package/src/sql-runtime.ts +272 -110
  28. package/dist/exports-BQZSVXXt.mjs.map +0 -1
  29. package/dist/index-yb51L_1h.d.mts.map +0 -1
  30. package/test/async-iterable-result.test.ts +0 -141
  31. package/test/before-compile-chain.test.ts +0 -223
  32. package/test/budgets.test.ts +0 -431
  33. package/test/context.types.test-d.ts +0 -68
  34. package/test/execution-stack.test.ts +0 -161
  35. package/test/json-schema-validation.test.ts +0 -571
  36. package/test/lints.test.ts +0 -160
  37. package/test/mutation-default-generators.test.ts +0 -254
  38. package/test/parameterized-types.test.ts +0 -529
  39. package/test/sql-context.test.ts +0 -384
  40. package/test/sql-family-adapter.test.ts +0 -103
  41. package/test/sql-runtime.test.ts +0 -792
  42. package/test/utils.ts +0 -297
@@ -1,792 +0,0 @@
1
- import type { Contract, ExecutionPlan } from '@prisma-next/contract/types';
2
- import { coreHash, profileHash } from '@prisma-next/contract/types';
3
- import {
4
- type ExecutionStackInstance,
5
- instantiateExecutionStack,
6
- type RuntimeDriverInstance,
7
- type RuntimeExtensionInstance,
8
- } from '@prisma-next/framework-components/execution';
9
- import type { SqlStorage } from '@prisma-next/sql-contract/types';
10
- import type {
11
- CodecRegistry,
12
- SqlDriver,
13
- SqlExecuteRequest,
14
- } from '@prisma-next/sql-relational-core/ast';
15
- import {
16
- BinaryExpr,
17
- ColumnRef,
18
- codec,
19
- createCodecRegistry,
20
- LiteralExpr,
21
- SelectAst,
22
- TableSource,
23
- } from '@prisma-next/sql-relational-core/ast';
24
- import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
25
- import { describe, expect, it, vi } from 'vitest';
26
- import type { SqlMiddleware } from '../src/middleware/sql-middleware';
27
- import type {
28
- SqlRuntimeAdapterDescriptor,
29
- SqlRuntimeAdapterInstance,
30
- SqlRuntimeTargetDescriptor,
31
- } from '../src/sql-context';
32
- import { createExecutionContext, createSqlExecutionStack } from '../src/sql-context';
33
- import { createRuntime, withTransaction } from '../src/sql-runtime';
34
-
35
- const testContract: Contract<SqlStorage> = {
36
- targetFamily: 'sql',
37
- target: 'postgres',
38
- profileHash: profileHash('sha256:test'),
39
- models: {},
40
- roots: {},
41
- storage: { storageHash: coreHash('sha256:test'), tables: {} },
42
- extensionPacks: {},
43
- capabilities: {},
44
- meta: {},
45
- };
46
-
47
- interface DriverMockSpies {
48
- rootExecute: ReturnType<typeof vi.fn>;
49
- connectionExecute: ReturnType<typeof vi.fn>;
50
- transactionExecute: ReturnType<typeof vi.fn>;
51
- connectionRelease: ReturnType<typeof vi.fn>;
52
- connectionDestroy: ReturnType<typeof vi.fn>;
53
- transactionCommit: ReturnType<typeof vi.fn>;
54
- transactionRollback: ReturnType<typeof vi.fn>;
55
- driverClose: ReturnType<typeof vi.fn>;
56
- }
57
-
58
- type MockSqlDriver = SqlDriver & { __spies: DriverMockSpies };
59
-
60
- function createStubCodecs(): CodecRegistry {
61
- const registry = createCodecRegistry();
62
- registry.register(
63
- codec({
64
- typeId: 'pg/int4@1',
65
- targetTypes: ['int4'],
66
- encode: (v: number) => v,
67
- decode: (w: number) => w,
68
- }),
69
- );
70
- return registry;
71
- }
72
-
73
- function createStubAdapter() {
74
- const codecs = createStubCodecs();
75
- return {
76
- familyId: 'sql' as const,
77
- targetId: 'postgres' as const,
78
- profile: {
79
- id: 'test-profile',
80
- target: 'postgres',
81
- capabilities: {},
82
- codecs() {
83
- return codecs;
84
- },
85
- readMarkerStatement() {
86
- return {
87
- sql: 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta from prisma_contract.marker where id = $1',
88
- params: [1],
89
- };
90
- },
91
- },
92
- lower(ast: SelectAst) {
93
- return Object.freeze({ sql: JSON.stringify(ast), params: [] });
94
- },
95
- };
96
- }
97
-
98
- function createMockDriver(): MockSqlDriver {
99
- const rootExecute = vi.fn().mockImplementation(async function* (_request: SqlExecuteRequest) {
100
- yield { id: 1 };
101
- });
102
- const connectionExecute = vi.fn().mockImplementation(async function* (
103
- _request: SqlExecuteRequest,
104
- ) {
105
- yield { id: 2 };
106
- });
107
- const transactionExecute = vi.fn().mockImplementation(async function* (
108
- _request: SqlExecuteRequest,
109
- ) {
110
- yield { id: 3 };
111
- });
112
-
113
- const query = vi.fn().mockResolvedValue({ rows: [], rowCount: 0 });
114
-
115
- const transaction = {
116
- execute: transactionExecute,
117
- query,
118
- commit: vi.fn().mockResolvedValue(undefined),
119
- rollback: vi.fn().mockResolvedValue(undefined),
120
- };
121
-
122
- const connection = {
123
- execute: connectionExecute,
124
- query,
125
- release: vi.fn().mockResolvedValue(undefined),
126
- destroy: vi.fn().mockResolvedValue(undefined),
127
- beginTransaction: vi.fn().mockResolvedValue(transaction),
128
- };
129
-
130
- const driverClose = vi.fn().mockResolvedValue(undefined);
131
-
132
- const driver: SqlDriver = {
133
- execute: rootExecute,
134
- query,
135
- connect: vi.fn().mockImplementation(async (_binding?: undefined) => undefined),
136
- acquireConnection: vi.fn().mockResolvedValue(connection),
137
- close: driverClose,
138
- };
139
-
140
- return Object.assign(driver, {
141
- __spies: {
142
- rootExecute,
143
- connectionExecute,
144
- transactionExecute,
145
- connectionRelease: connection.release,
146
- connectionDestroy: connection.destroy,
147
- transactionCommit: transaction.commit,
148
- transactionRollback: transaction.rollback,
149
- driverClose,
150
- },
151
- });
152
- }
153
-
154
- function createTestTargetDescriptor(): SqlRuntimeTargetDescriptor<'postgres'> {
155
- return {
156
- kind: 'target',
157
- id: 'postgres',
158
- version: '0.0.1',
159
- familyId: 'sql' as const,
160
- targetId: 'postgres' as const,
161
- codecs: () => createCodecRegistry(),
162
- parameterizedCodecs: () => [],
163
- create() {
164
- return { familyId: 'sql' as const, targetId: 'postgres' as const };
165
- },
166
- };
167
- }
168
-
169
- function createTestAdapterDescriptor(
170
- adapter: ReturnType<typeof createStubAdapter>,
171
- ): SqlRuntimeAdapterDescriptor<'postgres'> {
172
- const codecRegistry = adapter.profile.codecs();
173
- return {
174
- kind: 'adapter',
175
- id: 'test-adapter',
176
- version: '0.0.1',
177
- familyId: 'sql' as const,
178
- targetId: 'postgres' as const,
179
- codecs: () => codecRegistry,
180
- parameterizedCodecs: () => [],
181
- create() {
182
- return Object.assign(
183
- { familyId: 'sql' as const, targetId: 'postgres' as const },
184
- adapter,
185
- ) as SqlRuntimeAdapterInstance<'postgres'>;
186
- },
187
- };
188
- }
189
-
190
- function createTestSetup() {
191
- const adapter = createStubAdapter();
192
- const driver = createMockDriver();
193
-
194
- const targetDescriptor = createTestTargetDescriptor();
195
- const adapterDescriptor = createTestAdapterDescriptor(adapter);
196
-
197
- const stack = createSqlExecutionStack({
198
- target: targetDescriptor,
199
- adapter: adapterDescriptor,
200
- extensionPacks: [],
201
- });
202
- type SqlTestStackInstance = ExecutionStackInstance<
203
- 'sql',
204
- 'postgres',
205
- SqlRuntimeAdapterInstance<'postgres'>,
206
- RuntimeDriverInstance<'sql', 'postgres'>,
207
- RuntimeExtensionInstance<'sql', 'postgres'>
208
- >;
209
- const stackInstance = instantiateExecutionStack(stack) as SqlTestStackInstance;
210
-
211
- const context = createExecutionContext({
212
- contract: testContract,
213
- stack: { target: targetDescriptor, adapter: adapterDescriptor, extensionPacks: [] },
214
- });
215
-
216
- return { stackInstance, context, driver };
217
- }
218
-
219
- function createRawExecutionPlan<Row = Record<string, unknown>>(): ExecutionPlan<Row> {
220
- return {
221
- sql: 'select 1',
222
- params: [],
223
- meta: {
224
- target: testContract.target,
225
- targetFamily: testContract.targetFamily,
226
- storageHash: testContract.storage.storageHash,
227
- lane: 'raw',
228
- paramDescriptors: [],
229
- },
230
- };
231
- }
232
-
233
- describe('createRuntime', () => {
234
- it('creates runtime with context and driver', () => {
235
- const { stackInstance, context, driver } = createTestSetup();
236
-
237
- const runtime = createRuntime({
238
- stackInstance,
239
- context,
240
- driver,
241
- verify: { mode: 'onFirstUse', requireMarker: false },
242
- });
243
-
244
- expect(runtime).toBeDefined();
245
- expect(runtime.execute).toBeDefined();
246
- expect(runtime.telemetry).toBeDefined();
247
- expect(runtime.close).toBeDefined();
248
- });
249
-
250
- it('returns null telemetry when no events', () => {
251
- const { stackInstance, context, driver } = createTestSetup();
252
-
253
- const runtime = createRuntime({
254
- stackInstance,
255
- context,
256
- driver,
257
- verify: { mode: 'onFirstUse', requireMarker: false },
258
- });
259
-
260
- expect(runtime.telemetry()).toBeNull();
261
- });
262
-
263
- it('closes runtime and driver', async () => {
264
- const { stackInstance, context, driver } = createTestSetup();
265
-
266
- const runtime = createRuntime({
267
- stackInstance,
268
- context,
269
- driver,
270
- verify: { mode: 'onFirstUse', requireMarker: false },
271
- });
272
-
273
- await runtime.close();
274
- expect(driver.close).toHaveBeenCalled();
275
- });
276
-
277
- it('validates codec registry at startup when verify mode is startup', () => {
278
- const { stackInstance, context, driver } = createTestSetup();
279
-
280
- const runtime = createRuntime({
281
- stackInstance,
282
- context,
283
- driver,
284
- verify: { mode: 'startup', requireMarker: false },
285
- });
286
-
287
- expect(runtime).toBeDefined();
288
- });
289
-
290
- it('uses acquired connection queryable for connection.execute', async () => {
291
- const { stackInstance, context, driver } = createTestSetup();
292
- const runtime = createRuntime({
293
- stackInstance,
294
- context,
295
- driver,
296
- verify: { mode: 'onFirstUse', requireMarker: false },
297
- });
298
-
299
- const connection = await runtime.connection();
300
- await connection.execute(createRawExecutionPlan()).toArray();
301
-
302
- expect(driver.__spies.connectionExecute).toHaveBeenCalledTimes(1);
303
- expect(driver.__spies.transactionExecute).not.toHaveBeenCalled();
304
- expect(driver.__spies.rootExecute).not.toHaveBeenCalled();
305
-
306
- await connection.release();
307
- });
308
-
309
- it('delegates connection.destroy() to the driver connection', async () => {
310
- const { stackInstance, context, driver } = createTestSetup();
311
- const runtime = createRuntime({
312
- stackInstance,
313
- context,
314
- driver,
315
- verify: { mode: 'onFirstUse', requireMarker: false },
316
- });
317
-
318
- const connection = await runtime.connection();
319
- const reason = new Error('bad state');
320
- await connection.destroy(reason);
321
-
322
- expect(driver.__spies.connectionDestroy).toHaveBeenCalledOnce();
323
- expect(driver.__spies.connectionDestroy).toHaveBeenCalledWith(reason);
324
- expect(driver.__spies.connectionRelease).not.toHaveBeenCalled();
325
- });
326
-
327
- it('uses transaction queryable for transaction.execute', async () => {
328
- const { stackInstance, context, driver } = createTestSetup();
329
- const runtime = createRuntime({
330
- stackInstance,
331
- context,
332
- driver,
333
- verify: { mode: 'onFirstUse', requireMarker: false },
334
- });
335
-
336
- const connection = await runtime.connection();
337
- const transaction = await connection.transaction();
338
- await transaction.execute(createRawExecutionPlan()).toArray();
339
-
340
- expect(driver.__spies.transactionExecute).toHaveBeenCalledTimes(1);
341
- expect(driver.__spies.connectionExecute).not.toHaveBeenCalled();
342
- expect(driver.__spies.rootExecute).not.toHaveBeenCalled();
343
-
344
- await transaction.rollback();
345
- await connection.release();
346
- });
347
-
348
- it('keeps root execute on driver queryable for runtime.execute', async () => {
349
- const { stackInstance, context, driver } = createTestSetup();
350
- const runtime = createRuntime({
351
- stackInstance,
352
- context,
353
- driver,
354
- verify: { mode: 'onFirstUse', requireMarker: false },
355
- });
356
-
357
- await runtime.execute(createRawExecutionPlan()).toArray();
358
-
359
- expect(driver.__spies.rootExecute).toHaveBeenCalledTimes(1);
360
- expect(driver.__spies.connectionExecute).not.toHaveBeenCalled();
361
- expect(driver.__spies.transactionExecute).not.toHaveBeenCalled();
362
- });
363
-
364
- it('accepts a generic middleware (no familyId)', () => {
365
- const { stackInstance, context, driver } = createTestSetup();
366
- expect(() =>
367
- createRuntime({
368
- stackInstance,
369
- context,
370
- driver,
371
- verify: { mode: 'onFirstUse', requireMarker: false },
372
- middleware: [{ name: 'generic' }],
373
- }),
374
- ).not.toThrow();
375
- });
376
-
377
- it('accepts an SQL middleware', () => {
378
- const { stackInstance, context, driver } = createTestSetup();
379
- expect(() =>
380
- createRuntime({
381
- stackInstance,
382
- context,
383
- driver,
384
- verify: { mode: 'onFirstUse', requireMarker: false },
385
- middleware: [{ name: 'sql-lints', familyId: 'sql' }],
386
- }),
387
- ).not.toThrow();
388
- });
389
-
390
- it('rejects a Mongo middleware with a clear error', () => {
391
- const { stackInstance, context, driver } = createTestSetup();
392
- // Simulate a caller bypassing the SqlMiddleware type constraint (e.g. dynamically-loaded
393
- // middleware). Static typing already rejects familyId: 'mongo'; this tests the runtime guard.
394
- const mongoMiddleware = { name: 'mongo-mw', familyId: 'mongo' } as unknown as SqlMiddleware;
395
- expect(() =>
396
- createRuntime({
397
- stackInstance,
398
- context,
399
- driver,
400
- verify: { mode: 'onFirstUse', requireMarker: false },
401
- middleware: [mongoMiddleware],
402
- }),
403
- ).toThrow(
404
- "Middleware 'mongo-mw' requires family 'mongo' but the runtime is configured for family 'sql'",
405
- );
406
- });
407
-
408
- it('invokes beforeCompile and lowers the rewritten AST', async () => {
409
- const { stackInstance, context, driver } = createTestSetup();
410
- const debug = vi.fn();
411
- const softDeletePredicate = BinaryExpr.eq(
412
- ColumnRef.of('users', 'deleted_at'),
413
- LiteralExpr.of(null),
414
- );
415
- const softDelete: SqlMiddleware = {
416
- name: 'softDelete',
417
- familyId: 'sql',
418
- async beforeCompile(draft) {
419
- if (draft.ast.kind !== 'select') return;
420
- return { ...draft, ast: draft.ast.withWhere(softDeletePredicate) };
421
- },
422
- };
423
-
424
- const runtime = createRuntime({
425
- stackInstance,
426
- context,
427
- driver,
428
- verify: { mode: 'onFirstUse', requireMarker: false },
429
- middleware: [softDelete],
430
- log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug },
431
- });
432
-
433
- const queryPlan: SqlQueryPlan = {
434
- ast: SelectAst.from(TableSource.named('users')).withProjection([]),
435
- params: [],
436
- meta: {
437
- target: 'postgres',
438
- storageHash: testContract.storage.storageHash,
439
- lane: 'dsl',
440
- paramDescriptors: [],
441
- },
442
- };
443
-
444
- await runtime.execute(queryPlan).toArray();
445
-
446
- expect(driver.__spies.rootExecute).toHaveBeenCalledTimes(1);
447
- const request = driver.__spies.rootExecute.mock.calls[0]?.[0] as SqlExecuteRequest;
448
- expect(request.sql).toContain('deleted_at');
449
- expect(debug).toHaveBeenCalledWith({
450
- event: 'middleware.rewrite',
451
- middleware: 'softDelete',
452
- lane: 'dsl',
453
- });
454
- });
455
-
456
- it('invokes adapter.lower exactly once per execute regardless of chain length', async () => {
457
- const adapter = createStubAdapter();
458
- const lowerSpy = vi.spyOn(adapter, 'lower');
459
- const driver = createMockDriver();
460
-
461
- const targetDescriptor = createTestTargetDescriptor();
462
- const adapterDescriptor = createTestAdapterDescriptor(adapter);
463
- const stack = createSqlExecutionStack({
464
- target: targetDescriptor,
465
- adapter: adapterDescriptor,
466
- extensionPacks: [],
467
- });
468
- const stackInstance = instantiateExecutionStack(stack) as ExecutionStackInstance<
469
- 'sql',
470
- 'postgres',
471
- SqlRuntimeAdapterInstance<'postgres'>,
472
- RuntimeDriverInstance<'sql', 'postgres'>,
473
- RuntimeExtensionInstance<'sql', 'postgres'>
474
- >;
475
- const context = createExecutionContext({
476
- contract: testContract,
477
- stack: { target: targetDescriptor, adapter: adapterDescriptor, extensionPacks: [] },
478
- });
479
-
480
- const rewriteA: SqlMiddleware = {
481
- name: 'rewriteA',
482
- familyId: 'sql',
483
- async beforeCompile(draft) {
484
- if (draft.ast.kind !== 'select') return undefined;
485
- return {
486
- ...draft,
487
- ast: draft.ast.withWhere(BinaryExpr.eq(ColumnRef.of('users', 'a'), LiteralExpr.of(1))),
488
- };
489
- },
490
- };
491
- const rewriteB: SqlMiddleware = {
492
- name: 'rewriteB',
493
- familyId: 'sql',
494
- async beforeCompile(draft) {
495
- if (draft.ast.kind !== 'select') return undefined;
496
- return {
497
- ...draft,
498
- ast: draft.ast.withWhere(BinaryExpr.eq(ColumnRef.of('users', 'b'), LiteralExpr.of(2))),
499
- };
500
- },
501
- };
502
-
503
- const runtime = createRuntime({
504
- stackInstance,
505
- context,
506
- driver,
507
- verify: { mode: 'onFirstUse', requireMarker: false },
508
- middleware: [rewriteA, rewriteB],
509
- });
510
-
511
- const queryPlan: SqlQueryPlan = {
512
- ast: SelectAst.from(TableSource.named('users')).withProjection([]),
513
- params: [],
514
- meta: {
515
- target: 'postgres',
516
- storageHash: testContract.storage.storageHash,
517
- lane: 'dsl',
518
- paramDescriptors: [],
519
- },
520
- };
521
-
522
- await runtime.execute(queryPlan).toArray();
523
-
524
- expect(lowerSpy).toHaveBeenCalledTimes(1);
525
- const loweredAst = lowerSpy.mock.calls[0]?.[0] as SelectAst;
526
- expect(loweredAst.where?.kind).toBe('binary');
527
- });
528
-
529
- it('skips beforeCompile for raw execution plans with no AST', async () => {
530
- const { stackInstance, context, driver } = createTestSetup();
531
- const debug = vi.fn();
532
- const beforeCompile = vi.fn();
533
- const observer: SqlMiddleware = {
534
- name: 'observer',
535
- familyId: 'sql',
536
- beforeCompile,
537
- };
538
-
539
- const runtime = createRuntime({
540
- stackInstance,
541
- context,
542
- driver,
543
- verify: { mode: 'onFirstUse', requireMarker: false },
544
- middleware: [observer],
545
- log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug },
546
- });
547
-
548
- await runtime.execute(createRawExecutionPlan()).toArray();
549
-
550
- expect(beforeCompile).not.toHaveBeenCalled();
551
- expect(debug).not.toHaveBeenCalled();
552
- });
553
- });
554
-
555
- describe('withTransaction', () => {
556
- function createRuntimeForTransaction() {
557
- const { stackInstance, context, driver } = createTestSetup();
558
- const runtime = createRuntime({
559
- stackInstance,
560
- context,
561
- driver,
562
- verify: { mode: 'onFirstUse', requireMarker: false },
563
- });
564
- return { runtime, driver };
565
- }
566
-
567
- it('commits on successful callback and returns the result', async () => {
568
- const { runtime, driver } = createRuntimeForTransaction();
569
-
570
- const result = await withTransaction(runtime, async (tx) => {
571
- await tx.execute(createRawExecutionPlan()).toArray();
572
- return 42;
573
- });
574
-
575
- expect(result).toBe(42);
576
- expect(driver.__spies.transactionCommit).toHaveBeenCalledOnce();
577
- expect(driver.__spies.transactionRollback).not.toHaveBeenCalled();
578
- expect(driver.__spies.connectionRelease).toHaveBeenCalledOnce();
579
- });
580
-
581
- it('rolls back on callback error and re-throws', async () => {
582
- const { runtime, driver } = createRuntimeForTransaction();
583
- const error = new Error('test error');
584
-
585
- await expect(
586
- withTransaction(runtime, async () => {
587
- throw error;
588
- }),
589
- ).rejects.toBe(error);
590
-
591
- expect(driver.__spies.transactionRollback).toHaveBeenCalledOnce();
592
- expect(driver.__spies.transactionCommit).not.toHaveBeenCalled();
593
- expect(driver.__spies.connectionRelease).toHaveBeenCalledOnce();
594
- });
595
-
596
- it('releases connection after commit', async () => {
597
- const { runtime, driver } = createRuntimeForTransaction();
598
-
599
- await withTransaction(runtime, async () => 'ok');
600
-
601
- expect(driver.__spies.connectionRelease).toHaveBeenCalledOnce();
602
- });
603
-
604
- it('releases connection after rollback', async () => {
605
- const { runtime, driver } = createRuntimeForTransaction();
606
-
607
- await withTransaction(runtime, async () => {
608
- throw new Error('fail');
609
- }).catch(() => {});
610
-
611
- expect(driver.__spies.connectionRelease).toHaveBeenCalledOnce();
612
- });
613
-
614
- it('wraps commit failure and exposes the original error as cause', async () => {
615
- const { runtime, driver } = createRuntimeForTransaction();
616
- const commitError = new Error('commit failed');
617
- driver.__spies.transactionCommit.mockRejectedValueOnce(commitError);
618
-
619
- const result = withTransaction(runtime, async () => 'value');
620
-
621
- await expect(result).rejects.toMatchObject({
622
- code: 'RUNTIME.TRANSACTION_COMMIT_FAILED',
623
- cause: commitError,
624
- });
625
- });
626
-
627
- it('attempts best-effort rollback after commit fails and releases when it succeeds', async () => {
628
- const { runtime, driver } = createRuntimeForTransaction();
629
- const commitError = new Error('commit failed');
630
- driver.__spies.transactionCommit.mockRejectedValueOnce(commitError);
631
-
632
- await withTransaction(runtime, async () => 'value').catch(() => {});
633
-
634
- expect(driver.__spies.transactionCommit).toHaveBeenCalledOnce();
635
- expect(driver.__spies.transactionRollback).toHaveBeenCalledOnce();
636
- // A successful rollback after a failed commit means the server is no
637
- // longer in a transaction and the connection round-tripped cleanly, so
638
- // it is safe to return to the pool rather than evict it.
639
- expect(driver.__spies.connectionRelease).toHaveBeenCalledOnce();
640
- expect(driver.__spies.connectionDestroy).not.toHaveBeenCalled();
641
- });
642
-
643
- it('forwards the callback return value', async () => {
644
- const { runtime } = createRuntimeForTransaction();
645
-
646
- const result = await withTransaction(runtime, async () => ({
647
- name: 'test',
648
- count: 3,
649
- }));
650
-
651
- expect(result).toEqual({ name: 'test', count: 3 });
652
- });
653
-
654
- it('executes queries against the transaction', async () => {
655
- const { runtime, driver } = createRuntimeForTransaction();
656
-
657
- await withTransaction(runtime, async (tx) => {
658
- await tx.execute(createRawExecutionPlan()).toArray();
659
- });
660
-
661
- expect(driver.__spies.transactionExecute).toHaveBeenCalledOnce();
662
- expect(driver.__spies.rootExecute).not.toHaveBeenCalled();
663
- expect(driver.__spies.connectionExecute).not.toHaveBeenCalled();
664
- });
665
-
666
- it('throws on execute after commit (invalidation)', async () => {
667
- const { runtime } = createRuntimeForTransaction();
668
- let savedTx: { execute: (plan: ExecutionPlan) => unknown } | undefined;
669
-
670
- await withTransaction(runtime, async (tx) => {
671
- savedTx = tx;
672
- });
673
-
674
- expect(() => savedTx!.execute(createRawExecutionPlan())).toThrow(
675
- 'Cannot read from a query result after the transaction has ended',
676
- );
677
- });
678
-
679
- it('throws on iteration of escaped AsyncIterableResult after commit', async () => {
680
- const { runtime } = createRuntimeForTransaction();
681
-
682
- const escaped = await withTransaction(runtime, async (tx) => {
683
- return { result: tx.execute(createRawExecutionPlan()) };
684
- });
685
-
686
- await expect(escaped.result.toArray()).rejects.toThrow(
687
- 'Cannot read from a query result after the transaction has ended',
688
- );
689
- });
690
-
691
- it('sets invalidated flag after commit', async () => {
692
- const { runtime } = createRuntimeForTransaction();
693
- let txRef: { invalidated: boolean } | undefined;
694
-
695
- await withTransaction(runtime, async (tx) => {
696
- expect(tx.invalidated).toBe(false);
697
- txRef = tx;
698
- });
699
-
700
- expect(txRef!.invalidated).toBe(true);
701
- });
702
-
703
- it('wraps original error when rollback fails', async () => {
704
- const { runtime, driver } = createRuntimeForTransaction();
705
- const callbackError = new Error('callback failed');
706
- const rollbackError = new Error('rollback failed');
707
- driver.__spies.transactionRollback.mockRejectedValueOnce(rollbackError);
708
-
709
- const rejection = withTransaction(runtime, async () => {
710
- throw callbackError;
711
- });
712
-
713
- await expect(rejection).rejects.toThrow('Transaction rollback failed after callback error');
714
- await expect(rejection).rejects.toMatchObject({
715
- code: 'RUNTIME.TRANSACTION_ROLLBACK_FAILED',
716
- cause: callbackError,
717
- details: { rollbackError },
718
- });
719
- expect(driver.__spies.connectionDestroy).toHaveBeenCalledOnce();
720
- expect(driver.__spies.connectionRelease).not.toHaveBeenCalled();
721
- });
722
-
723
- it('destroys connection when rollback fails even if destroy also fails', async () => {
724
- const { runtime, driver } = createRuntimeForTransaction();
725
- const callbackError = new Error('callback failed');
726
- const rollbackError = new Error('rollback failed');
727
- const destroyError = new Error('destroy failed');
728
- driver.__spies.transactionRollback.mockRejectedValueOnce(rollbackError);
729
- driver.__spies.connectionDestroy.mockRejectedValueOnce(destroyError);
730
-
731
- const rejection = withTransaction(runtime, async () => {
732
- throw callbackError;
733
- });
734
-
735
- await expect(rejection).rejects.toMatchObject({
736
- code: 'RUNTIME.TRANSACTION_ROLLBACK_FAILED',
737
- cause: callbackError,
738
- details: { rollbackError },
739
- });
740
- expect(driver.__spies.connectionDestroy).toHaveBeenCalledOnce();
741
- expect(driver.__spies.connectionRelease).not.toHaveBeenCalled();
742
- });
743
-
744
- it('destroys connection when commit fails and best-effort rollback also fails', async () => {
745
- const { runtime, driver } = createRuntimeForTransaction();
746
- const commitError = new Error('commit failed');
747
- const rollbackError = new Error('rollback also failed');
748
- driver.__spies.transactionCommit.mockRejectedValueOnce(commitError);
749
- driver.__spies.transactionRollback.mockRejectedValueOnce(rollbackError);
750
-
751
- const rejection = withTransaction(runtime, async () => 'value');
752
-
753
- await expect(rejection).rejects.toMatchObject({
754
- code: 'RUNTIME.TRANSACTION_COMMIT_FAILED',
755
- cause: commitError,
756
- });
757
- expect(driver.__spies.connectionDestroy).toHaveBeenCalledOnce();
758
- expect(driver.__spies.connectionRelease).not.toHaveBeenCalled();
759
- });
760
-
761
- it('sets invalidated flag after rollback', async () => {
762
- const { runtime } = createRuntimeForTransaction();
763
- let txRef: { invalidated: boolean } | undefined;
764
-
765
- await withTransaction(runtime, async (tx) => {
766
- txRef = tx;
767
- throw new Error('fail');
768
- }).catch(() => {});
769
-
770
- expect(txRef!.invalidated).toBe(true);
771
- });
772
-
773
- it('releases connection independently across sequential transactions', async () => {
774
- const { runtime, driver } = createRuntimeForTransaction();
775
-
776
- await withTransaction(runtime, async (tx) => {
777
- await tx.execute(createRawExecutionPlan()).toArray();
778
- });
779
-
780
- await withTransaction(runtime, async (tx) => {
781
- await tx.execute(createRawExecutionPlan()).toArray();
782
- });
783
-
784
- await withTransaction(runtime, async () => {
785
- throw new Error('fail');
786
- }).catch(() => {});
787
-
788
- expect(driver.__spies.connectionRelease).toHaveBeenCalledTimes(3);
789
- expect(driver.__spies.transactionCommit).toHaveBeenCalledTimes(2);
790
- expect(driver.__spies.transactionRollback).toHaveBeenCalledTimes(1);
791
- });
792
- });