From dd9e9fc8f477405a18131298c9262ca02eef5aa7 Mon Sep 17 00:00:00 2001 From: Jason Coward Date: Fri, 27 Mar 2026 11:43:40 -0600 Subject: [PATCH] Fix createObjectContainer() uncaught PDOException on MySQL 8+ The SELECT COUNT(*) table-existence probe in all four driver managers threw an uncaught PDOException when the table did not exist and PDO was in ERRMODE_EXCEPTION mode. Wrapping the probe in try/catch allows the method to proceed to CREATE TABLE as intended. Closes #207. --- src/xPDO/Om/mysql/xPDOManager.php | 11 +- src/xPDO/Om/pgsql/xPDOManager.php | 11 +- src/xPDO/Om/sqlite/xPDOManager.php | 11 +- src/xPDO/Om/sqlsrv/xPDOManager.php | 11 +- test/complete.phpunit.xml | 1 + test/mysql.phpunit.xml | 1 + test/pgsql.phpunit.xml | 1 + test/sqlite.phpunit.xml | 1 + .../xPDOManagerCreateObjectContainerTest.php | 133 ++++++++++++++++++ 9 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 test/xPDO/Test/Om/xPDOManagerCreateObjectContainerTest.php diff --git a/src/xPDO/Om/mysql/xPDOManager.php b/src/xPDO/Om/mysql/xPDOManager.php index 26d2bdde..7cdcc9c0 100644 --- a/src/xPDO/Om/mysql/xPDOManager.php +++ b/src/xPDO/Om/mysql/xPDOManager.php @@ -114,9 +114,14 @@ public function createObjectContainer($className) { $instance= $this->xpdo->newObject($className); if ($instance) { $tableName= $this->xpdo->getTableName($className); - $existsStmt = $this->xpdo->query("SELECT COUNT(*) FROM {$tableName}"); - if ($existsStmt && $existsStmt->fetchAll()) { - return true; + try { + $existsStmt = $this->xpdo->query("SELECT COUNT(*) FROM {$tableName}"); + if ($existsStmt && $existsStmt->fetchAll()) { + return true; + } + } catch (\PDOException $e) { + /* Table does not exist; proceed to CREATE TABLE below. */ + $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Table {$tableName} does not exist, creating: " . $e->getMessage()); } $modelVersion= $this->xpdo->getModelVersion($className); $tableMeta= $this->xpdo->getTableMeta($className); diff --git a/src/xPDO/Om/pgsql/xPDOManager.php b/src/xPDO/Om/pgsql/xPDOManager.php index ee8a43d0..a9e429f1 100644 --- a/src/xPDO/Om/pgsql/xPDOManager.php +++ b/src/xPDO/Om/pgsql/xPDOManager.php @@ -132,9 +132,14 @@ public function createObjectContainer($className) $instance= $this->xpdo->newObject($className); if ($instance) { $tableName= $this->xpdo->getTableName($className); - $existsStmt = $this->xpdo->query("SELECT COUNT(*) FROM {$tableName}"); - if ($existsStmt && $existsStmt->fetchAll()) { - return true; + try { + $existsStmt = $this->xpdo->query("SELECT COUNT(*) FROM {$tableName}"); + if ($existsStmt && $existsStmt->fetchAll()) { + return true; + } + } catch (\PDOException $e) { + /* Table does not exist; proceed to CREATE TABLE below. */ + $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Table {$tableName} does not exist, creating: " . $e->getMessage()); } $tableMeta= $this->xpdo->getTableMeta($className); $sql= 'CREATE TABLE ' . $tableName . ' ('; diff --git a/src/xPDO/Om/sqlite/xPDOManager.php b/src/xPDO/Om/sqlite/xPDOManager.php index 844c232b..c8eaf18b 100644 --- a/src/xPDO/Om/sqlite/xPDOManager.php +++ b/src/xPDO/Om/sqlite/xPDOManager.php @@ -87,9 +87,14 @@ public function createObjectContainer($className) { $instance= $this->xpdo->newObject($className); if ($instance) { $tableName= $this->xpdo->getTableName($className); - $existsStmt = $this->xpdo->query("SELECT COUNT(*) FROM {$tableName}"); - if ($existsStmt && $existsStmt->fetchAll()) { - return true; + try { + $existsStmt = $this->xpdo->query("SELECT COUNT(*) FROM {$tableName}"); + if ($existsStmt && $existsStmt->fetchAll()) { + return true; + } + } catch (\PDOException $e) { + /* Table does not exist; proceed to CREATE TABLE below. */ + $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Table {$tableName} does not exist, creating: " . $e->getMessage()); } $tableMeta= $this->xpdo->getTableMeta($className); $sql= 'CREATE TABLE ' . $tableName . ' ('; diff --git a/src/xPDO/Om/sqlsrv/xPDOManager.php b/src/xPDO/Om/sqlsrv/xPDOManager.php index 15fab64d..2a01f7f3 100644 --- a/src/xPDO/Om/sqlsrv/xPDOManager.php +++ b/src/xPDO/Om/sqlsrv/xPDOManager.php @@ -109,9 +109,14 @@ public function createObjectContainer($className) { $instance= $this->xpdo->newObject($className); if ($instance) { $tableName= $this->xpdo->getTableName($className); - $existsStmt = $this->xpdo->query("SELECT COUNT(*) FROM {$tableName}"); - if ($existsStmt && $existsStmt->fetchAll()) { - return true; + try { + $existsStmt = $this->xpdo->query("SELECT COUNT(*) FROM {$tableName}"); + if ($existsStmt && $existsStmt->fetchAll()) { + return true; + } + } catch (\PDOException $e) { + /* Table does not exist; proceed to CREATE TABLE below. */ + $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Table {$tableName} does not exist, creating: " . $e->getMessage()); } $tableMeta= $this->xpdo->getTableMeta($className); $sql= 'CREATE TABLE ' . $tableName . ' ('; diff --git a/test/complete.phpunit.xml b/test/complete.phpunit.xml index db844f7d..87acd1e5 100644 --- a/test/complete.phpunit.xml +++ b/test/complete.phpunit.xml @@ -29,6 +29,7 @@ ./xPDO/Test/Om/xPDOQueryLimitTest.php ./xPDO/Test/Om/xPDOQuerySortByTest.php ./xPDO/Test/Om/xPDOGeneratorTest.php + ./xPDO/Test/Om/xPDOManagerCreateObjectContainerTest.php ./xPDO/Test/Cache/xPDOCacheManagerTest.php ./xPDO/Test/Cache/xPDOCacheDbTest.php ./xPDO/Test/Compression/xPDOZipTest.php diff --git a/test/mysql.phpunit.xml b/test/mysql.phpunit.xml index db844f7d..87acd1e5 100644 --- a/test/mysql.phpunit.xml +++ b/test/mysql.phpunit.xml @@ -29,6 +29,7 @@ ./xPDO/Test/Om/xPDOQueryLimitTest.php ./xPDO/Test/Om/xPDOQuerySortByTest.php ./xPDO/Test/Om/xPDOGeneratorTest.php + ./xPDO/Test/Om/xPDOManagerCreateObjectContainerTest.php ./xPDO/Test/Cache/xPDOCacheManagerTest.php ./xPDO/Test/Cache/xPDOCacheDbTest.php ./xPDO/Test/Compression/xPDOZipTest.php diff --git a/test/pgsql.phpunit.xml b/test/pgsql.phpunit.xml index 0052140b..31070e12 100644 --- a/test/pgsql.phpunit.xml +++ b/test/pgsql.phpunit.xml @@ -29,6 +29,7 @@ ./xPDO/Test/Om/xPDOQueryLimitTest.php ./xPDO/Test/Om/xPDOQuerySortByTest.php ./xPDO/Test/Om/xPDOGeneratorTest.php + ./xPDO/Test/Om/xPDOManagerCreateObjectContainerTest.php ./xPDO/Test/Cache/xPDOCacheManagerTest.php ./xPDO/Test/Cache/xPDOCacheDbTest.php ./xPDO/Test/Compression/xPDOZipTest.php diff --git a/test/sqlite.phpunit.xml b/test/sqlite.phpunit.xml index baae2486..fb433abd 100644 --- a/test/sqlite.phpunit.xml +++ b/test/sqlite.phpunit.xml @@ -29,6 +29,7 @@ ./xPDO/Test/Om/xPDOQueryLimitTest.php ./xPDO/Test/Om/xPDOQuerySortByTest.php ./xPDO/Test/Om/xPDOGeneratorTest.php + ./xPDO/Test/Om/xPDOManagerCreateObjectContainerTest.php ./xPDO/Test/Cache/xPDOCacheManagerTest.php ./xPDO/Test/Cache/xPDOCacheDbTest.php ./xPDO/Test/Compression/xPDOZipTest.php diff --git a/test/xPDO/Test/Om/xPDOManagerCreateObjectContainerTest.php b/test/xPDO/Test/Om/xPDOManagerCreateObjectContainerTest.php new file mode 100644 index 00000000..70aed37c --- /dev/null +++ b/test/xPDO/Test/Om/xPDOManagerCreateObjectContainerTest.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace xPDO\Test\Om; + +use xPDO\TestCase; +use xPDO\xPDO; + +/** + * Regression test for issue #207: + * xPDOManager->createObjectContainer() broken in MySQL 8+ (and other drivers) + * when the table does not yet exist. + * + * Prior to the fix, calling createObjectContainer() on a non-existent table + * would cause an uncaught PDOException on drivers that raise errors as + * exceptions (ERRMODE_EXCEPTION). The fix wraps the existence probe in a + * try/catch so that a missing table causes the method to proceed with + * CREATE TABLE rather than propagating an exception. + * + * @package xPDO\Test\Om + */ +class xPDOManagerCreateObjectContainerTest extends TestCase +{ + /** + * Set up for each test: ensure the test table does NOT exist beforehand + * so we exercise the "table missing" code path in createObjectContainer(). + * + * @before + */ + public function setUpFixtures() + { + parent::setUpFixtures(); + $this->xpdo->getManager(); + // Drop the table if it already exists so each test starts clean. + try { + $this->xpdo->exec( + 'DROP TABLE IF EXISTS ' . $this->xpdo->getTableName('xPDO\\Test\\Sample\\Person') + ); + } catch (\PDOException $e) { + // Ignore; the table may not exist yet. + } + } + + /** + * Tear down after each test: remove the created table. + * + * @after + */ + public function tearDownFixtures() + { + try { + $this->xpdo->exec( + 'DROP TABLE IF EXISTS ' . $this->xpdo->getTableName('xPDO\\Test\\Sample\\Person') + ); + } catch (\PDOException $e) { + // Ignore. + } + parent::tearDownFixtures(); + } + + /** + * Verify createObjectContainer() returns true and does not throw when the + * table does not yet exist. + * + * Regression: on MySQL 8+ (and any driver in ERRMODE_EXCEPTION mode) the + * SELECT COUNT(*) existence probe threw an uncaught PDOException instead of + * proceeding to CREATE TABLE. + */ + public function testCreateObjectContainerOnNonExistentTableReturnsTrue() + { + $this->xpdo->getManager(); + + // Force ERRMODE_EXCEPTION on the underlying PDO connection to reproduce + // the MySQL 8+ behaviour that triggered the bug. + if ($this->xpdo->pdo instanceof \PDO) { + $this->xpdo->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + } + + $threwException = false; + $result = false; + try { + $result = $this->xpdo->manager->createObjectContainer('xPDO\\Test\\Sample\\Person'); + } catch (\PDOException $e) { + $threwException = true; + } + + $this->assertFalse( + $threwException, + 'createObjectContainer() must not propagate a PDOException when the table does not yet exist (#207).' + ); + $this->assertTrue( + $result, + 'createObjectContainer() must return true after successfully creating the table.' + ); + } + + /** + * Verify createObjectContainer() returns true (idempotent) when the table + * already exists, and does not throw. + */ + public function testCreateObjectContainerOnExistingTableReturnsTrue() + { + $this->xpdo->getManager(); + + // Create the table first. + $this->xpdo->manager->createObjectContainer('xPDO\\Test\\Sample\\Person'); + + // Calling a second time must return true without exception. + $threwException = false; + $result = false; + try { + $result = $this->xpdo->manager->createObjectContainer('xPDO\\Test\\Sample\\Person'); + } catch (\PDOException $e) { + $threwException = true; + } + + $this->assertFalse( + $threwException, + 'createObjectContainer() must not throw when called on an already-existing table.' + ); + $this->assertTrue( + $result, + 'createObjectContainer() must return true when the table already exists (idempotent).' + ); + } +}