Skip to content

silvermine/tauri-plugin-sqlite

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

49 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tauri Plugin SQLite

CI

SQLite database interface for Tauri applications using sqlx and sqlx-sqlite-conn-mgr.

Features

  • Optimized Connection Pooling: Separate read and write pools for concurrent reads even while writing (configurable pool size and idle timeouts)

  • Write Serialization: Exclusive write connection

    From SQLite docs: "SQLite ... will only allow one writer at any instant in time."

  • WAL Mode: Enabled automatically on first write operation

  • Type Safety: Full TypeScript bindings

  • Migration Support: SQLx's migration framework

  • Resource Management: Proper cleanup on application exit

Architecture

Operation Type Method Pool Used Concurrency
SELECT (multiple) fetchAll() Read pool Multiple concurrent
SELECT (single) fetchOne() Read pool Multiple concurrent
INSERT/UPDATE/DELETE execute() Write connection Serialized
DDL (CREATE, etc.) execute() Write connection Serialized

See crates/sqlx-sqlite-conn-mgr/README.md for connection manager internals.

Installation

Requires Rust 1.77.2 or later

Rust

src-tauri/Cargo.toml:

[dependencies]
tauri-plugin-sqlite = { git = "https://github.com/silvermine/tauri-plugin-sqlite" }

JavaScript/TypeScript

npm install @silvermine/tauri-plugin-sqlite

Permissions

Add to src-tauri/capabilities/default.json:

{
   "permissions": ["sqlite:default"]
}

Or specify individual permissions:

{
   "permissions": [
      "sqlite:allow-load",
      "sqlite:allow-select",
      "sqlite:allow-select-one",
      "sqlite:allow-execute-write",
      "sqlite:allow-close",
      "sqlite:allow-close-all",
      "sqlite:allow-remove"
   ]
}

Usage

Setup

Register the plugin in your Tauri application:

src-tauri/src/lib.rs:

fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_sqlite::Builder::new().build())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Migrations

This plugin uses SQLx's migration system. Create numbered .sql files in a migrations directory:

src-tauri/migrations/
├── 0001_create_users.sql
├── 0002_add_email_column.sql
└── 0003_create_posts.sql

Register migrations using SQLx's migrate!() macro, which embeds them at compile time:

use tauri_plugin_sqlite::Builder;

fn main() {
    tauri::Builder::default()
        .plugin(
            Builder::new()
                .add_migrations("main.db", sqlx::migrate!("./migrations"))
                .build()
        )
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Timing: Migrations start automatically at plugin setup (non-blocking). When TypeScript calls Database.load(), it waits for migrations to complete before returning. If migrations fail, load() returns an error. Applied migrations are tracked in _sqlx_migrations — re-running is safe and idempotent.

Retrieving Migration Events

Use getMigrationEvents() to retrieve cached events:

import Database from '@silvermine/tauri-plugin-sqlite'

const db = await Database.load('mydb.db')

// Get all migration events (including ones emitted before listener could be registered)
const events = await db.getMigrationEvents()
for (const event of events) {
   console.log(`${event.status}: ${event.dbPath}`)
   if (event.status === 'failed') {
      console.error(`Migration error: ${event.error}`)
   }
}

Optional: Listen for real-time events, globally. May miss early events due the Rust layer completing some or all migrations before the frontend subscription initializes.

import { listen } from '@tauri-apps/api/event'
import type { MigrationEvent } from '@silvermine/tauri-plugin-sqlite'

await listen<MigrationEvent>('sqlite:migration', (event) => {
   const { dbPath, status, migrationCount, error } = event.payload
   // status: 'running' | 'completed' | 'failed'
})

Connecting

import Database from '@silvermine/tauri-plugin-sqlite'

// Path is relative to app config directory (no sqlite: prefix needed)
const db = await Database.load('mydb.db')

// With custom configuration
const db = await Database.load('mydb.db', {
   maxReadConnections: 10, // default: 6
   idleTimeoutSecs: 60     // default: 30
})

// Lazy initialization (connects on first query)
const db = Database.get('mydb.db')

Parameter Binding

All query methods use $1, $2, etc. syntax with SqlValue types:

type SqlValue = string | number | boolean | null | Uint8Array
SQLite Type TypeScript Type Notes
TEXT string Also for DATE, TIME, DATETIME
INTEGER number Integers preserved up to i64 range
REAL number Floating point
BOOLEAN boolean
NULL null
BLOB Uint8Array Binary data

Note: JavaScript safely represents integers up to ±2^53 - 1. The plugin binds integers as SQLite's INTEGER type (i64), maintaining full precision within that range.

Write Operations

Use execute() for INSERT, UPDATE, DELETE, CREATE, etc.:

await db.execute(
   'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)'
)

const result = await db.execute(
   'INSERT INTO users (name, email) VALUES ($1, $2)',
   ['Alice', 'alice@example.com']
)
console.log(result.rowsAffected, result.lastInsertId)

Read Operations

type User = { id: number; name: string; email: string }

// Multiple rows
const users = await db.fetchAll<User[]>(
   'SELECT * FROM users WHERE email LIKE $1',
   ['%@example.com']
)

// Single row (returns undefined if not found, throws if multiple rows)
const user = await db.fetchOne<User>(
   'SELECT * FROM users WHERE id = $1',
   [42]
)

Transactions

Execute multiple statements atomically:

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]]
])

Transactions use BEGIN IMMEDIATE, commit on success, and rollback on any failure.

Error Handling

import type { SqliteError } from '@silvermine/tauri-plugin-sqlite'

try {
   await db.execute('INSERT INTO users (id) VALUES ($1)', [1])
} catch (err) {
   const error = err as SqliteError
   console.error(error.code, error.message)
}

Common error codes:

  • SQLITE_CONSTRAINT - Constraint violation (unique, foreign key, etc.)
  • SQLITE_NOTFOUND - Table or column not found
  • DATABASE_NOT_LOADED - Database hasn't been loaded yet
  • INVALID_PATH - Invalid database path
  • IO_ERROR - File system error
  • MIGRATION_ERROR - Migration failed
  • MULTIPLE_ROWS_RETURNED - fetchOne() returned multiple rows

Closing and Removing

await db.close()           // Close this connection
await Database.closeAll()  // Close all connections
await db.remove()          // Close and DELETE database file(s) - irreversible!

API Reference

Static Methods

Method Description
Database.load(path, config?) Connect and return Database instance (or existing)
Database.get(path) Get instance without connecting (lazy init)
Database.closeAll() Close all database connections

Instance Methods

Method Description
execute(query, values?) Execute write query, returns { rowsAffected, lastInsertId }
executeTransaction(statements) Execute statements atomically
fetchAll<T>(query, values?) Execute SELECT, return all rows
fetchOne<T>(query, values?) Execute SELECT, return single row or undefined
close() Close connection, returns true if was loaded
remove() Close and delete database file(s), returns true if was loaded

Types

interface WriteQueryResult {
   rowsAffected: number
   lastInsertId: number  // 0 for WITHOUT ROWID tables
}

interface CustomConfig {
   maxReadConnections?: number  // default: 6
   idleTimeoutSecs?: number     // default: 30
}

interface SqliteError {
   code: string
   message: string
}

Tracing and Logging

The plugin uses tracing with release_max_level_off, so all logs are compiled out of release builds.

To see logs during development:

[dependencies]
tracing = { version = "0.1.41", default-features = false, features = ["std", "release_max_level_off"] }
tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter"] }
#[cfg(debug_assertions)]
fn init_tracing() {
    use tracing_subscriber::{fmt, EnvFilter};
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("trace"));
    fmt().with_env_filter(filter).compact().init();
}

#[cfg(not(debug_assertions))]
fn init_tracing() {}

fn main() {
    init_tracing();
    tauri::Builder::default()
        .plugin(tauri_plugin_sqlite::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Development

This project follows Silvermine standardization guidelines.

npm install              # Install dependencies
npm run build            # Build TypeScript bindings
cargo build              # Build Rust plugin
cargo test               # Run tests
npm run standards        # Lint and standards checks

License

MIT

Contributing

Contributions welcome! Follow the established coding standards and commit conventions.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •