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

[11.x] Fix inconsistent database parsing on PostgreSQL #49148

Merged
merged 5 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public function compileDropDatabaseIfExists($name)
/**
* Compile the query to determine if a table exists.
*
* @deprecated Will be removed in a future Laravel version.
*
* @return string
*/
public function compileTableExists()
Expand Down Expand Up @@ -159,12 +161,11 @@ public function compileColumnListing()
/**
* Compile the query to determine the columns.
*
* @param string $database
* @param string $schema
* @param string $table
* @return string
*/
public function compileColumns($database, $schema, $table)
public function compileColumns($schema, $table)
{
return sprintf(
'select quote_ident(a.attname) as name, t.typname as type_name, format_type(a.atttypid, a.atttypmod) as type, '
Expand Down
36 changes: 19 additions & 17 deletions src/Illuminate/Database/Schema/PostgresBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Illuminate\Database\Schema;

use Illuminate\Database\Concerns\ParsesSearchPath;
use InvalidArgumentException;

class PostgresBuilder extends Builder
{
Expand Down Expand Up @@ -44,13 +45,18 @@ public function dropDatabaseIfExists($name)
*/
public function hasTable($table)
{
[$database, $schema, $table] = $this->parseSchemaAndTable($table);
[$schema, $table] = $this->parseSchemaAndTable($table);

$table = $this->connection->getTablePrefix().$table;

return count($this->connection->selectFromWriteConnection(
$this->grammar->compileTableExists(), [$database, $schema, $table]
)) > 0;
foreach ($this->getTables() as $value) {
if (strtolower($table) === strtolower($value['name'])
&& strtolower($schema) === strtolower($value['schema'])) {
return true;
}
}

return false;
}

/**
Expand Down Expand Up @@ -213,12 +219,12 @@ public function dropAllTypes()
*/
public function getColumns($table)
{
[$database, $schema, $table] = $this->parseSchemaAndTable($table);
[$schema, $table] = $this->parseSchemaAndTable($table);

$table = $this->connection->getTablePrefix().$table;

$results = $this->connection->selectFromWriteConnection(
$this->grammar->compileColumns($database, $schema, $table)
$this->grammar->compileColumns($schema, $table)
);

return $this->connection->getPostProcessor()->processColumns($results);
Expand All @@ -232,7 +238,7 @@ public function getColumns($table)
*/
public function getIndexes($table)
{
[, $schema, $table] = $this->parseSchemaAndTable($table);
[$schema, $table] = $this->parseSchemaAndTable($table);

$table = $this->connection->getTablePrefix().$table;

Expand All @@ -249,7 +255,7 @@ public function getIndexes($table)
*/
public function getForeignKeys($table)
{
[, $schema, $table] = $this->parseSchemaAndTable($table);
[$schema, $table] = $this->parseSchemaAndTable($table);

$table = $this->connection->getTablePrefix().$table;

Expand All @@ -271,7 +277,7 @@ protected function getSchemas()
}

/**
* Parse the database object reference and extract the database, schema, and table.
* Parse the database object reference and extract the schema and table.
*
* @param string $reference
* @return array
Expand All @@ -280,14 +286,10 @@ protected function parseSchemaAndTable($reference)
{
$parts = explode('.', $reference);

$database = $this->connection->getConfig('database');

// If the reference contains a database name, we will use that instead of the
// default database name for the connection. This allows the database name
// to be specified in the query instead of at the full connection level.
if (count($parts) === 3) {
if (count($parts) > 2) {
$database = $parts[0];
array_shift($parts);

throw new InvalidArgumentException("Using 3-parts reference is not supported, you may use `Schema::connection('$database')` instead.");
}

// We will use the default schema unless the schema has been specified in the
Expand All @@ -300,7 +302,7 @@ protected function parseSchemaAndTable($reference)
array_shift($parts);
}

return [$database, $schema, $parts[0]];
return [$schema, $parts[0]];
}

/**
Expand Down
90 changes: 46 additions & 44 deletions tests/Database/DatabasePostgresBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,29 +52,35 @@ public function testHasTableWhenSchemaUnqualifiedAndSearchPathMissing()
$connection->shouldReceive('getConfig')->with('search_path')->andReturn(null);
$connection->shouldReceive('getConfig')->with('schema')->andReturn(null);
$grammar = m::mock(PostgresGrammar::class);
$processor = m::mock(PostgresProcessor::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'");
$connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'public', 'foo'])->andReturn(['countable_result']);
$connection->shouldReceive('getPostProcessor')->andReturn($processor);
$grammar->shouldReceive('compileTables')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'public', 'name' => 'foo']]);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$builder = $this->getBuilder($connection);
$processor->shouldReceive('processTables')->andReturn([['schema' => 'public', 'name' => 'foo']]);

$builder->hasTable('foo');
$this->assertTrue($builder->hasTable('foo'));
$this->assertTrue($builder->hasTable('public.foo'));
}

public function testHasTableWhenSchemaUnqualifiedAndSearchPathFilled()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('myapp,public');
$grammar = m::mock(PostgresGrammar::class);
$processor = m::mock(PostgresProcessor::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'");
$connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'myapp', 'foo'])->andReturn(['countable_result']);
$connection->shouldReceive('getPostProcessor')->andReturn($processor);
$grammar->shouldReceive('compileTables')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'myapp', 'name' => 'foo']]);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$builder = $this->getBuilder($connection);
$processor->shouldReceive('processTables')->andReturn([['schema' => 'myapp', 'name' => 'foo']]);

$builder->hasTable('foo');
$this->assertTrue($builder->hasTable('foo'));
$this->assertTrue($builder->hasTable('myapp.foo'));
}

public function testHasTableWhenSchemaUnqualifiedAndSearchPathFallbackFilled()
Expand All @@ -83,14 +89,17 @@ public function testHasTableWhenSchemaUnqualifiedAndSearchPathFallbackFilled()
$connection->shouldReceive('getConfig')->with('search_path')->andReturn(null);
$connection->shouldReceive('getConfig')->with('schema')->andReturn(['myapp', 'public']);
$grammar = m::mock(PostgresGrammar::class);
$processor = m::mock(PostgresProcessor::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'");
$connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'myapp', 'foo'])->andReturn(['countable_result']);
$connection->shouldReceive('getPostProcessor')->andReturn($processor);
$grammar->shouldReceive('compileTables')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'myapp', 'name' => 'foo']]);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$builder = $this->getBuilder($connection);
$processor->shouldReceive('processTables')->andReturn([['schema' => 'myapp', 'name' => 'foo']]);

$builder->hasTable('foo');
$this->assertTrue($builder->hasTable('foo'));
$this->assertTrue($builder->hasTable('myapp.foo'));
}

public function testHasTableWhenSchemaUnqualifiedAndSearchPathIsUserVariable()
Expand All @@ -99,41 +108,43 @@ public function testHasTableWhenSchemaUnqualifiedAndSearchPathIsUserVariable()
$connection->shouldReceive('getConfig')->with('username')->andReturn('foouser');
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('$user');
$grammar = m::mock(PostgresGrammar::class);
$processor = m::mock(PostgresProcessor::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'");
$connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'foouser', 'foo'])->andReturn(['countable_result']);
$connection->shouldReceive('getPostProcessor')->andReturn($processor);
$grammar->shouldReceive('compileTables')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'foouser', 'name' => 'foo']]);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$builder = $this->getBuilder($connection);
$processor->shouldReceive('processTables')->andReturn([['schema' => 'foouser', 'name' => 'foo']]);

$builder->hasTable('foo');
$this->assertTrue($builder->hasTable('foo'));
$this->assertTrue($builder->hasTable('foouser.foo'));
}

public function testHasTableWhenSchemaQualifiedAndSearchPathMismatches()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('public');
$grammar = m::mock(PostgresGrammar::class);
$processor = m::mock(PostgresProcessor::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'");
$connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'myapp', 'foo'])->andReturn(['countable_result']);
$connection->shouldReceive('getPostProcessor')->andReturn($processor);
$grammar->shouldReceive('compileTables')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'myapp', 'name' => 'foo']]);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$builder = $this->getBuilder($connection);
$processor->shouldReceive('processTables')->andReturn([['schema' => 'myapp', 'name' => 'foo']]);

$builder->hasTable('myapp.foo');
$this->assertTrue($builder->hasTable('myapp.foo'));
}

public function testHasTableWhenDatabaseAndSchemaQualifiedAndSearchPathMismatches()
{
$this->expectException(\InvalidArgumentException::class);

$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('public');
$grammar = m::mock(PostgresGrammar::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'");
$connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['mydatabase', 'myapp', 'foo'])->andReturn(['countable_result']);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$builder = $this->getBuilder($connection);

$builder->hasTable('mydatabase.myapp.foo');
Expand All @@ -146,10 +157,9 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathMissing()
$connection->shouldReceive('getConfig')->with('schema')->andReturn(null);
$grammar = m::mock(PostgresGrammar::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileColumns')->with('laravel', 'public', 'foo')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']);
$grammar->shouldReceive('compileColumns')->with('public', 'foo')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$processor = m::mock(PostgresProcessor::class);
$connection->shouldReceive('getPostProcessor')->andReturn($processor);
$processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]);
Expand All @@ -164,10 +174,9 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathFilled()
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('myapp,public');
$grammar = m::mock(PostgresGrammar::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileColumns')->with('laravel', 'myapp', 'foo')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']);
$grammar->shouldReceive('compileColumns')->with('myapp', 'foo')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$processor = m::mock(PostgresProcessor::class);
$connection->shouldReceive('getPostProcessor')->andReturn($processor);
$processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]);
Expand All @@ -183,10 +192,9 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathIsUserVari
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('$user');
$grammar = m::mock(PostgresGrammar::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileColumns')->with('laravel', 'foouser', 'foo')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']);
$grammar->shouldReceive('compileColumns')->with('foouser', 'foo')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$processor = m::mock(PostgresProcessor::class);
$connection->shouldReceive('getPostProcessor')->andReturn($processor);
$processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]);
Expand All @@ -201,10 +209,9 @@ public function testGetColumnListingWhenSchemaQualifiedAndSearchPathMismatches()
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('public');
$grammar = m::mock(PostgresGrammar::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileColumns')->with('laravel', 'myapp', 'foo')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']);
$grammar->shouldReceive('compileColumns')->with('myapp', 'foo')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$processor = m::mock(PostgresProcessor::class);
$connection->shouldReceive('getPostProcessor')->andReturn($processor);
$processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]);
Expand All @@ -215,17 +222,12 @@ public function testGetColumnListingWhenSchemaQualifiedAndSearchPathMismatches()

public function testGetColumnWhenDatabaseAndSchemaQualifiedAndSearchPathMismatches()
{
$this->expectException(\InvalidArgumentException::class);

$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('public');
$grammar = m::mock(PostgresGrammar::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileColumns')->with('mydatabase', 'myapp', 'foo')->andReturn('sql');
$connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$processor = m::mock(PostgresProcessor::class);
$connection->shouldReceive('getPostProcessor')->andReturn($processor);
$processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]);
$builder = $this->getBuilder($connection);

$builder->getColumnListing('mydatabase.myapp.foo');
Expand Down
Loading