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.

72 changes: 70 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ SQLite database interface for Tauri applications using
> "_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 (coming soon)
* **Resource Management**: Proper cleanup on application exit (coming soon)
* **Migration Support**: SQLx's migration framework
* **Resource Management**: Proper cleanup on application exit

## Architecture

Expand Down Expand Up @@ -96,6 +96,74 @@ fn main() {
}
```

### Migrations

This plugin uses [SQLx's migration system][sqlx-migrate]. Create numbered `.sql`
files in a migrations directory:

[sqlx-migrate]: https://docs.rs/sqlx/latest/sqlx/macro.migrate.html

```text
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:

```rust
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:

```typescript
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.

```typescript
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

```typescript
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.

5 changes: 4 additions & 1 deletion crates/sqlx-sqlite-conn-mgr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ edition = "2024"
rust-version = "1.89"

[dependencies]
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] }
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite", "migrate"] }
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
tracing = { version = "0.1.41", default-features = false, features = ["std", "release_max_level_off"] }
serde = { version = "1.0.228", features = ["derive"] }

[dev-dependencies]
tempfile = "3.23.0"
29 changes: 28 additions & 1 deletion crates/sqlx-sqlite-conn-mgr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,32 @@ let config = SqliteDatabaseConfig {
let db = SqliteDatabase::connect("example.db", Some(config)).await?;
```

### Migrations

Run [SQLx migrations][sqlx-migrate] directly:

[sqlx-migrate]: https://docs.rs/sqlx/latest/sqlx/macro.migrate.html

```rust
use sqlx_sqlite_conn_mgr::SqliteDatabase;

// Embed migrations at compile time (reads ./migrations/*.sql)
static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations");

async fn run() -> Result<(), sqlx_sqlite_conn_mgr::Error> {
let db = SqliteDatabase::connect("example.db", None).await?;
db.run_migrations(&MIGRATOR).await?;
Ok(())
}
```

Migrations are tracked in `_sqlx_migrations` — calling `run_migrations()` multiple
times is safe (already-applied migrations are skipped).

> **Note:** When using the Tauri plugin, migrations are handled automatically via
> `Builder::add_migrations()`. The plugin starts migrations at setup and waits for
> completion when `load()` is called.

## API Reference

### `SqliteDatabase`
Expand All @@ -75,8 +101,9 @@ let db = SqliteDatabase::connect("example.db", Some(config)).await?;
| `connect(path, config)` | Connect/create database, returns cached `Arc` if already open |
| `read_pool()` | Get read-only pool reference |
| `acquire_writer()` | Acquire exclusive `WriteGuard` (enables WAL on first call) |
| `run_migrations(migrator)` | Run pending migrations from a `Migrator` |
| `close()` | Close and remove from cache |
| `close_and_remove()` | Close and delete database files (.db, .db-wal, .db-shm) |
| `remove()` | Close and delete database files (.db, .db-wal, .db-shm) |

### `WriteGuard`

Expand Down
38 changes: 38 additions & 0 deletions crates/sqlx-sqlite-conn-mgr/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,44 @@ impl SqliteDatabase {
Ok(WriteGuard::new(conn))
}

/// Run database migrations using the provided migrator
///
/// This method runs all pending migrations from the provided `Migrator`.
/// Migrations are executed using the write connection to ensure exclusive access.
/// WAL mode is enabled automatically before running migrations.
///
/// SQLx tracks applied migrations in a `_sqlx_migrations` table, so calling
/// this method multiple times is safe - already-applied migrations are skipped.
///
/// # Arguments
///
/// * `migrator` - A reference to a `Migrator` containing the migrations to run.
/// Typically created using `sqlx::migrate!()` macro.
///
/// # Example
///
/// ```ignore
/// use sqlx_sqlite_conn_mgr::SqliteDatabase;
///
/// // sqlx::migrate! is evaluated at compile time
/// static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations");
///
/// let db = SqliteDatabase::connect("test.db", None).await?;
/// db.run_migrations(&MIGRATOR).await?;
/// ```
pub async fn run_migrations(&self, migrator: &sqlx::migrate::Migrator) -> Result<()> {
// Ensure WAL mode is initialized via acquire_writer
// (WriteGuard dropped immediately, returning connection to pool)
{
let _writer = self.acquire_writer().await?;
}

// Migrator acquires its own connection from the write pool
migrator.run(&self.write_conn).await?;

Ok(())
}

/// Close the database and clean up resources
///
/// This closes all connections in the pool and removes the database from the cache.
Expand Down
4 changes: 4 additions & 0 deletions crates/sqlx-sqlite-conn-mgr/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ pub enum Error {
#[error("Sqlx error: {0}")]
Sqlx(#[from] sqlx::Error),

/// Migration error from the sqlx migrate framework
#[error("Migration error: {0}")]
Migration(#[from] sqlx::migrate::MigrateError),

/// Database has been closed and cannot be used
#[error("Database has been closed")]
DatabaseClosed,
Expand Down
4 changes: 4 additions & 0 deletions crates/sqlx-sqlite-conn-mgr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//! - **[`SqliteDatabase`]**: Main database type with separate read and write connection pools
//! - **[`SqliteDatabaseConfig`]**: Configuration for connection pool settings
//! - **[`WriteGuard`]**: RAII guard ensuring exclusive write access
//! - **[`Migrator`]**: Re-exported from sqlx for running database migrations
//! - **[`Error`]**: Error type for database operations
//!
//! ## Architecture
Expand Down Expand Up @@ -71,5 +72,8 @@ pub use database::SqliteDatabase;
pub use error::Error;
pub use write_guard::WriteGuard;

// Re-export sqlx migrate types for convenience
pub use sqlx::migrate::Migrator;

/// A type alias for Results with our custom Error type
pub type Result<T> = std::result::Result<T, Error>;
Loading