laravel / framework

The Laravel Framework.
https://laravel.com
MIT License
32.31k stars 10.95k forks source link

migration rollback: $table->dropForeignIdFor($model, $column) attempts (and fails) to drop wrong column name #43753

Closed silas-prototype closed 2 years ago

silas-prototype commented 2 years ago

Description:

$table->dropForeignIdFor($model, $column) in migration attempts (and obviously fails) to drop wrong column name even though the column name (second parameter) is provided.

The issue seems to be with laravel extending the column name on its own going against the behaviour that I would expect. Namely just dropping the provided column name if a column name is provided (and if not a column that follows laravel naming conventions for the referenced model)

And laravel only produces this unexpected behaviour with the dropForeingIdFor method. The analogue foreignIdFor method with the same arguments creates a column with the correct (provided) column name without adding anything to it.

The issue should be neither with php nor the database.

Steps To Reproduce:

  1. Set up php, Laravel with Sail (including mysql), the db-connection etc.

  2. Add foreignId in the up() method of a migration using the foreignIdFor($model, $column) method and provide both the model as well as the column name like:

    public function up() { Schema::table('users', function (Blueprint $table) { $table->foreignIdFor(UserGlobalRole::class, 'global_role_id'); }); }

  3. Add the corresponding dropForeignIdFor($model, $column) to the down method of the same migration with the same model and column as in the up-method like:

    public function down() { Schema::table('users', function (Blueprint $table) { $table->dropForeignIdFor(UserGlobalRole::class, 'global_role_id'); }); }

  4. run migration in console (I simply used: sail artisan migrate)

    • replacing sail with php should have the same effect
    • if db-connection is correctly set up database shows the column was correctly created under the give column-name 'global_role_id'
  5. roll back last migration in console (I simply used: sail artisan migrate:rollback)

    • again: replacing sail with php should have the same effect if the db connection is set up
    • attempts to drop column 'users_global_role_id_foreign' even tho I handed over the column name 'global_role_id' and it throws error in command line like:

Illuminate\Database\QueryException

SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'users_global_role_id_foreign'; check that column/key exists (SQL: alter table users drop foreign key users_global_role_id_foreign)

at vendor/laravel/framework/src/Illuminate/Database/Connection.php:759

driesvints commented 2 years ago

Heya, thanks for reporting.

We'll need more info and/or code to debug this further. Can you please create a repository with the command below, commit the code that reproduces the issue as separate commits on the main/master branch and share the repository here? Please make sure that you have the latest version of the Laravel installer in order to run this command. Please also make sure you have both Git & the GitHub CLI tool properly set up.

laravel new bug-report --github="--public"

Please do not amend and create a separate commit with your custom changes. After you've posted the repository, we'll try to reproduce the issue.

Thanks!

ollieread commented 2 years ago

@driesvints Out of curiosity I went and had a nose at this, and I believe I have discovered the issue. The dropForeignIdFor calls dropForeign, but passes the column as an array.

https://github.com/laravel/framework/blob/9da642c4f7fa5d294551835ca6731e7f807e2125/src/Illuminate/Database/Schema/Blueprint.php#L429

Then, dropForeign calls dropIndexCommand, which has a specific clause for arrays.

https://github.com/laravel/framework/blob/9da642c4f7fa5d294551835ca6731e7f807e2125/src/Illuminate/Database/Schema/Blueprint.php#L1559-L1561

Then, createIndexName will create what it thinks the index name should be based on the columns provided.

I would hazard that this is either a mistake or something that isn't clearly documented; either way, I'm confident this is what's happening to @silas-prototype. I even checked it in 9.23.

driesvints commented 2 years ago

Yes this indeed seems like an issue. But I think we won't be able to change this on a minor/patch release without breaking apps. I think the correct line there should be:

return $this->dropForeign($column ?: [$model->getForeignKey()]);

Otherwise you can't provide a literal column like you're trying to do. The type should then be string|array|null.

We'd welcome a PR to master to change this.

driesvints commented 2 years ago

Here's the original PR btw: https://github.com/laravel/framework/pull/40950

ollieread commented 2 years ago

We'd welcome a PR to master to change this.

I'll give that a go this afternoon if no one else picks it up.

ollieread commented 2 years ago

Digging into this @driesvints, I suspect there's a bit more to it that we need to look into. Want me to create a new issue for a discussion about it?

driesvints commented 2 years ago

What's up?

ollieread commented 2 years ago

I think it's all just very unclear @driesvints. There isn't a bug, the method dropForeignIdFor only drops the index, but no index is created unless you call references() or constrained() on the foreignIdFor() definition.

I think @silas-prototype has made the very easy to make mistake of assuming that dropForeignIdFor is the counterpart for foreignIdFor.

driesvints commented 2 years ago

Right. In that case we'd appreciate a PR to the docs if someone wants to.