Skip to content

Commit 7bd5d08

Browse files
committed
feat: add tx api for Rust-only callers
- It was originally thought that the plain transaction and interruptible transaction API would be used from TS but we need it from Rust too. Now some operations will be done 100% in native (e.g. importing jwpubs)
1 parent ebd3e52 commit 7bd5d08

9 files changed

Lines changed: 852 additions & 231 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ generated ID or other computed values, then using that data in subsequent writes
254254

255255
```typescript
256256
// Begin transaction with initial insert
257-
let tx = await db.executeInterruptibleTransaction([
257+
let tx = await db.beginInterruptibleTransaction([
258258
['INSERT INTO orders (user_id, total) VALUES ($1, $2)', [userId, 0]]
259259
])
260260

@@ -266,7 +266,7 @@ const orders = await tx.read<Array<{ id: number }>>(
266266
const orderId = orders[0].id
267267

268268
// Continue transaction with the order ID
269-
tx = await tx.continue([
269+
tx = await tx.continueWith([
270270
['INSERT INTO order_items (order_id, product_id) VALUES ($1, $2)', [orderId, productId]],
271271
['UPDATE orders SET total = $1 WHERE id = $2', [itemTotal, orderId]]
272272
])
@@ -397,7 +397,7 @@ await db.remove() // Close and DELETE database file(s) - irreversible!
397397
| ------ | ----------- |
398398
| `execute(query, values?)` | Execute write query, returns `{ rowsAffected, lastInsertId }` |
399399
| `executeTransaction(statements)` | Execute statements atomically (use for batch writes) |
400-
| `executeInterruptibleTransaction(statements)` | Begin interruptible transaction, returns `InterruptibleTransaction` |
400+
| `beginInterruptibleTransaction(statements)` | Begin interruptible transaction, returns `InterruptibleTransaction` |
401401
| `fetchAll<T>(query, values?)` | Execute SELECT, return all rows |
402402
| `fetchOne<T>(query, values?)` | Execute SELECT, return single row or `undefined` |
403403
| `close()` | Close connection, returns `true` if was loaded |

guest-js/index.test.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ beforeEach(() => {
2121
if (cmd === 'plugin:sqlite|execute_transaction') {
2222
return [];
2323
}
24-
if (cmd === 'plugin:sqlite|execute_interruptible_transaction') {
24+
if (cmd === 'plugin:sqlite|begin_interruptible_transaction') {
2525
return { dbPath: (args as { db: string }).db, transactionId: 'test-tx-id' };
2626
}
2727
if (cmd === 'plugin:sqlite|transaction_continue') {
@@ -225,25 +225,54 @@ describe('Database commands', () => {
225225
expect(events).toEqual([]);
226226
});
227227

228-
it('executeInterruptibleTransaction', async () => {
229-
const tx = await Database.get('t.db').executeInterruptibleTransaction([
228+
it('beginInterruptibleTransaction', async () => {
229+
const tx = await Database.get('t.db').beginInterruptibleTransaction([
230230
[ 'INSERT INTO users (name) VALUES ($1)', [ 'Alice' ] ],
231231
]);
232232

233-
expect(lastCmd).toBe('plugin:sqlite|execute_interruptible_transaction');
233+
expect(lastCmd).toBe('plugin:sqlite|begin_interruptible_transaction');
234234
expect(lastArgs.db).toBe('t.db');
235235
expect(lastArgs.initialStatements).toEqual([
236236
{ query: 'INSERT INTO users (name) VALUES ($1)', values: [ 'Alice' ] },
237237
]);
238+
expect(lastArgs.attached).toBe(null);
239+
expect(tx).toBeInstanceOf(Object);
240+
});
241+
242+
it('beginInterruptibleTransaction with attached databases', async () => {
243+
const tx = await Database.get('main.db')
244+
.beginInterruptibleTransaction([
245+
[ 'DELETE FROM users WHERE id IN (SELECT user_id FROM archive.archived_users)' ],
246+
])
247+
.attach([
248+
{
249+
databasePath: 'archive.db',
250+
schemaName: 'archive',
251+
mode: 'readOnly',
252+
},
253+
]);
254+
255+
expect(lastCmd).toBe('plugin:sqlite|begin_interruptible_transaction');
256+
expect(lastArgs.db).toBe('main.db');
257+
expect(lastArgs.initialStatements).toEqual([
258+
{ query: 'DELETE FROM users WHERE id IN (SELECT user_id FROM archive.archived_users)', values: [] },
259+
]);
260+
expect(lastArgs.attached).toEqual([
261+
{
262+
databasePath: 'archive.db',
263+
schemaName: 'archive',
264+
mode: 'readOnly',
265+
},
266+
]);
238267
expect(tx).toBeInstanceOf(Object);
239268
});
240269

241-
it('InterruptibleTransaction.continue()', async () => {
242-
const tx = await Database.get('test.db').executeInterruptibleTransaction([
270+
it('InterruptibleTransaction.continueWith()', async () => {
271+
const tx = await Database.get('test.db').beginInterruptibleTransaction([
243272
[ 'INSERT INTO users (name) VALUES ($1)', [ 'Alice' ] ],
244273
]);
245274

246-
const tx2 = await tx.continue([
275+
const tx2 = await tx.continueWith([
247276
[ 'INSERT INTO users (name) VALUES ($1)', [ 'Bob' ] ],
248277
]);
249278

@@ -254,7 +283,7 @@ describe('Database commands', () => {
254283
});
255284

256285
it('InterruptibleTransaction.commit()', async () => {
257-
const tx = await Database.get('test.db').executeInterruptibleTransaction([
286+
const tx = await Database.get('test.db').beginInterruptibleTransaction([
258287
[ 'INSERT INTO users (name) VALUES ($1)', [ 'Alice' ] ],
259288
]);
260289

@@ -265,7 +294,7 @@ describe('Database commands', () => {
265294
});
266295

267296
it('InterruptibleTransaction.rollback()', async () => {
268-
const tx = await Database.get('test.db').executeInterruptibleTransaction([
297+
const tx = await Database.get('test.db').beginInterruptibleTransaction([
269298
[ 'INSERT INTO users (name) VALUES ($1)', [ 'Alice' ] ],
270299
]);
271300

@@ -276,7 +305,7 @@ describe('Database commands', () => {
276305
});
277306

278307
it('InterruptibleTransaction.read()', async () => {
279-
const tx = await Database.get('test.db').executeInterruptibleTransaction([
308+
const tx = await Database.get('test.db').beginInterruptibleTransaction([
280309
[ 'INSERT INTO users (name) VALUES ($1)', [ 'Alice' ] ],
281310
]);
282311

guest-js/index.ts

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export class InterruptibleTransaction {
100100
*
101101
* @example
102102
* ```ts
103-
* let tx = await db.executeInterruptibleTransaction([
103+
* let tx = await db.beginInterruptibleTransaction([
104104
* ['INSERT INTO users (name) VALUES ($1)', ['Alice']]
105105
* ]);
106106
*
@@ -119,7 +119,7 @@ export class InterruptibleTransaction {
119119
}
120120

121121
/**
122-
* **continue**
122+
* **continueWith**
123123
*
124124
* Execute additional statements within this transaction and return a new
125125
* transaction handle.
@@ -129,14 +129,14 @@ export class InterruptibleTransaction {
129129
*
130130
* @example
131131
* ```ts
132-
* let tx = await db.executeInterruptibleTransaction([...]);
133-
* tx = await tx.continue([
132+
* let tx = await db.beginInterruptibleTransaction([...]);
133+
* tx = await tx.continueWith([
134134
* ['INSERT INTO users (name) VALUES ($1)', ['Bob']]
135135
* ]);
136136
* await tx.commit();
137137
* ```
138138
*/
139-
public async continue(statements: Array<[string, SqlValue[]?]>): Promise<InterruptibleTransaction> {
139+
public async continueWith(statements: Array<[string, SqlValue[]?]>): Promise<InterruptibleTransaction> {
140140
const token = await invoke<{ dbPath: string; transactionId: string }>(
141141
'plugin:sqlite|transaction_continue',
142142
{
@@ -163,7 +163,7 @@ export class InterruptibleTransaction {
163163
*
164164
* @example
165165
* ```ts
166-
* let tx = await db.executeInterruptibleTransaction([...]);
166+
* let tx = await db.beginInterruptibleTransaction([...]);
167167
* [...]
168168
* await tx.commit();
169169
* ```
@@ -182,7 +182,7 @@ export class InterruptibleTransaction {
182182
*
183183
* @example
184184
* ```ts
185-
* let tx = await db.executeInterruptibleTransaction([...]);
185+
* let tx = await db.beginInterruptibleTransaction([...]);
186186
* [...]
187187
* await tx.rollback();
188188
* ```
@@ -419,6 +419,61 @@ class ExecuteBuilder implements PromiseLike<WriteQueryResult> {
419419
}
420420
}
421421

422+
/**
423+
* Builder for interruptible transaction operations
424+
*/
425+
class InterruptibleTransactionBuilder implements PromiseLike<InterruptibleTransaction> {
426+
private readonly _db: Database;
427+
private readonly _initialStatements: Array<[string, SqlValue[]?]>;
428+
private _attached: AttachedDatabaseSpec[];
429+
430+
public constructor(
431+
db: Database,
432+
initialStatements: Array<[string, SqlValue[]?]>,
433+
attached: AttachedDatabaseSpec[] = []
434+
) {
435+
this._db = db;
436+
this._initialStatements = initialStatements;
437+
this._attached = attached;
438+
}
439+
440+
/**
441+
* Attach databases for cross-database transactions
442+
*/
443+
public attach(specs: AttachedDatabaseSpec[]): this {
444+
this._attached = specs;
445+
return this;
446+
}
447+
448+
/**
449+
* Make the builder directly awaitable
450+
*/
451+
public then<TResult1 = InterruptibleTransaction, TResult2 = never>(
452+
onfulfilled?: ((value: InterruptibleTransaction) => TResult1 | PromiseLike<TResult1>) | null,
453+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
454+
): PromiseLike<TResult1 | TResult2> {
455+
return this._execute().then(onfulfilled, onrejected);
456+
}
457+
458+
private async _execute(): Promise<InterruptibleTransaction> {
459+
const token = await invoke<{ dbPath: string; transactionId: string }>(
460+
'plugin:sqlite|begin_interruptible_transaction',
461+
{
462+
db: this._db.path,
463+
initialStatements: this._initialStatements.map(([ query, values ]) => {
464+
return {
465+
query,
466+
values: values ?? [],
467+
};
468+
}),
469+
attached: this._attached.length > 0 ? this._attached : null,
470+
}
471+
);
472+
473+
return new InterruptibleTransaction(token.dbPath, token.transactionId);
474+
}
475+
}
476+
422477
/**
423478
* Builder for transaction operations
424479
*/
@@ -597,7 +652,7 @@ export default class Database {
597652
* **Use this method** when you have a batch of writes to execute and
598653
* don't need to read data mid-transaction. For transactions that
599654
* require reading uncommitted data to decide how to proceed, use
600-
* `executeInterruptibleTransaction()` instead.
655+
* `beginInterruptibleTransaction()` instead.
601656
*
602657
* The function automatically:
603658
* - Begins a transaction (BEGIN IMMEDIATE)
@@ -765,7 +820,7 @@ export default class Database {
765820
}
766821

767822
/**
768-
* **executeInterruptibleTransaction**
823+
* **beginInterruptibleTransaction**
769824
*
770825
* Begins an interruptible transaction for cases where you need to
771826
* **read data mid-transaction to decide how to proceed**. For example,
@@ -788,12 +843,12 @@ export default class Database {
788843
* writer connection is held for the entire duration - keep transactions short.
789844
*
790845
* @param initialStatements - Array of [query, values?] tuples to execute initially
791-
* @returns Promise that resolves with an InterruptibleTransaction handle
846+
* @returns Builder for setting up the transaction with optional attached databases
792847
*
793848
* @example
794849
* ```ts
795850
* // Insert an order and read back its ID
796-
* let tx = await db.executeInterruptibleTransaction([
851+
* let tx = await db.beginInterruptibleTransaction([
797852
* ['INSERT INTO orders (user_id, total) VALUES ($1, $2)', [userId, 0]]
798853
* ]);
799854
*
@@ -805,7 +860,7 @@ export default class Database {
805860
* const orderId = orders[0].id;
806861
*
807862
* // Use the ID in subsequent writes
808-
* tx = await tx.continue([
863+
* tx = await tx.continueWith([
809864
* [
810865
* 'INSERT INTO order_items (order_id, product_id) VALUES ($1, $2)',
811866
* [ orderId, productId ],
@@ -814,24 +869,29 @@ export default class Database {
814869
*
815870
* await tx.commit();
816871
* ```
872+
*
873+
* @example
874+
* ```ts
875+
* // Transaction with attached database
876+
* let tx = await db.beginInterruptibleTransaction([
877+
* ['DELETE FROM users WHERE archived = 1']
878+
* ]).attach([{
879+
* databasePath: 'archive.db',
880+
* schemaName: 'archive',
881+
* mode: 'readWrite'
882+
* }]);
883+
*
884+
* tx = await tx.continueWith([
885+
* ['INSERT INTO archive.users SELECT * FROM users WHERE archived = 1']
886+
* ]);
887+
*
888+
* await tx.commit();
889+
* ```
817890
*/
818-
public async executeInterruptibleTransaction(
891+
public beginInterruptibleTransaction(
819892
initialStatements: Array<[string, SqlValue[]?]>
820-
): Promise<InterruptibleTransaction> {
821-
const token = await invoke<{ dbPath: string; transactionId: string }>(
822-
'plugin:sqlite|execute_interruptible_transaction',
823-
{
824-
db: this.path,
825-
initialStatements: initialStatements.map(([ query, values ]) => {
826-
return {
827-
query,
828-
values: values ?? [],
829-
};
830-
}),
831-
}
832-
);
833-
834-
return new InterruptibleTransaction(token.dbPath, token.transactionId);
893+
): InterruptibleTransactionBuilder {
894+
return new InterruptibleTransactionBuilder(this, initialStatements);
835895
}
836896

837897
/**

0 commit comments

Comments
 (0)