Skip to content

Commit 039932d

Browse files
pmorris-devpamorris-jw
authored andcommitted
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 039932d

11 files changed

Lines changed: 1119 additions & 240 deletions

File tree

README.md

Lines changed: 160 additions & 4 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 |
@@ -418,7 +418,7 @@ return builders that are directly awaitable and support method chaining:
418418
| Method | Description |
419419
| ------ | ----------- |
420420
| `read<T>(query, values?)` | Read uncommitted data within this transaction |
421-
| `continue(statements)` | Execute additional statements, returns new `InterruptibleTransaction` |
421+
| `continueWith(statements)` | Execute additional statements, returns new `InterruptibleTransaction` |
422422
| `commit()` | Commit transaction and release write lock |
423423
| `rollback()` | Rollback transaction and release write lock |
424424

@@ -447,6 +447,162 @@ interface SqliteError {
447447
}
448448
```
449449

450+
## Rust-Only API
451+
452+
For Rust code that needs direct database access without going through Tauri commands,
453+
use `DatabaseWrapper`.
454+
455+
### Setup (Rust)
456+
457+
```rust
458+
use tauri_plugin_sqlite::DatabaseWrapper;
459+
use std::path::PathBuf;
460+
461+
// Load a database
462+
let db = DatabaseWrapper::load(PathBuf::from("/path/to/mydb.db"), None).await?;
463+
464+
// With custom configuration
465+
use tauri_plugin_sqlite::CustomConfig;
466+
let config = CustomConfig {
467+
max_read_connections: Some(10),
468+
idle_timeout_secs: Some(60),
469+
};
470+
let db = DatabaseWrapper::load(PathBuf::from("/path/to/mydb.db"), Some(config)).await?;
471+
```
472+
473+
### Basic Operations
474+
475+
```rust
476+
use serde_json::json;
477+
478+
// Write operations
479+
let result = db.execute(
480+
"INSERT INTO users (name, email) VALUES (?, ?)".into(),
481+
vec![json!("Alice"), json!("alice@example.com")]
482+
).await?;
483+
println!("Inserted row {}", result.last_insert_id);
484+
485+
// Read multiple rows
486+
let users = db.fetch_all(
487+
"SELECT * FROM users WHERE active = ?".into(),
488+
vec![json!(true)]
489+
).await?;
490+
491+
// Read single row
492+
let user = db.fetch_one(
493+
"SELECT * FROM users WHERE id = ?".into(),
494+
vec![json!(42)]
495+
).await?;
496+
```
497+
498+
### Simple Transactions
499+
500+
Use `execute_transaction()` for atomic execution of multiple statements:
501+
502+
```rust
503+
let results = db.execute_transaction(vec![
504+
("UPDATE accounts SET balance = balance - ? WHERE id = ?", vec![json!(100), json!(1)]),
505+
("UPDATE accounts SET balance = balance + ? WHERE id = ?", vec![json!(100), json!(2)]),
506+
("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)", vec![json!(1), json!(2), json!(100)]),
507+
]).await?;
508+
509+
// Returns Vec<WriteQueryResult> on success, rolls back on any failure
510+
```
511+
512+
### Interruptible Transactions (Rust)
513+
514+
For transactions that need to read data mid-transaction:
515+
516+
```rust
517+
// Begin transaction with initial statements
518+
let mut tx = db.begin_interruptible_transaction()
519+
.execute(vec![
520+
("INSERT INTO orders (user_id, total) VALUES (?, ?)", vec![json!(user_id), json!(0)]),
521+
])
522+
.await?;
523+
524+
// Read uncommitted data
525+
let orders = tx.read(
526+
"SELECT id FROM orders WHERE user_id = ? ORDER BY id DESC LIMIT 1".into(),
527+
vec![json!(user_id)]
528+
).await?;
529+
530+
let order_id = orders[0].get("id").unwrap().as_i64().unwrap();
531+
532+
// Continue with more statements
533+
tx.continue_with(vec![
534+
("INSERT INTO order_items (order_id, product_id) VALUES (?, ?)", vec![json!(order_id), json!(product_id)]),
535+
("UPDATE orders SET total = ? WHERE id = ?", vec![json!(item_total), json!(order_id)]),
536+
]).await?;
537+
538+
// Commit (or rollback)
539+
tx.commit().await?;
540+
// tx.rollback().await?; // Alternative: rollback changes
541+
```
542+
543+
### Cross-Database Operations
544+
545+
Attach other databases for cross-database queries:
546+
547+
```rust
548+
use tauri_plugin_sqlite::AttachedDatabaseSpec;
549+
550+
// Simple transaction with attached database
551+
let results = db.execute_transaction(vec![
552+
("INSERT INTO main.orders (user_id) VALUES (?)", vec![json!(1)]),
553+
("UPDATE stats.order_count SET count = count + 1", vec![]),
554+
])
555+
.attach(vec![AttachedDatabaseSpec {
556+
database_path: "stats.db".into(),
557+
schema_name: "stats".into(),
558+
mode: tauri_plugin_sqlite::AttachedDatabaseMode::ReadWrite,
559+
}])
560+
.await?;
561+
562+
// Interruptible transaction with attached database
563+
let tx = db.begin_interruptible_transaction()
564+
.attach(vec![AttachedDatabaseSpec {
565+
database_path: "inventory.db".into(),
566+
schema_name: "inv".into(),
567+
mode: tauri_plugin_sqlite::AttachedDatabaseMode::ReadWrite,
568+
}])
569+
.execute(vec![
570+
("UPDATE inv.stock SET quantity = quantity - ? WHERE product_id = ?", vec![json!(1), json!(product_id)]),
571+
])
572+
.await?;
573+
```
574+
575+
### Cleanup
576+
577+
```rust
578+
db.close().await?; // Close connection
579+
db.remove().await?; // Close and DELETE database file(s)
580+
```
581+
582+
### Rust API Reference
583+
584+
#### DatabaseWrapper Methods
585+
586+
| Method | Description |
587+
| ------ | ----------- |
588+
| `load(path, config?)` | Load database, returns `DatabaseWrapper` |
589+
| `execute(query, values)` | Execute write query |
590+
| `execute_transaction(statements)` | Execute statements atomically (builder) |
591+
| `begin_interruptible_transaction()` | Begin interruptible transaction (builder) |
592+
| `fetch_all(query, values)` | Fetch all rows |
593+
| `fetch_one(query, values)` | Fetch single row |
594+
| `close()` | Close connection |
595+
| `remove()` | Close and delete database file(s) |
596+
597+
#### InterruptibleTransaction Methods (Rust)
598+
599+
| Method | Description |
600+
| ------ | ----------- |
601+
| `read(query, values)` | Read uncommitted data within transaction |
602+
| `continue_with(statements)` | Execute additional statements |
603+
| `commit()` | Commit and release write lock |
604+
| `rollback()` | Rollback and release write lock |
605+
450606
## Tracing and Logging
451607

452608
The plugin uses [`tracing`](https://crates.io/crates/tracing) with

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

0 commit comments

Comments
 (0)