From 9b06379fca861e7376ab77047173ab6150dc057b Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 5 Jan 2025 00:22:30 -0500 Subject: [PATCH] Use cakephp/database for postgres schema reflection 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. --- src/Db/Adapter/PostgresAdapter.php | 149 ++++++++---------- .../Db/Adapter/PostgresAdapterTest.php | 7 +- 2 files changed, 75 insertions(+), 81 deletions(-) diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 515495a2..3d67b04f 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -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; @@ -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. * @@ -131,7 +144,6 @@ public function rollbackTransaction(): void */ public function quoteSchemaName(string $schemaName): string { - // TODO fix this return $this->quoteColumnName($schemaName); } @@ -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']); @@ -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); } /** @@ -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); } /** @@ -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, @@ -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(*) @@ -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; @@ -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' => []]; } /** @@ -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 = [ @@ -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) { @@ -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) { @@ -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_'"; diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index 91586ec5..e3ad3c54 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -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() @@ -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() @@ -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(