rivetkit 0.0.0-stack-chore-sync-engine-changes-from-enterprise-mszropyy.677d526 → 0.0.0-sync-sqlite.527e186

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 (123) hide show
  1. package/dist/browser/client.d.ts +25 -0
  2. package/dist/browser/client.js +17 -5
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.js +1 -1
  5. package/dist/browser/inspector/client.js.map +1 -1
  6. package/dist/tsup/agent-os/index.cjs +145 -3
  7. package/dist/tsup/agent-os/index.cjs.map +1 -1
  8. package/dist/tsup/agent-os/index.d.cts +40 -2
  9. package/dist/tsup/agent-os/index.d.ts +40 -2
  10. package/dist/tsup/agent-os/index.js +144 -2
  11. package/dist/tsup/agent-os/index.js.map +1 -1
  12. package/dist/tsup/{chunk-GPD77M3M.js → chunk-2UJVBM5X.js} +2 -2
  13. package/dist/tsup/chunk-2UJVBM5X.js.map +1 -0
  14. package/dist/tsup/{chunk-OWSXJF7Z.js → chunk-6DC5T27N.js} +20 -8
  15. package/dist/tsup/chunk-6DC5T27N.js.map +1 -0
  16. package/dist/tsup/{chunk-XPT7SGBW.js → chunk-6QHFQDW4.js} +4 -4
  17. package/dist/tsup/{chunk-ZXV3CG4Y.cjs → chunk-7KYMUJOU.cjs} +7 -7
  18. package/dist/tsup/{chunk-ZXV3CG4Y.cjs.map → chunk-7KYMUJOU.cjs.map} +1 -1
  19. package/dist/tsup/{chunk-M4RSGECU.js → chunk-7XKSZBR7.js} +2 -2
  20. package/dist/tsup/{chunk-VXJ26VPL.cjs → chunk-BPBPNCCY.cjs} +10 -10
  21. package/dist/tsup/{chunk-VXJ26VPL.cjs.map → chunk-BPBPNCCY.cjs.map} +1 -1
  22. package/dist/tsup/{chunk-YHYNYVNZ.js → chunk-CV5KMH2U.js} +3 -3
  23. package/dist/tsup/{chunk-FSD52J3H.cjs → chunk-D72VXRS4.cjs} +3 -3
  24. package/dist/tsup/{chunk-FSD52J3H.cjs.map → chunk-D72VXRS4.cjs.map} +1 -1
  25. package/dist/tsup/{chunk-L6DNN72S.js → chunk-GE2H2DHD.js} +2 -2
  26. package/dist/tsup/{chunk-UBGN2UE2.cjs → chunk-GNKZNRNM.cjs} +3 -3
  27. package/dist/tsup/{chunk-UBGN2UE2.cjs.map → chunk-GNKZNRNM.cjs.map} +1 -1
  28. package/dist/tsup/{chunk-TFACXNOP.cjs → chunk-W5MTJGSD.cjs} +121 -10
  29. package/dist/tsup/chunk-W5MTJGSD.cjs.map +1 -0
  30. package/dist/tsup/{chunk-JTHHCZCZ.js → chunk-WCEJTJHO.js} +34 -1
  31. package/dist/tsup/chunk-WCEJTJHO.js.map +1 -0
  32. package/dist/tsup/{chunk-7ZPRFCTV.cjs → chunk-X5N4RMK4.cjs} +156 -144
  33. package/dist/tsup/chunk-X5N4RMK4.cjs.map +1 -0
  34. package/dist/tsup/{chunk-LHT3TRDM.js → chunk-XGCQ4JOX.js} +115 -4
  35. package/dist/tsup/chunk-XGCQ4JOX.js.map +1 -0
  36. package/dist/tsup/{chunk-KVFIONKQ.cjs → chunk-XMNL3AE2.cjs} +35 -2
  37. package/dist/tsup/chunk-XMNL3AE2.cjs.map +1 -0
  38. package/dist/tsup/{chunk-ZYHZOZUQ.js → chunk-XW3HQQDX.js} +2 -2
  39. package/dist/tsup/{chunk-XNLSA7TO.cjs → chunk-Y4YVQXM7.cjs} +4 -4
  40. package/dist/tsup/{chunk-XNLSA7TO.cjs.map → chunk-Y4YVQXM7.cjs.map} +1 -1
  41. package/dist/tsup/{chunk-2TAUHMAC.cjs → chunk-YHTH6K2K.cjs} +2 -2
  42. package/dist/tsup/chunk-YHTH6K2K.cjs.map +1 -0
  43. package/dist/tsup/client/mod.cjs +6 -6
  44. package/dist/tsup/client/mod.d.cts +3 -3
  45. package/dist/tsup/client/mod.d.ts +3 -3
  46. package/dist/tsup/client/mod.js +5 -5
  47. package/dist/tsup/common/log.cjs +2 -2
  48. package/dist/tsup/common/log.js +1 -1
  49. package/dist/tsup/common/websocket.cjs +3 -3
  50. package/dist/tsup/common/websocket.js +2 -2
  51. package/dist/tsup/{config-ey2I2zz5.d.cts → config-B0q37XhG.d.cts} +31 -1
  52. package/dist/tsup/{config-ey2I2zz5.d.ts → config-B0q37XhG.d.ts} +31 -1
  53. package/dist/tsup/{config-BGTanB_3.d.ts → config-BPJQ0mDo.d.ts} +9 -1
  54. package/dist/tsup/{config-NHz6oMb9.d.cts → config-CGUJyT5L.d.cts} +9 -1
  55. package/dist/tsup/{context-SHomdVV8.d.ts → context-DZ_lu12i.d.ts} +2 -2
  56. package/dist/tsup/{context-DB8zG_Ib.d.cts → context-WLBsDXrQ.d.cts} +2 -2
  57. package/dist/tsup/db/drizzle.cjs +113 -10
  58. package/dist/tsup/db/drizzle.cjs.map +1 -1
  59. package/dist/tsup/db/drizzle.d.cts +2 -2
  60. package/dist/tsup/db/drizzle.d.ts +2 -2
  61. package/dist/tsup/db/drizzle.js +106 -3
  62. package/dist/tsup/db/drizzle.js.map +1 -1
  63. package/dist/tsup/db/mod.cjs +4 -4
  64. package/dist/tsup/db/mod.d.cts +4 -4
  65. package/dist/tsup/db/mod.d.ts +4 -4
  66. package/dist/tsup/db/mod.js +3 -3
  67. package/dist/tsup/dynamic/mod.d.cts +2 -2
  68. package/dist/tsup/dynamic/mod.d.ts +2 -2
  69. package/dist/tsup/inspector/mod.cjs +5 -5
  70. package/dist/tsup/inspector/mod.js +4 -4
  71. package/dist/tsup/inspector/workflow.cjs +4 -4
  72. package/dist/tsup/inspector/workflow.js +3 -3
  73. package/dist/tsup/inspector-tab/mod.d.cts +3 -3
  74. package/dist/tsup/inspector-tab/mod.d.ts +3 -3
  75. package/dist/tsup/mod.cjs +296 -88
  76. package/dist/tsup/mod.cjs.map +1 -1
  77. package/dist/tsup/mod.d.cts +4 -4
  78. package/dist/tsup/mod.d.ts +4 -4
  79. package/dist/tsup/mod.js +225 -17
  80. package/dist/tsup/mod.js.map +1 -1
  81. package/dist/tsup/test/mod.cjs +7 -7
  82. package/dist/tsup/test/mod.d.cts +2 -2
  83. package/dist/tsup/test/mod.d.ts +2 -2
  84. package/dist/tsup/test/mod.js +5 -5
  85. package/dist/tsup/unstable/migrations.d.cts +1 -1
  86. package/dist/tsup/unstable/migrations.d.ts +1 -1
  87. package/dist/tsup/utils.cjs +2 -2
  88. package/dist/tsup/utils.js +1 -1
  89. package/dist/tsup/workflow/mod.cjs +7 -7
  90. package/dist/tsup/workflow/mod.d.cts +4 -4
  91. package/dist/tsup/workflow/mod.d.ts +4 -4
  92. package/dist/tsup/workflow/mod.js +4 -4
  93. package/package.json +8 -8
  94. package/src/agent-os/actor/index.ts +6 -3
  95. package/src/agent-os/actor/preview.ts +5 -2
  96. package/src/client/actor-handle.ts +15 -8
  97. package/src/client/raw-utils.ts +14 -0
  98. package/src/common/database/config.ts +54 -0
  99. package/src/common/database/mod.test.ts +162 -1
  100. package/src/common/database/mod.ts +154 -6
  101. package/src/common/database/native-database.test.ts +81 -2
  102. package/src/common/database/native-database.ts +172 -3
  103. package/src/common/database/shared.ts +49 -1
  104. package/src/db/drizzle.test.ts +82 -1
  105. package/src/db/drizzle.ts +136 -6
  106. package/src/db/mod.ts +2 -0
  107. package/src/registry/napi-runtime.ts +71 -0
  108. package/src/registry/native.ts +48 -14
  109. package/src/registry/runtime.ts +46 -0
  110. package/src/registry/wasm-runtime.ts +63 -0
  111. package/dist/tsup/chunk-2TAUHMAC.cjs.map +0 -1
  112. package/dist/tsup/chunk-7ZPRFCTV.cjs.map +0 -1
  113. package/dist/tsup/chunk-GPD77M3M.js.map +0 -1
  114. package/dist/tsup/chunk-JTHHCZCZ.js.map +0 -1
  115. package/dist/tsup/chunk-KVFIONKQ.cjs.map +0 -1
  116. package/dist/tsup/chunk-LHT3TRDM.js.map +0 -1
  117. package/dist/tsup/chunk-OWSXJF7Z.js.map +0 -1
  118. package/dist/tsup/chunk-TFACXNOP.cjs.map +0 -1
  119. /package/dist/tsup/{chunk-XPT7SGBW.js.map → chunk-6QHFQDW4.js.map} +0 -0
  120. /package/dist/tsup/{chunk-M4RSGECU.js.map → chunk-7XKSZBR7.js.map} +0 -0
  121. /package/dist/tsup/{chunk-YHYNYVNZ.js.map → chunk-CV5KMH2U.js.map} +0 -0
  122. /package/dist/tsup/{chunk-L6DNN72S.js.map → chunk-GE2H2DHD.js.map} +0 -0
  123. /package/dist/tsup/{chunk-ZYHZOZUQ.js.map → chunk-XW3HQQDX.js.map} +0 -0
@@ -7,6 +7,7 @@ import type {
7
7
  SqliteDatabase,
8
8
  SqliteExecuteResult,
9
9
  SqliteTransactionDatabase,
10
+ SynchronousSqliteTransactionDatabase,
10
11
  } from "./config";
11
12
  import { db, registerNativeStateTransactionOpener } from "./mod";
12
13
 
@@ -19,7 +20,20 @@ class FakeSqliteDatabase implements SqliteDatabase {
19
20
  transactionTimeouts: Array<number | undefined> = [];
20
21
  transactionNames: Array<string | undefined> = [];
21
22
 
22
- async exec(): Promise<void> {}
23
+ async exec(
24
+ sql: string,
25
+ callback?: (row: unknown[], columns: string[]) => void,
26
+ ): Promise<void> {
27
+ this.execSync(sql, callback);
28
+ }
29
+
30
+ execSync(
31
+ sql: string,
32
+ callback?: (row: unknown[], columns: string[]) => void,
33
+ ): void {
34
+ this.record(sql);
35
+ callback?.([1], ["value"]);
36
+ }
23
37
 
24
38
  async execute(
25
39
  sql: string,
@@ -29,21 +43,40 @@ class FakeSqliteDatabase implements SqliteDatabase {
29
43
  return emptyResult();
30
44
  }
31
45
 
46
+ executeSync(sql: string, params?: SqliteBindings): SqliteExecuteResult {
47
+ this.record(sql, params);
48
+ return emptyResult();
49
+ }
50
+
32
51
  async beginTransaction(
33
52
  timeoutMs?: number,
34
53
  name?: string,
35
54
  ): Promise<SqliteTransactionDatabase> {
55
+ return this.beginTransactionSync(timeoutMs, name);
56
+ }
57
+
58
+ beginTransactionSync(
59
+ timeoutMs?: number,
60
+ name?: string,
61
+ ): SynchronousSqliteTransactionDatabase {
36
62
  this.transactionTimeouts.push(timeoutMs);
37
63
  this.transactionNames.push(name);
38
64
  this.record("BEGIN");
39
65
  return {
40
66
  exec: async () => {},
67
+ execSync: () => {},
41
68
  execute: async (sql, params) => {
42
69
  this.record(sql, params);
43
70
  return emptyResult();
44
71
  },
72
+ executeSync: (sql, params) => {
73
+ this.record(sql, params);
74
+ return emptyResult();
75
+ },
45
76
  commit: async () => this.record("COMMIT"),
77
+ commitSync: () => this.record("COMMIT"),
46
78
  rollback: async () => this.record("ROLLBACK"),
79
+ rollbackSync: () => this.record("ROLLBACK"),
47
80
  };
48
81
  }
49
82
  async beginStateTransaction(
@@ -53,10 +86,15 @@ class FakeSqliteDatabase implements SqliteDatabase {
53
86
  this.record("BEGIN_STATE");
54
87
  return {
55
88
  exec: async () => {},
89
+ execSync: () => {},
56
90
  execute: async (sql, params) => {
57
91
  this.record(sql, params);
58
92
  return emptyResult();
59
93
  },
94
+ executeSync: (sql, params) => {
95
+ this.record(sql, params);
96
+ return emptyResult();
97
+ },
60
98
  commit: async () => this.record("COMMIT"),
61
99
  rollback: async () => this.record("ROLLBACK"),
62
100
  };
@@ -173,6 +211,129 @@ describe("db", () => {
173
211
  });
174
212
  });
175
213
 
214
+ test("exposes synchronous raw queries on the built-in client", async () => {
215
+ const nativeDb = new FakeSqliteDatabase();
216
+ const client = await db().createClient(testProviderContext(nativeDb));
217
+
218
+ client.executeSync("SELECT ?", 42);
219
+ expect(
220
+ client.executeSync<{ value: number }>("SELECT 1; SELECT 2"),
221
+ ).toEqual([{ value: 1 }]);
222
+
223
+ expect(nativeDb.executeCalls).toEqual([
224
+ {
225
+ sql: "SELECT ?",
226
+ params: [42],
227
+ },
228
+ {
229
+ sql: "SELECT 1; SELECT 2",
230
+ params: undefined,
231
+ },
232
+ ]);
233
+ });
234
+
235
+ test("commits synchronous transaction work before returning", async () => {
236
+ const nativeDb = new FakeSqliteDatabase();
237
+ const client = await db().createClient(testProviderContext(nativeDb));
238
+
239
+ const value = client.transactionSync(
240
+ (tx) => {
241
+ tx.executeSync("INSERT INTO items(value) VALUES (?)", "inside");
242
+ return 42;
243
+ },
244
+ { name: "sync-insert", timeout: 120_000 },
245
+ );
246
+
247
+ expect(value).toBe(42);
248
+ expect(nativeDb.transactionTimeouts).toEqual([120_000]);
249
+ expect(nativeDb.transactionNames).toEqual(["sync-insert"]);
250
+ expect(nativeDb.executeCalls.map(({ sql }) => sql)).toEqual([
251
+ "BEGIN",
252
+ "INSERT INTO items(value) VALUES (?)",
253
+ "COMMIT",
254
+ ]);
255
+ });
256
+
257
+ test("rolls back synchronous transactions on callback and commit errors", async () => {
258
+ const callbackDb = new FakeSqliteDatabase();
259
+ const callbackClient = await db().createClient(
260
+ testProviderContext(callbackDb),
261
+ );
262
+ expect(() =>
263
+ callbackClient.transactionSync(() => {
264
+ throw new Error("callback failed");
265
+ }),
266
+ ).toThrow("callback failed");
267
+ expect(callbackDb.executeCalls.map(({ sql }) => sql)).toEqual([
268
+ "BEGIN",
269
+ "ROLLBACK",
270
+ ]);
271
+
272
+ const commitDb = new FakeSqliteDatabase();
273
+ commitDb.failSql.set("COMMIT", new Error("commit failed"));
274
+ const commitClient = await db().createClient(
275
+ testProviderContext(commitDb),
276
+ );
277
+ expect(() => commitClient.transactionSync(() => 1)).toThrow(
278
+ "commit failed",
279
+ );
280
+ expect(commitDb.executeCalls.map(({ sql }) => sql)).toEqual([
281
+ "BEGIN",
282
+ "COMMIT",
283
+ "ROLLBACK",
284
+ ]);
285
+ });
286
+
287
+ test("rejects async synchronous-transaction callbacks and outer-client queries", async () => {
288
+ const nativeDb = new FakeSqliteDatabase();
289
+ const client = await db().createClient(testProviderContext(nativeDb));
290
+
291
+ expect(() => client.transactionSync(async () => undefined)).toThrow(
292
+ "must not return a promise",
293
+ );
294
+ expect(nativeDb.executeCalls.map(({ sql }) => sql)).toEqual([
295
+ "BEGIN",
296
+ "ROLLBACK",
297
+ ]);
298
+
299
+ nativeDb.executeCalls = [];
300
+ let outerQuery: Promise<Record<string, unknown>[]> | undefined;
301
+ client.transactionSync((tx) => {
302
+ expect(Object.keys(tx)).toEqual(["executeSync"]);
303
+ expect(() => client.executeSync("SELECT 1")).toThrow(
304
+ "transaction callback's tx value",
305
+ );
306
+ outerQuery = client.execute("SELECT 1");
307
+ expect(() => client.transactionSync(() => undefined)).toThrow(
308
+ "Nested synchronous SQLite transactions",
309
+ );
310
+ tx.executeSync("SELECT 2");
311
+ });
312
+ await expect(outerQuery).rejects.toThrow(
313
+ "transaction callback's tx value",
314
+ );
315
+ expect(nativeDb.executeCalls.map(({ sql }) => sql)).toEqual([
316
+ "BEGIN",
317
+ "SELECT 2",
318
+ "COMMIT",
319
+ ]);
320
+ });
321
+
322
+ test("validates synchronous transaction options before beginning", async () => {
323
+ const nativeDb = new FakeSqliteDatabase();
324
+ const client = await db().createClient(testProviderContext(nativeDb));
325
+
326
+ for (const timeout of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
327
+ expect(() =>
328
+ client.transactionSync(() => undefined, { timeout }),
329
+ ).toThrow("positive finite");
330
+ }
331
+ expect(() =>
332
+ client.transactionSync(() => undefined, { name: "" }),
333
+ ).toThrow("must not be empty");
334
+ expect(nativeDb.executeCalls).toEqual([]);
335
+ });
336
+
176
337
  test("rolls back migrations when onMigrate fails", async () => {
177
338
  const nativeDb = new FakeSqliteDatabase();
178
339
  const provider = db({
@@ -7,17 +7,20 @@ import type {
7
7
  SqliteProfilingOptions,
8
8
  SqliteTransactionDatabase,
9
9
  SqliteTransactionOptions,
10
+ SynchronousRawAccess,
11
+ SynchronousTransactionAccess,
10
12
  } from "./config";
11
13
  import {
12
14
  isManualTransactionControl,
13
15
  isSqliteBindingObject,
14
16
  MIGRATION_TRANSACTION_TIMEOUT_MS,
17
+ runSqliteTransactionSync,
15
18
  toSqliteBindings,
16
19
  validateTransactionName,
17
20
  validateTransactionTimeout,
18
21
  } from "./shared";
19
22
 
20
- export type { RawAccess } from "./config";
23
+ export type { RawAccess, SynchronousRawAccess } from "./config";
21
24
 
22
25
  export interface DatabaseFactoryConfig {
23
26
  onMigrate?: (db: RawAccess) => Promise<void> | void;
@@ -84,8 +87,8 @@ export function db({
84
87
  onMigrate,
85
88
  warnOnManualTransactions = true,
86
89
  profiling,
87
- }: DatabaseFactoryConfig = {}): DatabaseProvider<RawAccess> {
88
- const provider: DatabaseProvider<RawAccess> = {
90
+ }: DatabaseFactoryConfig = {}): DatabaseProvider<SynchronousRawAccess> {
91
+ const provider: DatabaseProvider<SynchronousRawAccess> = {
89
92
  sqliteProfiling: profiling,
90
93
  createClient: async (ctx) => {
91
94
  const nativeDatabaseProvider = ctx.nativeDatabaseProvider;
@@ -98,6 +101,7 @@ export function db({
98
101
  const db = await nativeDatabaseProvider.open(ctx.actorId);
99
102
  let closed = false;
100
103
  let manualTransactionWarned = false;
104
+ let synchronousTransactionActive = false;
101
105
  const ensureOpen = () => {
102
106
  if (closed) {
103
107
  throw new Error(
@@ -105,13 +109,22 @@ export function db({
105
109
  );
106
110
  }
107
111
  };
112
+ const ensureSynchronousTransactionClient = (
113
+ transactionScoped: boolean,
114
+ ) => {
115
+ if (!transactionScoped && synchronousTransactionActive) {
116
+ throw new Error(
117
+ "Use the transaction callback's tx value for queries inside db.transactionSync().",
118
+ );
119
+ }
120
+ };
108
121
 
109
122
  const createClient = (
110
123
  target: SqliteDatabase | SqliteTransactionDatabase,
111
124
  transactionScoped = false,
112
125
  stateTransactionContext?: NativeStateTransactionContext,
113
- ): RawAccess => {
114
- const client: RawAccess = {
126
+ ): SynchronousRawAccess => {
127
+ const client: SynchronousRawAccess = {
115
128
  execute: async <
116
129
  TRow extends Record<string, unknown> = Record<
117
130
  string,
@@ -122,6 +135,7 @@ export function db({
122
135
  ...args: unknown[]
123
136
  ): Promise<TRow[]> => {
124
137
  ensureOpen();
138
+ ensureSynchronousTransactionClient(transactionScoped);
125
139
  if (
126
140
  !transactionScoped &&
127
141
  warnOnManualTransactions &&
@@ -132,7 +146,7 @@ export function db({
132
146
  manualTransactionWarned = true;
133
147
  getLogger("database").warn(
134
148
  { actorId: ctx.actorId },
135
- "Manual cross-call SQLite transactions can interleave with other actor work. Use db.transaction() for coordinated transactions. Set warnOnManualTransactions: false in your db(...) configuration to disable this warning.",
149
+ "Manual cross-call SQLite transactions can interleave with other actor work. Use db.transaction() or db.transactionSync() for coordinated transactions. Set warnOnManualTransactions: false in your db(...) configuration to disable this warning.",
136
150
  );
137
151
  }
138
152
 
@@ -188,10 +202,91 @@ export function db({
188
202
  }
189
203
  }
190
204
  },
205
+ executeSync: <
206
+ TRow extends Record<string, unknown> = Record<
207
+ string,
208
+ unknown
209
+ >,
210
+ >(
211
+ query: string,
212
+ ...args: unknown[]
213
+ ): TRow[] => {
214
+ ensureOpen();
215
+ ensureSynchronousTransactionClient(transactionScoped);
216
+ if (!target.executeSync) {
217
+ throw new Error(
218
+ "Synchronous SQLite queries are only available in the Node.js native runtime.",
219
+ );
220
+ }
221
+ if (
222
+ !transactionScoped &&
223
+ warnOnManualTransactions &&
224
+ !manualTransactionWarned &&
225
+ !hasMultipleStatements(query) &&
226
+ isManualTransactionControl(query)
227
+ ) {
228
+ manualTransactionWarned = true;
229
+ getLogger("database").warn(
230
+ { actorId: ctx.actorId },
231
+ "Manual cross-call SQLite transactions can interleave with other actor work. Use db.transaction() or db.transactionSync() for coordinated transactions. Set warnOnManualTransactions: false in your db(...) configuration to disable this warning.",
232
+ );
233
+ }
234
+
235
+ const kvReadsBefore = ctx.metrics?.totalKvReads ?? 0;
236
+ const kvWritesBefore = ctx.metrics?.totalKvWrites ?? 0;
237
+ const start = performance.now();
238
+
239
+ try {
240
+ if (args.length > 0) {
241
+ const bindings =
242
+ args.length === 1 &&
243
+ isSqliteBindingObject(args[0])
244
+ ? toSqliteBindings(args[0])
245
+ : toSqliteBindings(args);
246
+ const { rows, columns } = target.executeSync(
247
+ query,
248
+ bindings,
249
+ );
250
+ return rows.map((row) =>
251
+ rowToObject<TRow>(row, columns),
252
+ );
253
+ }
254
+
255
+ if (!hasMultipleStatements(query)) {
256
+ const { rows, columns } = target.executeSync(
257
+ query,
258
+ undefined,
259
+ );
260
+ return rows.map((row) =>
261
+ rowToObject<TRow>(row, columns),
262
+ );
263
+ }
264
+
265
+ return execMultiStatementSync<TRow>(target, query);
266
+ } finally {
267
+ const durationMs = performance.now() - start;
268
+ ctx.metrics?.trackSql(query, durationMs);
269
+ if (ctx.metrics) {
270
+ const kvReads =
271
+ ctx.metrics.totalKvReads - kvReadsBefore;
272
+ const kvWrites =
273
+ ctx.metrics.totalKvWrites - kvWritesBefore;
274
+ ctx.log?.debug({
275
+ msg: "sql query",
276
+ query: query.slice(0, 120),
277
+ durationMs,
278
+ kvReads,
279
+ kvWrites,
280
+ });
281
+ }
282
+ }
283
+ },
191
284
  transaction: async <T>(
192
285
  callback: (tx: RawAccess) => Promise<T> | T,
193
286
  options?: SqliteTransactionOptions,
194
287
  ): Promise<T> => {
288
+ ensureOpen();
289
+ ensureSynchronousTransactionClient(transactionScoped);
195
290
  validateTransactionTimeout(options?.timeout);
196
291
  validateTransactionName(options?.name);
197
292
  if (
@@ -249,6 +344,39 @@ export function db({
249
344
  }
250
345
  }
251
346
  },
347
+ transactionSync: <T>(
348
+ callback: (tx: SynchronousTransactionAccess) => T,
349
+ options?: Omit<
350
+ SqliteTransactionOptions,
351
+ "experimental"
352
+ >,
353
+ ): T => {
354
+ ensureOpen();
355
+ if (transactionScoped || synchronousTransactionActive) {
356
+ throw new Error(
357
+ "Nested synchronous SQLite transactions are not supported.",
358
+ );
359
+ }
360
+ return runSqliteTransactionSync(
361
+ db,
362
+ (transaction) => {
363
+ const transactionClient = createClient(
364
+ transaction,
365
+ true,
366
+ );
367
+ const tx: SynchronousTransactionAccess = {
368
+ executeSync: transactionClient.executeSync,
369
+ };
370
+ synchronousTransactionActive = true;
371
+ try {
372
+ return callback(tx);
373
+ } finally {
374
+ synchronousTransactionActive = false;
375
+ }
376
+ },
377
+ options,
378
+ );
379
+ },
252
380
  close: async () => {
253
381
  if (!closed) {
254
382
  closed = true;
@@ -304,6 +432,26 @@ async function execMultiStatement<TRow extends Record<string, unknown>>(
304
432
  return results as TRow[];
305
433
  }
306
434
 
435
+ function execMultiStatementSync<TRow extends Record<string, unknown>>(
436
+ db: Pick<SqliteDatabase, "execSync">,
437
+ query: string,
438
+ ): TRow[] {
439
+ if (!db.execSync) {
440
+ throw new Error(
441
+ "Synchronous SQLite queries are only available in the Node.js native runtime.",
442
+ );
443
+ }
444
+ const results: Record<string, unknown>[] = [];
445
+ let columnNames: string[] | null = null;
446
+ db.execSync(query, (row: unknown[], columns: string[]) => {
447
+ if (!columnNames) {
448
+ columnNames = columns;
449
+ }
450
+ results.push(rowToObject(row, columnNames));
451
+ });
452
+ return results as TRow[];
453
+ }
454
+
307
455
  async function withMigrationSavepoint<T>(
308
456
  client: RawAccess,
309
457
  callback: (leased: RawAccess) => Promise<T> | T,
@@ -20,28 +20,66 @@ function deferred<T>() {
20
20
 
21
21
  class FakeNativeDatabase implements JsNativeDatabaseLike {
22
22
  async beginTransaction() {
23
+ this.transactionEvents.push("BEGIN");
24
+ return this.#transaction();
25
+ }
26
+
27
+ beginTransactionSync() {
28
+ this.transactionEvents.push("BEGIN_SYNC");
29
+ return this.#transaction();
30
+ }
31
+
32
+ #transaction() {
23
33
  return {
24
34
  exec: async (_sql: string) => this.exec(),
35
+ execSync: (_sql: string) => this.execSync(),
25
36
  execute: async (sql: string, params?: NativeParams) =>
26
37
  this.execute(sql, params),
27
- commit: async () => {},
28
- rollback: async () => {},
38
+ executeSync: (sql: string, params?: NativeParams) =>
39
+ this.executeSync(sql, params),
40
+ commit: async () => {
41
+ this.transactionEvents.push("COMMIT");
42
+ },
43
+ commitSync: () => {
44
+ this.transactionEvents.push("COMMIT_SYNC");
45
+ },
46
+ rollback: async () => {
47
+ this.transactionEvents.push("ROLLBACK");
48
+ },
49
+ rollbackSync: () => {
50
+ this.transactionEvents.push("ROLLBACK_SYNC");
51
+ },
29
52
  };
30
53
  }
31
54
  active = 0;
32
55
  maxActive = 0;
33
56
  closed = false;
34
57
  executeCalls: { sql: string; params?: NativeParams; write: boolean }[] = [];
58
+ transactionEvents: string[] = [];
35
59
  #pending: ReturnType<typeof deferred<NativeExecuteResult>>[] = [];
36
60
 
37
61
  async exec() {
38
62
  return { columns: [], rows: [] };
39
63
  }
40
64
 
65
+ execSync() {
66
+ return { columns: ["value"], rows: [[1], [2]] };
67
+ }
68
+
41
69
  async execute(sql: string, params?: NativeParams) {
42
70
  return await this.#startExecute(sql, params, false);
43
71
  }
44
72
 
73
+ executeSync(sql: string, params?: NativeParams): NativeExecuteResult {
74
+ this.executeCalls.push({ sql, params, write: false });
75
+ return {
76
+ columns: ["value"],
77
+ rows: [[1]],
78
+ changes: 0,
79
+ lastInsertRowId: null,
80
+ };
81
+ }
82
+
45
83
  async query(sql: string, params?: NativeParams) {
46
84
  const { columns, rows } = await this.execute(sql, params);
47
85
  return { columns, rows };
@@ -155,6 +193,47 @@ describe("wrapJsNativeDatabase", () => {
155
193
  });
156
194
  });
157
195
 
196
+ test("executes synchronously with normalized bindings", () => {
197
+ const native = new FakeNativeDatabase();
198
+ const db = wrapJsNativeDatabase(native);
199
+
200
+ const result = db.executeSync?.("SELECT ?, ?", [true, "text"]);
201
+ const execRows: unknown[][] = [];
202
+ db.execSync?.("SELECT 1; SELECT 2", (row) => execRows.push(row));
203
+
204
+ expect(native.executeCalls[0]?.params).toEqual([
205
+ { kind: "int", intValue: 1 },
206
+ { kind: "text", textValue: "text" },
207
+ ]);
208
+ expect(result).toMatchObject({
209
+ columns: ["value"],
210
+ rows: [[1]],
211
+ });
212
+ expect(execRows).toEqual([[1], [2]]);
213
+ });
214
+
215
+ test("wraps synchronous transaction lifecycle methods", () => {
216
+ const native = new FakeNativeDatabase();
217
+ const db = wrapJsNativeDatabase(native);
218
+
219
+ const committed = db.beginTransactionSync?.(1_000, "commit");
220
+ if (!committed)
221
+ throw new Error("missing synchronous transaction support");
222
+ committed.executeSync("SELECT 1");
223
+ committed.commitSync();
224
+ const rolledBack = db.beginTransactionSync?.(1_000, "rollback");
225
+ if (!rolledBack)
226
+ throw new Error("missing synchronous transaction support");
227
+ rolledBack.rollbackSync();
228
+
229
+ expect(native.transactionEvents).toEqual([
230
+ "BEGIN_SYNC",
231
+ "COMMIT_SYNC",
232
+ "BEGIN_SYNC",
233
+ "ROLLBACK_SYNC",
234
+ ]);
235
+ });
236
+
158
237
  test("returns native execute metadata", async () => {
159
238
  const native = new FakeNativeDatabase();
160
239
  const db = wrapJsNativeDatabase(native);