diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index b1c4c99360c3..c4bcb723bf6b 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -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. * diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index e3ab9a11ba20..ee0988d5273a 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -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." ); diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 176432c33a97..2788a124fdb5 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -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. * @@ -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. * diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index d446dd7dfbc7..ea8333e40436 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -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) { diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index c5259e1005f0..40ee3c8b873d 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -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. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index f425feb0fdda..ef60d0ff820f 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Schema\Grammars; +use Illuminate\Database\Connection; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Fluent; @@ -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. * diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 9bb6fd8fa0f4..8c1dc24e772c 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -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. * @@ -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); + } } /** diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 5121d782efe3..4d7271ca3308 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Schema\Grammars; +use Illuminate\Database\Connection; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Fluent; @@ -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. * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 747e93818955..b1a89a427848 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -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) diff --git a/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php b/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php index 32f883627f5d..b6b081496be4 100644 --- a/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php +++ b/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php @@ -42,6 +42,7 @@ protected function tearDown(): void { Facade::clearResolvedInstances(); Facade::setFacadeApplication(null); + $this->db->connection()->getSchemaBuilder()->useNativeSchemaOperationsIfPossible(false); } public function testRenamingAndChangingColumnsWork() @@ -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) { diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index 9aaa92bb8d34..3e6ce17f5a43 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -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 () {