refinery icon indicating copy to clipboard operation
refinery copied to clipboard

Wrap embedded migrations with enums

Open superstator opened this issue 1 year ago • 8 comments

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?

superstator avatar Dec 29 '23 21:12 superstator

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

jxs avatar Jan 03 '24 12:01 jxs

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.

superstator avatar Jan 03 '24 17:01 superstator

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?

jxs avatar Feb 15 '24 10:02 jxs

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.

superstator avatar Feb 15 '24 18:02 superstator

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

jxs avatar Feb 22 '24 16:02 jxs

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

superstator avatar Feb 22 '24 18:02 superstator

and isn't that most probably unreachable!?

jxs avatar Feb 22 '24 19:02 jxs

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.

superstator avatar Feb 23 '24 04:02 superstator