From 255547315d1f7c7a841bb1939ae39a4a50bd96c0 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 12 Feb 2024 20:45:00 +0330 Subject: [PATCH] [11.x] Non-default schema names (#50019) * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix rename table on sqlsrv * wip * wip * fix rename column and rename index on sqlsrv * fix drop primary on pgsql * wip * wip * wip * wip * wip * wip * enable prefix tests * wip * wip * fix prefix auto-increment starting value on pgsql * use blueprint prefix on sqlite * use wrapTable where ever possible * wip * wip * fix index name with schema name+prefix * fix schema name+prefix * wip * fix wrapping table * fix auto-increment starting value on pgsql * wip * fix tests --- src/Illuminate/Database/Grammar.php | 52 ++- src/Illuminate/Database/Schema/Blueprint.php | 6 +- .../Schema/Grammars/PostgresGrammar.php | 7 +- .../Schema/Grammars/SQLiteGrammar.php | 4 +- .../Schema/Grammars/SqlServerGrammar.php | 19 +- .../Database/DatabaseSchemaBlueprintTest.php | 2 +- .../DatabaseSqlServerSchemaGrammarTest.php | 10 +- .../Database/DatabaseSchemaBlueprintTest.php | 10 +- .../Database/SchemaBuilderSchemaNameTest.php | 435 ++++++++++++++++++ 9 files changed, 505 insertions(+), 40 deletions(-) create mode 100644 tests/Integration/Database/SchemaBuilderSchemaNameTest.php diff --git a/src/Illuminate/Database/Grammar.php b/src/Illuminate/Database/Grammar.php index 9ce0ec352595..79da1724ccb4 100755 --- a/src/Illuminate/Database/Grammar.php +++ b/src/Illuminate/Database/Grammar.php @@ -43,21 +43,38 @@ public function wrapArray(array $values) */ public function wrapTable($table) { - if (! $this->isExpression($table)) { - return $this->wrap($this->tablePrefix.$table, true); + if ($this->isExpression($table)) { + return $this->getValue($table); } - return $this->getValue($table); + // If the table being wrapped has an alias we'll need to separate the pieces + // so we can prefix the table and then wrap each of the segments on their + // own and then join these both back together using the "as" connector. + if (stripos($table, ' as ') !== false) { + return $this->wrapAliasedTable($table); + } + + // If the table being wrapped has a custom schema name specified, we need to + // prefix the last segment as the table name then wrap each segment alone + // and eventually join them both back together using the dot connector. + if (str_contains($table, '.')) { + $table = substr_replace($table, '.'.$this->tablePrefix, strrpos($table, '.'), 1); + + return collect(explode('.', $table)) + ->map($this->wrapValue(...)) + ->implode('.'); + } + + return $this->wrapValue($this->tablePrefix.$table); } /** * Wrap a value in keyword identifiers. * * @param \Illuminate\Contracts\Database\Query\Expression|string $value - * @param bool $prefixAlias * @return string */ - public function wrap($value, $prefixAlias = false) + public function wrap($value) { if ($this->isExpression($value)) { return $this->getValue($value); @@ -67,7 +84,7 @@ public function wrap($value, $prefixAlias = false) // the pieces so we can wrap each of the segments of the expression on its // own, and then join these both back together using the "as" connector. if (stripos($value, ' as ') !== false) { - return $this->wrapAliasedValue($value, $prefixAlias); + return $this->wrapAliasedValue($value); } // If the given value is a JSON selector we will wrap it differently than a @@ -84,23 +101,28 @@ public function wrap($value, $prefixAlias = false) * Wrap a value that has an alias. * * @param string $value - * @param bool $prefixAlias * @return string */ - protected function wrapAliasedValue($value, $prefixAlias = false) + protected function wrapAliasedValue($value) { $segments = preg_split('/\s+as\s+/i', $value); - // If we are wrapping a table we need to prefix the alias with the table prefix - // as well in order to generate proper syntax. If this is a column of course - // no prefix is necessary. The condition will be true when from wrapTable. - if ($prefixAlias) { - $segments[1] = $this->tablePrefix.$segments[1]; - } - return $this->wrap($segments[0]).' as '.$this->wrapValue($segments[1]); } + /** + * Wrap a table that has an alias. + * + * @param string $value + * @return string + */ + protected function wrapAliasedTable($value) + { + $segments = preg_split('/\s+as\s+/i', $value); + + return $this->wrapTable($segments[0]).' as '.$this->wrapValue($this->tablePrefix.$segments[1]); + } + /** * Wrap the given value segments. * diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index d2c03f501e04..cd254b71b3f2 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -1599,7 +1599,11 @@ protected function dropIndexCommand($command, $type, $index) */ protected function createIndexName($type, array $columns) { - $index = strtolower($this->prefix.$this->table.'_'.implode('_', $columns).'_'.$type); + $table = str_contains($this->table, '.') + ? substr_replace($this->table, '.'.$this->prefix, strrpos($this->table, '.'), 1) + : $this->prefix.$this->table; + + $index = strtolower($table.'_'.implode('_', $columns).'_'.$type); return str_replace(['-', '.'], '_', $index); } diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index c71e266230e5..aad87542d552 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -230,7 +230,9 @@ public function compileAutoIncrementStartingValues(Blueprint $blueprint, Fluent { if ($command->column->autoIncrement && $value = $command->column->get('startingValue', $command->column->get('from'))) { - return 'alter sequence '.$blueprint->getTable().'_'.$command->column->name.'_seq restart with '.$value; + $table = last(explode('.', $blueprint->getTable())); + + return 'alter sequence '.$blueprint->getPrefix().$table.'_'.$command->column->name.'_seq restart with '.$value; } } @@ -483,7 +485,8 @@ public function compileDropColumn(Blueprint $blueprint, Fluent $command) */ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap("{$blueprint->getPrefix()}{$blueprint->getTable()}_pkey"); + $table = last(explode('.', $blueprint->getTable())); + $index = $this->wrap("{$blueprint->getPrefix()}{$table}_pkey"); return 'alter table '.$this->wrapTable($blueprint)." drop constraint {$index}"; } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 54ef73ef2a4f..5d969c3eaf93 100644 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -295,8 +295,8 @@ public function compileChange(Blueprint $blueprint, Fluent $command, Connection fn ($index) => $this->{'compile'.ucfirst($index->name)}($blueprint, $index) )->all(); - $tempTable = $this->wrap('__temp__'.$this->getTablePrefix().$table); - $table = $this->wrap($this->getTablePrefix().$table); + $tempTable = $this->wrap('__temp__'.$blueprint->getPrefix().$table); + $table = $this->wrapTable($blueprint); $columnNames = implode(', ', $columnNames); $foreignKeyConstraintsEnabled = $connection->scalar('pragma foreign_keys'); diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index b55ca11c394f..b719f127f3dc 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -215,8 +215,8 @@ public function compileAdd(Blueprint $blueprint, Fluent $command) */ public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection) { - return sprintf("sp_rename '%s', %s, 'COLUMN'", - $this->wrap($blueprint->getTable().'.'.$command->from), + return sprintf("sp_rename %s, %s, N'COLUMN'", + $this->quoteString($this->wrapTable($blueprint).'.'.$this->wrap($command->from)), $this->wrap($command->to) ); } @@ -358,7 +358,7 @@ public function compileDrop(Blueprint $blueprint, Fluent $command) public function compileDropIfExists(Blueprint $blueprint, Fluent $command) { return sprintf('if object_id(%s, \'U\') is not null drop table %s', - $this->quoteString($this->getTablePrefix().$blueprint->getTable()), + $this->quoteString($this->wrapTable($blueprint)), $this->wrapTable($blueprint) ); } @@ -403,7 +403,7 @@ public function compileDropDefaultConstraint(Blueprint $blueprint, Fluent $comma : "'".implode("','", $command->columns)."'"; $table = $this->wrapTable($blueprint); - $tableName = $this->quoteString($this->getTablePrefix().$blueprint->getTable()); + $tableName = $this->quoteString($this->wrapTable($blueprint)); $sql = "DECLARE @sql NVARCHAR(MAX) = '';"; $sql .= "SELECT @sql += 'ALTER TABLE $table DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' "; @@ -491,9 +491,10 @@ public function compileDropForeign(Blueprint $blueprint, Fluent $command) */ public function compileRename(Blueprint $blueprint, Fluent $command) { - $from = $this->wrapTable($blueprint); - - return "sp_rename {$from}, ".$this->wrapTable($command->to); + return sprintf('sp_rename %s, %s', + $this->quoteString($this->wrapTable($blueprint)), + $this->wrapTable($command->to) + ); } /** @@ -505,8 +506,8 @@ public function compileRename(Blueprint $blueprint, Fluent $command) */ public function compileRenameIndex(Blueprint $blueprint, Fluent $command) { - return sprintf("sp_rename N'%s', %s, N'INDEX'", - $this->wrap($blueprint->getTable().'.'.$command->from), + return sprintf("sp_rename %s, %s, N'INDEX'", + $this->quoteString($this->wrapTable($blueprint).'.'.$this->wrap($command->from)), $this->wrap($command->to) ); } diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index b8a6973f6cbc..a9bb895ec0d1 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -181,7 +181,7 @@ public function testRenameColumn() $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)); + $this->assertEquals(['sp_rename N\'"users"."foo"\', "bar", N\'COLUMN\''], $blueprint->toSql($connection, new SqlServerGrammar)); } public function testNativeRenameColumnOnMysql57() diff --git a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php index 5b2d4123e8fa..7fb9a89f08fc 100755 --- a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php +++ b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php @@ -83,14 +83,14 @@ public function testDropTableIfExists() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('if object_id(N\'users\', \'U\') is not null drop table "users"', $statements[0]); + $this->assertSame('if object_id(N\'"users"\', \'U\') is not null drop table "users"', $statements[0]); $blueprint = new Blueprint('users'); $blueprint->dropIfExists(); $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()->setTablePrefix('prefix_')); $this->assertCount(1, $statements); - $this->assertSame('if object_id(N\'prefix_users\', \'U\') is not null drop table "prefix_users"', $statements[0]); + $this->assertSame('if object_id(N\'"prefix_users"\', \'U\') is not null drop table "prefix_users"', $statements[0]); } public function testDropColumn() @@ -124,7 +124,7 @@ public function testDropColumnDropsCreatesSqlToDropDefaultConstraints() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame("DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"foo\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'foo') AND [name] in ('bar') AND [default_object_id] <> 0;EXEC(@sql);alter table \"foo\" drop column \"bar\"", $statements[0]); + $this->assertSame("DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"foo\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'\"foo\"') AND [name] in ('bar') AND [default_object_id] <> 0;EXEC(@sql);alter table \"foo\" drop column \"bar\"", $statements[0]); } public function testDropPrimary() @@ -185,7 +185,7 @@ public function testDropConstrainedForeignId() $this->assertCount(2, $statements); $this->assertSame('alter table "users" drop constraint "users_foo_foreign"', $statements[0]); - $this->assertSame('DECLARE @sql NVARCHAR(MAX) = \'\';SELECT @sql += \'ALTER TABLE "users" DROP CONSTRAINT \' + OBJECT_NAME([default_object_id]) + \';\' FROM sys.columns WHERE [object_id] = OBJECT_ID(N\'users\') AND [name] in (\'foo\') AND [default_object_id] <> 0;EXEC(@sql);alter table "users" drop column "foo"', $statements[1]); + $this->assertSame('DECLARE @sql NVARCHAR(MAX) = \'\';SELECT @sql += \'ALTER TABLE "users" DROP CONSTRAINT \' + OBJECT_NAME([default_object_id]) + \';\' FROM sys.columns WHERE [object_id] = OBJECT_ID(N\'"users"\') AND [name] in (\'foo\') AND [default_object_id] <> 0;EXEC(@sql);alter table "users" drop column "foo"', $statements[1]); } public function testDropTimestamps() @@ -226,7 +226,7 @@ public function testRenameTable() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('sp_rename "users", "foo"', $statements[0]); + $this->assertSame('sp_rename N\'"users"\', "foo"', $statements[0]); } public function testRenameIndex() diff --git a/tests/Integration/Database/DatabaseSchemaBlueprintTest.php b/tests/Integration/Database/DatabaseSchemaBlueprintTest.php index 4e88e12aecdf..42fe4de937d7 100644 --- a/tests/Integration/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Integration/Database/DatabaseSchemaBlueprintTest.php @@ -190,7 +190,7 @@ public function testNativeColumnModifyingOnSqlServer() }); $this->assertEquals([ - "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'users') AND [name] in ('added_at') AND [default_object_id] <> 0;EXEC(@sql)", + "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'\"users\"') AND [name] in ('added_at') AND [default_object_id] <> 0;EXEC(@sql)", 'alter table "users" alter column "added_at" datetime2(4) not null', 'alter table "users" add default CURRENT_TIMESTAMP for "added_at"', ], $blueprint->toSql($connection, new SqlServerGrammar)); @@ -200,7 +200,7 @@ public function testNativeColumnModifyingOnSqlServer() }); $this->assertEquals([ - "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'users') AND [name] in ('name') AND [default_object_id] <> 0;EXEC(@sql)", + "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'\"users\"') AND [name] in ('name') AND [default_object_id] <> 0;EXEC(@sql)", 'alter table "users" alter column "name" nchar(40) collate unicode null', 'alter table "users" add default \'easy\' for "name"', ], $blueprint->toSql($connection, new SqlServerGrammar)); @@ -210,7 +210,7 @@ public function testNativeColumnModifyingOnSqlServer() }); $this->assertEquals([ - "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'users') AND [name] in ('foo') AND [default_object_id] <> 0;EXEC(@sql)", + "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'\"users\"') AND [name] in ('foo') AND [default_object_id] <> 0;EXEC(@sql)", 'alter table "users" alter column "foo" int not null', ], $blueprint->toSql($connection, new SqlServerGrammar)); } @@ -448,7 +448,7 @@ public function testAddUniqueIndexWithoutNameWorks() $queries = $blueprintSqlServer->toSql(DB::connection(), new SqlServerGrammar); $expected = [ - "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'users') AND [name] in ('name') AND [default_object_id] <> 0;EXEC(@sql)", + "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'\"users\"') AND [name] in ('name') AND [default_object_id] <> 0;EXEC(@sql)", 'alter table "users" alter column "name" nvarchar(255) null', 'create unique index "users_name_unique" on "users" ("name")', ]; @@ -512,7 +512,7 @@ public function testAddUniqueIndexWithNameWorks() $queries = $blueprintSqlServer->toSql(DB::connection(), new SqlServerGrammar); $expected = [ - "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'users') AND [name] in ('name') AND [default_object_id] <> 0;EXEC(@sql)", + "DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE \"users\" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID(N'\"users\"') AND [name] in ('name') AND [default_object_id] <> 0;EXEC(@sql)", 'alter table "users" alter column "name" int null', 'create unique index "index1" on "users" ("name")', ]; diff --git a/tests/Integration/Database/SchemaBuilderSchemaNameTest.php b/tests/Integration/Database/SchemaBuilderSchemaNameTest.php new file mode 100644 index 000000000000..2d6d9512ed87 --- /dev/null +++ b/tests/Integration/Database/SchemaBuilderSchemaNameTest.php @@ -0,0 +1,435 @@ +driver, ['pgsql', 'sqlsrv'])) { + $this->markTestSkipped('Test requires a PostgreSQL or SQL Server connection.'); + } + + if ($this->driver === 'pgsql') { + DB::connection('without-prefix')->statement('create schema if not exists my_schema'); + DB::connection('with-prefix')->statement('create schema if not exists my_schema'); + } elseif ($this->driver === 'sqlsrv') { + DB::connection('without-prefix')->statement("if schema_id('my_schema') is null begin exec('create schema my_schema') end"); + DB::connection('with-prefix')->statement("if schema_id('my_schema') is null begin exec('create schema my_schema') end"); + } + } + + protected function defineEnvironment($app) + { + parent::defineEnvironment($app); + + $app['config']->set('database.connections.pgsql.search_path', 'public,my_schema'); + $app['config']->set('database.connections.without-prefix', $app['config']->get('database.connections.'.$this->driver)); + $app['config']->set('database.connections.with-prefix', $app['config']->get('database.connections.without-prefix')); + $app['config']->set('database.connections.with-prefix.prefix', 'example_'); + } + + #[DataProvider('connectionProvider')] + public function testCreate($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->id(); + }); + + $this->assertTrue($schema->hasTable('my_schema.table')); + $this->assertFalse($schema->hasTable('table')); + } + + #[DataProvider('connectionProvider')] + public function testRename($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->id(); + }); + $schema->create('table', function (Blueprint $table) { + $table->id(); + }); + + $this->assertTrue($schema->hasTable('my_schema.table')); + $this->assertFalse($schema->hasTable('my_schema.new_table')); + $this->assertTrue($schema->hasTable('table')); + $this->assertFalse($schema->hasTable('my_table')); + + $schema->rename('my_schema.table', 'new_table'); + $schema->rename('table', 'my_table'); + + $this->assertTrue($schema->hasTable('my_schema.new_table')); + $this->assertFalse($schema->hasTable('my_schema.table')); + $this->assertTrue($schema->hasTable('my_table')); + $this->assertFalse($schema->hasTable('table')); + } + + #[DataProvider('connectionProvider')] + public function testDrop($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->id(); + }); + $schema->create('table', function (Blueprint $table) { + $table->id(); + }); + + $this->assertTrue($schema->hasTable('my_schema.table')); + $this->assertTrue($schema->hasTable('table')); + + $schema->drop('my_schema.table'); + + $this->assertFalse($schema->hasTable('my_schema.table')); + $this->assertTrue($schema->hasTable('table')); + } + + #[DataProvider('connectionProvider')] + public function testDropIfExists($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->id(); + }); + $schema->create('table', function (Blueprint $table) { + $table->id(); + }); + + $this->assertTrue($schema->hasTable('my_schema.table')); + $this->assertTrue($schema->hasTable('table')); + + $schema->dropIfExists('my_schema.table'); + $schema->dropIfExists('my_schema.fake_table'); + $schema->dropIfExists('fake_schema.table'); + + $this->assertFalse($schema->hasTable('my_schema.table')); + $this->assertTrue($schema->hasTable('table')); + } + + #[DataProvider('connectionProvider')] + public function testAddColumns($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->id(); + $table->string('title')->default('default schema title'); + }); + $schema->create('my_table', function (Blueprint $table) { + $table->id(); + $table->string('name')->default('default name'); + }); + + $this->assertEquals(['id', 'title'], $schema->getColumnListing('my_schema.table')); + $this->assertEquals(['id', 'name'], $schema->getColumnListing('my_table')); + + $schema->table('my_schema.table', function (Blueprint $table) { + $table->string('name')->default('default schema name'); + $table->integer('count'); + }); + $schema->table('my_table', function (Blueprint $table) { + $table->integer('count'); + $table->string('title')->default('default title'); + }); + + $this->assertEquals(['id', 'title', 'name', 'count'], $schema->getColumnListing('my_schema.table')); + $this->assertEquals(['id', 'name', 'count', 'title'], $schema->getColumnListing('my_table')); + $this->assertStringContainsString('default schema name', collect($schema->getColumns('my_schema.table'))->firstWhere('name', 'name')['default']); + $this->assertStringContainsString('default schema title', collect($schema->getColumns('my_schema.table'))->firstWhere('name', 'title')['default']); + $this->assertStringContainsString('default name', collect($schema->getColumns('my_table'))->firstWhere('name', 'name')['default']); + $this->assertStringContainsString('default title', collect($schema->getColumns('my_table'))->firstWhere('name', 'title')['default']); + } + + #[DataProvider('connectionProvider')] + public function testRenameColumns($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->id(); + $table->string('title')->default('default schema title'); + }); + $schema->create('table', function (Blueprint $table) { + $table->id(); + $table->string('name')->default('default name'); + }); + + $this->assertTrue($schema->hasColumn('my_schema.table', 'title')); + $this->assertTrue($schema->hasColumn('table', 'name')); + + $schema->table('my_schema.table', function (Blueprint $table) { + $table->renameColumn('title', 'new_title'); + }); + $schema->table('table', function (Blueprint $table) { + $table->renameColumn('name', 'new_name'); + }); + + $this->assertFalse($schema->hasColumn('my_schema.table', 'title')); + $this->assertTrue($schema->hasColumn('my_schema.table', 'new_title')); + $this->assertFalse($schema->hasColumn('table', 'name')); + $this->assertTrue($schema->hasColumn('table', 'new_name')); + $this->assertStringContainsString('default schema title', collect($schema->getColumns('my_schema.table'))->firstWhere('name', 'new_title')['default']); + $this->assertStringContainsString('default name', collect($schema->getColumns('table'))->firstWhere('name', 'new_name')['default']); + } + + #[DataProvider('connectionProvider')] + public function testModifyColumns($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->integer('count'); + }); + $schema->create('my_table', function (Blueprint $table) { + $table->id(); + $table->string('title'); + $table->integer('count'); + }); + + $schema->table('my_schema.table', function (Blueprint $table) { + $table->string('name')->default('default schema name')->change(); + $table->bigInteger('count')->change(); + }); + $schema->table('my_table', function (Blueprint $table) { + $table->string('title')->default('default title')->change(); + $table->bigInteger('count')->change(); + }); + + $this->assertStringContainsString('default schema name', collect($schema->getColumns('my_schema.table'))->firstWhere('name', 'name')['default']); + $this->assertStringContainsString('default title', collect($schema->getColumns('my_table'))->firstWhere('name', 'title')['default']); + $this->assertEquals($this->driver === 'sqlsrv' ? 'nvarchar' : 'varchar', $schema->getColumnType('my_schema.table', 'name')); + $this->assertEquals($this->driver === 'sqlsrv' ? 'nvarchar' : 'varchar', $schema->getColumnType('my_table', 'title')); + $this->assertEquals($this->driver === 'pgsql' ? 'int8' : 'bigint', $schema->getColumnType('my_schema.table', 'count')); + $this->assertEquals($this->driver === 'pgsql' ? 'int8' : 'bigint', $schema->getColumnType('my_table', 'count')); + } + + #[DataProvider('connectionProvider')] + public function testDropColumns($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->id(); + $table->string('name')->default('default schema name'); + $table->integer('count')->default(20); + $table->string('title')->default('default schema title'); + }); + $schema->create('table', function (Blueprint $table) { + $table->id(); + $table->string('name')->default('default name'); + $table->integer('count')->default(10); + $table->string('title')->default('default title'); + }); + + $this->assertTrue($schema->hasColumns('my_schema.table', ['id', 'name', 'count', 'title'])); + $this->assertTrue($schema->hasColumns('table', ['id', 'name', 'count', 'title'])); + + $schema->dropColumns('my_schema.table', ['name', 'count']); + $schema->dropColumns('table', ['name', 'title']); + + $this->assertTrue($schema->hasColumns('my_schema.table', ['id', 'title'])); + $this->assertFalse($schema->hasColumn('my_schema.table', 'name')); + $this->assertFalse($schema->hasColumn('my_schema.table', 'count')); + $this->assertTrue($schema->hasColumns('table', ['id', 'count'])); + $this->assertFalse($schema->hasColumn('table', 'name')); + $this->assertFalse($schema->hasColumn('table', 'title')); + $this->assertStringContainsString('default schema title', collect($schema->getColumns('my_schema.table'))->firstWhere('name', 'title')['default']); + $this->assertStringContainsString('10', collect($schema->getColumns('table'))->firstWhere('name', 'count')['default']); + } + + #[DataProvider('connectionProvider')] + public function testIndexes($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->string('code')->primary(); + $table->string('email')->unique(); + $table->integer('name')->index(); + $table->integer('title')->index(); + }); + $schema->create('my_table', function (Blueprint $table) { + $table->string('code')->primary(); + $table->string('email')->unique(); + $table->integer('name')->index(); + $table->integer('title')->index(); + }); + + $this->assertTrue($schema->hasIndex('my_schema.table', ['code'], 'primary')); + $this->assertTrue($schema->hasIndex('my_schema.table', ['email'], 'unique')); + $this->assertTrue($schema->hasIndex('my_schema.table', ['name'])); + $this->assertTrue($schema->hasIndex('my_table', ['code'], 'primary')); + $this->assertTrue($schema->hasIndex('my_table', ['email'], 'unique')); + $this->assertTrue($schema->hasIndex('my_table', ['name'])); + + $schemaIndexName = $connection === 'with-prefix' ? 'my_schema_example_table_title_index' : 'my_schema_table_title_index'; + $indexName = $connection === 'with-prefix' ? 'example_my_table_title_index' : 'my_table_title_index'; + + $schema->table('my_schema.table', function (Blueprint $table) use ($schemaIndexName) { + $table->renameIndex($schemaIndexName, 'schema_new_index_name'); + }); + $schema->table('my_table', function (Blueprint $table) use ($indexName) { + $table->renameIndex($indexName, 'new_index_name'); + }); + + $this->assertTrue($schema->hasIndex('my_schema.table', 'schema_new_index_name')); + $this->assertFalse($schema->hasIndex('my_schema.table', $schemaIndexName)); + $this->assertTrue($schema->hasIndex('my_table', 'new_index_name')); + $this->assertFalse($schema->hasIndex('my_table', $indexName)); + + $schema->table('my_schema.table', function (Blueprint $table) { + $table->dropPrimary(['code']); + $table->dropUnique(['email']); + $table->dropIndex(['name']); + $table->dropIndex('schema_new_index_name'); + }); + $schema->table('my_table', function (Blueprint $table) { + $table->dropPrimary(['code']); + $table->dropUnique(['email']); + $table->dropIndex(['name']); + $table->dropIndex('new_index_name'); + }); + + $this->assertEmpty($schema->getIndexListing('my_schema.table')); + $this->assertEmpty($schema->getIndexListing('my_table')); + } + + #[DataProvider('connectionProvider')] + public function testForeignKeys($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_tables', function (Blueprint $table) { + $table->id(); + }); + $schema->create('my_schema.table', function (Blueprint $table) { + $table->id(); + $table->foreignId('my_table_id')->constrained(); + }); + $schema->create('table', function (Blueprint $table) { + $table->unsignedBigInteger('table_id'); + $table->foreign('table_id')->references('id')->on('my_schema.table'); + }); + + $schemaTableName = $connection === 'with-prefix' ? 'example_table' : 'table'; + $tableName = $connection === 'with-prefix' ? 'example_my_tables' : 'my_tables'; + + $this->assertTrue(collect($schema->getForeignKeys('my_schema.table'))->contains( + fn ($foreign) => $foreign['columns'] === ['my_table_id'] + && $foreign['foreign_table'] === $tableName && in_array($foreign['foreign_schema'], ['public', 'dbo']) + && $foreign['foreign_columns'] === ['id'] + )); + + $this->assertTrue(collect($schema->getForeignKeys('table'))->contains( + fn ($foreign) => $foreign['columns'] === ['table_id'] + && $foreign['foreign_table'] === $schemaTableName && $foreign['foreign_schema'] === 'my_schema' + && $foreign['foreign_columns'] === ['id'] + )); + + $schema->table('my_schema.table', function (Blueprint $table) { + $table->dropForeign(['my_table_id']); + }); + $schema->table('table', function (Blueprint $table) { + $table->dropForeign(['table_id']); + }); + + $this->assertEmpty($schema->getForeignKeys('my_schema.table')); + $this->assertEmpty($schema->getForeignKeys('table')); + } + + #[DataProvider('connectionProvider')] + public function testHasView($connection) + { + $connection = DB::connection($connection); + $schema = $connection->getSchemaBuilder(); + + $connection->statement('create view '.$connection->getSchemaGrammar()->wrapTable('my_schema.view').' (name) as select 1'); + $connection->statement('create view '.$connection->getSchemaGrammar()->wrapTable('my_view').' (name) as select 1'); + + $this->assertTrue($schema->hasView('my_schema.view')); + $this->assertTrue($schema->hasView('my_view')); + $this->assertTrue($schema->hasColumn('my_schema.view', 'name')); + $this->assertTrue($schema->hasColumn('my_view', 'name')); + + $connection->statement('drop view '.$connection->getSchemaGrammar()->wrapTable('my_schema.view')); + $connection->statement('drop view '.$connection->getSchemaGrammar()->wrapTable('my_view')); + + $this->assertFalse($schema->hasView('my_schema.view')); + $this->assertFalse($schema->hasView('my_view')); + } + + #[DataProvider('connectionProvider')] + public function testComment($connection) + { + if ($this->driver !== 'pgsql') { + $this->markTestSkipped('Test requires a PostgreSQL connection.'); + } + + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->comment('comment on schema table'); + $table->string('name')->comment('comment on schema column'); + }); + $schema->create('table', function (Blueprint $table) { + $table->comment('comment on table'); + $table->string('name')->comment('comment on column'); + }); + + $tables = collect($schema->getTables()); + $tableName = $connection === 'with-prefix' ? 'example_table' : 'table'; + + $this->assertEquals('comment on schema table', + $tables->first(fn ($table) => $table['name'] === $tableName && $table['schema'] === 'my_schema')['comment'] + ); + $this->assertEquals('comment on table', + $tables->first(fn ($table) => $table['name'] === $tableName && $table['schema'] === 'public')['comment'] + ); + $this->assertEquals('comment on schema column', + collect($schema->getColumns('my_schema.table'))->firstWhere('name', 'name')['comment'] + ); + $this->assertEquals('comment on column', + collect($schema->getColumns('table'))->firstWhere('name', 'name')['comment'] + ); + } + + #[DataProvider('connectionProvider')] + public function testAutoIncrementStartingValue($connection) + { + if ($this->driver !== 'pgsql') { + $this->markTestSkipped('Test requires a PostgreSQL connection.'); + } + + $this->expectNotToPerformAssertions(); + + $schema = Schema::connection($connection); + + $schema->create('my_schema.table', function (Blueprint $table) { + $table->increments('code')->from(25); + }); + $schema->create('table', function (Blueprint $table) { + $table->increments('code')->from(15); + }); + } + + public static function connectionProvider(): array + { + return [ + 'without prefix' => ['without-prefix'], + 'with prefix' => ['with-prefix'], + ]; + } +}