diff --git a/src/Illuminate/Database/Concerns/ParsesSearchPath.php b/src/Illuminate/Database/Concerns/ParsesSearchPath.php index 437ff2b26b3b..e822c722b72c 100644 --- a/src/Illuminate/Database/Concerns/ParsesSearchPath.php +++ b/src/Illuminate/Database/Concerns/ParsesSearchPath.php @@ -18,12 +18,8 @@ protected function parseSearchPath($searchPath) $searchPath = $matches[0]; } - $searchPath ??= []; - - array_walk($searchPath, static function (&$schema) { - $schema = trim($schema, '\'"'); - }); - - return $searchPath; + return array_map(function ($schema) { + return trim($schema, '\'"'); + }, $searchPath ?? []); } } diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 603eb24def72..6442212c1477 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -272,7 +272,7 @@ public function compileDropIfExists(Blueprint $blueprint, Fluent $command) */ public function compileDropAllTables($tables) { - return 'drop table "'.implode('","', $tables).'" cascade'; + return 'drop table '.implode(',', $this->escapeNames($tables)).' cascade'; } /** @@ -283,7 +283,7 @@ public function compileDropAllTables($tables) */ public function compileDropAllViews($views) { - return 'drop view "'.implode('","', $views).'" cascade'; + return 'drop view '.implode(',', $this->escapeNames($views)).' cascade'; } /** @@ -294,7 +294,7 @@ public function compileDropAllViews($views) */ public function compileDropAllTypes($types) { - return 'drop type "'.implode('","', $types).'" cascade'; + return 'drop type '.implode(',', $this->escapeNames($types)).' cascade'; } /** @@ -305,7 +305,7 @@ public function compileDropAllTypes($types) */ public function compileGetAllTables($searchPath) { - return "select tablename from pg_catalog.pg_tables where schemaname in ('".implode("','", (array) $searchPath)."')"; + return "select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('".implode("','", (array) $searchPath)."')"; } /** @@ -316,7 +316,7 @@ public function compileGetAllTables($searchPath) */ public function compileGetAllViews($searchPath) { - return "select viewname from pg_catalog.pg_views where schemaname in ('".implode("','", (array) $searchPath)."')"; + return "select viewname, concat('\"', schemaname, '\".\"', viewname, '\"') as qualifiedname from pg_catalog.pg_views where schemaname in ('".implode("','", (array) $searchPath)."')"; } /** @@ -486,6 +486,21 @@ public function compileComment(Blueprint $blueprint, Fluent $command) ); } + /** + * Quote-escape the given tables, views, or types. + * + * @param array $names + * @return array + */ + public function escapeNames($names) + { + return array_map(static function ($name) { + return '"'.collect(explode('.', $name)) + ->map(fn ($segment) => trim($segment, '\'"')) + ->implode('"."').'"'; + }, $names); + } + /** * Create the column definition for a char type. * diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index f0f866221185..976e09dc2091 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -62,15 +62,15 @@ public function dropAllTables() { $tables = []; - $excludedTables = $this->connection->getConfig('dont_drop') ?? ['spatial_ref_sys']; + $excludedTables = $this->grammar->escapeNames( + $this->connection->getConfig('dont_drop') ?? ['spatial_ref_sys'] + ); foreach ($this->getAllTables() as $row) { $row = (array) $row; - $table = reset($row); - - if (! in_array($table, $excludedTables)) { - $tables[] = $table; + if (empty(array_intersect($this->grammar->escapeNames($row), $excludedTables))) { + $tables[] = $row['qualifiedname'] ?? reset($row); } } @@ -95,7 +95,7 @@ public function dropAllViews() foreach ($this->getAllViews() as $row) { $row = (array) $row; - $views[] = reset($row); + $views[] = $row['qualifiedname'] ?? reset($row); } if (empty($views)) { @@ -239,14 +239,10 @@ protected function parseSchemaAndTable($reference) */ protected function parseSearchPath($searchPath) { - $searchPath = $this->baseParseSearchPath($searchPath); - - array_walk($searchPath, function (&$schema) { - $schema = $schema === '$user' + return array_map(function ($schema) { + return $schema === '$user' ? $this->connection->getConfig('username') : $schema; - }); - - return $searchPath; + }, $this->baseParseSearchPath($searchPath)); } } diff --git a/tests/Database/DatabasePostgresBuilderTest.php b/tests/Database/DatabasePostgresBuilderTest.php index 3cfd63d7ee7f..57e903f548f0 100644 --- a/tests/Database/DatabasePostgresBuilderTest.php +++ b/tests/Database/DatabasePostgresBuilderTest.php @@ -238,10 +238,12 @@ public function testDropAllTablesWhenSearchPathIsString() $connection->shouldReceive('getConfig')->with('dont_drop')->andReturn(['foo']); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileGetAllTables')->with(['public'])->andReturn("select tablename from pg_catalog.pg_tables where schemaname in ('public')"); - $connection->shouldReceive('select')->with("select tablename from pg_catalog.pg_tables where schemaname in ('public')")->andReturn(['users']); - $grammar->shouldReceive('compileDropAllTables')->with(['users'])->andReturn('drop table "'.implode('","', ['users']).'" cascade'); - $connection->shouldReceive('statement')->with('drop table "'.implode('","', ['users']).'" cascade'); + $grammar->shouldReceive('compileGetAllTables')->with(['public'])->andReturn("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('public')"); + $connection->shouldReceive('select')->with("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('public')")->andReturn([['tablename' => 'users', 'qualifiedname' => '"public"."users"']]); + $grammar->shouldReceive('escapeNames')->with(['foo'])->andReturn(['"foo"']); + $grammar->shouldReceive('escapeNames')->with(['tablename' => 'users', 'qualifiedname' => '"public"."users"'])->andReturn(['tablename' => '"users"', 'qualifiedname' => '"public"."users"']); + $grammar->shouldReceive('compileDropAllTables')->with(['"public"."users"'])->andReturn('drop table "public"."users" cascade'); + $connection->shouldReceive('statement')->with('drop table "public"."users" cascade'); $builder = $this->getBuilder($connection); $builder->dropAllTables(); @@ -255,10 +257,12 @@ public function testDropAllTablesWhenSearchPathIsStringOfMany() $connection->shouldReceive('getConfig')->with('dont_drop')->andReturn(['foo']); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileGetAllTables')->with(['foouser', 'public', 'foo_bar-Baz.Áüõß'])->andReturn("select tablename from pg_catalog.pg_tables where schemaname in ('foouser','public','foo_bar-Baz.Áüõß')"); - $connection->shouldReceive('select')->with("select tablename from pg_catalog.pg_tables where schemaname in ('foouser','public','foo_bar-Baz.Áüõß')")->andReturn(['users', 'users']); - $grammar->shouldReceive('compileDropAllTables')->with(['users', 'users'])->andReturn('drop table "'.implode('","', ['users', 'users']).'" cascade'); - $connection->shouldReceive('statement')->with('drop table "'.implode('","', ['users', 'users']).'" cascade'); + $grammar->shouldReceive('compileGetAllTables')->with(['foouser', 'public', 'foo_bar-Baz.Áüõß'])->andReturn("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('foouser','public','foo_bar-Baz.Áüõß')"); + $connection->shouldReceive('select')->with("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('foouser','public','foo_bar-Baz.Áüõß')")->andReturn([['tablename' => 'users', 'qualifiedname' => '"foouser"."users"']]); + $grammar->shouldReceive('escapeNames')->with(['foo'])->andReturn(['"foo"']); + $grammar->shouldReceive('escapeNames')->with(['tablename' => 'users', 'qualifiedname' => '"foouser"."users"'])->andReturn(['tablename' => '"users"', 'qualifiedname' => '"foouser"."users"']); + $grammar->shouldReceive('compileDropAllTables')->with(['"foouser"."users"'])->andReturn('drop table "foouser"."users" cascade'); + $connection->shouldReceive('statement')->with('drop table "foouser"."users" cascade'); $builder = $this->getBuilder($connection); $builder->dropAllTables(); @@ -277,10 +281,12 @@ public function testDropAllTablesWhenSearchPathIsArrayOfMany() $connection->shouldReceive('getConfig')->with('dont_drop')->andReturn(['foo']); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileGetAllTables')->with(['foouser', 'dev', 'test', 'spaced schema'])->andReturn("select tablename from pg_catalog.pg_tables where schemaname in ('foouser','dev','test','spaced schema')"); - $connection->shouldReceive('select')->with("select tablename from pg_catalog.pg_tables where schemaname in ('foouser','dev','test','spaced schema')")->andReturn(['users', 'users']); - $grammar->shouldReceive('compileDropAllTables')->with(['users', 'users'])->andReturn('drop table "'.implode('","', ['users', 'users']).'" cascade'); - $connection->shouldReceive('statement')->with('drop table "'.implode('","', ['users', 'users']).'" cascade'); + $grammar->shouldReceive('compileGetAllTables')->with(['foouser', 'dev', 'test', 'spaced schema'])->andReturn("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('foouser','dev','test','spaced schema')"); + $connection->shouldReceive('select')->with("select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('foouser','dev','test','spaced schema')")->andReturn([['tablename' => 'users', 'qualifiedname' => '"foouser"."users"']]); + $grammar->shouldReceive('escapeNames')->with(['foo'])->andReturn(['"foo"']); + $grammar->shouldReceive('escapeNames')->with(['tablename' => 'users', 'qualifiedname' => '"foouser"."users"'])->andReturn(['tablename' => '"users"', 'qualifiedname' => '"foouser"."users"']); + $grammar->shouldReceive('compileDropAllTables')->with(['"foouser"."users"'])->andReturn('drop table "foouser"."users" cascade'); + $connection->shouldReceive('statement')->with('drop table "foouser"."users" cascade'); $builder = $this->getBuilder($connection); $builder->dropAllTables(); diff --git a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php new file mode 100644 index 000000000000..e95ef8486f08 --- /dev/null +++ b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php @@ -0,0 +1,118 @@ +set('database.connections.pgsql.search_path', 'public,private'); + } + + protected function defineDatabaseMigrations() + { + parent::defineDatabaseMigrations(); + + DB::statement('create schema if not exists private'); + } + + protected function destroyDatabaseMigrations() + { + DB::statement('drop table if exists public.table'); + DB::statement('drop table if exists private.table'); + + DB::statement('drop view if exists public.foo'); + DB::statement('drop view if exists private.foo'); + + DB::statement('drop schema private'); + + parent::destroyDatabaseMigrations(); + } + + public function testDropAllTablesOnAllSchemas() + { + Schema::create('public.table', function (Blueprint $table) { + $table->increments('id'); + }); + Schema::create('private.table', function (Blueprint $table) { + $table->increments('id'); + }); + + Schema::dropAllTables(); + + $this->artisan('migrate:install'); + + $this->assertFalse(Schema::hasTable('public.table')); + $this->assertFalse(Schema::hasTable('private.table')); + } + + public function testDropAllTablesUsesDontDropConfigOnAllSchemas() + { + $this->app['config']->set('database.connections.pgsql.dont_drop', ['spatial_ref_sys', 'table']); + DB::purge('pgsql'); + + Schema::create('public.table', function (Blueprint $table) { + $table->increments('id'); + }); + Schema::create('private.table', function (Blueprint $table) { + $table->increments('id'); + }); + + Schema::dropAllTables(); + + $this->artisan('migrate:install'); + + $this->assertTrue(Schema::hasTable('public.table')); + $this->assertTrue(Schema::hasTable('private.table')); + } + + public function testDropAllTablesUsesDontDropConfigOnOneSchema() + { + $this->app['config']->set('database.connections.pgsql.dont_drop', ['spatial_ref_sys', 'private.table']); + DB::purge('pgsql'); + + Schema::create('public.table', function (Blueprint $table) { + $table->increments('id'); + }); + Schema::create('private.table', function (Blueprint $table) { + $table->increments('id'); + }); + + Schema::dropAllTables(); + + $this->artisan('migrate:install'); + + $this->assertFalse(Schema::hasTable('public.table')); + $this->assertTrue(Schema::hasTable('private.table')); + } + + public function testDropAllViewsOnAllSchemas() + { + DB::statement('create view public.foo (id) as select 1'); + DB::statement('create view private.foo (id) as select 1'); + + Schema::dropAllViews(); + + $this->assertFalse($this->hasView('public', 'foo')); + $this->assertFalse($this->hasView('private', 'foo')); + } + + protected function hasView($schema, $table) + { + return DB::table('information_schema.views') + ->where('table_catalog', $this->app['config']->get('database.connections.pgsql.database')) + ->where('table_schema', $schema) + ->where('table_name', $table) + ->exists(); + } +}