ic-sql-migrate
A lightweight database migration library for Internet Computer (ICP) canisters with support for SQLite (via ic-rusqlite) and Turso databases.
Table of Contents
- Installation
- Deployment Configuration
- Usage
- Data Seeding
- API Reference
- How It Works
- Migration Best Practices
- Troubleshooting
- Examples
- Differences Between Database Backends
- Contributing
- License
Installation
Prerequisites
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:
Add to Cargo.toml
For SQLite support (most common for ICP):
[]
= { = "0.0.4", = ["sqlite"] }
= { = "0.4.2", = ["precompiled"], = false }
= "0.18.7"
[]
= "0.0.4"
For Turso support:
[]
= { = "0.0.4", = ["turso"] }
= "0.1.4"
= "0.18.7"
[]
= "0.0.4"
Important:
- You MUST choose exactly one database feature (
sqliteorturso) - The features are mutually exclusive (cannot use both)
- There is no default feature - the library will not work without selecting one
Deployment Configuration
dfx.json Setup (Required for SQLite)
For SQLite support, you need to configure your dfx.json to compile for the wasm32-wasip1 target and use wasi2ic to process the binary:
This configuration:
- Compiles your canister for the
wasm32-wasip1target (required for SQLite) - Uses
wasi2icto convert WASI function calls to IC-compatible polyfills - Points dfx to the processed WASM file for deployment
Note: Turso canisters use the standard wasm32-unknown-unknown target and don't require wasi2ic processing.
Usage
1. Create Migration Files
Create a migrations/ directory with SQL files. Each migration should be:
- Numbered sequentially (e.g.,
000_initial.sql,001_add_users.sql) - Idempotent when possible (use
IF NOT EXISTSclauses) - Forward-only (this library doesn't support rollbacks)
Example migration file:
-- migrations/000_initial.sql
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT
);
2. Set Up build.rs
Use the Builder to configure discovery of migrations and seeds at compile time:
The Builder automatically handles missing directories by generating empty arrays.
3. Use in Your Canister
SQLite Example
use ;
use ;
static MIGRATIONS: & = include_migrations!;
Turso Example
use ;
use Connection;
static MIGRATIONS: & = include_migrations!;
thread_local!
async
async
async
async
Data Seeding
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.
Creating Seed Files
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.rs
SQLite Seed Example
File: src/seeds/seed_001_initial_users.rs
use MigrateResult;
use Connection;
Turso Seed Example
File: src/seeds/seed_001_initial_users.rs
use MigrateResult;
use Connection;
use Pin;
use Future;
Using Seeds in Your Canister
Step 1: Add the seeds module to your src/lib.rs:
// This is auto-generated by the build script
Step 2: Use seeds in your lifecycle functions (see examples above)
Seed Best Practices
- Naming Convention: Use sequential numbering with descriptive names (e.g.,
seed_001_initial_users.rs) - One Seed Per File: Each seed file should contain a single
pub fn seed()function - Part of Source Tree: Seeds are in
src/seeds/, giving you full IDE support and access to your app code - Import from Your App: You can import types, functions, and modules from your application using
crate:: - Forward-Only: Seeds do not support rollbacks - once applied, they remain
- Idempotent Functions: Write seed functions that can safely run multiple times if needed
- Alphabetical Order: Seeds are executed alphabetically by filename
- Run After Migrations: Seeds always execute after migrations to ensure schema is ready
API Reference
Core Functions
Migrations
For SQLite:
Executes all pending migrations synchronously.
For Turso:
pub async
Executes all pending migrations asynchronously.
Seeds
For SQLite:
Executes all pending seeds synchronously.
For Turso:
pub async
Executes all pending seeds asynchronously.
Build Script
Builder::new()
Creates a new builder with default settings.
// Use defaults (migrations/ and src/seeds/)
new.build.unwrap;
// Custom directories
new
.with_migrations_dir
.with_seeds_dir
.build
.unwrap;
Note: Missing directories are handled automatically - they generate empty arrays.
Macros
ic_sql_migrate::include_migrations!()
Includes all migrations discovered by the Builder at compile time.
static MIGRATIONS: & = include_migrations!;
ic_sql_migrate::seeds!()
Helper macro to manually create a static array of seeds (for advanced use cases).
static SEEDS: & = seeds!;
Note: In most cases, seeds are auto-discovered from src/seeds/ and accessed via the generated mod seeds module.
Types
Migration
Seed
Error
Custom error type that wraps database-specific errors and migration/seed failures.
Database Schema
The library automatically creates these tracking tables:
Migrations Table:
(
id TEXT PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
Seeds Table:
(
id TEXT PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
How It Works
-
Build Time:
Builderinbuild.rsscans your migrations and seeds directories- Migrations: SQL files embedded as static strings into your canister binary
- Seeds: Rust modules discovered and auto-generated into
src/seeds/mod.rswith aSEEDSconstant
-
WASI to IC Conversion: The
wasi2ictool converts WASI-specific function calls to IC-compatible polyfills (SQLite only) -
Canister Init/Upgrade:
- On
init: Callsmigrate()to set up the database schema, then callsseed()to populate initial data - On
post_upgrade: Callsmigrate()andseed()to apply any new migrations and seeds
- On
-
Migration Tracking:
- A
_migrationstable is automatically created to track which migrations have been applied - Pending migrations are executed in alphabetical order within a transaction
- Each successful migration is recorded to prevent duplicate execution
- A
-
Seed Tracking:
- A
_seedstable is automatically created to track which seeds have been applied - Pending seeds are executed in alphabetical order within transactions
- Each successful seed is recorded to prevent duplicate execution
- A
-
Transaction Safety: All pending migrations and seeds run in transactions. If any operation fails, changes are rolled back, ensuring data consistency.
Migration Best Practices
-
Naming Convention: Use sequential numbering like
001_description.sql,002_description.sqlto 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 EXISTSclauses -
Small Changes: Keep each migration focused on a single logical change
-
Test Locally: Always test migrations using
dfx deploy --localbefore mainnet deployment -
Document Changes: Include comments in your migration files explaining what each migration does
Troubleshooting
"Both features enabled" error
You can only use one database backend at a time. Ensure exactly one of sqlite or turso is enabled in your Cargo.toml.
Migrations not found
Ensure your migrations directory exists and contains .sql files, and that build.rs is properly configured to point to it.
"wasi2ic: command not found"
Install the wasi2ic tool:
Migration failures
Check the canister logs with dfx canister logs <canister_name> for detailed error messages. Common issues:
- Invalid SQL syntax in migration files
- Trying to create tables that already exist (use
IF NOT EXISTS) - Foreign key constraint violations
Seeds not executing
Verify:
- Seed files are in the
src/seeds/directory (or configured directory) - Each seed file exports a
pub fn seed()function - The module is declared in your canister code:
mod seeds;
Examples
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 migrations
Running the SQLite Example
Running the Turso Example
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Author
Kristofer Lund