query( "ALTER TABLE orders ADD COLUMN include_in_invoice TINYINT(1) NULL DEFAULT NULL AFTER created_at" ); } if (!self::columnExists($db, 'orders', 'safety_seal')) { $db->query( "ALTER TABLE orders ADD COLUMN safety_seal VARCHAR(255) NULL DEFAULT NULL AFTER po" ); } if (self::tableExists($db, 'order_items')) { if (!self::columnExists($db, 'order_items', 'reason_code')) { $db->query( "ALTER TABLE order_items ADD COLUMN reason_code VARCHAR(64) NULL DEFAULT NULL AFTER include_in_invoice" ); } if (!self::columnExists($db, 'order_items', 'reason_label_snapshot')) { $db->query( "ALTER TABLE order_items ADD COLUMN reason_label_snapshot VARCHAR(255) NULL DEFAULT NULL AFTER reason_code" ); } if (!self::columnExists($db, 'order_items', 'reason_comment')) { $db->query( "ALTER TABLE order_items ADD COLUMN reason_comment TEXT NULL AFTER reason_label_snapshot" ); } } self::backfillBookingPoDefaults($db); self::ensureIndex($db, 'orders', 'idx_orders_period_customer_created_deleted', 'customer_id, created_at, deleted_at'); self::ensureIndex($db, 'orders', 'idx_orders_period_created_deleted_customer', 'created_at, deleted_at, customer_id'); self::ensureIndex($db, 'order_items', 'idx_order_items_order_deleted', 'order_id, deleted_at'); self::ensureIndex($db, 'order_items', 'idx_order_items_reason_code', 'reason_code'); self::ensureIndex($db, 'customer_attributes', 'idx_customer_attributes_attribute_user', 'attribute, user_id'); self::$initialized = true; } private static function backfillBookingPoDefaults(object $db): void { if ( !self::tableExists($db, 'order_bookings') || !self::columnExists($db, 'orders', 'booking_id') || !self::columnExists($db, 'orders', 'po') || !self::columnExists($db, 'order_bookings', 'po') || !self::columnExists($db, 'order_bookings', 'customer_number') || !self::columnExists($db, 'order_bookings', 'department') || !self::columnExists($db, 'order_bookings', 'deleted_at') ) { return; } $db->query( "UPDATE orders o INNER JOIN order_bookings b ON b.id = o.booking_id AND b.customer_number = o.customer_id AND b.department = o.department_id AND b.deleted_at IS NULL SET o.po = b.po WHERE o.booking_id IS NOT NULL AND o.booking_id > 0 AND (o.po IS NULL OR TRIM(o.po) = '') AND b.po IS NOT NULL AND TRIM(b.po) <> ''" ); } private static function tableExists(object $db, string $table): bool { $table = self::escapeIdentifier($table); $result = $db->query("SHOW TABLES LIKE '{$table}'"); if ($result === false || !is_object($result) || !property_exists($result, 'num_rows')) { return false; } return (int)$result->num_rows > 0; } private static function columnExists(object $db, string $table, string $column): bool { $table = self::escapeIdentifier($table); $column = self::escapeIdentifier($column); $result = $db->query("SHOW COLUMNS FROM `{$table}` LIKE '{$column}'"); if ($result === false || !is_object($result) || !property_exists($result, 'num_rows')) { return false; } return (int)$result->num_rows > 0; } private static function ensureIndex(object $db, string $table, string $index, string $columns): void { if ( !self::tableExists($db, $table) || self::indexExists($db, $table, $index) || !self::columnsExist($db, $table, $columns) ) { return; } $table = self::escapeIdentifier($table); $index = self::escapeIdentifier($index); $db->query("ALTER TABLE `{$table}` ADD INDEX `{$index}` ({$columns})"); } private static function columnsExist(object $db, string $table, string $columns): bool { foreach (explode(',', $columns) as $column) { $column = trim($column, " \t\n\r\0\x0B`"); if ($column === '' || !self::columnExists($db, $table, $column)) { return false; } } return true; } private static function indexExists(object $db, string $table, string $index): bool { $table = self::escapeIdentifier($table); $index = self::escapeIdentifier($index); $result = $db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = '{$index}'"); if ($result === false || !is_object($result) || !property_exists($result, 'num_rows')) { return false; } return (int)$result->num_rows > 0; } private static function escapeIdentifier(string $value): string { return str_replace(['\\', "'", '`'], ['\\\\', "\\'", ''], $value); } }