Skip to content

Commit

Permalink
Use cakephp/database for postgres schema reflection
Browse files Browse the repository at this point in the history
There are a few gaps in the schema reflection features that cake
provides. The migrations tests also identified a bug in postgres
foreign key reflection that I'm going to fix.
  • Loading branch information
markstory committed Jan 5, 2025
1 parent 7f0c342 commit 9b06379
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 81 deletions.
149 changes: 70 additions & 79 deletions src/Db/Adapter/PostgresAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace Migrations\Db\Adapter;

use Cake\Database\Connection;
use Cake\Database\Schema\SchemaDialect;
use InvalidArgumentException;
use Migrations\Db\AlterInstructions;
use Migrations\Db\Literal;
Expand Down Expand Up @@ -123,6 +124,18 @@ public function rollbackTransaction(): void
$this->getConnection()->rollback();
}

/**
* Get the schema dialect for this adapter.
*
* @return \Cake\Database\Schema\SchemaDialect
*/
protected function getSchemaDialect(): SchemaDialect
{
$driver = $this->getConnection()->getDriver();

return $driver->schemaDialect();
}

/**
* Quotes a schema name for use in a query.
*
Expand All @@ -131,7 +144,6 @@ public function rollbackTransaction(): void
*/
public function quoteSchemaName(string $schemaName): string
{
// TODO fix this
return $this->quoteColumnName($schemaName);
}

Expand All @@ -140,7 +152,6 @@ public function quoteSchemaName(string $schemaName): string
*/
public function quoteTableName(string $tableName): string
{
// TODO fix this
$parts = $this->getSchemaName($tableName);

return $this->quoteSchemaName($parts['schema']) . '.' . $this->quoteColumnName($parts['table']);
Expand All @@ -151,8 +162,9 @@ public function quoteTableName(string $tableName): string
*/
public function quoteColumnName(string $columnName): string
{
// TODO fix this
return '"' . $columnName . '"';
$driver = $this->getConnection()->getDriver();

return $driver->quoteIdentifier($columnName);
}

/**
Expand All @@ -163,21 +175,16 @@ public function hasTable(string $tableName): bool
if ($this->hasCreatedTable($tableName)) {
return true;
}

// TODO fix this
$parts = $this->getSchemaName($tableName);
$connection = $this->getConnection();
$stmt = $connection->execute(
'SELECT *
FROM information_schema.tables
WHERE table_schema = ?
AND table_name = ?',
[$parts['schema'], $parts['table']]
);
$count = $stmt->rowCount();
$stmt->closeCursor();
$tableName = $parts['table'];

return $count === 1;
$dialect = $this->getSchemaDialect();
[$query, $params] = $dialect->listTablesSql(['schema' => $parts['schema']]);

$rows = $this->query($query, $params)->fetchAll();
$tables = array_column($rows, 0);

return in_array($tableName, $tables, true);
}

/**
Expand Down Expand Up @@ -378,9 +385,12 @@ public function truncateTable(string $tableName): void
*/
public function getColumns(string $tableName): array
{
// TODO fix this
$parts = $this->getSchemaName($tableName);
$columns = [];

// TODO We can't use cakephp/database here as several attributes are missing
// from the query cakephp prepares. We'll need to expand the cakephp/database
// query in a future release.
$sql = sprintf(
'SELECT column_name, data_type, udt_name, is_identity, is_nullable,
column_default, character_maximum_length, numeric_precision, numeric_scale,
Expand Down Expand Up @@ -457,7 +467,6 @@ public function getColumns(string $tableName): array
*/
public function hasColumn(string $tableName, string $columnName): bool
{
// TODO fix this
$parts = $this->getSchemaName($tableName);
$connection = $this->getConnection();
$sql = 'SELECT count(*)
Expand Down Expand Up @@ -679,41 +688,25 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa
*/
protected function getIndexes(string $tableName): array
{
$dialect = $this->getSchemaDialect();
$parts = $this->getSchemaName($tableName);

// TODO fix this
[$query, $params] = $dialect->describeIndexSql($parts['table'], [
'schema' => $parts['schema'],
'database' => $this->getOption('database'),
]);
$rows = $this->query($query, $params)->fetchAll('assoc');

$indexes = [];
$sql = sprintf(
"SELECT
i.relname AS index_name,
a.attname AS column_name
FROM
pg_class t,
pg_class i,
pg_index ix,
pg_attribute a,
pg_namespace nsp
WHERE
t.oid = ix.indrelid
AND i.oid = ix.indexrelid
AND a.attrelid = t.oid
AND a.attnum = ANY(ix.indkey)
AND t.relnamespace = nsp.oid
AND nsp.nspname = %s
AND t.relkind = 'r'
AND t.relname = %s
ORDER BY
t.relname,
i.relname",
$this->quoteString($parts['schema']),
$this->quoteString($parts['table'])
);
$rows = $this->fetchAll($sql);
foreach ($rows as $row) {
if (!isset($indexes[$row['index_name']])) {
$indexes[$row['index_name']] = ['columns' => []];
if (!isset($indexes[$row['relname']])) {
$indexes[$row['relname']] = [
'isPrimary' => false,
'columns' => [],
];
}
$indexes[$row['index_name']]['columns'][] = $row['column_name'];
$indexes[$row['relname']]['columns'][] = $row['attname'];
$indexes[$row['relname']]['isPrimary'] = $row['indisprimary'];
}

return $indexes;
Expand Down Expand Up @@ -839,35 +832,17 @@ public function hasPrimaryKey(string $tableName, $columns, ?string $constraint =
*/
public function getPrimaryKey(string $tableName): array
{
// TODO fix this
$parts = $this->getSchemaName($tableName);
$params = [
$parts['schema'],
$parts['table'],
];
$rows = $this->query(
"SELECT
tc.constraint_name,
kcu.column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
WHERE constraint_type = 'PRIMARY KEY'
AND tc.table_schema = ?
AND tc.table_name = ?
ORDER BY kcu.position_in_unique_constraint",
$params,
)->fetchAll('assoc');
$indexes = $this->getIndexes($tableName);

$primaryKey = [
'columns' => [],
];
foreach ($rows as $row) {
$primaryKey['constraint'] = $row['constraint_name'];
$primaryKey['columns'][] = $row['column_name'];
foreach ($indexes as $name => $index) {
if ($index['isPrimary']) {
$index['constraint'] = $name;

return $index;
}
}

return $primaryKey;
return ['columns' => []];
}

/**
Expand Down Expand Up @@ -905,7 +880,26 @@ public function hasForeignKey(string $tableName, $columns, ?string $constraint =
*/
protected function getForeignKeys(string $tableName): array
{
// TODO fix this
$parts = $this->getSchemaName($tableName);

// This should work but is blocked on a bug in cakephp/database
// The field ordering after reflection is lost
/*
$dialect = $this->getSchemaDialect();
[$query, $params] = $dialect->describeForeignKeySql($parts['table'], [
'schema' => $parts['schema'],
'database' => $this->getOption('database'),
]);
$rows = $this->query($query, $params)->fetchAll('assoc');
foreach ($rows as $row) {
$name = $row['name'];
$foreignKeys[$name]['table'] = $parts['table'];
$foreignKeys[$name]['columns'][] = $row['column_name'];
$foreignKeys[$name]['referenced_table'] = $row['references_table'];
$foreignKeys[$name]['references_columns'][] = $row['references_field'];
}
*/

$parts = $this->getSchemaName($tableName);
$foreignKeys = [];
$params = [
Expand Down Expand Up @@ -1153,7 +1147,6 @@ public function createDatabase(string $name, array $options = []): void
*/
public function hasDatabase(string $name): bool
{
// TODO fix this
$sql = sprintf("SELECT count(*) FROM pg_database WHERE datname = '%s'", $name);
$result = $this->fetchRow($sql);
if (!$result) {
Expand Down Expand Up @@ -1410,7 +1403,6 @@ public function createSchema(string $schemaName = 'public'): void
*/
public function hasSchema(string $schemaName): bool
{
// TODO fix this
$sql = 'SELECT count(*) FROM pg_namespace WHERE nspname = ?';
$result = $this->query($sql, [$schemaName])->fetch('assoc');
if (!$result) {
Expand Down Expand Up @@ -1457,7 +1449,6 @@ public function dropAllSchemas(): void
*/
public function getAllSchemas(): array
{
// TODO fix this?
$sql = "SELECT schema_name
FROM information_schema.schemata
WHERE schema_name <> 'information_schema' AND schema_name !~ '^pg_'";
Expand Down
7 changes: 5 additions & 2 deletions tests/TestCase/Db/Adapter/PostgresAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ public function testSchemaTableIsCreatedWithPrimaryKey()
public function testQuoteSchemaName()
{
$this->assertEquals('"schema"', $this->adapter->quoteSchemaName('schema'));
$this->assertEquals('"schema.schema"', $this->adapter->quoteSchemaName('schema.schema'));
// No . is supported in schema name.
$this->assertEquals('"schema"."schema"', $this->adapter->quoteSchemaName('schema.schema'));
}

public function testGetGlobalSchemaName()
Expand Down Expand Up @@ -172,7 +173,8 @@ public function testQuoteTableName()
public function testQuoteColumnName()
{
$this->assertEquals('"string"', $this->adapter->quoteColumnName('string'));
$this->assertEquals('"string.string"', $this->adapter->quoteColumnName('string.string'));
// No . is supported in column name.
$this->assertEquals('"string"."string"', $this->adapter->quoteColumnName('string.string'));
}

public function testCreateTable()
Expand Down Expand Up @@ -1686,6 +1688,7 @@ public function testDropForeignKeyWithMultipleColumns()
->save();

$this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id', 'ref_table_field1']));

$this->adapter->dropForeignKey($table->getName(), ['ref_table_id', 'ref_table_field1']);
$this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id', 'ref_table_field1']));
$this->assertTrue(
Expand Down

0 comments on commit 9b06379

Please sign in to comment.