rust-db / refinery

Powerful SQL migration toolkit for Rust.
MIT License
1.35k stars 126 forks source link

Wrap embedded migrations with enums #303

Open superstator opened 11 months ago

superstator commented 11 months ago

Now that I have a way to iterate over migrations as I run them, I'd like to be able to match strongly against them. Something like:

let mut conn = Connection::open("test.db").unwrap();

for migration in embedded::migrations::runner().run_iter(&mut conn) {
    match migration?.try_into()? {
        EmbeddedMigration::Initial(_) => println!("Initialized database"),
        EmbeddedMigration::AddCarsAndMotosTable(m) => println!("Now we have cars: {m:?}"),
        _ => ()
    }
}

I think this could be done without any changes to Migration or Runner, but just by emitting an enum and impl TryFrom<Migration> with embed_migrations!. Is this something you'd be open to another PR for?

jxs commented 10 months ago

Interesting, can you disclose you usecase for this? And why comparing the name would not be enough? Thanks!

superstator commented 10 months ago

Sure, it's just an extension of the usecase for iteration. For example, if my app stores metadata in sqlite but also stores complex binary data on disk, I might need to initialize a directory structure after the initial migration, and then reshuffle a bunch of files during a later migration.

fn main() -> Result<(), Error> {
    let mut conn = Connection::open("test.db").unwrap();

    for migration in embedded::migrations::runner().run_iter(&mut conn) {
        match migration?.try_into()? {
            EmbeddedMigration::Initial(_) => init_filesystem(),
            EmbeddedMigration::AddCarsAndMotosTable(_) => migrate_v2(),
            _ => ()
        }
    }

    Ok(())
}

fn init_filesystem() -> Result<(), Error> {
    // create fs junk
    Ok(())
}

fn migrate_v2() -> Result<(), Error> {
    // update fs junk folowing db changes
    Ok(())
}

I can absolutely achieve this just matching names, but enums would allow for compile-time checking. My thought was to use the migration version as the enum discriminant so strictly speaking I'd be matching an i32 instead of a string which would be more efficient and much less error prone.

jxs commented 9 months ago

The idea seems interesting, but if it's a TryInto it means there's still going to be a runtime check, which I assume is converting the migration name from string into the matching enum right?

superstator commented 9 months ago

My thought was to have the macro output a TryInto impl that maps migration.version to the enum discriminant, so no runtime string comparisons. That way the only failure mode for TryInto would be somehow instantiating a Migration struct with an invalid version number, and I think the only obvious way for that to happen would be to monkey with the schema history table or delete applied migrations.

jxs commented 9 months ago

then I'd say it can be an Into right?

superstator commented 9 months ago

It would have to panic on a bad migration version but otherwise sure

jxs commented 9 months ago

and isn't that most probably unreachable!?

superstator commented 9 months ago

Off the top of my head the obvious way to get there (short of unsafe) would be to have two sets of embedded migrations for two databases, and then try to convert one into the wrapping enum for the other. I'm fine with that being a panic if you are, and it would eliminate the need to make changes in error.rs.