Skip to content

Commit 1ff479e

Browse files
committed
feat(postgres,sqlite): add typed temp table creation in transactions
Signed-off-by: paulwer <paul@wer-ner.de>
1 parent 10d8b60 commit 1ff479e

15 files changed

Lines changed: 2029 additions & 13 deletions

File tree

packages/2-sql/5-runtime/src/sql-runtime.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ export interface RuntimeConnection extends RuntimeQueryable {
115115
export interface RuntimeTransaction extends RuntimeQueryable {
116116
commit(): Promise<void>;
117117
rollback(): Promise<void>;
118+
/**
119+
* Register a hook to run immediately before the transaction is committed.
120+
* Hooks are invoked in registration order and awaited sequentially. If any
121+
* hook throws, the commit is aborted and the error propagates to the caller.
122+
* Not invoked on rollback.
123+
*/
124+
registerPreCommitHook(hook: () => Promise<void>): void;
125+
runPreCommitHooks(): Promise<void>;
118126
}
119127

120128
export interface RuntimeQueryable extends RuntimeScope {
@@ -133,6 +141,13 @@ export interface RuntimeQueryable extends RuntimeScope {
133141

134142
export interface TransactionContext extends RuntimeQueryable {
135143
readonly invalidated: boolean;
144+
/**
145+
* Register a hook to run immediately before the transaction is committed.
146+
* Hooks are invoked in registration order and awaited sequentially. If any
147+
* hook throws, the commit is aborted and the error propagates to the caller.
148+
* Not invoked on rollback.
149+
*/
150+
registerPreCommitHook(hook: () => Promise<void>): void;
136151
}
137152

138153
export type { RuntimeTelemetryEvent, TelemetryOutcome, VerifyMarkerOption };
@@ -651,7 +666,17 @@ export abstract class SqlRuntimeBase<TContract extends Contract<SqlStorage> = Co
651666

652667
private wrapTransaction(driverTx: SqlTransaction): RuntimeTransaction {
653668
const self = this;
669+
const preCommitHooks: Array<() => Promise<void>> = [];
654670
return {
671+
registerPreCommitHook(hook: () => Promise<void>): void {
672+
preCommitHooks.push(hook);
673+
},
674+
async runPreCommitHooks(): Promise<void> {
675+
while (preCommitHooks.length > 0) {
676+
const hook = preCommitHooks.shift()!;
677+
await hook();
678+
}
679+
},
655680
async commit(): Promise<void> {
656681
await driverTx.commit();
657682
},
@@ -808,6 +833,12 @@ export async function withTransaction<R>(
808833
guardedStream(transaction.executePrepared(ps, params, options)),
809834
);
810835
},
836+
registerPreCommitHook(hook: () => Promise<void>): void {
837+
if (invalidated) {
838+
throw transactionClosedError();
839+
}
840+
transaction.registerPreCommitHook(hook);
841+
},
811842
};
812843

813844
let connectionDisposed = false;
@@ -822,6 +853,7 @@ export async function withTransaction<R>(
822853
let result: R;
823854
try {
824855
result = await fn(txContext);
856+
await transaction.runPreCommitHooks();
825857
} catch (error) {
826858
try {
827859
await transaction.rollback();

packages/2-sql/5-runtime/test/sql-runtime.test.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import type {
2828
SqlRuntimeTargetDescriptor,
2929
} from '../src/sql-context';
3030
import { createExecutionContext, createSqlExecutionStack } from '../src/sql-context';
31-
import { withTransaction } from '../src/sql-runtime';
31+
import { type TempTableQuerySource, withTransaction } from '../src/sql-runtime';
3232
import { createAsyncSecretCodec, decryptSecret } from './seeded-secret-codec';
3333
import { defineTestCodec } from './test-codec';
3434
import { createTestRuntime as createRuntime, descriptorsFromCodecs, stubAst } from './utils';
@@ -939,3 +939,110 @@ describe('withTransaction', () => {
939939
expect(driver.__spies.transactionRollback).toHaveBeenCalledTimes(1);
940940
});
941941
});
942+
943+
describe('RuntimeTransaction.registerPreCommitHook', () => {
944+
function createRuntimeForHooks() {
945+
const { stackInstance, context, driver } = createTestSetup();
946+
const runtime = createRuntime({ stackInstance, context, driver, verifyMarker: false });
947+
return { runtime, driver };
948+
}
949+
950+
it('runs the hook before driverTx.commit()', async () => {
951+
const { runtime, driver } = createRuntimeForHooks();
952+
const order: string[] = [];
953+
driver.__spies.transactionCommit.mockImplementation(async () => {
954+
order.push('commit');
955+
});
956+
957+
const conn = await runtime.connection();
958+
const tx = await conn.transaction();
959+
tx.registerPreCommitHook(async () => {
960+
order.push('hook');
961+
});
962+
await tx.commit();
963+
await conn.release();
964+
965+
expect(order).toEqual(['hook', 'commit']);
966+
});
967+
968+
it('runs multiple hooks in registration order before commit', async () => {
969+
const { runtime, driver } = createRuntimeForHooks();
970+
const order: string[] = [];
971+
driver.__spies.transactionCommit.mockImplementation(async () => {
972+
order.push('commit');
973+
});
974+
975+
const conn = await runtime.connection();
976+
const tx = await conn.transaction();
977+
tx.registerPreCommitHook(async () => {
978+
order.push('hook-1');
979+
});
980+
tx.registerPreCommitHook(async () => {
981+
order.push('hook-2');
982+
});
983+
tx.registerPreCommitHook(async () => {
984+
order.push('hook-3');
985+
});
986+
await tx.commit();
987+
await conn.release();
988+
989+
expect(order).toEqual(['hook-1', 'hook-2', 'hook-3', 'commit']);
990+
});
991+
992+
it('aborts commit and propagates the error when a hook throws', async () => {
993+
const { runtime, driver } = createRuntimeForHooks();
994+
const hookError = new Error('hook failed');
995+
996+
const conn = await runtime.connection();
997+
const tx = await conn.transaction();
998+
tx.registerPreCommitHook(async () => {
999+
throw hookError;
1000+
});
1001+
1002+
await expect(tx.commit()).rejects.toBe(hookError);
1003+
expect(driver.__spies.transactionCommit).not.toHaveBeenCalled();
1004+
await conn.release();
1005+
});
1006+
1007+
it('does not invoke subsequent hooks after an earlier hook throws', async () => {
1008+
const { runtime, driver } = createRuntimeForHooks();
1009+
const hook2 = vi.fn();
1010+
1011+
const conn = await runtime.connection();
1012+
const tx = await conn.transaction();
1013+
tx.registerPreCommitHook(async () => {
1014+
throw new Error('hook-1 failed');
1015+
});
1016+
tx.registerPreCommitHook(hook2);
1017+
1018+
await tx.commit().catch(() => {});
1019+
1020+
expect(hook2).not.toHaveBeenCalled();
1021+
expect(driver.__spies.transactionCommit).not.toHaveBeenCalled();
1022+
await conn.release();
1023+
});
1024+
1025+
it('does not invoke hooks on rollback', async () => {
1026+
const { runtime } = createRuntimeForHooks();
1027+
const hook = vi.fn();
1028+
1029+
const conn = await runtime.connection();
1030+
const tx = await conn.transaction();
1031+
tx.registerPreCommitHook(hook);
1032+
await tx.rollback();
1033+
await conn.release();
1034+
1035+
expect(hook).not.toHaveBeenCalled();
1036+
});
1037+
1038+
it('no hooks registered — commit proceeds normally', async () => {
1039+
const { runtime, driver } = createRuntimeForHooks();
1040+
1041+
const conn = await runtime.connection();
1042+
const tx = await conn.transaction();
1043+
await tx.commit();
1044+
await conn.release();
1045+
1046+
expect(driver.__spies.transactionCommit).toHaveBeenCalledOnce();
1047+
});
1048+
});

0 commit comments

Comments
 (0)