mod db;
#[cfg(feature = "turso")]
pub use crate::db::turso;
#[cfg(feature = "sqlite")]
pub use crate::db::sqlite;
#[cfg(feature = "turso")]
use ::turso as turso_crate;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Migration '{id}' failed: {message}")]
MigrationFailed { id: String, message: String },
#[error("Environment variable '{0}' not set")]
EnvVarNotFound(String),
#[error("Database error: {0}")]
Database(Box<dyn std::error::Error + Send + Sync>),
}
#[cfg(feature = "sqlite")]
impl From<rusqlite::Error> for Error {
fn from(err: rusqlite::Error) -> Self {
Error::Database(Box::new(err))
}
}
#[cfg(feature = "turso")]
impl From<turso_crate::Error> for Error {
fn from(err: turso_crate::Error) -> Self {
Error::Database(Box::new(err))
}
}
pub type MigrateResult<T> = std::result::Result<T, Error>;
#[cfg(feature = "sqlite")]
pub type SqliteSeedFn = fn(&rusqlite::Connection) -> MigrateResult<()>;
#[cfg(feature = "turso")]
pub type TursoSeedFn =
fn(
&turso_crate::Connection,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = MigrateResult<()>> + Send>>;
#[cfg(feature = "sqlite")]
#[derive(Clone, Copy)]
pub struct Seed {
pub id: &'static str,
pub seed_fn: SqliteSeedFn,
}
#[cfg(feature = "sqlite")]
impl Seed {
pub const fn new(id: &'static str, seed_fn: SqliteSeedFn) -> Self {
Self { id, seed_fn }
}
}
#[cfg(feature = "turso")]
#[derive(Clone, Copy)]
pub struct Seed {
pub id: &'static str,
pub seed_fn: TursoSeedFn,
}
#[cfg(feature = "turso")]
impl Seed {
pub const fn new(id: &'static str, seed_fn: TursoSeedFn) -> Self {
Self { id, seed_fn }
}
}
#[derive(Debug, Clone)]
pub struct Migration {
pub id: &'static str,
pub sql: &'static str,
}
impl Migration {
pub const fn new(id: &'static str, sql: &'static str) -> Self {
Self { id, sql }
}
}
#[macro_export]
macro_rules! include_migrations {
() => {
include!(concat!(env!("OUT_DIR"), "/migrations_gen.rs"))
};
}
pub struct Builder {
migrations_dir: String,
seeds_dir: String,
}
impl Builder {
pub fn new() -> Self {
Self {
migrations_dir: "migrations".to_string(),
seeds_dir: "src/seeds".to_string(),
}
}
pub fn with_migrations_dir(mut self, dir: impl Into<String>) -> Self {
self.migrations_dir = dir.into();
self
}
pub fn with_seeds_dir(mut self, dir: impl Into<String>) -> Self {
self.seeds_dir = dir.into();
self
}
pub fn build(self) -> std::io::Result<()> {
use std::env;
use std::fs;
use std::path::Path;
let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| {
std::io::Error::new(std::io::ErrorKind::NotFound, "CARGO_MANIFEST_DIR not set")
})?;
let out_dir = env::var("OUT_DIR")
.map_err(|_| std::io::Error::new(std::io::ErrorKind::NotFound, "OUT_DIR not set"))?;
let migrations_dir = Path::new(&manifest_dir).join(&self.migrations_dir);
println!("cargo:rerun-if-changed={}", migrations_dir.display());
let migrations_dest = Path::new(&out_dir).join("migrations_gen.rs");
if !migrations_dir.exists() {
fs::write(migrations_dest, "&[]")?;
} else {
let migration_files = collect_migration_files(&migrations_dir)?;
let generated_code = generate_migrations_code(&migration_files);
fs::write(migrations_dest, generated_code)?;
}
let seeds_dir = Path::new(&manifest_dir).join(&self.seeds_dir);
println!("cargo:rerun-if-changed={}", seeds_dir.display());
if seeds_dir.exists() {
let seed_files = collect_seed_files(&seeds_dir)?;
if !seed_files.is_empty() {
let generated_code = generate_seeds_code(&seed_files);
let mod_file = seeds_dir.join("mod.rs");
fs::write(mod_file, generated_code)?;
}
}
Ok(())
}
}
impl Default for Builder {
fn default() -> Self {
Self::new()
}
}
fn collect_migration_files(
migrations_dir: &std::path::Path,
) -> std::io::Result<Vec<(String, String)>> {
use std::fs;
let mut migration_files = Vec::new();
let entries = fs::read_dir(migrations_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("sql") {
continue;
}
if let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) {
let absolute_path = path.to_string_lossy().to_string();
migration_files.push((file_stem.to_string(), absolute_path));
println!("cargo:rerun-if-changed={}", path.display());
}
}
migration_files.sort_by(|a, b| a.0.cmp(&b.0));
Ok(migration_files)
}
fn generate_migrations_code(migration_files: &[(String, String)]) -> String {
let mut code = String::from("&[\n");
for (migration_id, file_path) in migration_files {
code.push_str(&format!(
" ic_sql_migrate::Migration::new(\"{migration_id}\", include_str!(\"{file_path}\")),\n"
));
}
code.push_str("]\n");
code
}
fn collect_seed_files(seeds_dir: &std::path::Path) -> std::io::Result<Vec<(String, String)>> {
use std::fs;
let mut seed_files = Vec::new();
let entries = fs::read_dir(seeds_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("rs") {
continue;
}
if let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) {
if file_stem == "mod" {
continue;
}
let absolute_path = path.to_string_lossy().to_string();
seed_files.push((file_stem.to_string(), absolute_path));
println!("cargo:rerun-if-changed={}", path.display());
}
}
seed_files.sort_by(|a, b| a.0.cmp(&b.0));
Ok(seed_files)
}
fn generate_seeds_code(seed_files: &[(String, String)]) -> String {
let mut code = String::new();
code.push_str("// This file is auto-generated by ic-sql-migrate\n");
code.push_str("// Do not edit manually\n\n");
for (seed_id, _) in seed_files {
code.push_str(&format!("pub mod {seed_id};\n"));
}
code.push('\n');
code.push_str("use ic_sql_migrate::Seed;\n\n");
code.push_str("pub static SEEDS: &[Seed] = &[\n");
for (seed_id, _) in seed_files {
code.push_str(&format!(" Seed::new(\"{seed_id}\", {seed_id}::seed),\n"));
}
code.push_str("];\n");
code
}