From e20fb2a792e751badca1a7b10a26c9d7b27bd78a Mon Sep 17 00:00:00 2001 From: Alexander Laurent <57456290+Helios531@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:24:51 -1000 Subject: [PATCH] feat(db): add MySQL 8.4 support (#4048) Add MySQL 8.4 compatibility, including a utf8mb4 conversion migration, hardened foreign key handling, and expanded CI database coverage. Migrations: - Add Mysql84Utf8mb4Conversion migration using sqlscripts/3.4.3_mysql84_utf8mb4_conversion.sql via executeScript() - Drop ospos_people composite index before conversion, since its prefix length exceeds the 3072-byte key limit under utf8mb4 - Add assertNoCollationCollisions() pre-flight check to fail fast on unique values that collide under utf8mb4_unicode_520_ci, instead of hitting "Duplicate entry" partway through ALTER TABLE - Wrap the conversion in try/catch so foreign keys are recreated on failure; log restore errors separately so they don't mask the original exception - Throw RuntimeException if re-adding the people index fails - Remove the re-add of the sales_items_taxes FK in down(), which MySQL 8.4 rejects without a unique key - Revert edits to historical migrations; fix FK handling and table names migration_helper: - Add shared dropAllForeignKeyConstraints() and recreateForeignKeyConstraints(); table/column now optional, dropping all FKs in the schema when omitted - Match CONSTRAINT_SCHEMA when joining KEY_COLUMN_USAGE to REFERENTIAL_CONSTRAINTS to avoid cross-schema name collisions - Rebuild FK lookup with the query builder and bound values instead of interpolated SQL CI/tests: - Add mysql:5.7, mariadb:10.6 and mariadb:10.11 to PHPUnit matrix - Use master instead of latest image tag; improve CI robustness - Mock table() in FailedQueryConnectionTrait so query builder chains work against the failing connection Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com> Co-authored-by: aliworksx08 <57456290+aliworksx08@users.noreply.github.com> Co-authored-by: jekkos Co-authored-by: objecttothis <17935339+objecttothis@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 --- .env.example | 1 + .github/workflows/phpunit.yml | 33 +++-- ...20240630000001_fix_keys_for_db_upgrade.php | 3 - ...0260405000000_Mysql84Utf8mb4Conversion.php | 109 +++++++++++++++++ .../3.4.3_mysql84_utf8mb4_conversion.sql | 41 +++++++ app/Helpers/migration_helper.php | 114 ++++++++++++------ app/Models/Reports/Summary_payments.php | 6 +- app/Models/Reports/Summary_report.php | 4 +- app/Models/Sale.php | 4 +- docker-compose.dev.yml | 2 +- docker/docker-mysql.yml | 2 +- tests/Support/FailedQueryConnectionTrait.php | 7 +- 12 files changed, 269 insertions(+), 57 deletions(-) create mode 100644 app/Database/Migrations/20260405000000_Mysql84Utf8mb4Conversion.php create mode 100644 app/Database/Migrations/sqlscripts/3.4.3_mysql84_utf8mb4_conversion.sql diff --git a/.env.example b/.env.example index d055638c1..828fa0364 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,7 @@ app.allowedHostnames='' #-------------------------------------------------------------------- # DATABASE #-------------------------------------------------------------------- +# MySQL 5.7, 8.0, 8.4 (LTS) and MariaDB 10.x supported. MySQL 8.4 requires mysqlnd (default in PHP 8.1+). database.default.hostname='localhost' database.default.database='ospos' diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index b1d14bb43..125c30be6 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -27,7 +27,7 @@ permissions: jobs: test: - name: PHP ${{ matrix.php-version }} Tests + name: PHP ${{ matrix.php-version }} / ${{ matrix.db-image }} Tests runs-on: ubuntu-22.04 strategy: @@ -37,6 +37,13 @@ jobs: - '8.2' - '8.3' - '8.4' + db-image: + - 'mariadb:10.5' + - 'mariadb:10.6' + - 'mariadb:10.11' + - 'mysql:5.7' + - 'mysql:8.0' + - 'mysql:8.4' steps: - name: Checkout @@ -68,7 +75,7 @@ jobs: - name: Install npm dependencies run: npm install - - name: Start MariaDB + - name: Start database run: | docker run -d --name mysql \ -e MYSQL_ROOT_PASSWORD=root \ @@ -76,13 +83,19 @@ jobs: -e MYSQL_USER=admin \ -e MYSQL_PASSWORD=pointofsale \ -p 3306:3306 \ - mariadb:10.5 - # Wait for MariaDB to be ready + ${{ matrix.db-image }} + # Wait for database to be ready (max 60 attempts) + retries=0 until docker exec mysql mysqladmin ping -h 127.0.0.1 -u root -proot --silent; do - echo "Waiting for MariaDB..." + retries=$((retries + 1)) + if [ $retries -ge 60 ]; then + echo "Database failed to start after 60 attempts" + exit 1 + fi + echo "Waiting for database... (attempt $retries/60)" sleep 2 done - echo "MariaDB is ready!" + echo "Database is ready!" # Grant admin the CREATE/DROP privileges it needs to drop and # recreate the test database at the start of each test class. @@ -140,14 +153,18 @@ jobs: MYSQL_HOST_NAME: 127.0.0.1 run: composer test -- --no-coverage --log-junit test-results/junit.xml + - name: Set artifact name + if: always() + run: echo "DB_LABEL=$(echo '${{ matrix.db-image }}' | tr ':' '-')" >> $GITHUB_ENV + - name: Upload test results uses: actions/upload-artifact@v4 if: always() with: - name: test-results-php-${{ matrix.php-version }} + name: test-results-php-${{ matrix.php-version }}-${{ env.DB_LABEL }} path: test-results/ retention-days: 30 - - name: Stop MariaDB + - name: Stop database if: always() run: docker stop mysql && docker rm mysql diff --git a/app/Database/Migrations/20240630000001_fix_keys_for_db_upgrade.php b/app/Database/Migrations/20240630000001_fix_keys_for_db_upgrade.php index 2ee75cc7f..765c49ec6 100644 --- a/app/Database/Migrations/20240630000001_fix_keys_for_db_upgrade.php +++ b/app/Database/Migrations/20240630000001_fix_keys_for_db_upgrade.php @@ -59,8 +59,5 @@ class Migration_fix_keys_for_db_upgrade extends Migration $this->db->query('ALTER TABLE ' . $this->db->prefixTable('sales_items_taxes') . ' DROP FOREIGN KEY ospos_sales_items_taxes_ibfk_1'); } - $this->db->query('ALTER TABLE ' . $this->db->prefixTable('sales_items_taxes') - . ' ADD CONSTRAINT ospos_sales_items_taxes_ibfk_1 FOREIGN KEY (sale_id) ' - . ' REFERENCES ' . $this->db->prefixTable('sales_items') . ' (sale_id)'); } } diff --git a/app/Database/Migrations/20260405000000_Mysql84Utf8mb4Conversion.php b/app/Database/Migrations/20260405000000_Mysql84Utf8mb4Conversion.php new file mode 100644 index 000000000..cd1ba2383 --- /dev/null +++ b/app/Database/Migrations/20260405000000_Mysql84Utf8mb4Conversion.php @@ -0,0 +1,109 @@ +assertNoCollationCollisions(); + + $foreignKeys = dropAllForeignKeyConstraints(); + + try { + if (indexExists('people', 'first_name')) { + $this->db->query('ALTER TABLE ' . $this->db->prefixTable('people') . ' DROP INDEX first_name'); + } + + $script = APPPATH . 'Database/Migrations/sqlscripts/3.4.3_mysql84_utf8mb4_conversion.sql'; + if (!executeScript($script)) { + throw new RuntimeException('Failed to execute utf8mb4 conversion migration: ' . $script); + } + + if (!$this->db->query('ALTER TABLE ' . $this->db->prefixTable('people') + . ' ADD INDEX(`first_name`(191), `last_name`(191), `email`(191), `phone_number`(191))')) { + throw new RuntimeException('Failed to add composite index on people table'); + } + } catch (\Throwable $exception) { + log_message('error', 'utf8mb4 conversion failed; recovering dropped foreign keys: ' + . json_encode($foreignKeys)); + + try { + recreateForeignKeyConstraints($foreignKeys); + } catch (\Throwable $restoreError) { + log_message('error', 'Foreign key recovery failed: ' . $restoreError->getMessage()); + } + + throw $exception; + } + + recreateForeignKeyConstraints($foreignKeys); + } + + // Intentionally irreversible: converting back to utf8 could cause data loss for utf8mb4 characters. + public function down(): void + { + } + + /** + * utf8mb4_unicode_520_ci treats invisible/ignorable Unicode characters (e.g. the LTR + * mark U+200E) as equal weight to nothing, unlike utf8_general_ci. Values that were + * previously distinct under a UNIQUE constraint can collide once converted, which aborts + * the ALTER TABLE mid-script. Detect those collisions up front and fail with an actionable + * message rather than the raw "Duplicate entry" error partway through the conversion. + */ + private function assertNoCollationCollisions(): void + { + $uniqueColumns = $this->db->query(" + SELECT DISTINCT s.TABLE_NAME, s.COLUMN_NAME + FROM information_schema.STATISTICS s + JOIN information_schema.COLUMNS c + ON c.TABLE_SCHEMA = s.TABLE_SCHEMA + AND c.TABLE_NAME = s.TABLE_NAME + AND c.COLUMN_NAME = s.COLUMN_NAME + WHERE s.TABLE_SCHEMA = DATABASE() + AND s.NON_UNIQUE = 0 + AND s.TABLE_NAME LIKE '" . $this->db->getPrefix() . "%' + AND c.DATA_TYPE IN ('char', 'varchar', 'text', 'tinytext', 'mediumtext', 'longtext') + AND ( + SELECT COUNT(*) FROM information_schema.STATISTICS s2 + WHERE s2.TABLE_SCHEMA = s.TABLE_SCHEMA + AND s2.TABLE_NAME = s.TABLE_NAME + AND s2.INDEX_NAME = s.INDEX_NAME + ) = 1 + ")->getResultArray(); + + $problems = []; + foreach ($uniqueColumns as $uniqueColumn) { + $table = $uniqueColumn['TABLE_NAME']; + $column = $uniqueColumn['COLUMN_NAME']; + + $collisions = $this->db->query(" + SELECT CONVERT(`$column` USING utf8mb4) COLLATE utf8mb4_unicode_520_ci AS value, + COUNT(*) AS count + FROM `$table` + WHERE `$column` IS NOT NULL + GROUP BY CONVERT(`$column` USING utf8mb4) COLLATE utf8mb4_unicode_520_ci + HAVING count > 1 + ")->getResultArray(); + + if ($collisions) { + $problems[] = "$table.$column (" . count($collisions) . ' colliding value(s))'; + } + } + + if ($problems) { + throw new RuntimeException( + 'Cannot convert to utf8mb4: the following unique column(s) contain values that ' + . 'collide under utf8mb4_unicode_520_ci (often due to invisible Unicode characters ' + . "that this collation treats as equivalent):\n - " . implode("\n - ", $problems) + . "\nClean up or merge the colliding rows before retrying this migration." + ); + } + } +} diff --git a/app/Database/Migrations/sqlscripts/3.4.3_mysql84_utf8mb4_conversion.sql b/app/Database/Migrations/sqlscripts/3.4.3_mysql84_utf8mb4_conversion.sql new file mode 100644 index 000000000..7d4a0badb --- /dev/null +++ b/app/Database/Migrations/sqlscripts/3.4.3_mysql84_utf8mb4_conversion.sql @@ -0,0 +1,41 @@ +-- Convert all tables to utf8mb4 for MySQL 8.4 compatibility +-- Foreign keys are dropped and recreated in the PHP migration to avoid collation mismatches +ALTER TABLE ospos_app_config CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_attribute_definitions CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_attribute_links CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_attribute_values CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_cash_up CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_customers CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_customers_packages CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_customers_points CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_dinner_tables CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_employees CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_expenses CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_expense_categories CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_giftcards CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_grants CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_inventory CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_items CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_items_taxes CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_item_kits CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_item_kit_items CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_item_quantities CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_migrations CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_modules CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_people CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_permissions CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_receivings CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_receivings_items CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_sales CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_sales_items CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_sales_items_taxes CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_sales_payments CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_sales_reward_points CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_sales_taxes CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_sessions CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_stock_locations CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_suppliers CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_tax_categories CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_tax_codes CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_tax_jurisdictions CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE ospos_tax_rates CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; diff --git a/app/Helpers/migration_helper.php b/app/Helpers/migration_helper.php index d7da83922..c8dd3d71b 100644 --- a/app/Helpers/migration_helper.php +++ b/app/Helpers/migration_helper.php @@ -118,40 +118,64 @@ function createPrimaryKey(string $table, string $index): void { } /** - * Drops all foreign key constraints that reference the provided table and column. + * Drops foreign key constraints that reference the provided table and column. + * When $table and $column are omitted, drops all foreign key constraints in the schema. * * A failed metadata query is treated as an error (thrown) rather than mistaken * for "no constraints found", so callers never proceed with schema changes on a * wrong assumption. * - * @param string $table - * @param string $column + * @param string|null $table + * @param string|null $column * @param BaseConnection|null $db Connection to use (defaults to the default connection). * * @return array containing the deleted constraints in case they need to be recreated after. * * @throws DatabaseException If the metadata query fails. */ -function dropAllForeignKeyConstraints(string $table, string $column, ?BaseConnection $db = null): array { +function dropAllForeignKeyConstraints(?string $table = null, ?string $column = null, ?BaseConnection $db = null): array { $db ??= Database::connect(); - $result = $db->query(" - SELECT DISTINCT - kcu.CONSTRAINT_NAME, - kcu.TABLE_NAME, - kcu.COLUMN_NAME, - kcu.REFERENCED_TABLE_NAME, - kcu.REFERENCED_COLUMN_NAME, - rc.DELETE_RULE, - rc.UPDATE_RULE - FROM information_schema.KEY_COLUMN_USAGE kcu - LEFT JOIN information_schema.REFERENTIAL_CONSTRAINTS rc - ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME - AND kcu.TABLE_NAME = rc.TABLE_NAME - WHERE kcu.TABLE_SCHEMA = DATABASE() - AND ((kcu.REFERENCED_TABLE_NAME = '" . $db->getPrefix() . "$table' AND kcu.REFERENCED_COLUMN_NAME = '$column') - OR (kcu.TABLE_NAME = '" . $db->getPrefix() . "$table' AND kcu.COLUMN_NAME = '$column')) - AND rc.CONSTRAINT_NAME IS NOT NULL - "); + $prefixedTable = $table !== null ? $db->getPrefix() . $table : null; + $prefix = overridePrefix(); + + $builder = $db->table('information_schema.KEY_COLUMN_USAGE kcu'); + $builder->distinct(); + $builder->select('kcu.CONSTRAINT_NAME, kcu.TABLE_NAME, kcu.COLUMN_NAME, kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME, kcu.ORDINAL_POSITION, rc.DELETE_RULE, rc.UPDATE_RULE'); + $builder->join( + 'information_schema.REFERENTIAL_CONSTRAINTS rc', + 'kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA AND kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND kcu.TABLE_NAME = rc.TABLE_NAME', + 'left' + ); + + if ($table !== null && $column !== null) { + $scopedBuilder = $db->table('information_schema.KEY_COLUMN_USAGE scoped'); + $scopedBuilder->distinct(); + $scopedBuilder->select('scoped.CONSTRAINT_NAME, scoped.TABLE_NAME'); + $scopedBuilder->where('scoped.TABLE_SCHEMA', $db->database); + $scopedBuilder->groupStart(); + $scopedBuilder->where('scoped.REFERENCED_TABLE_NAME', $prefixedTable); + $scopedBuilder->where('scoped.REFERENCED_COLUMN_NAME', $column); + $scopedBuilder->groupEnd(); + $scopedBuilder->orGroupStart(); + $scopedBuilder->where('scoped.TABLE_NAME', $prefixedTable); + $scopedBuilder->where('scoped.COLUMN_NAME', $column); + $scopedBuilder->groupEnd(); + + $builder->join( + '(' . $scopedBuilder->getCompiledSelect() . ') matched', + 'matched.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND matched.TABLE_NAME = kcu.TABLE_NAME', + 'inner', + false + ); + } + + $builder->where('kcu.TABLE_SCHEMA', $db->database); + $builder->where('rc.CONSTRAINT_NAME IS NOT NULL', null, false); + $builder->orderBy('kcu.CONSTRAINT_NAME'); + $builder->orderBy('kcu.ORDINAL_POSITION'); + + $result = $builder->get(); + overridePrefix($prefix); if ($result === false) { $detail = 'unknown error'; @@ -164,26 +188,46 @@ function dropAllForeignKeyConstraints(string $table, string $column, ?BaseConnec // 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 - )); + $scope = $table !== null && $column !== null + ? sprintf('referencing table "%s" column "%s"', $table, $column) + : 'in the schema'; + + throw new DatabaseException(sprintf('Failed to look up foreign key constraints %s: %s', $scope, $detail)); } $deletedConstraints = []; foreach ($result->getResultArray() as $constraint) { - $deletedConstraints[] = [ - 'constraintName' => $constraint['CONSTRAINT_NAME'], - 'tableName' => str_replace($db->DBPrefix, '', $constraint['TABLE_NAME']), - 'columnName' => $constraint['COLUMN_NAME'], - 'referencedTable' => str_replace($db->DBPrefix, '', $constraint['REFERENCED_TABLE_NAME']), - 'referencedColumn' => $constraint['REFERENCED_COLUMN_NAME'], - 'onDelete' => $constraint['DELETE_RULE'], - 'onUpdate' => $constraint['UPDATE_RULE'], - ]; + $key = $constraint['TABLE_NAME'] . '.' . $constraint['CONSTRAINT_NAME']; + + if (!isset($deletedConstraints[$key])) { + $deletedConstraints[$key] = [ + 'constraintName' => $constraint['CONSTRAINT_NAME'], + 'tableName' => str_replace($db->DBPrefix, '', $constraint['TABLE_NAME']), + 'columnName' => [], + 'referencedTable' => str_replace($db->DBPrefix, '', $constraint['REFERENCED_TABLE_NAME']), + 'referencedColumn' => [], + 'onDelete' => $constraint['DELETE_RULE'], + 'onUpdate' => $constraint['UPDATE_RULE'], + 'seenPositions' => [], + ]; + } + + $position = $constraint['ORDINAL_POSITION']; + if (!isset($deletedConstraints[$key]['seenPositions'][$position])) { + $deletedConstraints[$key]['seenPositions'][$position] = true; + $deletedConstraints[$key]['columnName'][] = $constraint['COLUMN_NAME']; + $deletedConstraints[$key]['referencedColumn'][] = $constraint['REFERENCED_COLUMN_NAME']; + } } + foreach ($deletedConstraints as &$constraint) { + unset($constraint['seenPositions']); + } + unset($constraint); + + $deletedConstraints = array_values($deletedConstraints); + if ($deletedConstraints) { $forge = Database::forge(); foreach ($deletedConstraints as $foreignKey) { diff --git a/app/Models/Reports/Summary_payments.php b/app/Models/Reports/Summary_payments.php index 838d5270c..b207ab55e 100644 --- a/app/Models/Reports/Summary_payments.php +++ b/app/Models/Reports/Summary_payments.php @@ -150,7 +150,7 @@ class Summary_payments extends Summary_report LEFT OUTER JOIN ' . $this->db->prefixTable('sales_taxes') . ' AS sales_taxes ON sales.sale_id = sales_taxes.sale_id WHERE ' . $where . ' AND sales_taxes.tax_type = \'1\' - GROUP BY sale_id + GROUP BY sales.sale_id )' ); @@ -164,7 +164,7 @@ class Summary_payments extends Summary_report . 'ON sales.sale_id = sales_items.sale_id ' . 'LEFT OUTER JOIN ' . $this->db->prefixTable('sumpay_taxes_temp') . ' AS sumpay_taxes ' . 'ON sales.sale_id = sumpay_taxes.sale_id ' - . 'WHERE ' . $where . ' GROUP BY sale_id + . 'WHERE ' . $where . ' GROUP BY sales.sale_id )' ); @@ -184,7 +184,7 @@ class Summary_payments extends Summary_report LEFT OUTER JOIN ' . $this->db->prefixTable('sales_payments') . ' AS sales_payments ON sales.sale_id = sales_payments.sale_id WHERE ' . $where . ' - GROUP BY sale_id + GROUP BY sales.sale_id )' ); } diff --git a/app/Models/Reports/Summary_report.php b/app/Models/Reports/Summary_report.php index c652f370e..e64024e69 100644 --- a/app/Models/Reports/Summary_report.php +++ b/app/Models/Reports/Summary_report.php @@ -58,7 +58,7 @@ abstract class Summary_report extends Report INNER JOIN ' . $this->db->prefixTable('sales_items') . ' AS sales_items ON sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.line = sales_items_taxes.line WHERE ' . $where . ' - GROUP BY sale_id, item_id, line + GROUP BY sales_items_taxes.sale_id, sales_items_taxes.item_id, sales_items_taxes.line )' ); @@ -76,7 +76,7 @@ abstract class Summary_report extends Report INNER JOIN ' . $this->db->prefixTable('sales') . ' AS sales ON sales.sale_id = payments.sale_id WHERE ' . $where . ' - GROUP BY sale_id + GROUP BY payments.sale_id )' ); diff --git a/app/Models/Sale.php b/app/Models/Sale.php index 45f84ab49..86992bc55 100644 --- a/app/Models/Sale.php +++ b/app/Models/Sale.php @@ -1088,7 +1088,7 @@ class Sale extends Model INNER JOIN ' . $this->db->prefixTable('sales_items') . ' AS sales_items ON sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.line = sales_items_taxes.line WHERE ' . $where . ' - GROUP BY sale_id, item_id, line + GROUP BY sales_items_taxes.sale_id, sales_items_taxes.item_id, sales_items_taxes.line )'; $this->db->query($sql); @@ -1174,7 +1174,7 @@ class Sale extends Model LEFT OUTER JOIN ' . $this->db->prefixTable('sales_items_taxes_temp') . ' AS sales_items_taxes ON sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line WHERE ' . $where . ' - GROUP BY sale_id, item_id, line + GROUP BY sales.sale_id, items.item_id, sales_items.line, sales_items.discount_type )'; $this->db->query($sql); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3b0a2f2c6..cb859bcc3 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -12,7 +12,7 @@ networks: services: mysql: - image: mariadb:10.5 + image: ${OSPOS_DB_IMAGE:-mariadb:10.5} container_name: mysql restart: always expose: diff --git a/docker/docker-mysql.yml b/docker/docker-mysql.yml index 69171c645..4334b7195 100644 --- a/docker/docker-mysql.yml +++ b/docker/docker-mysql.yml @@ -12,7 +12,7 @@ networks: services: mysql: - image: mariadb:10.5 + image: ${OSPOS_DB_IMAGE:-mariadb:10.5} container_name: mysql restart: always expose: diff --git a/tests/Support/FailedQueryConnectionTrait.php b/tests/Support/FailedQueryConnectionTrait.php index 6906e703e..c86a7edfc 100644 --- a/tests/Support/FailedQueryConnectionTrait.php +++ b/tests/Support/FailedQueryConnectionTrait.php @@ -9,14 +9,17 @@ trait FailedQueryConnectionTrait { protected function failingConnection(): BaseConnection { - $driverClass = get_class(\Config\Database::connect($this->DBGroup)); + $realConnection = \Config\Database::connect($this->DBGroup); + $driverClass = get_class($realConnection); + $builderClass = str_replace('Connection', 'Builder', $driverClass); $db = $this->getMockBuilder($driverClass) ->disableOriginalConstructor() - ->onlyMethods(['query', 'error']) + ->onlyMethods(['query', 'error', 'table']) ->getMock(); $db->method('query')->willReturn(false); $db->method('error')->willReturn(['code' => 1, 'message' => 'simulated query failure']); + $db->method('table')->willReturnCallback(static fn (string $tableName) => new $builderClass($tableName, $db)); return $db; }