Add Guards to Database Migration (#4571)

* Properly replace key in config file when encryption key is updated

This fixes a break caused if there is a commented out key in the .env. It's a more robust replacement.

Signed-off-by: objec <objecttothis@gmail.com>

* Guard against dropping constraint that doesn't exist

- Updated wording in migration_helper.php PHPdoc
- Use migration_helper function to drop key which only drops the constraint if it exists.

The core logic was not changed here. It only adds a safety mechanism.

Signed-off-by: objec <objecttothis@gmail.com>

* Remove duplicate call to getResultArray in attribute_links loop

Signed-off-by: objec <objecttothis@gmail.com>

* PSR refactoring

Signed-off-by: objec <objecttothis@gmail.com>

* Remove dead parameter from reassignDuplicateAttributeValues method

The attribute value was not needed and is never used since we have the attribute_ids and those are unique.

Signed-off-by: objec <objecttothis@gmail.com>

* Create potentially missing primary keys before attempting to add constraints.

Signed-off-by: objec <objecttothis@gmail.com>

* Guard datetime creation

Signed-off-by: objec <objecttothis@gmail.com>

* Update regex pattern to handle double-quoted and non-quoted encryption keys

Signed-off-by: objec <objecttothis@gmail.com>

* Issue warning and fallback on unparseable attribute_date during

Signed-off-by: objec <objecttothis@gmail.com>

---------

Signed-off-by: objec <objecttothis@gmail.com>
This commit is contained in:
objecttothis authored and GitHub committed 2026-06-06 02:02:42 +04:00
1 parent 4173d7f350
commit 450c0866b5
4 files changed
+77 -66

No files matched your search

@@ -18,12 +18,35 @@ class Migration_database_optimizations extends Migration
{
log_message('info', 'Migrating database optimizations.');
helper('migration');
dropForeignKeyConstraints(['ospos_customers_ibfk_1'], 'customers');
dropForeignKeyConstraints(['ospos_customers_points_ibfk_1'], 'customers_points');
dropForeignKeyConstraints(['ospos_sales_ibfk_2'], 'sales');
dropForeignKeyConstraints(['ospos_sales_payments_ibfk_2'], 'sales_payments');
dropForeignKeyConstraints(['ospos_sales_ibfk_1'], 'sales');
dropForeignKeyConstraints(['ospos_receivings_ibfk_1'], 'receivings');
dropForeignKeyConstraints(['ospos_inventory_ibfk_2'], 'inventory');
dropForeignKeyConstraints(['ospos_grants_ibfk_2'], 'grants');
dropForeignKeyConstraints(['ospos_expenses_ibfk_2'], 'expenses');
dropForeignKeyConstraints(['ospos_employees_ibfk_1'], 'employees');
dropForeignKeyConstraints(['ospos_cash_up_ibfk_1'], 'cash_up');
dropForeignKeyConstraints(['ospos_cash_up_ibfk_2'], 'cash_up');
dropForeignKeyConstraints(['ospos_items_ibfk_1'], 'items');
dropForeignKeyConstraints(['ospos_expenses_ibfk_3'], 'expenses');
dropForeignKeyConstraints(['ospos_receivings_ibfk_2'], 'receivings');
dropForeignKeyConstraints(['ospos_suppliers_ibfk_1'], 'suppliers');
createPrimaryKey('customers', 'person_id');
createPrimaryKey('employees', 'person_id');
createPrimaryKey('suppliers', 'person_id');
$attribute = model(Attribute::class);
$attribute->deleteOrphanedValues();
$this->migrate_duplicate_attribute_values(DECIMAL);
$this->migrate_duplicate_attribute_values(DATE);
$this->migrateDuplicateAttributeValues(DECIMAL);
$this->migrateDuplicateAttributeValues(DATE);
// Select all attributes that have data in more than one column
$builder = $this->db->table('attribute_values');
@@ -36,51 +59,60 @@ class Migration_database_optimizations extends Migration
$builder->where('attribute_value IS NOT NULL');
$builder->where('attribute_decimal IS NOT NULL');
$builder->groupEnd();
$attribute_values = $builder->get();
$attributeValues = $builder->get();
$this->db->transStart();
// Clean up Attribute values table where there is an attribute value and an attribute_date/attribute_decimal
foreach ($attribute_values->getResultArray() as $attribute_value) {
foreach ($attributeValues->getResultArray() as $attributeValue) {
$builder = $this->db->table('attribute_values');
$builder->delete(['attribute_id' => $attribute_value['attribute_id']]);
$builder->delete(['attribute_id' => $attributeValue['attribute_id']]);
$builder = $this->db->table('attribute_links');
$builder->select('links.definition_id, links.item_id, links.attribute_id, defs.definition_type');
$builder->join('attribute_definitions defs', 'defs.definition_id = links.definition_id');
$builder->where('attribute_id', $attribute_value['attribute_id']);
$attribute_links = $builder->get();
$builder->where('attribute_id', $attributeValue['attribute_id']);
$attributeLinks = $builder->get();
if ($attribute_links) {
if ($attributeLinks) {
$builder = $this->db->table('attribute_links');
$attribute_links = $attribute_links->getResultArray() ?: [];
$attributeLinks = $attributeLinks->getResultArray() ?: [];
foreach ($attribute_links->getResultArray() as $attribute_link) {
$builder->where('attribute_id', $attribute_link['attribute_id']);
$builder->where('item_id', $attribute_link['item_id']);
foreach ($attributeLinks as $attributeLink) {
$builder->where('attribute_id', $attributeLink['attribute_id']);
$builder->where('item_id', $attributeLink['item_id']);
$builder->delete();
switch ($attribute_link['definition_type']) {
switch ($attributeLink['definition_type']) {
case DECIMAL:
$value = $attribute_value['attribute_decimal'];
$value = $attributeValue['attribute_decimal'];
break;
case DATE:
$config = config(OSPOS::class)->settings;
$attribute_date = DateTime::createFromFormat('Y-m-d', $attribute_value['attribute_date']);
$value = $attribute_date->format($config['dateformat']);
$attributeDate = DateTime::createFromFormat('Y-m-d', (string) $attributeValue['attribute_date']);
if ($attributeDate === false) {
log_message('warning', 'Migration 20210422000000: unparseable attribute_date "' . $attributeValue['attribute_date'] . '" for attribute_id ' . $attributeValue['attribute_id'] . ' — preserving raw value.');
$value = (string) $attributeValue['attribute_date'];
} else {
$dateFormat = empty($config['dateformat']) ? 'Y-m-d' : $config['dateformat'];
if (empty($config['dateformat'])) {
log_message('warning', 'Migration 20210422000000: dateformat config empty, falling back to Y-m-d for attribute_id ' . $attributeValue['attribute_id'] . '.');
}
$value = $attributeDate->format($dateFormat);
}
break;
default:
$value = $attribute_value['attribute_value'];
$value = $attributeValue['attribute_value'];
break;
}
$attribute->saveAttributeValue($value, $attribute_link['definition_id'], $attribute_link['item_id'], false, $attribute_link['definition_type']);
$attribute->saveAttributeValue($value, $attributeLink['definition_id'], $attributeLink['item_id'], false, $attributeLink['definition_type']);
}
}
}
$this->db->transComplete();
helper('migration');
execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.4.0_database_optimizations.sql');
log_message('info', 'Finished migrating database optimizations.');
}
@@ -88,58 +120,57 @@ class Migration_database_optimizations extends Migration
/**
* Given the type of attribute, deletes any duplicates it finds in the attribute_values table and reassigns those
*/
private function migrate_duplicate_attribute_values($attribute_type): void
private function migrateDuplicateAttributeValues(string $attributeType): void
{
// Remove duplicate attribute values needed to make attribute_decimals and attribute_dates unique
$this->db->transStart();
$column = 'attribute_' . strtolower($attribute_type);
$column = 'attribute_' . strtolower($attributeType);
$builder = $this->db->table('attribute_values');
$builder->select("$column");
$builder->groupBy($column);
$builder->having("COUNT($column) > 1");
$duplicated_values = $builder->get();
$duplicatedValues = $builder->get();
foreach ($duplicated_values->getResultArray() as $duplicated_value) {
$subquery_builder = $this->db->table('attribute_values');
$subquery_builder->select('attribute_id');
$subquery_builder->where($column, $duplicated_value[$column]);
$subquery = $subquery_builder->getCompiledSelect();
foreach ($duplicatedValues->getResultArray() as $duplicatedValue) {
$subqueryBuilder = $this->db->table('attribute_values');
$subqueryBuilder->select('attribute_id');
$subqueryBuilder->where($column, $duplicatedValue[$column]);
$subquery = $subqueryBuilder->getCompiledSelect();
$builder = $this->db->table('attribute_values');
$builder->select('attribute_id');
$builder->where($column, $duplicated_value[$column]);
$builder->where($column, $duplicatedValue[$column]);
$builder->where("attribute_id IN ($subquery)", null, false);
$attribute_ids_to_fix = $builder->get();
$attributeIdsToFix = $builder->get();
$this->reassign_duplicate_attribute_values($attribute_ids_to_fix, $duplicated_value);
$this->reassignDuplicateAttributeValues($attributeIdsToFix);
}
$this->db->transComplete();
}
/**
* Updates the attribute_id in all attribute_link rows with duplicated attribute_ids then deletes unneeded rows from attribute_values
* Updates the attribute_id in all attribute_link rows with duplicated attributeIds then deletes unneeded rows from attributeValues
*
* @param ResultInterface $attribute_ids_to_fix All attribute_ids that need to parsed
* @param array $attribute_value The attribute value in question.
* @param ResultInterface $attributeIdsToFix All attributeIds that need to parsed
*/
private function reassign_duplicate_attribute_values(ResultInterface $attribute_ids_to_fix, array $attribute_value): void
private function reassignDuplicateAttributeValues(ResultInterface $attributeIdsToFix): void
{
$attribute_ids = $attribute_ids_to_fix->getResultArray();
$retain_attribute_id = $attribute_ids[0]['attribute_id'];
$attributeIds = $attributeIdsToFix->getResultArray();
$retainAttributeId = $attributeIds[0]['attribute_id'];
foreach ($attribute_ids as $attribute_id) {
foreach ($attributeIds as $attributeId) {
// Update attribute_link with the attribute_id we are keeping
$builder = $this->db->table('attribute_links');
$builder->where('attribute_id', $attribute_id['attribute_id']);
$builder->update(['attribute_id' => $retain_attribute_id]);
$builder->where('attribute_id', $attributeId['attribute_id']);
$builder->update(['attribute_id' => $retainAttributeId]);
// Delete the row from attribute_values if it isn't our keeper
if ($attribute_id['attribute_id'] !== $retain_attribute_id) {
if ($attributeId['attribute_id'] !== $retainAttributeId) {
$builder = $this->db->table('attribute_values');
$builder->delete(['attribute_id' => $attribute_id['attribute_id']]);
$builder->delete(['attribute_id' => $attributeId['attribute_id']]);
}
}
}
@@ -11,10 +11,6 @@ ALTER TABLE `ospos_attribute_definitions` ADD INDEX(`definition_type`);
ALTER TABLE `ospos_cash_up` MODIFY `deleted` tinyint(1) DEFAULT 0 NOT NULL;
#ospos_customers table
ALTER TABLE `ospos_customers` DROP FOREIGN KEY `ospos_customers_ibfk_1`;
ALTER TABLE `ospos_customers_points` DROP FOREIGN KEY `ospos_customers_points_ibfk_1`;
ALTER TABLE `ospos_sales` DROP FOREIGN KEY `ospos_sales_ibfk_2`;
ALTER TABLE `ospos_customers` MODIFY `taxable` tinyint(1) DEFAULT 1 NOT NULL;
ALTER TABLE `ospos_customers` MODIFY `deleted` tinyint(1) DEFAULT 0 NOT NULL;
ALTER TABLE `ospos_customers` MODIFY `discount_type` tinyint(1) DEFAULT 0 NOT NULL;
@@ -31,16 +27,6 @@ ALTER TABLE `ospos_dinner_tables` MODIFY `deleted` tinyint(1) DEFAULT 0 NOT NULL
ALTER TABLE `ospos_dinner_tables` ADD INDEX(`status`);
#ospos_employees table
ALTER TABLE `ospos_sales_payments` DROP FOREIGN KEY `ospos_sales_payments_ibfk_2`;
ALTER TABLE `ospos_sales` DROP FOREIGN KEY `ospos_sales_ibfk_1`;
ALTER TABLE `ospos_receivings` DROP FOREIGN KEY `ospos_receivings_ibfk_1`;
ALTER TABLE `ospos_inventory` DROP FOREIGN KEY `ospos_inventory_ibfk_2`;
ALTER TABLE `ospos_grants` DROP FOREIGN KEY `ospos_grants_ibfk_2`;
ALTER TABLE `ospos_expenses` DROP FOREIGN KEY `ospos_expenses_ibfk_2`;
ALTER TABLE `ospos_employees` DROP FOREIGN KEY `ospos_employees_ibfk_1`;
ALTER TABLE `ospos_cash_up` DROP FOREIGN KEY `ospos_cash_up_ibfk_1`;
ALTER TABLE `ospos_cash_up` DROP FOREIGN KEY `ospos_cash_up_ibfk_2`;
ALTER TABLE `ospos_employees` MODIFY `deleted` tinyint(1) DEFAULT 0 NOT NULL;
ALTER TABLE `ospos_employees` MODIFY `hash_version` tinyint(1) DEFAULT 2 NOT NULL;
@@ -67,7 +53,6 @@ ALTER TABLE `ospos_expense_categories` ADD INDEX(`category_description`);
ALTER TABLE `ospos_giftcards` MODIFY `deleted` tinyint(1) DEFAULT 0 NOT NULL;
#ospos_items table
ALTER TABLE `ospos_items` DROP FOREIGN KEY `ospos_items_ibfk_1`;
ALTER TABLE `ospos_items` MODIFY `deleted` tinyint(1) DEFAULT 0 NOT NULL;
ALTER TABLE `ospos_items` MODIFY `stock_type` tinyint(1) DEFAULT 0 NOT NULL;
ALTER TABLE `ospos_items` MODIFY `item_type` tinyint(1) DEFAULT 0 NOT NULL;
@@ -112,10 +97,6 @@ ALTER TABLE `ospos_sessions` ADD INDEX(`ip_address`);
ALTER TABLE `ospos_stock_locations` MODIFY `deleted` tinyint(1) DEFAULT 0 NOT NULL;
#ospos_suppliers table
ALTER TABLE `ospos_expenses` DROP FOREIGN KEY `ospos_expenses_ibfk_3`;
ALTER TABLE `ospos_receivings` DROP FOREIGN KEY `ospos_receivings_ibfk_2`;
ALTER TABLE `ospos_suppliers` DROP FOREIGN KEY `ospos_suppliers_ibfk_1`;
ALTER TABLE `ospos_suppliers` MODIFY `deleted` tinyint(1) DEFAULT 0 NOT NULL;
ALTER TABLE `ospos_suppliers` MODIFY `category` tinyint(1) NOT NULL;
ALTER TABLE `ospos_suppliers` ADD INDEX(`category`);
+1 -1
View File
@@ -88,7 +88,7 @@ function executeScriptWithTransaction(string $path): bool
}
/**
* Drops provided foreign key constraints from given table.
* Drops provided foreign key constraints from a given table if the constraint exists.
* This is required to successfully create the generated unique constraint.
*
* @param array $foreignKeys names of the foreign key constraints to drop
+4 -5
View File
@@ -40,17 +40,16 @@ function check_encryption(): bool
$config_file = file_get_contents($config_path);
if (strpos($config_file, 'encryption.key') !== false) {
$config_file = preg_replace("/(encryption\.key.*=.*)('.*')/", "$1'$key'", $config_file);
if (preg_match('/^\s*encryption\.key\s*=/m', $config_file)) {
$config_file = preg_replace("/^(\s*encryption\.key\s*=\s*).*/m", "\$1'$key'", $config_file, 1);
} else {
$config_file .= "\nencryption.key = '$key'\n";
}
if (!empty($old_key)) {
$old_line = "# encryption.key = '$old_key' REMOVE IF UNNEEDED\r\n";
$insertion_point = stripos($config_file, 'encryption.key');
if ($insertion_point !== false) {
$config_file = substr_replace($config_file, $old_line, $insertion_point, 0);
if (preg_match('/^encryption\.key\s*=/m', $config_file, $matches, PREG_OFFSET_CAPTURE)) {
$config_file = substr_replace($config_file, $old_line, $matches[0][1], 0);
}
}