okengine 0.23.0 → 0.23.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.
Files changed (128) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/ai/mcp.mdx +4 -4
  3. package/site/content/docs/elements/channel/index.mdx +4 -4
  4. package/site/content/docs/elements/channel/receipts.mdx +7 -5
  5. package/site/content/docs/elements/channel/sms.mdx +2 -1
  6. package/site/content/docs/elements/flow/index.mdx +22 -18
  7. package/site/content/docs/elements/signal/broadcast.mdx +4 -2
  8. package/site/content/docs/elements/signal/index.mdx +15 -14
  9. package/site/content/docs/plugins/otp.mdx +3 -3
  10. package/site/content/docs/reference/configuration.mdx +16 -0
  11. package/site/content/docs/reference/errors.mdx +31 -19
  12. package/site/content/docs/reference/fx.mdx +12 -4
  13. package/src/auth/tenants.ts +17 -0
  14. package/src/cli/doctor-diff.test.ts +29 -1
  15. package/src/cli/doctor-diff.ts +35 -0
  16. package/src/cli/manifest-pr-diff.ts +16 -0
  17. package/src/compiler/aot.test.ts +3 -2
  18. package/src/compiler/aot.ts +45 -1
  19. package/src/compiler/dynamic.ts +1 -1
  20. package/src/compiler/effects-fetch.test.ts +77 -0
  21. package/src/compiler/effects-infer.ts +87 -16
  22. package/src/compiler/effects-join.test.ts +48 -0
  23. package/src/compiler/extract.test.ts +28 -0
  24. package/src/compiler/extract.ts +11 -1
  25. package/src/compiler/fx-index.ts +58 -4
  26. package/src/compiler/http-parse.test.ts +101 -0
  27. package/src/compiler/http-parse.ts +188 -9
  28. package/src/compiler/interpret.ts +25 -2
  29. package/src/console/index.ts +1 -1
  30. package/src/console/ui-next/dist/assets/{access-page-DDKkhT9v.js → access-page-zBMhZnFm.js} +1 -1
  31. package/src/console/ui-next/dist/assets/{agent-disclosure-62Xts8Xi.js → agent-disclosure-CBj9f7Ju.js} +1 -1
  32. package/src/console/ui-next/dist/assets/{cache-glyph-DyeoKHKA.js → cache-glyph-D3MY49h3.js} +1 -1
  33. package/src/console/ui-next/dist/assets/{call-pii-button-DvfpLXsU.js → call-pii-button-L9JxdBhp.js} +1 -1
  34. package/src/console/ui-next/dist/assets/{collapsible-BY03SeCg.js → collapsible-BvFaX-Zt.js} +1 -1
  35. package/src/console/ui-next/dist/assets/{decisions-page-CgkKlA56.js → decisions-page-DgB76m-X.js} +1 -1
  36. package/src/console/ui-next/dist/assets/{duration-tone-Y6HLjyJ5.js → duration-tone-l1DSJ2Vz.js} +1 -1
  37. package/src/console/ui-next/dist/assets/{flows-page-DLPfNy-_.js → flows-page-Bb9t1lu6.js} +1 -1
  38. package/src/console/ui-next/dist/assets/{highlighted-json-D7nNzpgB.js → highlighted-json-DMvsi3Ti.js} +1 -1
  39. package/src/console/ui-next/dist/assets/{http-method-DdL19zzh.js → http-method-BwhJBTcQ.js} +1 -1
  40. package/src/console/ui-next/dist/assets/{index-C0lc_s9d.js → index-RhV2jT_7.js} +3 -3
  41. package/src/console/ui-next/dist/assets/{observability-page-CRlcyLCC.js → observability-page-BRfUILBq.js} +1 -1
  42. package/src/console/ui-next/dist/assets/{replica-lag-Bb3FWUu9.js → replica-lag-BsceAzIG.js} +1 -1
  43. package/src/console/ui-next/dist/assets/{request-meta-DYBMWhX8.js → request-meta-C6lTyCXp.js} +1 -1
  44. package/src/console/ui-next/dist/assets/{store-page-CcE-SXC8.js → store-page-YmE806bJ.js} +1 -1
  45. package/src/console/ui-next/dist/assets/{trace-detail-sheet-CKMSEQ4s.js → trace-detail-sheet-Di4irS49.js} +1 -1
  46. package/src/console/ui-next/dist/assets/{tree-expand-toggle-QOfk0M0H.js → tree-expand-toggle-DYeL6Rye.js} +1 -1
  47. package/src/console/ui-next/dist/assets/{units-page-DKC1CDDd.js → units-page-07agasRA.js} +1 -1
  48. package/src/console/ui-next/dist/assets/{vault-page-B54v4Yit.js → vault-page-CHBck4n_.js} +1 -1
  49. package/src/console/ui-next/dist/index.html +1 -1
  50. package/src/console/ui-next/seed-parked-approval.ts +5 -1
  51. package/src/drivers/journal-postgres.test.ts +106 -2
  52. package/src/drivers/journal-postgres.ts +475 -14
  53. package/src/drivers/postgres.test.ts +60 -0
  54. package/src/drivers/postgres.ts +79 -5
  55. package/src/drivers/signal-compete.test.ts +238 -0
  56. package/src/drivers/signal-postgres.test.ts +170 -0
  57. package/src/drivers/signal-postgres.ts +486 -213
  58. package/src/drivers/signal-redis.test.ts +224 -0
  59. package/src/drivers/signal-redis.ts +934 -93
  60. package/src/drivers/signal-types.ts +12 -0
  61. package/src/elements/ai/approval.test.ts +15 -3
  62. package/src/elements/ai/approval.ts +15 -9
  63. package/src/elements/ai/mcp-protocol.ts +5 -6
  64. package/src/elements/ai/runtime.ts +20 -19
  65. package/src/elements/channel/runtime.ts +42 -4
  66. package/src/elements/channel/sql-ledger.test.ts +404 -0
  67. package/src/elements/channel/sql-ledger.ts +395 -0
  68. package/src/elements/clock/chaos-child.ts +12 -2
  69. package/src/elements/clock/durable.ts +11 -0
  70. package/src/elements/signal/runtime.ts +9 -0
  71. package/src/elements/signal.test.ts +1 -0
  72. package/src/elements/store/cache-bus.test.ts +121 -0
  73. package/src/elements/store/cache-bus.ts +278 -0
  74. package/src/elements/store/cache.test.ts +179 -15
  75. package/src/elements/store/cache.ts +357 -51
  76. package/src/elements/store/prepare-row.test.ts +6 -2
  77. package/src/elements/store/runtime.ts +44 -2
  78. package/src/elements/store/sql-errors.test.ts +131 -0
  79. package/src/elements/store/sql-errors.ts +39 -16
  80. package/src/elements/store/sql-nested-tx.test.ts +344 -0
  81. package/src/elements/store/sql-session.test.ts +2 -0
  82. package/src/elements/store/sql-session.ts +275 -9
  83. package/src/elements/store.ts +1 -0
  84. package/src/kernel/app.ts +207 -43
  85. package/src/kernel/auto-cache.test.ts +228 -0
  86. package/src/kernel/auto-registry.test.ts +5 -1
  87. package/src/kernel/boot-bind/channel.ts +63 -2
  88. package/src/kernel/boot-bind/honor-config.test.ts +40 -10
  89. package/src/kernel/boot-bind/signal.ts +72 -25
  90. package/src/kernel/boot-bind/store.ts +16 -1
  91. package/src/kernel/boot.test.ts +2 -2
  92. package/src/kernel/boot.ts +54 -10
  93. package/src/kernel/boundary-contract.ts +7 -0
  94. package/src/kernel/builtin-errors.ts +2 -0
  95. package/src/kernel/call.test.ts +1 -0
  96. package/src/kernel/errors-compiler.ts +11 -0
  97. package/src/kernel/errors-text.ts +16 -0
  98. package/src/kernel/errors.ts +8 -0
  99. package/src/kernel/flow.ts +33 -3
  100. package/src/kernel/fx-call-types.test.ts +32 -0
  101. package/src/kernel/fx-decide.ts +11 -7
  102. package/src/kernel/fx-sql-handle.ts +52 -7
  103. package/src/kernel/fx.test.ts +1 -3
  104. package/src/kernel/fx.ts +27 -16
  105. package/src/kernel/horizontal-child.ts +54 -1
  106. package/src/kernel/horizontal.integration.test.ts +131 -7
  107. package/src/kernel/idempotency.test.ts +4 -4
  108. package/src/kernel/journal-boot.test.ts +83 -6
  109. package/src/kernel/journal.test.ts +158 -0
  110. package/src/kernel/journal.ts +515 -54
  111. package/src/kernel/pipeline-tenant.ts +5 -1
  112. package/src/kernel/pipeline.ts +5 -0
  113. package/src/kernel/signal-tx.ts +28 -0
  114. package/src/kernel/store-transaction.test.ts +95 -0
  115. package/src/mcp/authorization.ts +5 -11
  116. package/src/mcp/confirmation.ts +203 -38
  117. package/src/mcp/docs-server.ts +26 -4
  118. package/src/mcp/index.ts +4 -0
  119. package/src/mcp/mcp.test.ts +404 -33
  120. package/src/mcp/protocol.ts +8 -2
  121. package/src/mcp/server.ts +31 -5
  122. package/src/mcp/session.ts +7 -0
  123. package/src/mcp/tools.ts +68 -19
  124. package/src/mcp/versions.ts +60 -0
  125. package/src/plugins/index.ts +1 -0
  126. package/src/plugins/mena.ts +56 -0
  127. package/src/runtime/bun.ts +5 -0
  128. package/src/runtime/types.ts +5 -0
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { DomainDdlMode } from "../../config/index.ts";
9
+ import type { SignalEmitOptions, SignalTransaction } from "../../drivers/signal-types.ts";
9
10
  import type { ClassificationMap, SqlConnection, SqlRow } from "../../drivers/types.ts";
10
11
  import {
11
12
  buildRlsIdentityPreludeSql,
@@ -16,6 +17,7 @@ import {
16
17
  import { throwOke } from "../../kernel/errors.ts";
17
18
  import { isFlowFailure } from "../../kernel/hooks.ts";
18
19
  import { lazyRequire } from "../../kernel/lazy-require.ts";
20
+ import { currentSignalTransaction, runInSignalTransaction } from "../../kernel/signal-tx.ts";
19
21
  import { isRetryableSqlError, sqlErrorToFailure } from "./sql-errors.ts";
20
22
  import { AsyncLocalStorage } from "node:async_hooks";
21
23
  import { maskRows, tableFromSql } from "./classify.ts";
@@ -51,6 +53,24 @@ export type { WhereMap } from "./sql-condition.ts";
51
53
  */
52
54
  export type InferSelectRow<T> = T extends { readonly $inferSelect: infer R } ? R : SqlRow;
53
55
 
56
+ /**
57
+ * Insert row from a Drizzle table's `$inferInsert`, or {@link SqlRow}.
58
+ *
59
+ * @typeParam T - Table handle or Drizzle table
60
+ */
61
+ export type InferInsertRow<T> = T extends { readonly $inferInsert: infer R } ? R : SqlRow;
62
+
63
+ /**
64
+ * Equality map or a Drizzle SQL fragment (`queryChunks`).
65
+ * A bare `unknown` would swallow the row type.
66
+ *
67
+ * @typeParam TRow - Selected or inserted row
68
+ */
69
+ export type SqlPredicate<TRow> =
70
+ | { readonly [K in keyof TRow]?: TRow[K] }
71
+ | { readonly queryChunks: readonly unknown[] }
72
+ | undefined;
73
+
54
74
  /** Serialize stamp frames on a shared connection so concurrent identities cannot interleave. */
55
75
  const rlsStampTails = new WeakMap<SqlConnection, Promise<unknown>>();
56
76
 
@@ -119,6 +139,29 @@ function notifySqlCdc(event: {
119
139
  */
120
140
  const cdcMutationStorage = new AsyncLocalStorage<{ readonly mutationId: string }>();
121
141
 
142
+ /**
143
+ * One frame of {@link SqlStoreHandle.transaction}.
144
+ * Depth 0 is the driver `BEGIN`. Deeper frames are savepoints on that pin.
145
+ */
146
+ interface SqlTxFrame {
147
+ readonly connection: SqlConnection;
148
+ /** 0 for the driver transaction; 1+ for `SAVEPOINT`. */
149
+ readonly depth: number;
150
+ /**
151
+ * Promise chain for direct child transactions on this pin.
152
+ * Siblings append here so their statements cannot interleave. A child
153
+ * waits on this chain, not on a connection-wide tail, so a nested call
154
+ * does not deadlock behind its own parent.
155
+ */
156
+ childTail: Promise<void>;
157
+ }
158
+
159
+ /** Pinned connection for {@link SqlStoreHandle.transaction}. */
160
+ const sqlTxStorage = new AsyncLocalStorage<SqlTxFrame>();
161
+
162
+ /** Monotonic savepoint counter, keyed by the pinned connection. */
163
+ const savepointSeq = new WeakMap<SqlConnection, number>();
164
+
122
165
  /**
123
166
  * Read the ambient mutation id, or `undefined` outside a stamped request.
124
167
  */
@@ -193,6 +236,110 @@ async function withRlsStampLock<T>(connection: SqlConnection, fn: () => Promise<
193
236
  }
194
237
  }
195
238
 
239
+ /**
240
+ * Next `oke_sp_<depth>_<n>` name. `n` is per pinned connection so siblings
241
+ * and nested savepoints never reuse a name, even after `ROLLBACK TO`.
242
+ *
243
+ * @param connection - Pinned connection
244
+ * @param depth - Savepoint depth (1 for the first nested call)
245
+ */
246
+ function nextSavepointId(connection: SqlConnection, depth: number): string {
247
+ const n = (savepointSeq.get(connection) ?? 0) + 1;
248
+ savepointSeq.set(connection, n);
249
+ return `oke_sp_${String(depth)}_${String(n)}`;
250
+ }
251
+
252
+ /**
253
+ * Run `fn` after earlier direct children of `frame` finish.
254
+ *
255
+ * @param frame - Parent transaction frame
256
+ * @param fn - Child body, including savepoint control
257
+ */
258
+ async function withSqlChildLock<T>(frame: SqlTxFrame, fn: () => Promise<T>): Promise<T> {
259
+ const prev = frame.childTail;
260
+ let release!: () => void;
261
+ const gate = new Promise<void>((resolve) => {
262
+ release = resolve;
263
+ });
264
+ frame.childTail = prev.then(
265
+ () => gate,
266
+ () => gate,
267
+ );
268
+ await prev.catch(() => undefined);
269
+ try {
270
+ return await fn();
271
+ } finally {
272
+ release();
273
+ }
274
+ }
275
+
276
+ /** One staged signal op inside a nested transaction. */
277
+ type StagedSignalOp =
278
+ | { readonly kind: "write"; readonly key: string; readonly value: unknown }
279
+ | {
280
+ readonly kind: "emit";
281
+ readonly signal: string;
282
+ readonly payload: unknown;
283
+ readonly options?: SignalEmitOptions;
284
+ };
285
+
286
+ /**
287
+ * Child staging scope. Emits buffer here and merge into `parent` on success.
288
+ * Discard drops them. Neither path publishes — the outermost commit does.
289
+ *
290
+ * @param parent - Ambient signal transaction
291
+ */
292
+ function openChildSignalScope(parent: SignalTransaction): {
293
+ readonly transaction: SignalTransaction;
294
+ readonly merge: () => Promise<void>;
295
+ readonly discard: () => void;
296
+ } {
297
+ const staged: StagedSignalOp[] = [];
298
+ let open = true;
299
+
300
+ function assertOpen(): void {
301
+ if (!open) throw new Error("transaction finished");
302
+ }
303
+
304
+ const transaction: SignalTransaction = {
305
+ async write(key, value) {
306
+ assertOpen();
307
+ staged.push({ kind: "write", key, value });
308
+ },
309
+ async emit(signal, payload, options) {
310
+ assertOpen();
311
+ staged.push(
312
+ options === undefined
313
+ ? { kind: "emit", signal, payload }
314
+ : { kind: "emit", signal, payload, options },
315
+ );
316
+ },
317
+ async commit() {
318
+ throw new Error("nested signal scope publishes on the outermost commit");
319
+ },
320
+ async rollback() {
321
+ open = false;
322
+ staged.length = 0;
323
+ },
324
+ };
325
+
326
+ return {
327
+ transaction,
328
+ async merge() {
329
+ if (!open) return;
330
+ open = false;
331
+ for (const op of staged) {
332
+ if (op.kind === "write") await parent.write(op.key, op.value);
333
+ else await parent.emit(op.signal, op.payload, op.options);
334
+ }
335
+ },
336
+ discard() {
337
+ open = false;
338
+ staged.length = 0;
339
+ },
340
+ };
341
+ }
342
+
196
343
  /** Options for a SQL session. */
197
344
  export interface SqlSessionOptions {
198
345
  /** Open connection (already role-routed). */
@@ -274,7 +421,7 @@ export interface SelectFromBuilder<TRow = SqlRow> extends PromiseLike<TRow[]> {
274
421
  *
275
422
  * @param where - Condition
276
423
  */
277
- where(where: unknown): SelectWhereBuilder<TRow>;
424
+ where(where: SqlPredicate<TRow>): SelectWhereBuilder<TRow>;
278
425
  /**
279
426
  * Order rows with Drizzle `asc()` / `desc()` terms.
280
427
  *
@@ -312,14 +459,18 @@ export interface SelectBuilder<TLocked extends SqlRow | undefined = undefined> {
312
459
  ): SelectFromBuilder<TLocked extends undefined ? InferSelectRow<TTable> : TLocked>;
313
460
  }
314
461
 
315
- /** Fluent insert builder. */
316
- export interface InsertBuilder {
462
+ /**
463
+ * Fluent insert builder.
464
+ *
465
+ * @typeParam TRow - `$inferInsert` when the table is a Drizzle table
466
+ */
467
+ export interface InsertBuilder<TRow = SqlRow> {
317
468
  /**
318
469
  * Provide row values.
319
470
  *
320
471
  * @param row - Row to insert
321
472
  */
322
- values(row: SqlRow): InsertValuesBuilder;
473
+ values(row: TRow): InsertValuesBuilder;
323
474
  }
324
475
 
325
476
  /**
@@ -340,7 +491,7 @@ export interface DeleteBuilder {
340
491
  *
341
492
  * @param where - Equality map or Drizzle SQL
342
493
  */
343
- where(where: unknown): Promise<number>;
494
+ where(where: SqlPredicate<SqlRow>): Promise<number>;
344
495
  }
345
496
 
346
497
  /** Fluent update builder. */
@@ -360,7 +511,7 @@ export interface UpdateSetBuilder {
360
511
  *
361
512
  * @param where - Equality map or Drizzle SQL
362
513
  */
363
- where(where: unknown): Promise<number>;
514
+ where(where: SqlPredicate<SqlRow>): Promise<number>;
364
515
  }
365
516
 
366
517
  /**
@@ -393,7 +544,7 @@ export interface SqlStoreHandle {
393
544
  *
394
545
  * @param table - Target table
395
546
  */
396
- insert(table: TableHandle | unknown): InsertBuilder;
547
+ insert<TTable>(table: TTable): InsertBuilder<InferInsertRow<TTable>>;
397
548
  /**
398
549
  * Start an update on `table`.
399
550
  *
@@ -415,6 +566,25 @@ export interface SqlStoreHandle {
415
566
  */
416
567
  delete(table: TableHandle | unknown): DeleteBuilder;
417
568
  delete(table: TableHandle | unknown, id: string): Promise<boolean>;
569
+ /**
570
+ * Run `fn` on one pinned connection. `fx.emit` inside `fn` stages on the
571
+ * signal outbox and publishes only after this SQL transaction commits.
572
+ * A call that already has a pinned connection opens `SAVEPOINT` on that
573
+ * connection. Nested emits stage on a child scope and merge into the
574
+ * parent on success; only the outermost commit publishes.
575
+ *
576
+ * @param fn - Transaction body. `tx` is this handle, bound to the pin.
577
+ */
578
+ transaction<T>(fn: (tx: SqlStoreHandle) => Promise<T>): Promise<T>;
579
+ /**
580
+ * Execute a Drizzle select, insert, update, or delete builder.
581
+ * Calls `toSQL()` so `drizzle-orm` stays off the edge graph.
582
+ *
583
+ * @param query - Builder with `toSQL()`
584
+ */
585
+ run<T extends SqlRow = SqlRow>(query: {
586
+ toSQL(): { readonly sql: string; readonly params: readonly unknown[] };
587
+ }): Promise<T[]>;
418
588
  /**
419
589
  * True when at least one row matches.
420
590
  *
@@ -564,6 +734,10 @@ export function createSqlStoreHandle(
564
734
  }
565
735
  }
566
736
 
737
+ function activeConnection(): SqlConnection {
738
+ return sqlTxStorage.getStore()?.connection ?? connection;
739
+ }
740
+
567
741
  function query(sql: string, params: readonly unknown[] = []): Promise<SqlRow[]> {
568
742
  return withSchemaGuard(() => withRlsStamp((conn) => conn.query(sql, params), sql));
569
743
  }
@@ -572,9 +746,29 @@ export function createSqlStoreHandle(
572
746
  return withSchemaGuard(() => withRlsStamp((conn) => conn.exec(sql, params), sql));
573
747
  }
574
748
 
749
+ /**
750
+ * Rewrite Drizzle `$1` placeholders to the connection's `?` form.
751
+ *
752
+ * @param sql - Dialect SQL
753
+ * @param params - Bindings in `$n` order (1-based)
754
+ */
755
+ function fromDialect(
756
+ sql: string,
757
+ params: readonly unknown[],
758
+ ): { sql: string; params: unknown[] } {
759
+ if (!sql.includes("$")) return { sql, params: [...params] };
760
+ const ordered: unknown[] = [];
761
+ const text = sql.replace(/\$(\d+)/g, (_match, index: string) => {
762
+ ordered.push(params[Number(index) - 1]);
763
+ return "?";
764
+ });
765
+ return { sql: text, params: ordered };
766
+ }
767
+
575
768
  async function withRlsStamp<T>(fn: (conn: SqlConnection) => Promise<T>, sql: string): Promise<T> {
576
- if (!rls || !RLS_CONTEXT_DRIVERS.has(connection.driverId)) return fn(connection);
577
- if (isRlsStampExemptSql(sql)) return fn(connection);
769
+ if (!rls || !RLS_CONTEXT_DRIVERS.has(connection.driverId)) return fn(activeConnection());
770
+ if (isRlsStampExemptSql(sql)) return fn(activeConnection());
771
+ if (sqlTxStorage.getStore()) return fn(activeConnection());
578
772
  const run = (): Promise<T> => applyRlsStamp(fn);
579
773
  // PGlite is one backend session — concurrent identities must not interleave.
580
774
  // Pooled postgres pins each stamp via `transaction()` instead.
@@ -910,6 +1104,53 @@ export function createSqlStoreHandle(
910
1104
  };
911
1105
  }
912
1106
 
1107
+ /**
1108
+ * Nested `transaction()` on the pinned connection: `SAVEPOINT`, then
1109
+ * `RELEASE` or `ROLLBACK TO`. Sibling calls share `parent.childTail`.
1110
+ *
1111
+ * @param parent - Frame already on the async store
1112
+ * @param fn - Nested body
1113
+ */
1114
+ async function runNestedTransaction<T>(
1115
+ parent: SqlTxFrame,
1116
+ fn: (tx: SqlStoreHandle) => Promise<T>,
1117
+ ): Promise<T> {
1118
+ return withSqlChildLock(parent, async () => {
1119
+ const depth = parent.depth + 1;
1120
+ const name = quoteIdent(nextSavepointId(parent.connection, depth));
1121
+ const conn = parent.connection;
1122
+ await conn.exec(`SAVEPOINT ${name}`);
1123
+ const child: SqlTxFrame = {
1124
+ connection: conn,
1125
+ depth,
1126
+ childTail: Promise.resolve(),
1127
+ };
1128
+ const parentSignal = currentSignalTransaction();
1129
+ const staging = parentSignal ? openChildSignalScope(parentSignal) : undefined;
1130
+ let released = false;
1131
+ try {
1132
+ const result = await sqlTxStorage.run(child, () => {
1133
+ const body = () => fn(handle as SqlStoreHandle);
1134
+ return staging ? runInSignalTransaction(staging.transaction, body) : body();
1135
+ });
1136
+ await conn.exec(`RELEASE SAVEPOINT ${name}`);
1137
+ released = true;
1138
+ if (staging) await staging.merge();
1139
+ return result;
1140
+ } catch (err) {
1141
+ if (!released) {
1142
+ try {
1143
+ await conn.exec(`ROLLBACK TO SAVEPOINT ${name}`);
1144
+ } catch {
1145
+ // The pin may already be aborted; the original error still wins.
1146
+ }
1147
+ }
1148
+ staging?.discard();
1149
+ throw err;
1150
+ }
1151
+ });
1152
+ }
1153
+
913
1154
  const handle = {
914
1155
  ref,
915
1156
  routedRole: options.routedRole,
@@ -1332,6 +1573,31 @@ export function createSqlStoreHandle(
1332
1573
  });
1333
1574
  },
1334
1575
 
1576
+ async transaction<T>(fn: (tx: SqlStoreHandle) => Promise<T>): Promise<T> {
1577
+ const current = sqlTxStorage.getStore();
1578
+ if (current) return runNestedTransaction(current, fn);
1579
+ if (!connection.transaction) {
1580
+ throw new Error("fx.store().transaction needs SqlConnection.transaction");
1581
+ }
1582
+ return connection.transaction(async (txConn) =>
1583
+ sqlTxStorage.run({ connection: txConn, depth: 0, childTail: Promise.resolve() }, () =>
1584
+ fn(handle as SqlStoreHandle),
1585
+ ),
1586
+ );
1587
+ },
1588
+
1589
+ async run<T extends SqlRow = SqlRow>(builder: {
1590
+ toSQL(): { readonly sql: string; readonly params: readonly unknown[] };
1591
+ }): Promise<T[]> {
1592
+ const compiled = builder.toSQL();
1593
+ const dialect = fromDialect(compiled.sql, compiled.params);
1594
+ if (isSqlDml(dialect.sql)) {
1595
+ await exec(dialect.sql, dialect.params);
1596
+ return [];
1597
+ }
1598
+ return query(dialect.sql, dialect.params) as Promise<T[]>;
1599
+ },
1600
+
1335
1601
  async ensureTable(table: TableHandle) {
1336
1602
  const cols = Object.values(table.columns);
1337
1603
  const pk = resolvePkColumn(table);
@@ -137,6 +137,7 @@ export type {
137
137
  SelectWhereBuilder,
138
138
  SelectOrderBuilder,
139
139
  InferSelectRow,
140
+ SqlPredicate,
140
141
  InsertBuilder,
141
142
  InsertValuesBuilder,
142
143
  SqlSessionOptions,