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

[9.x] Query PostgresBuilder fixes for renamed config 'search_path' #41215

Merged
merged 1 commit into from
Feb 24, 2022
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
29 changes: 29 additions & 0 deletions src/Illuminate/Database/Concerns/ParsesSearchPath.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Illuminate\Database\Concerns;

trait ParsesSearchPath
{
/**
* Parse the Postgres "search_path" configuration value into an array.
*
* @param string|array|null $searchPath
* @return array
*/
protected function parseSearchPath($searchPath)
{
if (is_string($searchPath)) {
preg_match_all('/[^\s,"\']+/', $searchPath, $matches);

$searchPath = $matches[0];
}

$searchPath ??= [];

array_walk($searchPath, static function (&$schema) {
$schema = trim($schema, '\'"');
});

return $searchPath;
}
}
26 changes: 3 additions & 23 deletions src/Illuminate/Database/Connectors/PostgresConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

namespace Illuminate\Database\Connectors;

use Illuminate\Database\Concerns\ParsesSearchPath;
use PDO;

class PostgresConnector extends Connector implements ConnectorInterface
{
use ParsesSearchPath;

/**
* The default PDO connection options.
*
Expand Down Expand Up @@ -118,29 +121,6 @@ protected function configureSearchPath($connection, $config)
}
}

/**
* Parse the "search_path" configuration value into an array.
*
* @param string|array $searchPath
* @return array
*/
protected function parseSearchPath($searchPath)
{
if (is_string($searchPath)) {
preg_match_all('/[^\s,"\']+/', $searchPath, $matches);

$searchPath = $matches[0];
}

$searchPath ??= [];

array_walk($searchPath, function (&$schema) {
$schema = trim($schema, '\'"');
});

return $searchPath;
}

/**
* Format the search path for the DSN.
*
Expand Down
26 changes: 11 additions & 15 deletions src/Illuminate/Database/Schema/PostgresBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

namespace Illuminate\Database\Schema;

use Illuminate\Database\Concerns\ParsesSearchPath;

class PostgresBuilder extends Builder
{
use ParsesSearchPath {
parseSearchPath as baseParseSearchPath;
}

/**
* Create a database in the schema.
*
Expand Down Expand Up @@ -197,7 +203,7 @@ public function getColumnListing($table)
protected function parseSchemaAndTable($reference)
{
$searchPath = $this->parseSearchPath(
$this->connection->getConfig('search_path') ?: 'public'
$this->connection->getConfig('search_path') ?: $this->connection->getConfig('schema') ?: 'public'
);

$parts = explode('.', $reference);
Expand All @@ -215,9 +221,7 @@ protected function parseSchemaAndTable($reference)
// We will use the default schema unless the schema has been specified in the
// query. If the schema has been specified in the query then we can use it
// instead of a default schema configured in the connection search path.
$schema = $searchPath[0] === '$user'
? $this->connection->getConfig('username')
: $searchPath[0];
$schema = $searchPath[0];

if (count($parts) === 2) {
$schema = $parts[0];
Expand All @@ -228,24 +232,16 @@ protected function parseSchemaAndTable($reference)
}

/**
* Parse the "search_path" value into an array.
* Parse the "search_path" configuration value into an array.
*
* @param string|array $searchPath
* @param string|array|null $searchPath
* @return array
*/
protected function parseSearchPath($searchPath)
{
if (is_string($searchPath)) {
preg_match_all('/[a-zA-z0-9$]{1,}/i', $searchPath, $matches);

$searchPath = $matches[0];
}

$searchPath ??= [];
$searchPath = $this->baseParseSearchPath($searchPath);

array_walk($searchPath, function (&$schema) {
$schema = trim($schema, '\'"');

$schema = $schema === '$user'
? $this->connection->getConfig('username')
: $schema;
Expand Down
140 changes: 55 additions & 85 deletions tests/Database/DatabasePostgresBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,11 @@ public function testDropDatabaseIfExists()
$builder->dropDatabaseIfExists('my_database_a');
}

/**
* Ensure that when the reference is unqualified (i.e., does not contain a
* database name or a schema), and the search_path is empty, the database
* specified on the connection is used, and the default schema ('public')
* is used.
*/
public function testWhenSearchPathEmptyHasTableWithUnqualifiedReferenceIsCorrect()
public function testHasTableWhenSchemaUnqualifiedAndSearchPathMissing()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn(null);
$connection->shouldReceive('getConfig')->with('schema')->andReturn(null);
$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'");
Expand All @@ -67,13 +62,7 @@ public function testWhenSearchPathEmptyHasTableWithUnqualifiedReferenceIsCorrect
$builder->hasTable('foo');
}

/**
* Ensure that when the reference is unqualified (i.e., does not contain a
* database name or a schema), and the first schema in the search_path is
* NOT the default ('public'), the database specified on the connection is
* used, and the first schema in the search_path is used.
*/
public function testWhenSearchPathNotEmptyHasTableWithUnqualifiedSchemaReferenceIsCorrect()
public function testHasTableWhenSchemaUnqualifiedAndSearchPathFilled()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('myapp,public');
Expand All @@ -88,14 +77,23 @@ public function testWhenSearchPathNotEmptyHasTableWithUnqualifiedSchemaReference
$builder->hasTable('foo');
}

/**
* Ensure that when the reference is unqualified (i.e., does not contain a
* database name or a schema), and the first schema in the search_path is
* the special variable '$user', the database specified on the connection is
* used, the first schema in the search_path is used, and the variable
* resolves to the username specified on the connection.
*/
public function testWhenFirstSchemaInSearchPathIsVariableHasTableWithUnqualifiedSchemaReferenceIsCorrect()
public function testHasTableWhenSchemaUnqualifiedAndSearchPathFallbackFilled()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn(null);
$connection->shouldReceive('getConfig')->with('schema')->andReturn(['myapp', '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('select')->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('getTablePrefix');
$connection->shouldReceive('getConfig')->with('database')->andReturn('laravel');
$builder = $this->getBuilder($connection);

$builder->hasTable('foo');
}

public function testHasTableWhenSchemaUnqualifiedAndSearchPathIsUserVariable()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('username')->andReturn('foouser');
Expand All @@ -111,12 +109,7 @@ public function testWhenFirstSchemaInSearchPathIsVariableHasTableWithUnqualified
$builder->hasTable('foo');
}

/**
* Ensure that when the reference is qualified only with a schema, that
* the database specified on the connection is used, and the specified
* schema is used, even if it is not within the search_path.
*/
public function testWhenSchemaNotInSearchPathHasTableWithQualifiedSchemaReferenceIsCorrect()
public function testHasTableWhenSchemaQualifiedAndSearchPathMismatches()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('public');
Expand All @@ -131,12 +124,7 @@ public function testWhenSchemaNotInSearchPathHasTableWithQualifiedSchemaReferenc
$builder->hasTable('myapp.foo');
}

/**
* Ensure that when the reference is qualified with a database AND a schema,
* and the database is NOT the database configured for the connection, the
* specified database is used instead.
*/
public function testWhenDatabaseNotDefaultHasTableWithFullyQualifiedReferenceIsCorrect()
public function testHasTableWhenDatabaseAndSchemaQualifiedAndSearchPathMismatches()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('public');
Expand All @@ -151,16 +139,11 @@ public function testWhenDatabaseNotDefaultHasTableWithFullyQualifiedReferenceIsC
$builder->hasTable('mydatabase.myapp.foo');
}

/**
* Ensure that when the reference is unqualified (i.e., does not contain a
* database name or a schema), and the search_path is empty, the database
* specified on the connection is used, and the default schema ('public')
* is used.
*/
public function testWhenSearchPathEmptyGetColumnListingWithUnqualifiedReferenceIsCorrect()
public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathMissing()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn(null);
$connection->shouldReceive('getConfig')->with('schema')->andReturn(null);
$grammar = m::mock(PostgresGrammar::class);
$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$grammar->shouldReceive('compileColumnListing')->andReturn('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?');
Expand All @@ -175,13 +158,7 @@ public function testWhenSearchPathEmptyGetColumnListingWithUnqualifiedReferenceI
$builder->getColumnListing('foo');
}

/**
* Ensure that when the reference is unqualified (i.e., does not contain a
* database name or a schema), and the first schema in the search_path is
* NOT the default ('public'), the database specified on the connection is
* used, and the first schema in the search_path is used.
*/
public function testWhenSearchPathNotEmptyGetColumnListingWithUnqualifiedSchemaReferenceIsCorrect()
public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathFilled()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('myapp,public');
Expand All @@ -199,14 +176,7 @@ public function testWhenSearchPathNotEmptyGetColumnListingWithUnqualifiedSchemaR
$builder->getColumnListing('foo');
}

/**
* Ensure that when the reference is unqualified (i.e., does not contain a
* database name or a schema), and the first schema in the search_path is
* the special variable '$user', the database specified on the connection is
* used, the first schema in the search_path is used, and the variable
* resolves to the username specified on the connection.
*/
public function testWhenFirstSchemaInSearchPathIsVariableGetColumnListingWithUnqualifiedSchemaReferenceIsCorrect()
public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathIsUserVariable()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('username')->andReturn('foouser');
Expand All @@ -225,12 +195,7 @@ public function testWhenFirstSchemaInSearchPathIsVariableGetColumnListingWithUnq
$builder->getColumnListing('foo');
}

/**
* Ensure that when the reference is qualified only with a schema, that
* the database specified on the connection is used, and the specified
* schema is used, even if it is not within the search_path.
*/
public function testWhenSchemaNotInSearchPathGetColumnListingWithQualifiedSchemaReferenceIsCorrect()
public function testGetColumnListingWhenSchemaQualifiedAndSearchPathMismatches()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('public');
Expand All @@ -248,12 +213,7 @@ public function testWhenSchemaNotInSearchPathGetColumnListingWithQualifiedSchema
$builder->getColumnListing('myapp.foo');
}

/**
* Ensure that when the reference is qualified with a database AND a schema,
* and the database is NOT the database configured for the connection, the
* specified database is used instead.
*/
public function testWhenDatabaseNotDefaultGetColumnListingWithFullyQualifiedReferenceIsCorrect()
public function testGetColumnWhenDatabaseAndSchemaQualifiedAndSearchPathMismatches()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('public');
Expand All @@ -271,12 +231,7 @@ public function testWhenDatabaseNotDefaultGetColumnListingWithFullyQualifiedRefe
$builder->getColumnListing('mydatabase.myapp.foo');
}

/**
* Ensure that when the search_path contains just one schema, only that
* schema is passed into the query that is executed to acquire the list
* of tables to be dropped.
*/
public function testDropAllTablesWithOneSchemaInSearchPath()
public function testDropAllTablesWhenSearchPathIsString()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('public');
Expand All @@ -292,23 +247,38 @@ public function testDropAllTablesWithOneSchemaInSearchPath()
$builder->dropAllTables();
}

/**
* Ensure that when the search_path contains more than one schema, both
* schemas are passed into the query that is executed to acquire the list
* of tables to be dropped. Furthermore, ensure that the special '$user'
* variable is resolved to the username specified on the database connection
* in the process.
*/
public function testDropAllTablesWithMoreThanOneSchemaInSearchPath()
public function testDropAllTablesWhenSearchPathIsStringOfMany()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('username')->andReturn('foouser');
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('"$user", public, foo_bar-Baz.Áüõß');
$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');
$builder = $this->getBuilder($connection);

$builder->dropAllTables();
}

public function testDropAllTablesWhenSearchPathIsArrayOfMany()
{
$connection = $this->getConnection();
$connection->shouldReceive('getConfig')->with('username')->andReturn('foouser');
$connection->shouldReceive('getConfig')->with('search_path')->andReturn('"$user", public');
$connection->shouldReceive('getConfig')->with('search_path')->andReturn([
'$user',
'"dev"',
"'test'",
'spaced schema',
]);
$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'])->andReturn("select tablename from pg_catalog.pg_tables where schemaname in ('foouser','public')");
$connection->shouldReceive('select')->with("select tablename from pg_catalog.pg_tables where schemaname in ('foouser','public')")->andReturn(['users', 'users']);
$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');
$builder = $this->getBuilder($connection);
Expand Down