@prisma-next/sql-runtime 0.4.0-dev.9 → 0.4.2
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.
- package/dist/{exports-BO6Fl7yn.mjs → exports-BQZSVXXt.mjs} +102 -10
- package/dist/exports-BQZSVXXt.mjs.map +1 -0
- package/dist/{index-n6z6trta.d.mts → index-yb51L_1h.d.mts} +70 -18
- package/dist/index-yb51L_1h.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/test/utils.d.mts +1 -1
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +6 -9
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +11 -11
- package/src/exports/index.ts +5 -1
- package/src/lower-sql-plan.ts +2 -4
- package/src/middleware/before-compile-chain.ts +28 -0
- package/src/middleware/budgets.ts +16 -27
- package/src/middleware/lints.ts +3 -3
- package/src/middleware/sql-middleware.ts +31 -2
- package/src/sql-runtime.ts +163 -9
- package/test/before-compile-chain.test.ts +223 -0
- package/test/budgets.test.ts +6 -6
- package/test/execution-stack.test.ts +1 -4
- package/test/lints.test.ts +5 -4
- package/test/sql-runtime.test.ts +436 -11
- package/test/utils.ts +2 -5
- package/dist/exports-BO6Fl7yn.mjs.map +0 -1
- package/dist/index-n6z6trta.d.mts.map +0 -1
package/test/sql-runtime.test.ts
CHANGED
|
@@ -9,19 +9,28 @@ import {
|
|
|
9
9
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
10
10
|
import type {
|
|
11
11
|
CodecRegistry,
|
|
12
|
-
SelectAst,
|
|
13
12
|
SqlDriver,
|
|
14
13
|
SqlExecuteRequest,
|
|
15
14
|
} from '@prisma-next/sql-relational-core/ast';
|
|
16
|
-
import {
|
|
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';
|
|
17
25
|
import { describe, expect, it, vi } from 'vitest';
|
|
26
|
+
import type { SqlMiddleware } from '../src/middleware/sql-middleware';
|
|
18
27
|
import type {
|
|
19
28
|
SqlRuntimeAdapterDescriptor,
|
|
20
29
|
SqlRuntimeAdapterInstance,
|
|
21
30
|
SqlRuntimeTargetDescriptor,
|
|
22
31
|
} from '../src/sql-context';
|
|
23
32
|
import { createExecutionContext, createSqlExecutionStack } from '../src/sql-context';
|
|
24
|
-
import { createRuntime } from '../src/sql-runtime';
|
|
33
|
+
import { createRuntime, withTransaction } from '../src/sql-runtime';
|
|
25
34
|
|
|
26
35
|
const testContract: Contract<SqlStorage> = {
|
|
27
36
|
targetFamily: 'sql',
|
|
@@ -35,13 +44,18 @@ const testContract: Contract<SqlStorage> = {
|
|
|
35
44
|
meta: {},
|
|
36
45
|
};
|
|
37
46
|
|
|
38
|
-
interface
|
|
47
|
+
interface DriverMockSpies {
|
|
39
48
|
rootExecute: ReturnType<typeof vi.fn>;
|
|
40
49
|
connectionExecute: ReturnType<typeof vi.fn>;
|
|
41
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>;
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
type MockSqlDriver = SqlDriver & { __spies:
|
|
58
|
+
type MockSqlDriver = SqlDriver & { __spies: DriverMockSpies };
|
|
45
59
|
|
|
46
60
|
function createStubCodecs(): CodecRegistry {
|
|
47
61
|
const registry = createCodecRegistry();
|
|
@@ -76,10 +90,7 @@ function createStubAdapter() {
|
|
|
76
90
|
},
|
|
77
91
|
},
|
|
78
92
|
lower(ast: SelectAst) {
|
|
79
|
-
return {
|
|
80
|
-
profileId: 'test-profile',
|
|
81
|
-
body: Object.freeze({ sql: JSON.stringify(ast), params: [] }),
|
|
82
|
-
};
|
|
93
|
+
return Object.freeze({ sql: JSON.stringify(ast), params: [] });
|
|
83
94
|
},
|
|
84
95
|
};
|
|
85
96
|
}
|
|
@@ -112,15 +123,18 @@ function createMockDriver(): MockSqlDriver {
|
|
|
112
123
|
execute: connectionExecute,
|
|
113
124
|
query,
|
|
114
125
|
release: vi.fn().mockResolvedValue(undefined),
|
|
126
|
+
destroy: vi.fn().mockResolvedValue(undefined),
|
|
115
127
|
beginTransaction: vi.fn().mockResolvedValue(transaction),
|
|
116
128
|
};
|
|
117
129
|
|
|
130
|
+
const driverClose = vi.fn().mockResolvedValue(undefined);
|
|
131
|
+
|
|
118
132
|
const driver: SqlDriver = {
|
|
119
133
|
execute: rootExecute,
|
|
120
134
|
query,
|
|
121
135
|
connect: vi.fn().mockImplementation(async (_binding?: undefined) => undefined),
|
|
122
136
|
acquireConnection: vi.fn().mockResolvedValue(connection),
|
|
123
|
-
close:
|
|
137
|
+
close: driverClose,
|
|
124
138
|
};
|
|
125
139
|
|
|
126
140
|
return Object.assign(driver, {
|
|
@@ -128,6 +142,11 @@ function createMockDriver(): MockSqlDriver {
|
|
|
128
142
|
rootExecute,
|
|
129
143
|
connectionExecute,
|
|
130
144
|
transactionExecute,
|
|
145
|
+
connectionRelease: connection.release,
|
|
146
|
+
connectionDestroy: connection.destroy,
|
|
147
|
+
transactionCommit: transaction.commit,
|
|
148
|
+
transactionRollback: transaction.rollback,
|
|
149
|
+
driverClose,
|
|
131
150
|
},
|
|
132
151
|
});
|
|
133
152
|
}
|
|
@@ -287,6 +306,24 @@ describe('createRuntime', () => {
|
|
|
287
306
|
await connection.release();
|
|
288
307
|
});
|
|
289
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
|
+
|
|
290
327
|
it('uses transaction queryable for transaction.execute', async () => {
|
|
291
328
|
const { stackInstance, context, driver } = createTestSetup();
|
|
292
329
|
const runtime = createRuntime({
|
|
@@ -352,16 +389,404 @@ describe('createRuntime', () => {
|
|
|
352
389
|
|
|
353
390
|
it('rejects a Mongo middleware with a clear error', () => {
|
|
354
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;
|
|
355
395
|
expect(() =>
|
|
356
396
|
createRuntime({
|
|
357
397
|
stackInstance,
|
|
358
398
|
context,
|
|
359
399
|
driver,
|
|
360
400
|
verify: { mode: 'onFirstUse', requireMarker: false },
|
|
361
|
-
middleware: [
|
|
401
|
+
middleware: [mongoMiddleware],
|
|
362
402
|
}),
|
|
363
403
|
).toThrow(
|
|
364
404
|
"Middleware 'mongo-mw' requires family 'mongo' but the runtime is configured for family 'sql'",
|
|
365
405
|
);
|
|
366
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
|
+
});
|
|
367
792
|
});
|
package/test/utils.ts
CHANGED
|
@@ -132,7 +132,7 @@ export function createTestAdapterDescriptor(
|
|
|
132
132
|
codecs: () => codecRegistry,
|
|
133
133
|
parameterizedCodecs: () => [],
|
|
134
134
|
mutationDefaultGenerators: createTestMutationDefaultGenerators,
|
|
135
|
-
create(): SqlRuntimeAdapterInstance<'postgres'> {
|
|
135
|
+
create(_stack): SqlRuntimeAdapterInstance<'postgres'> {
|
|
136
136
|
return Object.assign({ familyId: 'sql' as const, targetId: 'postgres' as const }, adapter);
|
|
137
137
|
},
|
|
138
138
|
};
|
|
@@ -256,10 +256,7 @@ export function createStubAdapter(): Adapter<SelectAst, Contract<SqlStorage>, Lo
|
|
|
256
256
|
},
|
|
257
257
|
lower(ast: SelectAst, ctx: { contract: Contract<SqlStorage>; params?: readonly unknown[] }) {
|
|
258
258
|
const sqlText = JSON.stringify(ast);
|
|
259
|
-
return {
|
|
260
|
-
profileId: this.profile.id,
|
|
261
|
-
body: Object.freeze({ sql: sqlText, params: ctx.params ? [...ctx.params] : [] }),
|
|
262
|
-
};
|
|
259
|
+
return Object.freeze({ sql: sqlText, params: ctx.params ? [...ctx.params] : [] });
|
|
263
260
|
},
|
|
264
261
|
};
|
|
265
262
|
}
|