Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ futures-core = "0.3.31"
time = "0.3.44"
tokio = { version = "1.48.0", features = ["sync"] }
indexmap = { version = "2.12.1", features = ["serde"] }
base64 = "0.22.1"

# SQLx for types and queries (time feature enables datetime type decoding)
sqlx = { version = "0.8.6", features = ["sqlite", "json", "time", "runtime-tokio"] }
Expand Down
83 changes: 39 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,18 @@ type SqlValue = string | number | boolean | null | Uint8Array
Supported SQLite types:

* **TEXT** - `string` values (also used for DATE, TIME, DATETIME)
* **INTEGER** - `number` values (integers)
* **INTEGER** - `number` values (integers, preserved up to i64 range)
* **REAL** - `number` values (floating point)
* **BOOLEAN** - `boolean` values
* **NULL** - `null` value
* **BLOB** - `Uint8Array` for binary data

> **Note:** JavaScript's `number` type can safely represent integers up to
> ±2^53 - 1 (±9,007,199,254,740,991). The plugin preserves integer precision by
> binding integers as SQLite's INTEGER type (i64). For values within the i64
> range (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807), full precision
> is maintained. Values outside this range may lose precision.

```typescript
// Example with different types
await db.execute(
Expand Down Expand Up @@ -265,7 +271,6 @@ Common error codes include:
* `INVALID_PATH` - Invalid database path
* `IO_ERROR` - File system error
* `MIGRATION_ERROR` - Migration failed
* `READ_ONLY_QUERY_IN_EXECUTE` - Attempted to use execute() for a read-only query
* `MULTIPLE_ROWS_RETURNED` - `fetchOne()` query returned multiple rows

### Executing SELECT Queries
Expand Down Expand Up @@ -304,45 +309,35 @@ if (user) {

### Using Transactions

Transactions ensure that multiple operations either all succeed or all fail together,
maintaining data consistency:
Execute multiple database operations atomically using `executeTransaction()`. All
statements either succeed together or fail together, maintaining data consistency:

```typescript
// Begin a transaction
await db.beginTransaction();

try {
// Execute multiple operations atomically
await db.execute(
'INSERT INTO users (name, email) VALUES ($1, $2)',
['Alice', 'alice@example.com']
);

await db.execute(
'INSERT INTO audit_log (action, user) VALUES ($1, $2)',
['user_created', 'Alice']
);

// Commit if all operations succeed
await db.commitTransaction();
console.log('Transaction completed successfully');

} catch (error) {
// Rollback if any operation fails
await db.rollbackTransaction();
console.error('Transaction failed, rolled back:', error);
throw error;
}
```

**Important Notes:**

* All operations between `beginTransaction()` and
`commitTransaction()`/`rollbackTransaction()` are executed as a single atomic unit
* If an error occurs, call `rollbackTransaction()` to discard all changes
* Nested transactions are not supported
* Always ensure transactions are either committed or rolled back to avoid locking
issues
// Execute multiple inserts atomically
const results = await db.executeTransaction([
['INSERT INTO users (name, email) VALUES ($1, $2)', ['Alice', 'alice@example.com']],
['INSERT INTO audit_log (action, user) VALUES ($1, $2)', ['user_created', 'Alice']]
]);
console.log(`User ID: ${results[0].lastInsertId}`);
console.log(`Log rows affected: ${results[1].rowsAffected}`);

// Bank transfer example - all operations succeed or all fail
const results = await db.executeTransaction([
['UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 1]],
['UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 2]],
['INSERT INTO transfers (from_id, to_id, amount) VALUES ($1, $2, $3)', [1, 2, 100]]
]);
console.log(`Transfer ID: ${results[2].lastInsertId}`);
```

**How it works:**

* Automatically executes `BEGIN` before running statements
* Executes all statements in order
* Commits with `COMMIT` if all statements succeed
* Rolls back with `ROLLBACK` if any statement fails
* The write connection is held for the entire transaction, ensuring atomicity
* Errors are thrown after rollback, preserving the original error message

### Closing Connections

Expand Down Expand Up @@ -465,8 +460,8 @@ const filtered = await db.fetchAll<User[]>(
)
```

> **Important:** Do NOT use `execute()` for read-only queries. It will return
> an error. Always use `fetchAll()` or `fetchOne()` for reads.
> **Note:** Use `execute()` and `executeTransaction()` for write operations.
> For SELECT queries, use `fetchAll()` or `fetchOne()`.

## Configuration

Expand Down Expand Up @@ -541,7 +536,7 @@ await Database.closeAll()

#### Instance Methods

##### `execute(query: string, bindValues?: unknown[]): Promise<QueryResult>`
##### `execute(query: string, bindValues?: unknown[]): Promise<WriteQueryResult>`

Execute a write query (INSERT, UPDATE, DELETE, CREATE, etc.).

Expand Down Expand Up @@ -602,9 +597,9 @@ await db.remove()
### TypeScript Interfaces

```typescript
interface QueryResult {
interface WriteQueryResult {
rowsAffected: number // Number of rows modified
lastInsertId: number // ROWID of last inserted row
lastInsertId: number // ROWID of last inserted row (not set for WITHOUT ROWID tables, returns 0)
}

interface CustomConfig {
Expand Down
2 changes: 1 addition & 1 deletion api-iife.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
fn main() {
// TODO: Add commands to the plugin
tauri_plugin::Builder::new(&["hello"]).build();
}
137 changes: 57 additions & 80 deletions guest-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,17 @@ import { invoke } from '@tauri-apps/api/core'
*/
export type SqlValue = string | number | boolean | null | Uint8Array

export interface QueryResult {
/** The number of rows affected by the query. */
/**
* Result returned from write operations (INSERT, UPDATE, DELETE, etc.).
*/
export interface WriteQueryResult {
/** The number of rows affected by the write operation. */
rowsAffected: number
/** The last inserted row ID (SQLite ROWID). */
/**
* The last inserted row ID (SQLite ROWID).
* Only set for INSERT operations on tables with a ROWID.
* Tables created with WITHOUT ROWID will not set this value (returns 0).
*/
lastInsertId: number
}

Expand Down Expand Up @@ -107,11 +114,9 @@ export default class Database {
* **execute**
*
* Executes a write query against the database (INSERT, UPDATE, DELETE, etc.).
* This method is specifically for mutations that modify data.
* This method is for mutations that modify data.
*
* **Important:** Do NOT use this for SELECT queries. Use `fetchX()` instead.
* Using `execute()` for read queries will trigger an error to prevent unnecessary
* write mode initialization.
* For SELECT queries, use `fetchAll()` or `fetchOne()` instead.
*
* SQLite uses `$1`, `$2`, etc. for parameter binding.
*
Expand All @@ -132,7 +137,7 @@ export default class Database {
* );
* ```
*/
async execute(query: string, bindValues?: SqlValue[]): Promise<QueryResult> {
async execute(query: string, bindValues?: SqlValue[]): Promise<WriteQueryResult> {
const [rowsAffected, lastInsertId] = await invoke<[number, number]>(
'plugin:sqlite|execute',
{
Expand All @@ -147,6 +152,50 @@ export default class Database {
}
}

/**
* **executeTransaction**
*
* Executes multiple write statements atomically within a transaction.
* All statements either succeed together or fail together.
*
* The function automatically:
* - Begins a transaction (BEGIN)
* - Executes all statements in order
* - Commits on success (COMMIT)
* - Rolls back on any error (ROLLBACK)
*
* @param statements - Array of [query, values?] tuples to execute
* @returns Promise that resolves with results for each statement when all complete successfully
* @throws SqliteError if any statement fails (after rollback)
*
* @example
* ```ts
* // Execute multiple inserts atomically
* const results = await db.executeTransaction([
* ['INSERT INTO users (name, email) VALUES ($1, $2)', ['Alice', 'alice@example.com']],
* ['INSERT INTO audit_log (action, user) VALUES ($1, $2)', ['user_created', 'Alice']]
* ]);
* console.log(`User ID: ${results[0].lastInsertId}`);
* console.log(`Log rows affected: ${results[1].rowsAffected}`);
*
* // Mixed operations
* const results = await db.executeTransaction([
* ['UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 1]],
* ['UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 2]],
* ['INSERT INTO transfers (from_id, to_id, amount) VALUES ($1, $2, $3)', [1, 2, 100]]
* ]);
* ```
*/
async executeTransaction(statements: Array<[string, SqlValue[]?]>): Promise<WriteQueryResult[]> {
return await invoke<WriteQueryResult[]>('plugin:sqlite|execute_transaction', {
db: this.path,
statements: statements.map(([query, values]) => ({
query,
values: values ?? []
}))
})
}

/**
* **fetchAll**
*
Expand Down Expand Up @@ -211,78 +260,6 @@ export default class Database {
return result
}

/**
* **beginTransaction**
*
* Begins a new database transaction. All subsequent operations will be
* part of this transaction until `commitTransaction()` or `rollbackTransaction()`
* is called.
*
* Transactions provide atomicity - either all operations succeed or all are rolled back.
*
* @example
* ```ts
* await db.beginTransaction();
* try {
* await db.execute('INSERT INTO users (name) VALUES ($1)', ['Alice']);
* await db.execute('INSERT INTO logs (action) VALUES ($1)', ['user_created']);
* await db.commitTransaction();
* } catch (error) {
* await db.rollbackTransaction();
* throw error;
* }
* ```
*/
async beginTransaction(): Promise<void> {
await invoke('plugin:sqlite|begin_transaction', {
db: this.path
})
}

/**
* **commitTransaction**
*
* Commits the current transaction, making all changes permanent.
*
* @example
* ```ts
* await db.beginTransaction();
* await db.execute('INSERT INTO users (name) VALUES ($1)', ['Alice']);
* await db.execute('INSERT INTO logs (action) VALUES ($1)', ['user_created']);
* await db.commitTransaction();
* ```
*/
async commitTransaction(): Promise<void> {
await invoke('plugin:sqlite|commit_transaction', {
db: this.path
})
}

/**
* **rollbackTransaction**
*
* Rolls back the current transaction, discarding all changes made since
* `beginTransaction()` was called.
*
* @example
* ```ts
* await db.beginTransaction();
* try {
* await db.execute('INSERT INTO users (name) VALUES ($1)', ['Alice']);
* await db.execute('INSERT INTO logs (action) VALUES ($1)', ['user_created']);
* await db.commitTransaction();
* } catch (error) {
* await db.rollbackTransaction();
* throw error;
* }
* ```
*/
async rollbackTransaction(): Promise<void> {
await invoke('plugin:sqlite|rollback_transaction', {
db: this.path
})
}

/**
* **close**
*
Expand Down
Loading