Skip to content

Commit 0161372

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 0161372

15 files changed

Lines changed: 1305 additions & 11 deletions

File tree

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ 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;
118125
}
119126

120127
export interface RuntimeQueryable extends RuntimeScope {
@@ -133,6 +140,13 @@ export interface RuntimeQueryable extends RuntimeScope {
133140

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

138152
export type { RuntimeTelemetryEvent, TelemetryOutcome, VerifyMarkerOption };
@@ -651,8 +665,15 @@ export abstract class SqlRuntimeBase<TContract extends Contract<SqlStorage> = Co
651665

652666
private wrapTransaction(driverTx: SqlTransaction): RuntimeTransaction {
653667
const self = this;
668+
const preCommitHooks: Array<() => Promise<void>> = [];
654669
return {
670+
registerPreCommitHook(hook: () => Promise<void>): void {
671+
preCommitHooks.push(hook);
672+
},
655673
async commit(): Promise<void> {
674+
for (const hook of preCommitHooks) {
675+
await hook();
676+
}
656677
await driverTx.commit();
657678
},
658679
async rollback(): Promise<void> {
@@ -808,6 +829,12 @@ export async function withTransaction<R>(
808829
guardedStream(transaction.executePrepared(ps, params, options)),
809830
);
810831
},
832+
registerPreCommitHook(hook: () => Promise<void>): void {
833+
if (invalidated) {
834+
throw transactionClosedError();
835+
}
836+
transaction.registerPreCommitHook(hook);
837+
},
811838
};
812839

813840
let connectionDisposed = false;

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)