Skip to content

Conversation

ryanrigby17
Copy link

@ryanrigby17 ryanrigby17 commented Oct 7, 2021

🚨🚨🚨Merge in #27 first!🚨🚨🚨

Implement before/after name concepts for ColumnOperation to fix reduction logic

Summary

This PR addresses the issue identified by @joshmcrae in the final comment where column operations couldn't be properly matched during reduction when CHANGE operations were involved. The core problem was that when reducing operations like:

  1. Rename username to email
  2. Add new username column
  3. Rename new username to user_id

The reduction logic couldn't properly match operations because it was only comparing the primary name field, not understanding the relationship between "before" and "after" names in CHANGE operations.

Key Changes:

  • Added beforeName and afterName properties to ColumnOperation
  • Updated constructor to set these based on operation type (for CHANGE: beforeName = old name, afterName = new name)
  • Modified TableOperation::apply() method to use before/after name matching instead of direct name comparison
  • Updated reverse operation logic to work with the new name concepts
  • Added getter methods getBeforeName() and getAfterName()

Review & Testing Checklist for Human

  • Verify the core reduction scenario works: Test the exact migration sequence from joshmcrae's comment (create username → change to email → add username → change to user_id) and confirm it reduces to the expected final state
  • Test rollback functionality: Ensure that reverse operations still work correctly with the new before/after name logic, especially for CHANGE operations
  • Run full integration test suite: Set up proper database configuration and run MigrationIntegrationTest::testReduceRewindMigrationsWithChange and related tests to verify end-to-end functionality
  • Code review name matching logic: Carefully review the changes in TableOperation::apply() method, particularly the new before/after name matching logic around lines 211, 282, and 308-310
  • Test edge cases: Verify that the new logic handles edge cases correctly, such as operations on columns that were never renamed, or complex chains of multiple renames

Recommended Test Plan:

  1. Set up local database using tests/db.yml.example
  2. Run the integration tests: ./vendor/bin/phpunit tests/MigrationIntegrationTest.php
  3. Create a simple test script that replicates joshmcrae's example scenario step by step
  4. Test both forward migrations and rollbacks with the example scenario

Diagram

%%{ init : { "theme" : "default" }}%%
graph TD
    TM["src/TableMigration.php<br/>changeColumn() method"]:::context
    CO["src/Operation/ColumnOperation.php<br/>Core operation class"]:::major-edit
    TO["src/Operation/TableOperation.php<br/>apply() and reverse() methods"]:::major-edit
    MIT["tests/MigrationIntegrationTest.php<br/>testReduceRewindMigrationsWithChange"]:::context
    
    TM -->|"creates"| CO
    CO -->|"used by"| TO
    TO -->|"tested by"| MIT
    
    CO -->|"new properties"| BeforeAfter["beforeName<br/>afterName<br/>getBeforeName()<br/>getAfterName()"]:::major-edit
    TO -->|"updated logic"| Matching["before/after name<br/>matching in apply()"]:::major-edit
    
    subgraph Legend
        L1[Major Edit]:::major-edit
        L2[Minor Edit]:::minor-edit  
        L3[Context/No Edit]:::context
    end

    classDef major-edit fill:#90EE90
    classDef minor-edit fill:#87CEEB
    classDef context fill:#FFFFFF
Loading

Notes

  • This implementation directly addresses @joshmcrae's feedback about needing "before" and "after" name concepts for column operations
  • The changes are focused on the core operation classes and should not affect the public API
  • While unit tests pass, full integration testing requires database setup that wasn't completed due to environment constraints
  • Session Info: Requested by Ben Sinclair (@bensinclair) - Link to Devin run

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.

);
}

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.

devin-ai-integration bot and others added 9 commits July 28, 2025 23:21
- Update composer.json to support PHP versions 7.4 through 8.4
- Update CI workflow to test against PHP 7.4, 8.0, 8.1, 8.2, 8.3, 8.4
- Update composer.lock to reflect new PHP version constraints

Co-Authored-By: Ben Sinclair <[email protected]>
- Update actions/checkout from v2 to v4
- Update actions/cache from v2 to v4
- Resolves CI failure due to deprecated action versions

Co-Authored-By: Ben Sinclair <[email protected]>
…tion logic

- Add beforeName and afterName properties to ColumnOperation
- Update TableOperation apply method to use before/after name matching
- Fix reduction scenarios where CHANGE operations are followed by operations on renamed columns
- Addresses feedback from joshmcrae in PR #22

This resolves the issue where column operations couldn't be properly matched during
reduction when CHANGE operations were involved. The solution introduces explicit
before/after name tracking so operations can be matched based on the relationship
between the old name (beforeName) and new name (afterName).

Co-Authored-By: Ben Sinclair <[email protected]>
- Update actions/checkout from v2 to v4
- Update actions/cache from v2 to v4
- Resolves CI failure due to deprecated action versions

Co-Authored-By: Ben Sinclair <[email protected]>
…ymfony/deprecation-contracts

Co-Authored-By: Ben Sinclair <[email protected]>
devin-ai-integration bot and others added 8 commits July 28, 2025 23:46
…egrate master changes

- Resolved conflict in .github/workflows/tests.yml by keeping updated actions v4 and extended PHP version matrix
- Integrated PostgreSQL support, stored procedures, and other master branch improvements
- Preserved before/after name implementation for ColumnOperation reduction logic

Co-Authored-By: Ben Sinclair <[email protected]>
- Update phpspec/prophecy to v1.22.0 to support PHP 8.3/8.4
- Update PHPUnit and related packages for compatibility
- Fixes CI failures on extended PHP version matrix

Co-Authored-By: Ben Sinclair <[email protected]>
- Replace deprecated contains() with assertStringContainsString()
- Resolves 'Call to undefined method' errors in MigrationIntegrationTest
- Addresses CI failures on PHP 8.2 with MySQL 5.7 and PostgreSQL 14

Co-Authored-By: Ben Sinclair <[email protected]>
- Use appropriate matching logic for CHANGE vs other operations
- CHANGE operations match by before/after names
- Other operations match by column name
- Resolves testReduceRewindMigrationsWithChange failure

Co-Authored-By: Ben Sinclair <[email protected]>
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.

4 participants