Skip to content
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

[9.x] Add support for native rename/drop column commands #45258

Merged
merged 12 commits into from Dec 12, 2022
10 changes: 10 additions & 0 deletions src/Illuminate/Database/Connection.php
Expand Up @@ -1076,6 +1076,16 @@ public function isDoctrineAvailable()
return class_exists('Doctrine\DBAL\Connection');
}

/**
* Indicates whether native alter operations will be used when dropping or renaming columns, even if Doctrine DBAL is installed.
*
* @return bool
*/
public function usingNativeSchemaOperations()
{
return ! $this->isDoctrineAvailable() || SchemaBuilder::$alwaysUsesNativeSchemaOperationsIfPossible;
}

/**
* Get a Doctrine Schema Column instance.
*
Expand Down
3 changes: 2 additions & 1 deletion src/Illuminate/Database/Schema/Blueprint.php
Expand Up @@ -152,7 +152,8 @@ public function toSql(Connection $connection, Grammar $grammar)
protected function ensureCommandsAreValid(Connection $connection)
{
if ($connection instanceof SQLiteConnection) {
if ($this->commandsNamed(['dropColumn', 'renameColumn'])->count() > 1) {
if ($this->commandsNamed(['dropColumn', 'renameColumn'])->count() > 1
&& ! $connection->usingNativeSchemaOperations()) {
throw new BadMethodCallException(
"SQLite doesn't support multiple calls to dropColumn / renameColumn in a single modification."
);
Expand Down
18 changes: 18 additions & 0 deletions src/Illuminate/Database/Schema/Builder.php
Expand Up @@ -45,6 +45,13 @@ class Builder
*/
public static $defaultMorphKeyType = 'int';

/**
* Indicates whether Doctrine DBAL usage will be prevented if possible when dropping and renaming columns.
*
* @var bool
*/
public static $alwaysUsesNativeSchemaOperationsIfPossible = false;

/**
* Create a new database Schema manager.
*
Expand Down Expand Up @@ -105,6 +112,17 @@ public static function morphUsingUlids()
return static::defaultMorphKeyType('ulid');
}

/**
* Attempt to use native schema operations for dropping and renaming columns, even if Doctrine DBAL is installed.
*
* @param bool $value
* @return void
*/
public static function useNativeSchemaOperationsIfPossible(bool $value = true)
{
static::$alwaysUsesNativeSchemaOperationsIfPossible = $value;
}

/**
* Create a database in the schema.
*
Expand Down
2 changes: 1 addition & 1 deletion src/Illuminate/Database/Schema/Grammars/Grammar.php
Expand Up @@ -64,7 +64,7 @@ public function compileDropDatabaseIfExists($name)
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @param \Illuminate\Database\Connection $connection
* @return array
* @return array|string
*/
public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection)
{
Expand Down
19 changes: 19 additions & 0 deletions src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php
Expand Up @@ -203,6 +203,25 @@ public function compileAutoIncrementStartingValues(Blueprint $blueprint)
})->all();
}

/**
* Compile a rename column command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @param \Illuminate\Database\Connection $connection
* @return array|string
*/
public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection)
{
return $connection->usingNativeSchemaOperations()
? sprintf('alter table %s rename column %s to %s',
$this->wrapTable($blueprint),
$this->wrap($command->from),
$this->wrap($command->to)
)
: parent::compileRenameColumn($blueprint, $command, $connection);
}

/**
* Compile a primary key command.
*
Expand Down
20 changes: 20 additions & 0 deletions src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Database\Schema\Grammars;

use Illuminate\Database\Connection;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Fluent;

Expand Down Expand Up @@ -129,6 +130,25 @@ public function compileAutoIncrementStartingValues(Blueprint $blueprint)
})->all();
}

/**
* Compile a rename column command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @param \Illuminate\Database\Connection $connection
* @return array|string
*/
public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection)
{
return $connection->usingNativeSchemaOperations()
? sprintf('alter table %s rename column %s to %s',
$this->wrapTable($blueprint),
$this->wrap($command->from),
$this->wrap($command->to)
)
: parent::compileRenameColumn($blueprint, $command, $connection);
}

/**
* Compile a primary key command.
*
Expand Down
45 changes: 37 additions & 8 deletions src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php
Expand Up @@ -144,6 +144,25 @@ public function compileAdd(Blueprint $blueprint, Fluent $command)
})->all();
}

/**
* Compile a rename column command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @param \Illuminate\Database\Connection $connection
* @return array|string
*/
public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection)
{
return $connection->usingNativeSchemaOperations()
? sprintf('alter table %s rename column %s to %s',
$this->wrapTable($blueprint),
$this->wrap($command->from),
$this->wrap($command->to)
)
: parent::compileRenameColumn($blueprint, $command, $connection);
}

/**
* Compile a unique key command.
*
Expand Down Expand Up @@ -286,17 +305,27 @@ public function compileRebuild()
*/
public function compileDropColumn(Blueprint $blueprint, Fluent $command, Connection $connection)
{
$tableDiff = $this->getDoctrineTableDiff(
$blueprint, $schema = $connection->getDoctrineSchemaManager()
);
if ($connection->usingNativeSchemaOperations()) {
$table = $this->wrapTable($blueprint);

foreach ($command->columns as $name) {
$tableDiff->removedColumns[$name] = $connection->getDoctrineColumn(
$this->getTablePrefix().$blueprint->getTable(), $name
$columns = $this->prefixArray('drop column', $this->wrapArray($command->columns));

return collect($columns)->map(fn ($column) =>
'alter table '.$table.' '.$column
)->all();
} else {
$tableDiff = $this->getDoctrineTableDiff(
$blueprint, $schema = $connection->getDoctrineSchemaManager()
);
}

return (array) $schema->getDatabasePlatform()->getAlterTableSQL($tableDiff);
foreach ($command->columns as $name) {
$tableDiff->removedColumns[$name] = $connection->getDoctrineColumn(
$this->getTablePrefix().$blueprint->getTable(), $name
);
}

return (array) $schema->getDatabasePlatform()->getAlterTableSQL($tableDiff);
}
}

/**
Expand Down
19 changes: 19 additions & 0 deletions src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Database\Schema\Grammars;

use Illuminate\Database\Connection;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Fluent;

Expand Down Expand Up @@ -107,6 +108,24 @@ public function compileAdd(Blueprint $blueprint, Fluent $command)
);
}

/**
* Compile a rename column command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @param \Illuminate\Database\Connection $connection
* @return array|string
*/
public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection)
{
return $connection->usingNativeSchemaOperations()
? sprintf("sp_rename '%s', %s, 'COLUMN'",
$this->wrap($blueprint->getTable().'.'.$command->from),
$this->wrap($command->to)
)
: parent::compileRenameColumn($blueprint, $command, $connection);
}

/**
* Compile a primary key command.
*
Expand Down
1 change: 1 addition & 0 deletions src/Illuminate/Support/Facades/Schema.php
Expand Up @@ -7,6 +7,7 @@
* @method static void defaultMorphKeyType(string $type)
* @method static void morphUsingUuids()
* @method static void morphUsingUlids()
* @method static void useNativeSchemaOperationsIfPossible(bool $value = true)
* @method static bool createDatabase(string $name)
* @method static bool dropDatabaseIfExists(string $name)
* @method static bool hasTable(string $table)
Expand Down
53 changes: 53 additions & 0 deletions tests/Database/DatabaseSchemaBlueprintIntegrationTest.php
Expand Up @@ -42,6 +42,7 @@ protected function tearDown(): void
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication(null);
$this->db->connection()->getSchemaBuilder()->useNativeSchemaOperationsIfPossible(false);
}

public function testRenamingAndChangingColumnsWork()
Expand Down Expand Up @@ -101,6 +102,58 @@ public function testRenamingAndChangingColumnsWork()
$this->assertContains($queries, $expected);
}

public function testRenamingColumnsWithoutDoctrineWorks()
{
$connection = $this->db->connection();
$schema = $connection->getSchemaBuilder();

$schema->useNativeSchemaOperationsIfPossible();

$base = new Blueprint('users', function ($table) {
$table->renameColumn('name', 'new_name');
});

$blueprint = clone $base;
$this->assertEquals(['alter table `users` rename column `name` to `new_name`'], $blueprint->toSql($connection, new MySqlGrammar));

$blueprint = clone $base;
$this->assertEquals(['alter table "users" rename column "name" to "new_name"'], $blueprint->toSql($connection, new PostgresGrammar));

$blueprint = clone $base;
$this->assertEquals(['alter table "users" rename column "name" to "new_name"'], $blueprint->toSql($connection, new SQLiteGrammar));

$blueprint = clone $base;
$this->assertEquals(['sp_rename \'"users"."name"\', "new_name", \'COLUMN\''], $blueprint->toSql($connection, new SqlServerGrammar));

$schema->create('test', function (Blueprint $table) {
$table->string('foo');
$table->string('baz');
});

$schema->table('test', function (Blueprint $table) {
$table->renameColumn('foo', 'bar');
$table->renameColumn('baz', 'qux');
});

$this->assertFalse($schema->hasColumn('test', 'foo'));
$this->assertFalse($schema->hasColumn('test', 'baz'));
$this->assertTrue($schema->hasColumns('test', ['bar', 'qux']));
}

public function testDroppingColumnsWithoutDoctrineWorks()
{
$connection = $this->db->connection();
$schema = $connection->getSchemaBuilder();

$schema->useNativeSchemaOperationsIfPossible();

$blueprint = new Blueprint('users', function ($table) {
$table->dropColumn('name');
});

$this->assertEquals(['alter table "users" drop column "name"'], $blueprint->toSql($connection, new SQLiteGrammar));
}

public function testChangingColumnWithCollationWorks()
{
$this->db->connection()->getSchemaBuilder()->create('users', function ($table) {
Expand Down
44 changes: 44 additions & 0 deletions tests/Database/DatabaseSchemaBlueprintTest.php
Expand Up @@ -173,6 +173,50 @@ public function testRemoveColumn()
$this->assertEquals(['alter table `users` add `foo` varchar(255) not null'], $blueprint->toSql($connection, new MySqlGrammar));
}

public function testRenameColumnWithoutDoctrine()
{
$base = new Blueprint('users', function ($table) {
$table->renameColumn('foo', 'bar');
});

$connection = m::mock(Connection::class);
$connection->shouldReceive('usingNativeSchemaOperations')->andReturn(true);

$blueprint = clone $base;
$this->assertEquals(['alter table `users` rename column `foo` to `bar`'], $blueprint->toSql($connection, new MySqlGrammar));

$blueprint = clone $base;
$this->assertEquals(['alter table "users" rename column "foo" to "bar"'], $blueprint->toSql($connection, new PostgresGrammar));

$blueprint = clone $base;
$this->assertEquals(['alter table "users" rename column "foo" to "bar"'], $blueprint->toSql($connection, new SQLiteGrammar));

$blueprint = clone $base;
$this->assertEquals(['sp_rename \'"users"."foo"\', "bar", \'COLUMN\''], $blueprint->toSql($connection, new SqlServerGrammar));
}

public function testDropColumnWithoutDoctrine()
{
$base = new Blueprint('users', function ($table) {
$table->dropColumn('foo');
});

$connection = m::mock(Connection::class);
$connection->shouldReceive('usingNativeSchemaOperations')->andReturn(true);

$blueprint = clone $base;
$this->assertEquals(['alter table `users` drop `foo`'], $blueprint->toSql($connection, new MySqlGrammar));

$blueprint = clone $base;
$this->assertEquals(['alter table "users" drop column "foo"'], $blueprint->toSql($connection, new PostgresGrammar));

$blueprint = clone $base;
$this->assertEquals(['alter table "users" drop column "foo"'], $blueprint->toSql($connection, new SQLiteGrammar));

$blueprint = clone $base;
$this->assertStringContainsString('alter table "users" drop column "foo"', $blueprint->toSql($connection, new SqlServerGrammar)[0]);
}

public function testMacroable()
{
Blueprint::macro('foo', function () {
Expand Down