query($sql); } self::ensureColumn( 'department_lanes', 'machine_type_id', 'ALTER TABLE department_lanes ADD COLUMN machine_type_id INT NULL AFTER dynamic_image_id' ); self::ensureColumn( 'department_lanes', 'selfserve_enabled', 'ALTER TABLE department_lanes ADD COLUMN selfserve_enabled TINYINT(1) NOT NULL DEFAULT 1 AFTER machine_type_id' ); self::ensureColumn( 'department_selfserve_conditions', 'machine_type_id', 'ALTER TABLE department_selfserve_conditions ADD COLUMN machine_type_id INT NULL AFTER product' ); self::ensureColumn( 'department_selfserve_tasks', 'machine_type_id', 'ALTER TABLE department_selfserve_tasks ADD COLUMN machine_type_id INT NULL AFTER product' ); self::ensureColumn( 'department_selfserve_tasks', 'gate_type', "ALTER TABLE department_selfserve_tasks ADD COLUMN gate_type VARCHAR(16) NULL DEFAULT 'ALWAYS' AFTER condition_id" ); self::ensureColumn( 'department_selfserve_tasks', 'gate_ref_id', 'ALTER TABLE department_selfserve_tasks ADD COLUMN gate_ref_id INT NULL AFTER gate_type' ); self::ensureColumnDataType( 'department_selfserve_tasks', 'description', ['text', 'mediumtext', 'longtext'], 'ALTER TABLE department_selfserve_tasks MODIFY COLUMN description TEXT NULL AFTER task' ); self::ensureColumnDataType( 'selfserve_wash_session_tasks', 'description', ['text', 'mediumtext', 'longtext'], 'ALTER TABLE selfserve_wash_session_tasks MODIFY COLUMN description TEXT NULL AFTER task_text' ); self::ensureColumn( 'selfserve_wash_session_tasks', 'dynamic_images_vehicle_type', 'ALTER TABLE selfserve_wash_session_tasks ADD COLUMN dynamic_images_vehicle_type INT NULL AFTER buttons' ); self::ensureColumn( 'selfserve_wash_sessions', 'customer_number', 'ALTER TABLE selfserve_wash_sessions ADD COLUMN customer_number INT NULL AFTER department_id' ); self::ensureColumn( 'selfserve_wash_sessions', 'subuser_id', 'ALTER TABLE selfserve_wash_sessions ADD COLUMN subuser_id INT NULL AFTER customer_number' ); self::ensureColumn( 'selfserve_wash_sessions', 'wash_started_at', 'ALTER TABLE selfserve_wash_sessions ADD COLUMN wash_started_at DATETIME NULL AFTER machine_start_triggered_at' ); self::ensureIndex( 'selfserve_wash_sessions', 'idx_selfserve_wash_sessions_customer', 'ALTER TABLE selfserve_wash_sessions ADD INDEX idx_selfserve_wash_sessions_customer (customer_number)' ); self::ensureIndex( 'selfserve_wash_sessions', 'idx_selfserve_wash_sessions_subuser_active', 'ALTER TABLE selfserve_wash_sessions ADD INDEX idx_selfserve_wash_sessions_subuser_active (subuser_id, completed_at)' ); self::ensureIndex( 'selfserve_wash_sessions', 'idx_selfserve_wash_sessions_customer_subuser_active', 'ALTER TABLE selfserve_wash_sessions ADD INDEX idx_selfserve_wash_sessions_customer_subuser_active (customer_number, subuser_id, completed_at)' ); self::ensureIndex( 'selfserve_wash_sessions', 'idx_selfserve_wash_sessions_department_completed', 'ALTER TABLE selfserve_wash_sessions ADD INDEX idx_selfserve_wash_sessions_department_completed (department_id, completed_at)' ); self::ensureIndex( 'selfserve_wash_sessions', 'idx_selfserve_wash_sessions_order', 'ALTER TABLE selfserve_wash_sessions ADD INDEX idx_selfserve_wash_sessions_order (order_id)' ); self::$initialized = true; } public static function tableHasColumn(string $table, string $column): bool { global $db; $table = $db->escape_string($table); $column = $db->escape_string($column); $database = $db->escape_string($db->getDatabase()); $sql = "SELECT COUNT(*) AS c FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$database' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$column'"; $result = $db->query($sql); if (!$result) { return false; } $row = $result->fetch_assoc(); return ((int)($row['c'] ?? 0)) > 0; } public static function ensureColumn(string $table, string $column, string $alterSql): void { global $db; if (self::tableHasColumn($table, $column)) { return; } $db->query($alterSql); } public static function tableHasIndex(string $table, string $index): bool { global $db; $table = $db->escape_string($table); $index = $db->escape_string($index); $database = $db->escape_string($db->getDatabase()); $sql = "SELECT COUNT(*) AS c FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = '$database' AND TABLE_NAME = '$table' AND INDEX_NAME = '$index'"; $result = $db->query($sql); if (!$result) { return false; } $row = $result->fetch_assoc(); return ((int)($row['c'] ?? 0)) > 0; } public static function ensureIndex(string $table, string $index, string $alterSql): void { global $db; if (self::tableHasIndex($table, $index)) { return; } $db->query($alterSql); } /** * @param array $acceptedDataTypes */ public static function ensureColumnDataType(string $table, string $column, array $acceptedDataTypes, string $alterSql): void { global $db; $columnInfo = self::columnInfo($table, $column); if ($columnInfo === null) { return; } $dataType = strtolower((string)($columnInfo['DATA_TYPE'] ?? '')); $acceptedDataTypes = array_map(static fn(string $type): string => strtolower($type), $acceptedDataTypes); if (in_array($dataType, $acceptedDataTypes, true)) { return; } $db->query($alterSql); } /** * @return array|null */ public static function columnInfo(string $table, string $column): ?array { global $db; $table = $db->escape_string($table); $column = $db->escape_string($column); $database = $db->escape_string($db->getDatabase()); $sql = "SELECT DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$database' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$column' LIMIT 1"; $result = $db->query($sql); if (!$result) { return null; } $row = $result->fetch_assoc(); return is_array($row) ? $row : null; } }