Skip to content

Commit 3c4a346

Browse files
committed
feat: clarify known errors and allowed sql types
1 parent 7895e29 commit 3c4a346

4 files changed

Lines changed: 172 additions & 10 deletions

File tree

README.md

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,32 @@ const db = await Database.load('mydb.db', {
173173
> **Note:** Database paths are relative to the app's config directory. Unlike
174174
> `tauri-plugin-sql`, no `sqlite:` prefix is needed.
175175
176+
### Parameter Binding and Types
177+
178+
All query methods (`execute`, `fetchAll`, `fetchOne`) support parameter binding using
179+
the `$1`, `$2`, etc. syntax. Values must be of type `SqlValue`:
180+
181+
```typescript
182+
type SqlValue = string | number | boolean | null | Uint8Array
183+
```
184+
185+
Supported SQLite types:
186+
187+
* **TEXT** - `string` values (also used for DATE, TIME, DATETIME)
188+
* **INTEGER** - `number` values (integers)
189+
* **REAL** - `number` values (floating point)
190+
* **BOOLEAN** - `boolean` values
191+
* **NULL** - `null` value
192+
* **BLOB** - `Uint8Array` for binary data
193+
194+
```typescript
195+
// Example with different types
196+
await db.execute(
197+
'INSERT INTO data (text, int, real, bool, blob) VALUES ($1, $2, $3, $4, $5)',
198+
['hello', 42, 3.14, true, new Uint8Array([1, 2, 3])]
199+
)
200+
```
201+
176202
### Executing Write Operations
177203
178204
Use `execute()` for INSERT, UPDATE, DELETE, or any query that modifies data:
@@ -205,9 +231,46 @@ const deleteResult = await db.execute(
205231
)
206232
```
207233

234+
### Handling Errors
235+
236+
Handle database errors gracefully using structured error responses:
237+
238+
```typescript
239+
import type { SqliteError } from '@silvermine/tauri-plugin-sqlite';
240+
241+
try {
242+
await db.execute(
243+
'INSERT INTO users (id, name) VALUES ($1, $2)',
244+
[1, 'Alice']
245+
);
246+
} catch (err) {
247+
const error = err as SqliteError;
248+
249+
// Check error code for specific handling
250+
if (error.code.startsWith('SQLITE_CONSTRAINT')) {
251+
console.error('Constraint violation:', error.message);
252+
} else if (error.code === 'DATABASE_NOT_LOADED') {
253+
console.error('Database not initialized');
254+
} else {
255+
console.error('Database error:', error.code, error.message);
256+
}
257+
}
258+
```
259+
260+
Common error codes include:
261+
262+
* `SQLITE_CONSTRAINT` - Constraint violation (unique, foreign key, etc.)
263+
* `SQLITE_NOTFOUND` - Table or column not found
264+
* `DATABASE_NOT_LOADED` - Database hasn't been loaded yet
265+
* `INVALID_PATH` - Invalid database path
266+
* `IO_ERROR` - File system error
267+
* `MIGRATION_ERROR` - Migration failed
268+
* `READ_ONLY_QUERY_IN_EXECUTE` - Attempted to use execute() for a read-only query
269+
* `MULTIPLE_ROWS_RETURNED` - `fetchOne()` query returned multiple rows
270+
208271
### Executing SELECT Queries
209272

210-
Use `fetchAll()` for all read operations:
273+
Use `fetchAll()` or `fetchOne()` for all read operations:
211274

212275
```typescript
213276
type User = {id: number, name: string, email: string}
@@ -234,6 +297,11 @@ if (user) {
234297
}
235298
```
236299

300+
> **Note:** `fetchOne()` validates that the query returns exactly 0 or 1 rows. If your
301+
> query returns multiple rows, it will throw a `MULTIPLE_ROWS_RETURNED` error. This helps
302+
> catch bugs where a query unexpectedly returns multiple results. Use `fetchAll()` if you
303+
> expect multiple rows.
304+
237305
### Closing Connections
238306

239307
```typescript

api-iife.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

guest-js/index.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
11
import { invoke } from '@tauri-apps/api/core'
22

3+
/**
4+
* Valid SQLite parameter binding value types.
5+
*
6+
* SQLite supports a limited set of types for parameter binding:
7+
* - `string` - TEXT, DATE, TIME, DATETIME
8+
* - `number` - INTEGER, REAL
9+
* - `boolean` - BOOLEAN
10+
* - `null` - NULL
11+
* - `Uint8Array` - BLOB (binary data)
12+
*/
13+
export type SqlValue = string | number | boolean | null | Uint8Array
14+
315
export interface QueryResult {
416
/** The number of rows affected by the query. */
517
rowsAffected: number
618
/** The last inserted row ID (SQLite ROWID). */
719
lastInsertId: number
820
}
921

22+
/**
23+
* Structured error returned from SQLite operations.
24+
*
25+
* All errors thrown by the plugin will have this structure.
26+
*/
27+
export interface SqliteError {
28+
/** Machine-readable error code (e.g., "SQLITE_CONSTRAINT", "DATABASE_NOT_LOADED") */
29+
code: string
30+
/** Human-readable error message */
31+
message: string
32+
}
33+
1034
/**
1135
* Custom configuration for SQLite database connection
1236
*/
@@ -108,7 +132,7 @@ export default class Database {
108132
* );
109133
* ```
110134
*/
111-
async execute(query: string, bindValues?: unknown[]): Promise<QueryResult> {
135+
async execute(query: string, bindValues?: SqlValue[]): Promise<QueryResult> {
112136
const [rowsAffected, lastInsertId] = await invoke<[number, number]>(
113137
'plugin:sqlite|execute',
114138
{
@@ -145,7 +169,7 @@ export default class Database {
145169
* );
146170
* ```
147171
*/
148-
async fetchAll<T>(query: string, bindValues?: unknown[]): Promise<T> {
172+
async fetchAll<T>(query: string, bindValues?: SqlValue[]): Promise<T> {
149173
const result = await invoke<T>('plugin:sqlite|fetch_all', {
150174
db: this.path,
151175
query,
@@ -177,8 +201,8 @@ export default class Database {
177201
* }
178202
* ```
179203
*/
180-
async fetchOne<T>(query: string, bindValues?: unknown[]): Promise<T | undefined> {
181-
const result = await invoke<T | undefined>('plugin:sqlite|select_one', {
204+
async fetchOne<T>(query: string, bindValues?: SqlValue[]): Promise<T | undefined> {
205+
const result = await invoke<T | undefined>('plugin:sqlite|fetch_one', {
182206
db: this.path,
183207
query,
184208
values: bindValues ?? []

src/error.rs

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,89 @@
11
use serde::{Serialize, Serializer};
22

3+
/// Result type alias for plugin operations.
34
pub type Result<T> = std::result::Result<T, Error>;
45

6+
/// Structured error response for frontend.
7+
#[derive(Serialize)]
8+
struct ErrorResponse {
9+
code: String,
10+
message: String,
11+
}
12+
13+
/// Error types for the SQLite plugin.
514
#[derive(Debug, thiserror::Error)]
615
pub enum Error {
7-
// TODO: Add specific error variants for different failure cases
8-
#[error("{0}")]
9-
Custom(String),
16+
/// Error from SQLx operations.
17+
#[error(transparent)]
18+
Sqlx(#[from] sqlx::Error),
19+
20+
/// Error from the connection manager.
21+
#[error(transparent)]
22+
ConnectionManager(#[from] sqlx_sqlite_conn_mgr::Error),
23+
24+
/// Error from database migrations.
25+
#[error(transparent)]
26+
Migration(#[from] sqlx::migrate::MigrateError),
27+
28+
/// Invalid database path provided.
29+
#[error("invalid database path: {0}")]
30+
InvalidPath(String),
31+
32+
/// Attempted to access a database that hasn't been loaded.
33+
#[error("database {0} not loaded")]
34+
DatabaseNotLoaded(String),
35+
36+
/// SQLite type that cannot be mapped to JSON.
37+
#[error("unsupported datatype: {0}")]
38+
UnsupportedDatatype(String),
39+
40+
/// I/O error when accessing database files.
41+
#[error("io error: {0}")]
42+
Io(#[from] std::io::Error),
43+
44+
/// Read-only query executed with execute command.
45+
#[error("execute() should not be used for read-only queries. Use fetchX() instead.")]
46+
ReadOnlyQueryInExecute,
47+
48+
/// Multiple rows returned from fetchOne query.
49+
#[error("fetchOne() query returned {0} rows, expected 0 or 1")]
50+
MultipleRowsReturned(usize),
51+
}
52+
53+
impl Error {
54+
/// Extract a structured error code from the error type.
55+
///
56+
/// This provides machine-readable error codes for frontend error handling.
57+
fn error_code(&self) -> String {
58+
match self {
59+
Error::Sqlx(e) => {
60+
// Extract SQLite error codes from sqlx errors
61+
if let Some(code) = e.as_database_error().and_then(|db_err| db_err.code()) {
62+
return format!("SQLITE_{}", code);
63+
}
64+
"SQLX_ERROR".to_string()
65+
}
66+
Error::ConnectionManager(_) => "CONNECTION_ERROR".to_string(),
67+
Error::Migration(_) => "MIGRATION_ERROR".to_string(),
68+
Error::InvalidPath(_) => "INVALID_PATH".to_string(),
69+
Error::DatabaseNotLoaded(_) => "DATABASE_NOT_LOADED".to_string(),
70+
Error::UnsupportedDatatype(_) => "UNSUPPORTED_DATATYPE".to_string(),
71+
Error::Io(_) => "IO_ERROR".to_string(),
72+
Error::ReadOnlyQueryInExecute => "READ_ONLY_QUERY_IN_EXECUTE".to_string(),
73+
Error::MultipleRowsReturned(_) => "MULTIPLE_ROWS_RETURNED".to_string(),
74+
}
75+
}
1076
}
1177

1278
impl Serialize for Error {
1379
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1480
where
1581
S: Serializer,
1682
{
17-
serializer.serialize_str(self.to_string().as_ref())
83+
let response = ErrorResponse {
84+
code: self.error_code(),
85+
message: self.to_string(),
86+
};
87+
response.serialize(serializer)
1888
}
1989
}

0 commit comments

Comments
 (0)