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
A couple features to facilitate a branch-by-default workflow #656
Comments
Hi there - why dont you use the haven't tried any of this but that's 99% of it? if it seems to be the right idea, let me know if that actually works. if so, I think it should be possible to add a little bit of logic to your env.py to make that last "--head" part automatic too, if you have a programmatic way of getting the current release version of your application from its source base. |
assuming those steps work then this becomes a recipe to add at https://alembic.sqlalchemy.org/en/latest/cookbook.html |
Good idea! I'll give this a try. Even if |
So to start, I'm trying to automate the def process_revision_directives(context, revision, directives):
def get_last_release_rev_id():
script_dir = ScriptDirectory('alembic')
for script in script_dir.walk_revisions():
if script.doc.startswith('[RELEASE-MERGE]'):
return script.revision
raise Exception('No release revision found in history')
script = directives[0]
if script.head is None:
script.head = get_last_release_rev_id()
script.splice = True
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
include_schemas=True,
process_revision_directives=process_revision_directives
)
with context.begin_transaction():
context.run_migrations() |
take a look at https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.environment.EnvironmentContext.configure.params.process_revision_directives - you need to set revision_environment=true in your alembic.ini |
Thanks, that did the trick!
However, since branch labels apply to all downstream revisions this feels a bit unclear semantically, and as if we're relying on undefined behaviour? It also seems like this would get messy over time, though I'm not sure if this would actually cause any problems. >>> [script for script in ScriptDirectory('.').walk_revisions()]
[Script('140731f2cb4b', '2ef283797ea2', branch_labels={'v1.78.0', 'v1.79.0'}),
Script('55e8950bc829', '2ef283797ea2', branch_labels={'v1.78.0', 'v1.79.0'}),
Script('2ef283797ea2', ('4a9cd0f3844c', '9300c463ce7d'), branch_labels={'v1.78.0', 'v1.79.0'}),
Script('4a9cd0f3844c', '2186ee192ce5', branch_labels={'v1.78.0'}),
Script('9300c463ce7d', '2186ee192ce5', branch_labels={'v1.78.0'}),
Script('2186ee192ce5', ('91e81a0698ce', 'df3635e75327'), branch_labels={'v1.78.0'}),
Script('91e81a0698ce', '9b1e08350ef2'),
Script('df3635e75327', '9b1e08350ef2'),
Script('9b1e08350ef2', '26cb3dbd4be8'),
Script('26cb3dbd4be8', None)] One consequence of using I think we'll stick with using the message field to indicate release revisions, at least for the time being. IMO the cleanest solution would be some sort of boolean field to indicate that the revision is the new default branch point, so there would no longer be "magic values" involved, eg But honestly, I'm bikeshedding at this point. I'm really pleased to have removed the need for For a cookbook entry, would you prefer the Feel free to close this, or turn it into an issue for the cookbook entry, or whatever you see fit. Thanks again for your help with this! |
that should be,
I think you can still walk through the revisions, get all the branch labels, then find the latest one using version number comparison schemes. if you're in env.py this structure should be present for programmatic access.
you could...put it in both? im not sure if this means you arent actually using the branch_label thing or not.
the "message" version has all your custom scripts? so far from what I'm understanding , the "branch_label" version is the more canonical approach. Both "magic symbol in the message" and "branch label" are virtually the same thing structurally, that is, it's some variable inside the module scope of a revision script that says something. I don't see what advantage there is to embedding it in the message alone rather than using a branch_label.
|
I think there is some misunderstanding. From what I can tell, since
Since, the heads of any branch are the same as
Yeah, sorry, that wasn't very clear. For now at least I'm only using
No, |
I went back to your original description:
So diagrams would help here, I guess you are trying to have all your revisions come off the same parent. starting with:
"branch off of the most recent "release revision"", that means this:
etc., that is, all your revisions are non-linear and are off of v1.0.0, then you want to "merge them at some point":
then a new release:
now you want to branch off v1.1.0 each time:
so, then yes, you don't want the The way this is supposed to work, not accounting for bugs I'm not aware of (as indicated before, all of this stuff is very likely to have weird bugs in it still so I am not assuming it's working perfectly), is that, supposing revision a859bd has the branch label "v1.1.0" inside it, that means, it's in the a859bd.py python file. When you say, So basically, branch_label gives you 1. a symbolic indicator of a specific version and 2. a symbolic indicator that can be used to locate a singular branch head as well, if one exists. I am not thinking of another possibility of something that can be "looked up". |
Yes! I think we're on the same page now. Sorry, I should have described the DAG structure more explicitly in my OP, and yeah, diagrams are helpful. Here, I made one (for my team's internal documentation, and for the eventual cookbook recipe, if that happens): So as long as
|
just as a hypothetical can you check if this diff causes history to only display a branch label if it's immediately present?
this would be the start of alembic beginning to distinguish between a branch label as a total-branch label vs. an immediate revision label. that is, your concern that this is not "expected" that a branch label also refers to a specific revision is something that should be solidified. I'm not sure to what degree the tests are asserting this right now (might be a lot though, haven't dealt with this area in some years). |
yes can confirm this all works, when you make the new revision that is a new head, you specify the branch name as the |
Cool! Yep, I do And yeah, come to think of it, it wouldn't be so hard at all to determine the branch point with this approach -- could be something like: from distutils.version import StrictVersion
max(ScriptDirectory('.').get_heads().pop().branch_labels, key=StrictVersion) Hopefully I will have a chance soon to write up the cookbook entry! Do you still need me to test that diff, or did you try it out? Do you intend to commit it? |
sure, I think I should commit something like that but I would have to take the time to review everywhere that kind of thing is happening, add tests, etc. |
Out of curiosity- why would you want to do this (I have always worked in teams that are relatively free to update their practices, so miss out on some things). You decsribe the problem as
I was hoping to understand more about the problems of "moving code between environments". I have never worked in anything outside of trunk based where the same code goes from dev > staging > prod. So was wondering what kinds of problems you're solving that requires branching of the database migrations? Also, how big does your team have to be that merging the migrations into a single trunk was so hard? |
if someone wants to contribute, if we could take the patch in #656 (comment) and make it work like this:
|
Preface
My team recently made the switch to Alembic, and it's been great! However, it took some jimmy-rigging to set up the workflow we need. I imagine there must be other teams that would benefit from a similar setup, so I thought I'd post here describing (1) what I came up with, and (2) a few minor(?) features that would make it even nicer. Of course, I realize Alembic can't cater to every possible workflow, but no harm in asking right ;). I would be interested in implementing said features, if there is interest. Alternatively -- critique my approach! Maybe I missed something which renders these features unnecessary.
Background
I work on a team with five other developers and, due to the nature of the project, database revisions are common: we add about three per week (collectively), sometimes more. We have three environments in our normal flow -- qa, stage, production -- plus a sandbox which doesn't get much use. We release to production about twice a month.
The problem
Our previous migration tool did not support branching, and trying to force a strictly linear migration history was proving to be a nightmare, especially in regards to moving code between environments. This was especially annoying considering that most of the time, the order in which revisions from the same sprint are applied doesn't actually matter! ... but the tool required a linear history. What we really want is a "semi-ordered" migration history, where revisions from 6 months ago will certainly precede revisions from this month, but on a smaller time-scale (say, a sprint) migrations are unordered by default. (By default is key -- sometimes we do need to specify a dependency between migrations from the same sprint.)
The solution
So that's what we were after, and Alembic's branching got us 90% there, but a little more is needed for a complete, developer-friendly solution. There were two points to still address:
When do merges happen?
Before each release to production, on the release branch, add a revision merging all heads, and give it a special message. We call it a "release revision", and use this script:
After the release, we merge production back into qa and stage so that they get the changes added in the release branch (version number, changelog, and now the release revision too).
Creating a new revision...
When a developer needs to create a new revision, and its only dependencies are already in production (as is usually the case), they are to branch off of the most recent "release revision", as generated by the script above. To make this easy, we use the following script:
This script acts just like
alembic revision
, except it automatically sets the--head
flag to the most recent release revision, and also passes--splice
. If the new migration depends on another migration which is not yet released, the developer must instead uses barealembic revision
and set--head
themselves.So that's what we've got, and we're so glad to finally have a system that works, technically speaking, even if it's just a little bit clunky. Now, if story A is merged into QA before story B, B can still be merged into stage before A! What a concept!
Feature proposal
That said, ideally we would not need to use a script for an operation as basic as creating a new revision, and I believe it would be fairly straightforward to give this workflow proper support in a non-invasive manner. It would look something like this:
branch_by_default
perhaps, which would default toFalse
, but whenTrue
, would makealembic revision
branch off of the most recent “release revision”, unless--head
is specified, in which case it would work as it does nowalembic merge
which would somehow designate the revision a "release revision" (as I've been calling it in this post). It would maybe be called--release
, but it would be nice to think of something more general, since these revisions don't necessarily need to correspond with releases.The main question here is of course how to designate release revisions. Would it just be a
message
prefix, as in the scripts above, or is there a better way? I've seen something about "tags" when browsing the docs, but haven't made sense of them, and come to think of it, I'm not even sure they apply to revisions, but if they do, that sounds like it could be a candidate?The text was updated successfully, but these errors were encountered: