Skip to content

Commit

Permalink
[9.x] Add support for native rename/drop column commands (#45258)
Browse files Browse the repository at this point in the history
* add `compileRenameColumn` to DB grammar classes

* add tests

* add a static property to schema builder

* add native drop column for SQLite

* formatting

* add the optional column keyword to drop command

* fix php 8.0 windows tests

* merge previously added schema methods into one

* better test

* formatting and renaming

* formatting

* fix tests

Co-authored-by: Taylor Otwell <taylor@laravel.com>
  • Loading branch information
hafezdivandari and taylorotwell committed Dec 12, 2022
1 parent 26c476d commit 72f6e37
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 10 deletions.
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

0 comments on commit 72f6e37

Please sign in to comment.