-
Notifications
You must be signed in to change notification settings - Fork 127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature Request: Access to DB connection inside migration #119
Comments
Hi, thanks for your interest. Yeah, when I mention flyway is more in the sense of not having undo migrations.
|
UPDATE: DO NOT USE THIS METHOD! IT IS BUGGY See: #119 (comment) I'm kind of new to Rust / this use-case with this library. Sharing in hopes that this helps someone out there. My current workaround for this is to open another connection to the database within the migration itself. The trick is to check that the migration isn't already ran first yourself because refinery always runs the I did this by checking that the version number doesn't already exist in the database (in the I've sprinkled in some comments that I hope help guide the reader. The code is kind of very crude and terrible (lol I'm also very well aware that the below SQL migration could be ran without resorting to this hack, but, it's easier to grok than my actual use-case of migrating a bunch of blob / binary data. Without further ado: use async_std::task;
use std::path::Path;
/// Should we run this migration file?
///
/// Checks that this migration files version number does not yet
/// exist in the `refinery_schema_history` table.
///
/// While this is specific to sqlx / postgres, you can easily
/// modify this to support your database driver.
///
async fn should_run(pool: &sqlx::Pool<sqlx::Postgres>) -> bool {
// - - -
// hack to pull out this files version with the file! macro.
// @todo: move *this* `should_run` function into it's own macro.
let file_name = Path::new(std::file!())
.file_name()
.unwrap()
.to_str()
.unwrap();
let version = file_name
.split("__")
.next()
.unwrap()
.trim_start_matches("V");
let version = version.parse::<i32>().unwrap();
let sql = "SELECT version FROM refinery_schema_history WHERE version = $1";
// This query will error if refinery has never ran, e.g.
//
// --> 'relation "refinery_schema_history" does not exist'
//
match sqlx::query(&sql).bind(version).fetch_optional(pool).await {
Ok(row) => row.is_none(),
Err(_e) => true, // @todo: maybe check correct error type? eh.
}
}
/// Refinery migration --- this is called EVERY TIME refinery is invoked.
pub fn migration() -> String {
// The SQL that we wish to run, however, it is NOT going to
// be handled by refinery's database connection...
let sql = "CREATE TABLE message (
-- an auto-incrementing id for this message. (rust type: i32)
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
-- What public_key is this session for?
text TEXT,
-- When this message was created
created_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY(id)
)";
// Instead, it is handled by OUR APPLICATION's database connection
// logic (this will be specific to YOUR application & how you acquire
// database connections and what-not)
task::block_on(async {
// How my app gets a sqlx postgresql connection...
let info = crate::postgres::ConnectionInfo::from_env().unwrap();
let pool = info.connect().await.unwrap();
//
if should_run(&pool).await {
sqlx::query(sql).execute(&pool).await.unwrap();
}
});
// Because refinery isn't handling this SQL logic, we can return
// an empty to make the compiler happy. Refinery happily accepts
// this value and records it in the `refinery_schema_history` table.
"".to_string()
} glhf. EDIT: Here is a macro you can shove in the root of your crate (or where ever you'd like) that can be used like this (in the above example) that replaces the if crate::should_run!(&pool).await {
// . . .
} #[macro_export]
macro_rules! should_run {
($pool:expr) => {
async {
let file_name = std::path::Path::new(std::file!())
.file_name()
.unwrap()
.to_str()
.unwrap();
let version = file_name
.split("__")
.next()
.unwrap()
.trim_start_matches("V");
let version = version.parse::<i32>().unwrap();
let sql = "SELECT version FROM refinery_schema_history WHERE version = $1";
// This query will error if refinery has never ran, e.g.
//
// --> 'relation "refinery_schema_history" does not exist'
//
match sqlx::query(&sql).bind(version).fetch_optional($pool).await {
Ok(row) => row.is_none(),
Err(_e) => true, // @todo: maybe check correct error type? eh.
}
}
};
} This will save you from copy/pasting that logic in all migrations that you have to get dirty with. |
Hi, thanks for your example! |
Hi @jxs (and the internet!) --- The solution I came up with is buggy & doesn't work when starting from a fresh instance of a database (e.g. in test cases where you start with a fresh database). I thought it worked fine because I was migrating an existing database, but, it does NOT work as you'd expect from a fresh database... It seems like my second-connection hack eagerly evaluates the SQL before refinery actually runs migrations that come before it. To further illustrate this, imagine that you're starting on a fresh database and have these two migrations:
It seems like the I'm coming to this conclusion because in my V1 script, I'm creating tables, and in the "complex migration V2 script" I'm manipulating those tables, but I get this error:
Which leads me to believe that something interesting is going on, and I shouldn't take this approach...
I'll spend tomorrow on this and ping here with how far I get. Thank you so much for the guidance! . . . EDIT: I think I understand why my approach is buggy, because, it's running the migrations on the 'gather phase' of this library, i.e.
https://docs.rs/refinery-core/0.5.1/src/refinery_core/runner.rs.html#262-264 when what I want it for them to be ran in the 'run phase' --- 😄 |
I was considering it as a possibility to address this issue, and comparing with the other suggestion #119 (comment) to provide a callback to |
Oh ok. This may be out of reach for me. Thank you for your time. |
would you like to try to tackle as a provided callback function to |
Hey @jxs --- I found a library called schemamama that has a PostgreSQL driver. It requires a little more code to set it up, but it does what I need at this moment. I think I'm really looking for an "abstract migration" tool similar to node-migrate and not anything tied to SQL-specific use cases. i.e. something that keeps track of functions ran with-or-without database modifications. Forking schemamama (or implementing my own trait?) will be easier for me to accomplish as it's a much smaller codebase (it's a single It seems like the majority of the migrate tools on crates.io are SQL specific. Now I'm thinking that refinery shouldn't worry about this use case, stay purely SQL, and defer this sort of "data migration" to another tool. 🤔 |
I'd like to chime in on this issue by asking: how do Related, is there any way (apart from global variables) to pass additional context data (such as cryptographic keys that are never stored in the database, but which are used to construct public keys that are stored) to a migration? |
Hi!
they don't,
can you give a use case? |
I've actually opted to go with This performs a couple operations in Rust in the process of the migration:
|
Is this still something you'd be interested in a PR for? Here's my use case: my system uses sqlite to catalog a large number of binary files on disk, indexing some basic attributes like time or content type. During a migration I may want to update that sqlite database and simultaneously update or transform those files in some way. I want to be able to do this stepwise with migrations, and ideally let those updates raise errors that could abort the migration process. I've been playing around with it a little in a fork, and it seems like the simplest approach is to allow for adding pre/post migration hooks (closures) to run with each migration. The closure would have access to a reference to the current migration, and could include whatever other context it wanted, including a database connection. The only obvious downside to me is that the closure wouldn't have access to the actual migration transaction, but at least for us that's not a showstopper. let mut connection = Connection::open(&db_path).unwrap();
let mut runner = crate::embedded::migrations::runner();
let hook_connection = Connection::open(&db_path).unwrap();
runner.add_post_migration_hook(move |migration| {
hook_connection.execute("select foo from bar", rusqlite::params![]).map_err(|_| anyhow::anyhow!("oh noe"))?;
warn!("we just ran this migration: {migration:?}");
Ok(())
});
runner.run(&mut connection)?; |
Hi, yeah I am def interested if this is something that helps users having more control. Curious, why do you prefer closures over something like having a |
I could go either way. The advantage to closures I think is just the scope of the changes; it's relatively easy to supply some Another thought I had as I played with this is that it would be nice if there was an enum generated along with the migrations, so that in a hook/enumeration block I could match against that rather than a string for migration in runner {
match migration.variant {
Migrations::V9__cleanup => do_cleanup(),
_ => () // do nothing
}
} |
Actually, I think the iterator approach isn't so much harder after all. But in going through the various refinery/refinery_core/src/traits/sync.rs Line 71 in 1f646f9
If supplied, the migrations table is updated but the actual migration query isn't executed. The regular un-batched migrate implementations don't honor that flag, and only look for Target::Version(_) : refinery/refinery_core/src/traits/sync.rs Line 29 in 1f646f9
Is that intentional? It feels like a bug, but I thought I'd check before I just fixed it unilaterally. |
I have iteration working, but: the iterator needs to hold onto the database connection. So, either you need a non-standard |
Hi, and sorry for the delay! The problem with the first is that it's not only breaking it also breaks the current way the macros work as they don't have access to the connection when the instantiate the |
for migration in runner {
...
} you need the None of which is a big deal, and I have this all working in my branch. Certainly not the only way to do it, but it felt the most natural to me. Maybe it will be clearer if I just send you the PR and we can discuss there. |
I've been searching around for some way to manage my database migrations in Rust. I'm considering using Diesel, but Diesel only supports defining migrations in SQL. I anticipate wanting to make migrations which would be difficult or impractical to represent in SQL alone - modifying blobs, making complex format changes, etc. - so that is a non-starter for me.
Refinery's README says that Refinery's design is based on Flyway and it supports defining migrations in Rust code instead of SQL, which looked promising. Flyway's document for Java-based migrations says explicitly that this kind of use case is what they're targeted at:
However, it doesn't seem Refinery's "migrations in a Rust module" support gets me what I need. For example, see Flyway's getting started tutorial for Java-based migrations: https://flywaydb.org/getstarted/java. Implementing this tutorial in the same way does not seem to be possible in Refinery, as the migration would need access to the database connection. (The specific modification in the example is trivial to implement in SQL, but imagine calculating the updated name was an operation that was impractical in SQL.)
Are there any plans to support more complex, manually coded migrations in Refinery?
The text was updated successfully, but these errors were encountered: