-
Notifications
You must be signed in to change notification settings - Fork 24
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
Implement safe rename_table #54
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open for any feedback.
I think I'm missing rename_table
in IdemPotentStatements
, should I add it there?
fd2ba97
to
c954c32
Compare
Yes, the rationale behind Idempotent statements is to be able to retry migration if part of it failed. rename_table :foo, :bar
add_column :bar, :col, :text if the (it has to do with disabling transaction in migration, which could be improved) |
And thanks a lot for the contribution! |
c954c32
to
82b2c7f
Compare
$ rake test MiniTest::Unit::TestCase is now Minitest::Test. From /Users/pirj/source/safe-pg-migrations/test/useless_statement_logger_test.rb:5:in `<top (required)>'
If we execute our code, that is designed to be atomic, in a new transaction, and it will be a nested transaction, if the transaction fails, the outer, parent transaction will not be rolled back due to the uncontrollable Active Record behaviour.
ac9ae73
to
2ee1e05
Compare
Tests pass. Please have another look. |
yield | ||
else | ||
# Open a new transaction | ||
transaction(requires_new: true, joinable: false) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need those options if we are not in a transaction?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the sound of my OCD chiming in.
You're right, we don't need requires_new
since we make sure there is no parent transaction.
I'm on the fence of keeping joinable: false
or not.
If we keep it as false
, that means that if the block passed to all_or_nothing_transaction
opens a transaction, and that transaction will fail, what is outside the nested transaction will not fail. This would be perplexing for a migration to be applied in parts out of order, or if certain parts are swallowed without notice.
If we set joinable
to true
, it doesn't really mean that we are protected from this behaviour. If the nested transaction will be opened with requires_new: true
, it would work the same way as if we open the parent one with joinable: false
.
That all leads me to a though it's not such a bad idea to override transaction
, and do a super(requires_new: false, joinable: true)
. Does this sound reasonable to you?
Thanks for bringing this up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
requires_new: false, joinable: true
seems to be the default options for transaction, unless I missed something.
(which looks like a sane default to me)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is, yes. I think it boils down to:
- Make a note that the block passed to this method should open transactions with the same default options. To make sure they are all joined into one.
- Double-check if
transaction_open?
that it is a joinable transaction.
Do you agree?
all_or_nothing_transaction do | ||
super(table_name, new_name) # Actually rename the table | ||
|
||
execute "CREATE VIEW #{quoted_table_name} AS SELECT * FROM #{quoted_new_name}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just thought about it, there is one gotcha with this approach: ActiveRecord does not manage to find the primary key when using a view (ie: Model.primary_key
is nil
).
In the happy path, it should not be problem, as this information is cached until the application is reloaded, but if for any reason (let's say rollback of the application code) the application reloads, it will fail to determine the primary key from the view, so a simple Model.find(1)
would fail.
I see two options:
- print a message to indicate that the primary key should be specified explicitly in the model class (
self.primary_key = :id
) - do it the other way around: create the view with the new table name, and then in a second migration, drop the view and rename the table. This way developers are forced to indicate
self.primary_key = :id
as a lot of tests will probably fail with the model relying on the view. A bit less elegant than your approach though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch!
I see what you mean. It is practically possible even without rollback. Say, during the deployment, the traffic increases, and we start up another node with the old code that will query the schema info and would fail to fetch PK info.
Actually, I favour the second approach. We don't leave the rubbish temporary views behind with a barely visible "remove me later" schema comment. There is no warning to ignore.
The first migration runs and can make sure that revision A (cached PK info) and revision B (model points to the view with the new name and explicit self.primary_key =
) are compatible to the schema.
The only problem I can think of is that those two migrations (create new view, drop the view and rename the table) should not be deployed together. Wondering if there is a mechanism to prevent this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that I'm aware of, we could probably do something at a higher level, to prevent those two migrations from running in a single db:migrate
, but it's not bullet proof (and probably complicated to implement).
I think outputting a warning to the user could do the trick.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(that's the drawback of this approach, we can not simply override rename_table
anymore, we most likely need to provide additional DSL method)
# add_column :accounts, :primary, :boolean | ||
# end | ||
# end | ||
def all_or_nothing_transaction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused by the naming, "all or nothing" and "transaction" seem to be redundant (but I get the idea). I'd go for something like ensure_in_transaction
, up to you though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more like ensure_not_to_open_a_nested_transaction
.
It seems to me that the new approach, when we introduce a view with the new name, is prone to the same problem with inferring PKs. Imagine, the deployment went fine, and the new code has The only reasonable solution I can think of is to explicitly tell AR what the key is, as you suggest in the option 1. There is one subtle, but important difference between "create a view with the new name; drop the view and rename the table" vs "rename the table and create a view with the old name". For the first approach, we could potentially raise an error if However, it is not strictly necessary that:
This leads me to a thought that what we can reasonably recommend is:
This, certainly, is not mistake-proof and should be done consciously and thoroughly. It still feels like an improvement over the approach currently suggested by What do you think? No offence taken if you feel that this approach fails to align with the philosophy of this gem. |
Yes you're right, the rationale behind this proposal being safer than the other one was that tests would most likely catch this (as a few basic ActiveRecord operations are broken). I'll think a bit about it, I feel like the developers still have to do the heavy lifting with this approach (well, renaming tables should not be really frequent, so maybe it's acceptable) |
It doesn't seem to be worth it to recommend this approach with some use cases that have undesirable side effects. |
partially addresses #53 (rename_table, but not rename_column)