Skip to content

Commit

Permalink
PostgresBuilder fixes for renamed `config('database.connections.pgsql…
Browse files Browse the repository at this point in the history
….search_path')`

1. `PostgresBuilder::parseSchemaAndTable()`
   * Needs a fallback to <= 8.x `config('database.connections.pgsql.schema')`
     when 9.x renamed `config('database.connections.pgsql.search_path')`
     is missing.
   * Remove duplicate `$user` variable `config('database.connections.pgsql.username')`
     replacement handling already done by `parseSearchPath()`.
2. `PostgresBuilder::getAllTables()` + `getAllViews()` + `parseSchemaAndTable()`
   Apply the `parseSearchPath()` fixes applied to PostgresConnector:
   laravel#41088
3. `DatabasePostgresBuilderTest`
   Add more test cases and use terse method names instead of extensive
   comments to concisely communicate each case.
  • Loading branch information
derekmd committed Feb 24, 2022
1 parent 6c0d272 commit fa8a17b
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 123 deletions.
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

0 comments on commit fa8a17b

Please sign in to comment.