diff --git a/app/Helpers/migration_helper.php b/app/Helpers/migration_helper.php index 0e86143c6..d7da83922 100644 --- a/app/Helpers/migration_helper.php +++ b/app/Helpers/migration_helper.php @@ -1,5 +1,7 @@ query(" SELECT DISTINCT kcu.CONSTRAINT_NAME, @@ -144,6 +153,23 @@ function dropAllForeignKeyConstraints(string $table, string $column): array { AND rc.CONSTRAINT_NAME IS NOT NULL "); + if ($result === false) { + $detail = 'unknown error'; + try { + $error = $db->error(); + if (is_array($error) && !empty($error['message'])) { + $detail = (string) $error['message']; + } + } catch (\Throwable $e) { + // A disconnected or mocked connection may not support error(); keep the default. + } + + throw new DatabaseException(sprintf( + 'Failed to look up foreign key constraints referencing table "%s" column "%s": %s', + $table, $column, $detail + )); + } + $deletedConstraints = []; foreach ($result->getResultArray() as $constraint) { @@ -187,22 +213,72 @@ function deleteIndex(string $table, string $index): void { * * @param string $table * @param string $index + * @param BaseConnection|null $db Connection to use (defaults to the default connection). + * * @return bool + * + * @throws DatabaseException If the metadata query fails. */ -function indexExists(string $table, string $index): bool { - $db = Database::connect(); +function indexExists(string $table, string $index, ?BaseConnection $db = null): bool { + $db ??= Database::connect(); $result = $db->query('SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = \'' . $db->getPrefix() . "$table' AND index_name = '$index'"); + + if ($result === false) { + $detail = 'unknown error'; + try { + $error = $db->error(); + if (is_array($error) && !empty($error['message'])) { + $detail = (string) $error['message']; + } + } catch (\Throwable $e) { + // A disconnected or mocked connection may not support error(); keep the default. + } + + throw new DatabaseException(sprintf( + 'Failed to check whether index "%s" exists on table "%s": %s', + $index, $table, $detail + )); + } + $row_array = $result->getRowArray(); - return $row_array && $row_array['COUNT(*)'] > 0; + return $row_array !== null && $row_array['COUNT(*)'] > 0; } -function primaryKeyExists(string $table): bool { - $db = Database::connect(); +/** + * Checks if the specified table has a primary key. + * + * @param string $table + * @param BaseConnection|null $db Connection to use (defaults to the default connection). + * + * @return bool + * + * @throws DatabaseException If the metadata query fails. + */ +function primaryKeyExists(string $table, ?BaseConnection $db = null): bool { + $db ??= Database::connect(); $result = $db->query('SELECT COUNT(*) FROM information_schema.table_constraints WHERE table_schema = DATABASE() AND table_name = \'' . $db->getPrefix() . "$table' AND constraint_type = 'PRIMARY KEY'"); + + if ($result === false) { + $detail = 'unknown error'; + try { + $error = $db->error(); + if (is_array($error) && !empty($error['message'])) { + $detail = (string) $error['message']; + } + } catch (\Throwable $e) { + // A disconnected or mocked connection may not support error(); keep the default. + } + + throw new DatabaseException(sprintf( + 'Failed to check whether table "%s" has a primary key: %s', + $table, $detail + )); + } + $row_array = $result->getRowArray(); - return $row_array && $row_array['COUNT(*)'] > 0; + return $row_array !== null && $row_array['COUNT(*)'] > 0; } function recreateForeignKeyConstraints(array $constraints): void { diff --git a/app/Models/Reports/Inventory_low.php b/app/Models/Reports/Inventory_low.php index 65f7a1b14..7dfb16d8a 100644 --- a/app/Models/Reports/Inventory_low.php +++ b/app/Models/Reports/Inventory_low.php @@ -47,7 +47,7 @@ class Inventory_low extends Report AND stock_locations.deleted = 0 ORDER BY items.name"); - return $query->getResultArray() ?: []; + return $query ? $query->getResultArray() : []; } /** diff --git a/app/Models/Sale.php b/app/Models/Sale.php index e1a0da097..45f84ab49 100644 --- a/app/Models/Sale.php +++ b/app/Models/Sale.php @@ -1193,7 +1193,7 @@ class Sale extends Model . $this->db->prefixTable('sales') . ' where sale_status = ' . SUSPENDED . ' AND customer_id = ' . $customer_id); } - return $query->getResultArray() ?: []; + return $query ? $query->getResultArray() : []; } /** diff --git a/app/Models/Tax.php b/app/Models/Tax.php index 61e6e9361..dfa7783e3 100644 --- a/app/Models/Tax.php +++ b/app/Models/Tax.php @@ -127,7 +127,7 @@ class Tax extends Model $query = $this->db->query($sql); - return $query->getResultArray() ?: []; + return $query ? $query->getResultArray() : []; } /** diff --git a/tests/Models/Reports/Inventory_lowTest.php b/tests/Models/Reports/Inventory_lowTest.php new file mode 100644 index 000000000..92840294f --- /dev/null +++ b/tests/Models/Reports/Inventory_lowTest.php @@ -0,0 +1,34 @@ +modelWithFailingQuery(Inventory_low::class); + + $this->assertSame([], $report->getData([])); + } +} diff --git a/tests/Models/SaleTest.php b/tests/Models/SaleTest.php index aff3b79c5..e6372d754 100644 --- a/tests/Models/SaleTest.php +++ b/tests/Models/SaleTest.php @@ -7,6 +7,7 @@ use CodeIgniter\Database\Config; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use Tests\Support\EmployeeFixtureTrait; +use Tests\Support\FailedQueryConnectionTrait; use Tests\Support\ItemFixtureTrait; /** @@ -18,6 +19,7 @@ class SaleTest extends CIUnitTestCase { use DatabaseTestTrait; use EmployeeFixtureTrait; + use FailedQueryConnectionTrait; use ItemFixtureTrait; protected $migrate = true; @@ -434,4 +436,14 @@ class SaleTest extends CIUnitTestCase $this->assertTrue($deleteResult); $this->assertEqualsWithDelta(10.0, $this->getItemQuantity($itemId, self::LOCATION_ID), 0.001); } + + public function testGetAllSuspended_ReturnsEmptyArrayWhenQueryFails(): void + { + $model = $this->modelWithFailingQuery(Sale::class); + + // Exercises both the NEW_ENTRY branch and the customer-id branch. + $this->assertSame([], $model->get_all_suspended()); + $this->assertSame([], $model->get_all_suspended(NEW_ENTRY)); + $this->assertSame([], $model->get_all_suspended(123)); + } } diff --git a/tests/Models/TaxTest.php b/tests/Models/TaxTest.php new file mode 100644 index 000000000..9075cf70c --- /dev/null +++ b/tests/Models/TaxTest.php @@ -0,0 +1,27 @@ +modelWithFailingQuery(Tax::class); + + $this->assertSame([], $model->get_taxes(1, 1)); + } +} diff --git a/tests/Support/FailedQueryConnectionTrait.php b/tests/Support/FailedQueryConnectionTrait.php new file mode 100644 index 000000000..6906e703e --- /dev/null +++ b/tests/Support/FailedQueryConnectionTrait.php @@ -0,0 +1,41 @@ +DBGroup)); + + $db = $this->getMockBuilder($driverClass) + ->disableOriginalConstructor() + ->onlyMethods(['query', 'error']) + ->getMock(); + $db->method('query')->willReturn(false); + $db->method('error')->willReturn(['code' => 1, 'message' => 'simulated query failure']); + + return $db; + } + + /** + * @template T of object + * + * @param class-string $class + * + * @return T + */ + protected function modelWithFailingQuery(string $class) + { + $model = new $class(); + + $prop = new \ReflectionProperty(BaseModel::class, 'db'); + $prop->setAccessible(true); + $prop->setValue($model, $this->failingConnection()); + + return $model; + } +} diff --git a/tests/helpers/migration_helperTest.php b/tests/helpers/migration_helperTest.php new file mode 100644 index 000000000..5fe2e5555 --- /dev/null +++ b/tests/helpers/migration_helperTest.php @@ -0,0 +1,53 @@ +requireMigrationHelper(); + $this->expectException(DatabaseException::class); + + indexExists('some_table', 'some_index', $this->failingConnection()); + } + + public function testPrimaryKeyExists_ThrowsWhenQueryFails(): void + { + $this->requireMigrationHelper(); + $this->expectException(DatabaseException::class); + + primaryKeyExists('some_table', $this->failingConnection()); + } + + public function testDropAllForeignKeyConstraints_ThrowsWhenQueryFails(): void + { + $this->requireMigrationHelper(); + $this->expectException(DatabaseException::class); + + dropAllForeignKeyConstraints('some_table', 'some_column', $this->failingConnection()); + } + + /** + * Ensure the migration helper functions are loaded (they are global). + */ + private function requireMigrationHelper(): void + { + if (! function_exists('indexExists')) { + require APPPATH . 'Helpers/migration_helper.php'; + } + } +}