Skip to content

Commit 037354b

Browse files
authored
Fix hasTable() returning stale results when mixing API and execute() (#982)
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.
1 parent 5bbfca7 commit 037354b

5 files changed

Lines changed: 44 additions & 5 deletions

File tree

src/Db/Adapter/MysqlAdapter.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,10 @@ public function quoteTableName(string $tableName): string
203203
*/
204204
public function hasTable(string $tableName): bool
205205
{
206-
if ($this->hasCreatedTable($tableName)) {
206+
// Only use the cache in dry-run mode where tables aren't actually created.
207+
// In normal mode, always check the database to handle cases where tables
208+
// are dropped via execute() which doesn't update the cache.
209+
if ($this->isDryRunEnabled() && $this->hasCreatedTable($tableName)) {
207210
return true;
208211
}
209212

src/Db/Adapter/PostgresAdapter.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,13 @@ public function quoteTableName(string $tableName): string
100100
*/
101101
public function hasTable(string $tableName): bool
102102
{
103-
if ($this->hasCreatedTable($tableName)) {
103+
// Only use the cache in dry-run mode where tables aren't actually created.
104+
// In normal mode, always check the database to handle cases where tables
105+
// are dropped via execute() which doesn't update the cache.
106+
if ($this->isDryRunEnabled() && $this->hasCreatedTable($tableName)) {
104107
return true;
105108
}
109+
106110
$parts = $this->getSchemaName($tableName);
107111
$tableName = $parts['table'];
108112

src/Db/Adapter/SqliteAdapter.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,14 @@ protected function resolveTable(string $tableName): array
229229
*/
230230
public function hasTable(string $tableName): bool
231231
{
232-
return $this->hasCreatedTable($tableName) || $this->resolveTable($tableName)['exists'];
232+
// Only use the cache in dry-run mode where tables aren't actually created.
233+
// In normal mode, always check the database to handle cases where tables
234+
// are dropped via execute() which doesn't update the cache.
235+
if ($this->isDryRunEnabled() && $this->hasCreatedTable($tableName)) {
236+
return true;
237+
}
238+
239+
return $this->resolveTable($tableName)['exists'];
233240
}
234241

235242
/**

src/Db/Adapter/SqlserverAdapter.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,17 @@ public function quoteTableName(string $tableName): string
6767
*/
6868
public function hasTable(string $tableName): bool
6969
{
70-
if ($this->hasCreatedTable($tableName)) {
70+
// Only use the cache in dry-run mode where tables aren't actually created.
71+
// In normal mode, always check the database to handle cases where tables
72+
// are dropped via execute() which doesn't update the cache.
73+
if ($this->isDryRunEnabled() && $this->hasCreatedTable($tableName)) {
7174
return true;
7275
}
76+
7377
$parts = $this->getSchemaName($tableName);
7478
$dialect = $this->getSchemaDialect();
7579

76-
return $dialect->hasTable($tableName, $parts['schema']);
80+
return $dialect->hasTable($parts['table'], $parts['schema']);
7781
}
7882

7983
/**

tests/TestCase/Db/Adapter/SqliteAdapterTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2402,6 +2402,27 @@ public static function provideTableNamesForPresenceCheck()
24022402
];
24032403
}
24042404

2405+
/**
2406+
* Test that hasTable() returns false after a table is dropped via execute().
2407+
*
2408+
* This verifies that hasTable() always checks the database rather than
2409+
* relying on an internal cache that could become stale when raw SQL is used.
2410+
*/
2411+
public function testHasTableAfterExecuteDrop(): void
2412+
{
2413+
// Create table via API
2414+
$table = new Table('cache_test', [], $this->adapter);
2415+
$table->addColumn('name', 'string')
2416+
->save();
2417+
2418+
$this->assertTrue($this->adapter->hasTable('cache_test'));
2419+
2420+
// Drop via execute() - hasTable() must still return false
2421+
$this->adapter->execute('DROP TABLE "cache_test"');
2422+
2423+
$this->assertFalse($this->adapter->hasTable('cache_test'));
2424+
}
2425+
24052426
#[DataProvider('provideIndexColumnsToCheck')]
24062427
public function testHasIndex($tableDef, $cols, $exp)
24072428
{

0 commit comments

Comments
 (0)