| Crates.io | ic-sql-migrate |
| lib.rs | ic-sql-migrate |
| version | 0.0.5 |
| created_at | 2025-08-30 14:24:42.589777+00 |
| updated_at | 2025-10-17 08:28:37.624641+00 |
| description | A lightweight database migration library for Internet Computer (ICP) canisters with SQLite and Turso support. |
| homepage | https://github.com/kristoferlund/ic-sql-migrate |
| repository | https://github.com/kristoferlund/ic-sql-migrate |
| max_upload_size | |
| id | 1817665 |
| size | 115,724 |
A lightweight database migration library for Internet Computer (ICP) canisters with support for SQLite (via ic-rusqlite) and Turso databases.
IMPORTANT: You must enable exactly one database feature (sqlite or turso) for this library to work. There is no default feature.
In addition to having the Rust toolchain setup and dfx, you need to install the wasi2ic tool (for SQLite only) that replaces WebAssembly System Interface (WASI) specific function calls with their corresponding polyfill implementations:
cargo install wasi2ic
For SQLite support (most common for ICP):
[dependencies]
ic-sql-migrate = { version = "0.0.4", features = ["sqlite"] }
ic-rusqlite = { version = "0.4.2", features = ["precompiled"], default-features = false }
ic-cdk = "0.18.7"
[build-dependencies]
ic-sql-migrate = "0.0.4"
For Turso support:
[dependencies]
ic-sql-migrate = { version = "0.0.4", features = ["turso"] }
turso = "0.1.4"
ic-cdk = "0.18.7"
[build-dependencies]
ic-sql-migrate = "0.0.4"
Important:
sqlite or turso)For SQLite support, you need to configure your dfx.json to compile for the wasm32-wasip1 target and use wasi2ic to process the binary:
{
"canisters": {
"your_canister": {
"candid": "your_canister.did",
"package": "your_canister",
"type": "custom",
"build": [
"cargo build --target wasm32-wasip1 --release",
"wasi2ic target/wasm32-wasip1/release/your_canister.wasm target/wasm32-wasip1/release/your_canister-wasi2ic.wasm"
],
"wasm": "target/wasm32-wasip1/release/your_canister-wasi2ic.wasm"
}
}
}
This configuration:
wasm32-wasip1 target (required for SQLite)wasi2ic to convert WASI function calls to IC-compatible polyfillsNote: Turso canisters use the standard wasm32-unknown-unknown target and don't require wasi2ic processing.
Create a migrations/ directory with SQL files. Each migration should be:
000_initial.sql, 001_add_users.sql)IF NOT EXISTS clauses)Example migration file:
-- migrations/000_initial.sql
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT
);
Use the Builder to configure discovery of migrations and seeds at compile time:
fn main() {
ic_sql_migrate::Builder::new()
.with_migrations_dir("migrations")
.with_seeds_dir("src/seeds")
.build()
.unwrap();
}
The Builder automatically handles missing directories by generating empty arrays.
use ic_cdk::{init, post_upgrade, pre_upgrade};
use ic_rusqlite::{close_connection, with_connection, Connection};
mod seeds;
static MIGRATIONS: &[ic_sql_migrate::Migration] = ic_sql_migrate::include_migrations!();
fn run_migrations_and_seeds() {
with_connection(|mut conn| {
let conn: &mut Connection = &mut conn;
ic_sql_migrate::sqlite::migrate(conn, MIGRATIONS).unwrap();
ic_sql_migrate::sqlite::seed(conn, seeds::SEEDS).unwrap();
});
}
#[init]
fn init() {
run_migrations_and_seeds();
}
#[pre_upgrade]
fn pre_upgrade() {
close_connection();
}
#[post_upgrade]
fn post_upgrade() {
run_migrations_and_seeds();
}
use ic_cdk::{init, post_upgrade, pre_upgrade};
use turso::Connection;
mod seeds;
static MIGRATIONS: &[ic_sql_migrate::Migration] = ic_sql_migrate::include_migrations!();
thread_local! {
static CONNECTION: RefCell<Option<Connection>> = const { RefCell::new(None) };
}
async fn get_connection() -> Connection {
if let Some(conn) = CONNECTION.with_borrow(|c| c.clone()) {
conn
} else {
// Initialize connection
init_db().await
}
}
async fn run_migrations_and_seeds() {
let mut conn = get_connection().await;
ic_sql_migrate::turso::migrate(&mut conn, MIGRATIONS).await.unwrap();
ic_sql_migrate::turso::seed(&mut conn, seeds::SEEDS).await.unwrap();
}
#[init]
async fn init() {
run_migrations_and_seeds().await;
}
#[post_upgrade]
async fn post_upgrade() {
run_migrations_and_seeds().await;
}
In addition to schema migrations, this library supports data seeding using Rust functions. Seeds are useful for populating initial data, test data, or reference data.
Create seed files in the src/seeds/ directory (or a custom directory specified in build.rs). Each seed file is a regular Rust module (.rs file) that exports a seed function.
Seed files are executed in alphabetical order by filename, so use a sortable prefix:
src/seeds/seed_001_initial_users.rssrc/seeds/seed_002_categories.rsFile: src/seeds/seed_001_initial_users.rs
use ic_sql_migrate::MigrateResult;
use ic_rusqlite::Connection;
pub fn seed(conn: &Connection) -> MigrateResult<()> {
conn.execute(
"INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')",
[],
)?;
conn.execute(
"INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com')",
[],
)?;
Ok(())
}
File: src/seeds/seed_001_initial_users.rs
use ic_sql_migrate::MigrateResult;
use turso::Connection;
use std::pin::Pin;
use std::future::Future;
pub fn seed(conn: &Connection) -> Pin<Box<dyn Future<Output = MigrateResult<()>> + Send>> {
let conn = conn.clone();
Box::pin(async move {
conn.execute(
"INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')",
(),
).await?;
Ok(())
})
}
Step 1: Add the seeds module to your src/lib.rs:
mod seeds; // This is auto-generated by the build script
Step 2: Use seeds in your lifecycle functions (see examples above)
seed_001_initial_users.rs)pub fn seed() functionsrc/seeds/, giving you full IDE support and access to your app codecrate::For SQLite:
pub fn migrate(conn: &mut rusqlite::Connection, migrations: &[Migration]) -> MigrateResult<()>
Executes all pending migrations synchronously.
For Turso:
pub async fn migrate(conn: &mut turso::Connection, migrations: &[Migration]) -> MigrateResult<()>
Executes all pending migrations asynchronously.
For SQLite:
pub fn seed(conn: &mut rusqlite::Connection, seeds: &[Seed]) -> MigrateResult<()>
Executes all pending seeds synchronously.
For Turso:
pub async fn seed(conn: &mut turso::Connection, seeds: &[Seed]) -> MigrateResult<()>
Executes all pending seeds asynchronously.
Builder::new()Creates a new builder with default settings.
// Use defaults (migrations/ and src/seeds/)
ic_sql_migrate::Builder::new().build().unwrap();
// Custom directories
ic_sql_migrate::Builder::new()
.with_migrations_dir("db/migrations")
.with_seeds_dir("src/db/seeds")
.build()
.unwrap();
Note: Missing directories are handled automatically - they generate empty arrays.
ic_sql_migrate::include_migrations!()Includes all migrations discovered by the Builder at compile time.
static MIGRATIONS: &[ic_sql_migrate::Migration] = ic_sql_migrate::include_migrations!();
ic_sql_migrate::seeds!()Helper macro to manually create a static array of seeds (for advanced use cases).
static SEEDS: &[ic_sql_migrate::Seed] = ic_sql_migrate::seeds![
Seed::new("001_users", my_seed_fn),
];
Note: In most cases, seeds are auto-discovered from src/seeds/ and accessed via the generated mod seeds module.
Migrationpub struct Migration {
pub id: &'static str, // Unique identifier (filename without extension)
pub sql: &'static str, // SQL statements to execute
}
Seedpub struct Seed {
pub id: &'static str, // Unique identifier
pub seed_fn: SeedFn, // Function to execute
}
ErrorCustom error type that wraps database-specific errors and migration/seed failures.
The library automatically creates these tracking tables:
Migrations Table:
CREATE TABLE _migrations (
id TEXT PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
Seeds Table:
CREATE TABLE _seeds (
id TEXT PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
Build Time: Builder in build.rs scans your migrations and seeds directories
src/seeds/mod.rs with a SEEDS constantWASI to IC Conversion: The wasi2ic tool converts WASI-specific function calls to IC-compatible polyfills (SQLite only)
Canister Init/Upgrade:
init: Calls migrate() to set up the database schema, then calls seed() to populate initial datapost_upgrade: Calls migrate() and seed() to apply any new migrations and seedsMigration Tracking:
_migrations table is automatically created to track which migrations have been appliedSeed Tracking:
_seeds table is automatically created to track which seeds have been appliedTransaction Safety: All pending migrations and seeds run in transactions. If any operation fails, changes are rolled back, ensuring data consistency.
Naming Convention: Use sequential numbering like 001_description.sql, 002_description.sql to ensure correct execution order
Forward-Only: This library only supports forward migrations (no rollbacks). Plan your schema changes carefully.
Idempotent SQL: While migrations are tracked, write idempotent SQL when possible using IF NOT EXISTS clauses
Small Changes: Keep each migration focused on a single logical change
Test Locally: Always test migrations using dfx deploy --local before mainnet deployment
Document Changes: Include comments in your migration files explaining what each migration does
You can only use one database backend at a time. Ensure exactly one of sqlite or turso is enabled in your Cargo.toml.
Ensure your migrations directory exists and contains .sql files, and that build.rs is properly configured to point to it.
Install the wasi2ic tool:
cargo install wasi2ic
Check the canister logs with dfx canister logs <canister_name> for detailed error messages. Common issues:
IF NOT EXISTS)Verify:
src/seeds/ directory (or configured directory)pub fn seed() functionmod seeds;Complete working examples are available in the repository:
examples/sqlite - Advanced example with the Chinook database and complex queriesexamples/turso - Turso integration example with basic migrationscd examples/sqlite
dfx start --clean
dfx deploy
dfx canister call sqlite-example verify_migrations
cd examples/turso
dfx start --clean
dfx deploy
dfx canister call turso run
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
Kristofer Lund