From d9dfd8c0d741e7001fbc2d932edf53d2246c6480 Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 26 Dec 2025 23:39:13 +0100 Subject: [PATCH] Fix hasTable() returning stale results when mixing API and execute() When a table was created via the API (e.g. $this->table()->create()) and then dropped via execute() (raw SQL), hasTable() would incorrectly return true because it checked an internal cache before querying the database. This fix limits the cache usage to dry-run mode only, where it's necessary to track what tables "would" exist. In normal mode, hasTable() now always queries the database to ensure accurate results. Also fixes SqlserverAdapter to pass the table name without schema prefix to the dialect, which was a latent bug hidden by the cache. --- src/Db/Adapter/MysqlAdapter.php | 5 ++++- src/Db/Adapter/PostgresAdapter.php | 6 +++++- src/Db/Adapter/SqliteAdapter.php | 9 +++++++- src/Db/Adapter/SqlserverAdapter.php | 8 +++++-- .../TestCase/Db/Adapter/SqliteAdapterTest.php | 21 +++++++++++++++++++ 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index c01bce6f..35c9d8f3 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -203,7 +203,10 @@ public function quoteTableName(string $tableName): string */ public function hasTable(string $tableName): bool { - if ($this->hasCreatedTable($tableName)) { + // Only use the cache in dry-run mode where tables aren't actually created. + // In normal mode, always check the database to handle cases where tables + // are dropped via execute() which doesn't update the cache. + if ($this->isDryRunEnabled() && $this->hasCreatedTable($tableName)) { return true; } diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 6ec16ce0..e229a180 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -100,9 +100,13 @@ public function quoteTableName(string $tableName): string */ public function hasTable(string $tableName): bool { - if ($this->hasCreatedTable($tableName)) { + // Only use the cache in dry-run mode where tables aren't actually created. + // In normal mode, always check the database to handle cases where tables + // are dropped via execute() which doesn't update the cache. + if ($this->isDryRunEnabled() && $this->hasCreatedTable($tableName)) { return true; } + $parts = $this->getSchemaName($tableName); $tableName = $parts['table']; diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index 4c163755..7f5bd972 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -229,7 +229,14 @@ protected function resolveTable(string $tableName): array */ public function hasTable(string $tableName): bool { - return $this->hasCreatedTable($tableName) || $this->resolveTable($tableName)['exists']; + // Only use the cache in dry-run mode where tables aren't actually created. + // In normal mode, always check the database to handle cases where tables + // are dropped via execute() which doesn't update the cache. + if ($this->isDryRunEnabled() && $this->hasCreatedTable($tableName)) { + return true; + } + + return $this->resolveTable($tableName)['exists']; } /** diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index ec3c0fc0..e970da2d 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -67,13 +67,17 @@ public function quoteTableName(string $tableName): string */ public function hasTable(string $tableName): bool { - if ($this->hasCreatedTable($tableName)) { + // Only use the cache in dry-run mode where tables aren't actually created. + // In normal mode, always check the database to handle cases where tables + // are dropped via execute() which doesn't update the cache. + if ($this->isDryRunEnabled() && $this->hasCreatedTable($tableName)) { return true; } + $parts = $this->getSchemaName($tableName); $dialect = $this->getSchemaDialect(); - return $dialect->hasTable($tableName, $parts['schema']); + return $dialect->hasTable($parts['table'], $parts['schema']); } /** diff --git a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php index 5c76b523..ee9ef10e 100644 --- a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php @@ -2402,6 +2402,27 @@ public static function provideTableNamesForPresenceCheck() ]; } + /** + * Test that hasTable() returns false after a table is dropped via execute(). + * + * This verifies that hasTable() always checks the database rather than + * relying on an internal cache that could become stale when raw SQL is used. + */ + public function testHasTableAfterExecuteDrop(): void + { + // Create table via API + $table = new Table('cache_test', [], $this->adapter); + $table->addColumn('name', 'string') + ->save(); + + $this->assertTrue($this->adapter->hasTable('cache_test')); + + // Drop via execute() - hasTable() must still return false + $this->adapter->execute('DROP TABLE "cache_test"'); + + $this->assertFalse($this->adapter->hasTable('cache_test')); + } + #[DataProvider('provideIndexColumnsToCheck')] public function testHasIndex($tableDef, $cols, $exp) {