Browse Source

Allow for excluding views from listTables() (#16123)

Adds new method for listing tables without views. Currently listTables()
generally includes views (except in SQLite). In the new fixture system we
rely on `listTables()` to truncate tables. Having views included in this output
causes failures in applications that rely on Database Views. The new methods
provides access to the list of tables without views. 

Closes #16122
rmarsh1ua 4 years ago
parent
commit
64e56a4cba

+ 17 - 1
src/Database/Schema/CachedCollection.php

@@ -61,9 +61,25 @@ class CachedCollection implements CollectionInterface
     /**
      * @inheritDoc
      */
+    public function listTablesAndViews(): array
+    {
+        return $this->collection->listTablesAndViews();
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function listTablesWithoutViews(): array
+    {
+        return $this->collection->listTablesWithoutViews();
+    }
+
+    /**
+     * @inheritDoc
+     */
     public function listTables(): array
     {
-        return $this->collection->listTables();
+        return $this->collection->listTablesAndViews();
     }
 
     /**

+ 43 - 1
src/Database/Schema/Collection.php

@@ -25,6 +25,11 @@ use PDOException;
  *
  * Used to access information about the tables,
  * and other data in a database.
+ *
+ * @method array<string> listTablesAndViews() Get the list of tables available in the current connection.
+ * This will include any views in the schema.
+ * @method array<string> listTablesWithoutViews() Get the list of tables available in the current connection.
+ * This will exclude any views in the schema.
  */
 class Collection implements CollectionInterface
 {
@@ -54,13 +59,50 @@ class Collection implements CollectionInterface
     }
 
     /**
+     * Get the list of tables, excluding any views, available in the current connection.
+     *
+     * @return array<string> The list of tables in the connected database/schema.
+     */
+    public function listTablesWithoutViews(): array
+    {
+        [$sql, $params] = $this->_dialect->listTablesWithoutViewsSql($this->_connection->config());
+        $result = [];
+        $statement = $this->_connection->execute($sql, $params);
+        while ($row = $statement->fetch()) {
+            $result[] = $row[0];
+        }
+        $statement->closeCursor();
+
+        return $result;
+    }
+
+    /**
      * Get the list of tables available in the current connection.
      *
+     * @deprecated in 4.3.3 Use {@link listTablesAndViews()} instead.
      * @return array<string> The list of tables in the connected database/schema.
      */
     public function listTables(): array
     {
-        [$sql, $params] = $this->_dialect->listTablesSql($this->_connection->config());
+        [$sql, $params] = $this->_dialect->listTablesAndViewsSql($this->_connection->config());
+        $result = [];
+        $statement = $this->_connection->execute($sql, $params);
+        while ($row = $statement->fetch()) {
+            $result[] = $row[0];
+        }
+        $statement->closeCursor();
+
+        return $result;
+    }
+
+    /**
+     * Get the list of tables available in the current connection.
+     *
+     * @return array<string> The list of tables in the connected database/schema.
+     */
+    public function listTablesAndViews(): array
+    {
+        [$sql, $params] = $this->_dialect->listTablesAndViewsSql($this->_connection->config());
         $result = [];
         $statement = $this->_connection->execute($sql, $params);
         while ($row = $statement->fetch()) {

+ 6 - 0
src/Database/Schema/CollectionInterface.php

@@ -21,12 +21,18 @@ namespace Cake\Database\Schema;
  *
  * Used to access information about the tables,
  * and other data in a database.
+ *
+ * @method array<string> listTablesAndViews() Get the list of tables available in the current connection.
+ * This will include any views in the schema.
+ * @method array<string> listTablesWithoutViews() Get the list of tables available in the current connection.
+ * This will exclude any views in the schema.
  */
 interface CollectionInterface
 {
     /**
      * Get the list of tables available in the current connection.
      *
+     * @deprecated in 4.3.3
      * @return array<string> The list of tables in the connected database/schema.
      */
     public function listTables(): array;

+ 34 - 2
src/Database/Schema/MysqlSchemaDialect.php

@@ -34,11 +34,43 @@ class MysqlSchemaDialect extends SchemaDialect
     protected $_driver;
 
     /**
-     * @inheritDoc
+     * Generate the SQL to list the tables and views.
+     *
+     * @deprecated 4.3.3 Use {@link listTablesAndViewsSql()} instead.
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array<mixed> An array of (sql, params) to execute.
      */
     public function listTablesSql(array $config): array
     {
-        return ['SHOW TABLES FROM ' . $this->_driver->quoteIdentifier($config['database']), []];
+        return $this->listTablesAndViewsSql($config);
+    }
+
+    /**
+     * Generate the SQL to list the tables, excluding all views.
+     *
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array<mixed> An array of (sql, params) to execute.
+     */
+    public function listTablesWithoutViewsSql(array $config): array
+    {
+        return [
+            'SHOW FULL TABLES FROM ' . $this->_driver->quoteIdentifier($config['database'])
+            . ' WHERE Table_type LIKE "%TABLE%"'
+        , []];
+    }
+
+    /**
+     * Generate the SQL to list the tables and views.
+     *
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array<mixed> An array of (sql, params) to execute.
+     */
+    public function listTablesAndViewsSql(array $config): array
+    {
+        return ['SHOW FULL TABLES FROM ' . $this->_driver->quoteIdentifier($config['database']), []];
     }
 
     /**

+ 36 - 2
src/Database/Schema/PostgresSchemaDialect.php

@@ -26,11 +26,45 @@ use Cake\Database\Exception\DatabaseException;
 class PostgresSchemaDialect extends SchemaDialect
 {
     /**
-     * @inheritDoc
+     * Generate the SQL to list the tables and views.
+     *
+     * @deprecated 4.3.3 Use {@link listTablesAndViewsSql()} instead.
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array An array of (sql, params) to execute.
      */
     public function listTablesSql(array $config): array
     {
-        $sql = 'SELECT table_name as name FROM information_schema.tables WHERE table_schema = ? ORDER BY name';
+        return $this->listTablesAndViewsSql($config);
+    }
+
+    /**
+     * Generate the SQL to list the tables, excluding all views.
+     *
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array<mixed> An array of (sql, params) to execute.
+     */
+    public function listTablesWithoutViewsSql(array $config): array
+    {
+        $sql = 'SELECT table_name as name FROM information_schema.tables
+                WHERE table_schema = ? AND table_type = \'BASE TABLE\' ORDER BY name';
+        $schema = empty($config['schema']) ? 'public' : $config['schema'];
+
+        return [$sql, [$schema]];
+    }
+
+    /**
+     * Generate the SQL to list the tables and views.
+     *
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array<mixed> An array of (sql, params) to execute.
+     */
+    public function listTablesAndViewsSql(array $config): array
+    {
+        $sql = 'SELECT table_name as name FROM information_schema.tables
+                WHERE table_schema = ? ORDER BY name';
         $schema = empty($config['schema']) ? 'public' : $config['schema'];
 
         return [$sql, [$schema]];

+ 4 - 0
src/Database/Schema/SchemaDialect.php

@@ -26,6 +26,9 @@ use InvalidArgumentException;
  *
  * This class contains methods that are common across
  * the various SQL dialects.
+ *
+ * @method array<mixed> listTablesAndViewsSql(array $config) Generate the SQL to list the tables and views.
+ * @method array<mixed> listTablesWithoutViewsSql(array $config) Generate the SQL to list the tables, excluding all views.
  */
 abstract class SchemaDialect
 {
@@ -183,6 +186,7 @@ abstract class SchemaDialect
     /**
      * Generate the SQL to list the tables.
      *
+     * @deprecated 4.3.3
      * @param array<string, mixed> $config The connection configuration to use for
      *    getting tables from.
      * @return array An array of (sql, params) to execute.

+ 35 - 1
src/Database/Schema/SqliteSchemaDialect.php

@@ -152,10 +152,44 @@ class SqliteSchemaDialect extends SchemaDialect
     }
 
     /**
-     * @inheritDoc
+     * Generate the SQL to list the tables and views.
+     *
+     * @deprecated 4.3.3 Use {@link listTablesAndViewsSql()} instead.
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array An array of (sql, params) to execute.
      */
     public function listTablesSql(array $config): array
     {
+        return $this->listTablesWithoutViewsSql($config);
+    }
+
+    /**
+     * Generate the SQL to list the tables and views.
+     *
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array<mixed> An array of (sql, params) to execute.
+     */
+    public function listTablesAndViewsSql(array $config): array
+    {
+        return [
+            'SELECT name FROM sqlite_master ' .
+            'WHERE (type="table" OR type="view") ' .
+            'AND name != "sqlite_sequence" ORDER BY name',
+            [],
+        ];
+    }
+
+    /**
+     * Generate the SQL to list the tables, excluding all views.
+     *
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array<mixed> An array of (sql, params) to execute.
+     */
+    public function listTablesWithoutViewsSql(array $config): array
+    {
         return [
             'SELECT name FROM sqlite_master WHERE type="table" ' .
             'AND name != "sqlite_sequence" ORDER BY name',

+ 37 - 1
src/Database/Schema/SqlserverSchemaDialect.php

@@ -29,10 +29,27 @@ class SqlserverSchemaDialect extends SchemaDialect
     public const DEFAULT_SCHEMA_NAME = 'dbo';
 
     /**
-     * @inheritDoc
+     * Generate the SQL to list the tables and views.
+     *
+     * @deprecated 4.3.3 Use {@link listTablesAndViewsSql()} instead.
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array An array of (sql, params) to execute.
      */
     public function listTablesSql(array $config): array
     {
+        return $this->listTablesAndViewsSql($config);
+    }
+
+    /**
+     * Generate the SQL to list the tables and views.
+     *
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array<mixed> An array of (sql, params) to execute.
+     */
+    public function listTablesAndViewsSql(array $config): array
+    {
         $sql = "SELECT TABLE_NAME
             FROM INFORMATION_SCHEMA.TABLES
             WHERE TABLE_SCHEMA = ?
@@ -44,6 +61,25 @@ class SqlserverSchemaDialect extends SchemaDialect
     }
 
     /**
+     * Generate the SQL to list the tables, excluding all views.
+     *
+     * @param array<string, mixed> $config The connection configuration to use for
+     *    getting tables from.
+     * @return array<mixed> An array of (sql, params) to execute.
+     */
+    public function listTablesWithoutViewsSql(array $config): array
+    {
+        $sql = "SELECT TABLE_NAME
+            FROM INFORMATION_SCHEMA.TABLES
+            WHERE TABLE_SCHEMA = ?
+            AND (TABLE_TYPE = 'BASE TABLE')
+            ORDER BY TABLE_NAME";
+        $schema = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema'];
+
+        return [$sql, [$schema]];
+    }
+
+    /**
      * @inheritDoc
      */
     public function describeColumnSql(string $tableName, array $config): array

+ 6 - 2
src/TestSuite/ConnectionHelper.php

@@ -86,7 +86,11 @@ class ConnectionHelper
         $connection = ConnectionManager::get($connectionName);
         $collection = $connection->getSchemaCollection();
 
-        $allTables = $collection->listTables();
+        $allTables = [];
+        if (method_exists($collection, 'listTablesWithoutViews')) {
+            $allTables = $collection->listTablesWithoutViews();
+        }
+
         $tables = $tables !== null ? array_intersect($tables, $allTables) : $allTables;
         $schemas = array_map(function ($table) use ($collection) {
             return $collection->describe($table);
@@ -120,7 +124,7 @@ class ConnectionHelper
         $connection = ConnectionManager::get($connectionName);
         $collection = $connection->getSchemaCollection();
 
-        $allTables = $collection->listTables();
+        $allTables = $collection->listTablesWithoutViews();
         $tables = $tables !== null ? array_intersect($tables, $allTables) : $allTables;
         $schemas = array_map(function ($table) use ($collection) {
             return $collection->describe($table);

+ 2 - 2
src/TestSuite/Fixture/FixtureManager.php

@@ -295,7 +295,7 @@ class FixtureManager
         try {
             $createTables = function (ConnectionInterface $db, array $fixtures) use ($test): void {
                 /** @var array<\Cake\Datasource\FixtureInterface> $fixtures */
-                $tables = $db->getSchemaCollection()->listTables();
+                $tables = $db->getSchemaCollection()->listTablesAndViews();
                 $configName = $db->configName();
                 $this->_insertionMap[$configName] = $this->_insertionMap[$configName] ?? [];
 
@@ -467,7 +467,7 @@ class FixtureManager
         }
 
         if (!$this->isFixtureSetup($connection->configName(), $fixture)) {
-            $sources = $connection->getSchemaCollection()->listTables();
+            $sources = $connection->getSchemaCollection()->listTablesAndViews();
             $this->_setupTable($fixture, $connection, $sources, $dropTables);
         }
 

+ 20 - 2
tests/TestCase/Database/Schema/MysqlSchemaTest.php

@@ -262,6 +262,7 @@ class MysqlSchemaTest extends TestCase
         $connection->execute('DROP TABLE IF EXISTS schema_articles');
         $connection->execute('DROP TABLE IF EXISTS schema_authors');
         $connection->execute('DROP TABLE IF EXISTS schema_json');
+        $connection->execute('DROP VIEW IF EXISTS schema_articles_v');
 
         $table = <<<SQL
             CREATE TABLE schema_authors (
@@ -290,6 +291,12 @@ SQL;
 SQL;
         $connection->execute($table);
 
+        $table = <<<SQL
+            CREATE OR REPLACE VIEW schema_articles_v
+                AS SELECT 1
+SQL;
+        $connection->execute($table);
+
         if ($connection->getDriver()->supports(DriverInterface::FEATURE_JSON)) {
             $table = <<<SQL
                 CREATE TABLE schema_json (
@@ -308,13 +315,24 @@ SQL;
     {
         $connection = ConnectionManager::get('test');
         $this->_createTables($connection);
-
         $schema = new SchemaCollection($connection);
-        $result = $schema->listTables();
 
+        $result = $schema->listTables();
         $this->assertIsArray($result);
         $this->assertContains('schema_articles', $result);
+        $this->assertContains('schema_articles_v', $result);
         $this->assertContains('schema_authors', $result);
+
+        $resultAll = $schema->listTablesAndViews();
+        $resultNoViews = $schema->listTablesWithoutViews();
+
+        $this->assertIsArray($resultAll);
+        $this->assertContains('schema_articles', $resultAll);
+        $this->assertContains('schema_articles_v', $resultAll);
+        $this->assertContains('schema_authors', $resultAll);
+        $this->assertIsArray($resultNoViews);
+        $this->assertNotContains('schema_articles_v', $resultNoViews);
+        $this->assertContains('schema_articles', $resultNoViews);
     }
 
     /**

+ 20 - 1
tests/TestCase/Database/Schema/PostgresSchemaTest.php

@@ -48,6 +48,7 @@ class PostgresSchemaTest extends TestCase
     {
         $this->_needsConnection();
 
+        $connection->execute('DROP VIEW IF EXISTS schema_articles_v');
         $connection->execute('DROP TABLE IF EXISTS schema_articles');
         $connection->execute('DROP TABLE IF EXISTS schema_authors');
 
@@ -87,6 +88,12 @@ SQL;
         $connection->execute($table);
         $connection->execute('COMMENT ON COLUMN "schema_articles"."title" IS \'a title\'');
         $connection->execute('CREATE INDEX "author_idx" ON "schema_articles" ("author_id")');
+
+        $table = <<<SQL
+CREATE VIEW schema_articles_v AS
+SELECT * FROM schema_articles
+SQL;
+        $connection->execute($table);
     }
 
     /**
@@ -284,12 +291,24 @@ SQL;
     {
         $connection = ConnectionManager::get('test');
         $this->_createTables($connection);
-
         $schema = new SchemaCollection($connection);
+
         $result = $schema->listTables();
         $this->assertIsArray($result);
         $this->assertContains('schema_articles', $result);
+        $this->assertContains('schema_articles_v', $result);
         $this->assertContains('schema_authors', $result);
+
+        $resultAll = $schema->listTablesAndViews();
+        $resultNoViews = $schema->listTablesWithoutViews();
+
+        $this->assertIsArray($resultAll);
+        $this->assertContains('schema_articles', $resultAll);
+        $this->assertContains('schema_articles_v', $resultAll);
+        $this->assertContains('schema_authors', $resultAll);
+        $this->assertIsArray($resultNoViews);
+        $this->assertNotContains('schema_articles_v', $resultNoViews);
+        $this->assertContains('schema_articles', $resultNoViews);
     }
 
     /**

+ 14 - 2
tests/TestCase/Database/Schema/SqliteSchemaTest.php

@@ -283,13 +283,25 @@ SQL;
     {
         $connection = ConnectionManager::get('test');
         $this->_createTables($connection);
-
         $schema = new SchemaCollection($connection);
-        $result = $schema->listTables();
 
+        $result = $schema->listTables();
         $this->assertIsArray($result);
         $this->assertContains('schema_articles', $result);
         $this->assertContains('schema_authors', $result);
+        $this->assertContains('view_schema_articles', $result);
+
+        $resultAll = $schema->listTablesAndViews();
+        $resultNoViews = $schema->listTablesWithoutViews();
+
+        $this->assertIsArray($resultNoViews);
+        $this->assertContains('schema_articles', $resultNoViews);
+        $this->assertNotContains('view_schema_articles', $resultNoViews);
+
+        $this->assertIsArray($resultAll);
+        $this->assertContains('schema_articles', $resultAll);
+        $this->assertContains('schema_authors', $resultAll);
+        $this->assertContains('view_schema_articles', $resultAll);
     }
 
     /**

+ 20 - 1
tests/TestCase/Database/Schema/SqlserverSchemaTest.php

@@ -47,6 +47,7 @@ class SqlserverSchemaTest extends TestCase
     {
         $this->_needsConnection();
 
+        $connection->execute("IF OBJECT_ID('schema_articles_v', 'V') IS NOT NULL DROP VIEW schema_articles_v");
         $connection->execute("IF OBJECT_ID('schema_articles', 'U') IS NOT NULL DROP TABLE schema_articles");
         $connection->execute("IF OBJECT_ID('schema_authors', 'U') IS NOT NULL DROP TABLE schema_authors");
 
@@ -82,6 +83,12 @@ CONSTRAINT [author_idx] FOREIGN KEY ([author_id]) REFERENCES [schema_authors] ([
 SQL;
         $connection->execute($table);
         $connection->execute('CREATE INDEX [author_idx] ON [schema_articles] ([author_id])');
+
+        $table = <<<SQL
+CREATE VIEW schema_articles_v AS
+SELECT * FROM schema_articles
+SQL;
+        $connection->execute($table);
     }
 
     /**
@@ -334,12 +341,24 @@ SQL;
     {
         $connection = ConnectionManager::get('test');
         $this->_createTables($connection);
-
         $schema = new SchemaCollection($connection);
+
         $result = $schema->listTables();
         $this->assertIsArray($result);
         $this->assertContains('schema_articles', $result);
+        $this->assertContains('schema_articles_v', $result);
         $this->assertContains('schema_authors', $result);
+
+        $resultAll = $schema->listTablesAndViews();
+        $resultNoViews = $schema->listTablesWithoutViews();
+
+        $this->assertIsArray($resultAll);
+        $this->assertContains('schema_articles', $resultAll);
+        $this->assertContains('schema_articles_v', $resultAll);
+        $this->assertContains('schema_authors', $resultAll);
+        $this->assertIsArray($resultNoViews);
+        $this->assertNotContains('schema_articles_v', $resultNoViews);
+        $this->assertContains('schema_articles', $resultNoViews);
     }
 
     /**