Skip to content

[Enhance] Add support for renaming columns via the change clause. #22

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

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

ryanrigby17
Copy link

This PR adds support for the change clause to allow renaming columns.
Portions I'm still not 100% on are the code parts concerning reversing/applying migrations on-top of other migrations. They will likely need closer scrutiny.

I hope to follow this up with another PR for renaming tables, if this PR proves valuable.

Copy link
Member

@joshmcrae joshmcrae left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good! I've left some recommendations below with regards to code simplicity and developer experience, however as you noted in the PR description more time does need to be spent making sure reversal / application of the new operation works.

I'm happy to help out here, but I'll explain how this is intended to work first.

Reversal

Reversal is simply concerned with transforming an operation so that it can be used in a rollback. For the CHANGE COLUMN old_col_name new_col_name column_definition operation, reversal involves switching the order of names and replacing column_definition with whatever it was prior to the change. If MODIFY COLUMN has been implemented correctly already, this should be trivial as you'll just have to deal with swapping the names.

Application

This is concerned with 'reducing' multiple operations for the same thing (e.g. a table or column) such that a single SQL query needs to be executed rather than several individual queries. This is most useful when creating a new tenant database as each table can be created according to the entire history of migrations through one CREATE TABLE command, instead of a CREATE TABLE followed by several ALTER TABLE commands.

What's unique about this new operation is that after it is executed, the column has a different name. Since we look for previous operations by column name, we might end up missing previous operations or reducing operations that aren't relevant. I say might because I haven't had time to think through this thoroughly. Given that we only apply one operation to another at a time, this might not be a problem.

At the very least, you'll need some logic added around TableOperation.php:219 as well as what you've already got, because this handles the application of an ALTER TABLE to a CREATE TABLE.

Here are some scenarios to think about:

  1. Applying a CHANGE COLUMN to an ADD COLUMN should result in an ADD COLUMN for the new name and column definition.
  2. Applying a CHANGE COLUMN to a MODIFY COLUMN should result in a MODIFY COLUMN for the new name and column definition.
  3. Applying a CHANGE COLUMN to a CHANGE COLUMN should result in a CHANGE COLUMN for the new name and definition. In the case that the names are the same (but switched) between them and the column definitions are identical, the operation shouldn't happen at all because there's no net change.

Writing all of this down, it seems to me like this should work identically to MODIFY COLUMN except that the column operation's name (column name) should be the new column name.

See if you can work this logic into your changes and I'll help out if needed.

@@ -104,6 +104,18 @@ public function reverse(?ReversibleOperationInterface $originalOperation = null)
$originalColumn->getOptions()
);
}

if ($columnOperation->getOperation() === ColumnOperation::CHANGE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition could be combined with the previous (as you've done with the switch statement lower down).

The second argument to ColumnOperation just needs to be changed to $columnOperation->getOperation().

saria3rabbi
saria3rabbi previously approved these changes Oct 12, 2021
Copy link

@saria3rabbi saria3rabbi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's gonna work!
P.S. @ryanrigby17 do you think we should add more test cases to test Josh's example scenarios?

@ryanrigby17
Copy link
Author

@saria3rabbi Definitely, I think more tests would be great. I'm still trying to fully wrap my head around the intricacies of them 😅

@ryanrigby17
Copy link
Author

@joshmcrae Thanks for the excellent write up. I've made changes based on what you've written and your review recommendations. This feels more correct now. I feel like the best way to test these now will be via running the actual migration commands. How are the migrations within the tests folder actually used? I was wonder if I should add some change command and see if they are working correctly.

@joshmcrae
Copy link
Member

joshmcrae commented Oct 13, 2021

@ryanrigby17 those migrations are only used by FinderTest currently, to make sure it can read from the filesystem and construct a properly ordered history. It wouldn't be a bad idea to write some integration tests that use migrations in that directory and evaluate that the reduced operations come out as expected.

A class like MigrationIntegrationTest should do the trick. If you want to create a tests/fixtures directory to put different sets of migrations under (e.g. a directory per test case), go ahead! This kind of coverage would provide confidence that the complex scenarios are handled correctly.

To reiterate what my concern is with renaming columns, I want to make sure the logic for applying table operations takes a column's new name into account once the change has been applied. Given that we only apply one migration to another at a time, and these are done in series, there's probably nothing to worry about here in reality. Take the following as an example:

$migrations[] = \Exo\TableMigration::create('users')
    ->addColumn('username', ['type' => 'string']);

$migrations[] = \Exo\TableMigration::alter('users')
    ->changeColumn('username', 'email', ['type' => 'string']);

$migrations[] = \Exo\TableMigration::alter('users')
    ->addColumn('username', ['type' => 'integer']);

$migrations[] = \Exo\TableMigration::alter('users')
    ->changeColumn('username', 'user_id', ['type' => 'integer']);

Reducing all of these into a single migration should be the equivalent of running:

$migrations[] = \Exo\TableMigration::create('users')
    ->addColumn('email', ['type' => 'string'])
    ->addColumn('user_id', ['type' => 'integer']);

Exo would get there by running TableOperation::apply() three times, each time applying the new migration to the result of the last apply. I don't think there's any way for the 3rd migration's username column to be confused for the first migration's column of the same name.

If you think my reasoning is sound here, then there's no unique problem presented by CHANGE COLUMN and the changes you've already made should be sufficient.

@ryanrigby17
Copy link
Author

@joshmcrae I've created a test case that utilizes migrations based on your provided examples. I'm happy that it's working how I still have one question regarding reducing ColumnOperations. The test case testReduceRewindMigrationsWithChange in the new MigrationIntegrationTest file still produces 2 ColumnOperations when it reduces the two TableOperations. Is this to be expected? I imagine it would be best to have it DROP the column and ignore the CHANGE, but I can't find an elegant solution for that.

@joshmcrae
Copy link
Member

@ryanrigby17 It will have to be a single column operation, as you can't rename a column and then drop it by its new name in a single SQL statement. In other words, the following would not work:

ALTER TABLE users
CHANGE user_id username varchar(255),
DROP COLUMN username;

You'll get Can't DROP 'username'; check that column/key exists.

The reason it's not reducing those operations is because the name field for the CHANGE does not match that of the DROP and thus they're not seen as operations relating to the same column. This has prompted me to realise that reducing the following two migrations likely won't work either:

  1. Rename username column to email
  2. Add username column

In this case they do share the same name and will be applied, but you cannot apply an INSERT to an ALTER or CHANGE.

We'll need to introduce the concept of 'before' and 'after' names for column operations. That way we can apply column operations for a given 'before' name to operations with a matching 'after' name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants