Skip to content

Commit 20a0f47

Browse files
Merge pull request #7142 from NicoDucou/OFAJ23120Mig
Migration: #fix Update collation for specific columns in all missing …
2 parents c762ff0 + 4586320 commit 20a0f47

File tree

1 file changed

+317
-0
lines changed

1 file changed

+317
-0
lines changed
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/* For licensing terms, see /license.txt */
6+
7+
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
8+
9+
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
10+
use Doctrine\DBAL\Schema\Schema;
11+
12+
final class Version20251202103000 extends AbstractMigrationChamilo
13+
{
14+
private const TARGET_CHARSET = 'utf8mb4';
15+
private const TARGET_COLLATION = 'utf8mb4_unicode_ci';
16+
17+
private const LEGACY_CHARSET = 'utf8mb3';
18+
private const LEGACY_COLLATION = 'utf8mb3_unicode_ci';
19+
20+
public function getDescription(): string
21+
{
22+
return 'Convert remaining core tables to utf8mb4 / utf8mb4_unicode_ci';
23+
}
24+
25+
public function up(Schema $schema): void
26+
{
27+
$this->abortIf(
28+
!\in_array($this->connection->getDatabasePlatform()->getName(), ['mysql', 'mariadb'], true),
29+
'This migration only supports MySQL/MariaDB.'
30+
);
31+
32+
foreach ($this->getTablesToConvert() as $table) {
33+
$this->convertTable($table, self::TARGET_CHARSET, self::TARGET_COLLATION);
34+
}
35+
}
36+
37+
public function down(Schema $schema): void
38+
{
39+
$this->abortIf(
40+
!\in_array($this->connection->getDatabasePlatform()->getName(), ['mysql', 'mariadb'], true),
41+
'This migration only supports MySQL/MariaDB.'
42+
);
43+
44+
$tables = $this->getTablesToConvert();
45+
$offenders = $this->findUtf8mb3IncompatibleColumns($tables);
46+
47+
if (!empty($offenders)) {
48+
$message = "Cannot downgrade to utf8mb3. Found 4-byte Unicode characters in:\n- " .
49+
implode("\n- ", $offenders) .
50+
"\n\nRemove those characters first or restore a pre-migration backup.";
51+
52+
if (method_exists($this, 'throwIrreversibleMigrationException')) {
53+
$this->throwIrreversibleMigrationException($message);
54+
}
55+
56+
$this->abortIf(true, $message);
57+
}
58+
59+
foreach ($tables as $table) {
60+
$this->convertTable($table, self::LEGACY_CHARSET, self::LEGACY_COLLATION);
61+
}
62+
}
63+
64+
private function findUtf8mb3IncompatibleColumns(array $tables): array
65+
{
66+
$db = $this->connection->fetchOne('SELECT DATABASE()');
67+
68+
$in = implode(',', array_fill(0, count($tables), '?'));
69+
70+
$cols = $this->connection->fetchAllAssociative(
71+
"SELECT TABLE_NAME, COLUMN_NAME
72+
FROM information_schema.COLUMNS
73+
WHERE TABLE_SCHEMA = ?
74+
AND TABLE_NAME IN ($in)
75+
AND COLLATION_NAME LIKE 'utf8mb4%'
76+
AND DATA_TYPE IN ('char','varchar','tinytext','text','mediumtext','longtext')",
77+
array_merge([$db], $tables)
78+
);
79+
80+
$bad = [];
81+
82+
foreach ($cols as $col) {
83+
$t = $col['TABLE_NAME'];
84+
$c = $col['COLUMN_NAME'];
85+
86+
// MySQL 8+ supports this Unicode range regexp.
87+
// If your server doesn't support it, this query will throw and we fallback.
88+
try {
89+
$hit = $this->connection->fetchOne(
90+
"SELECT 1 FROM `$t` WHERE `$c` REGEXP '[\\x{10000}-\\x{10FFFF}]' LIMIT 1"
91+
);
92+
} catch (\Throwable $e) {
93+
// Fallback: any UTF-8 4-byte sequence starts with lead bytes F0-F4.
94+
$hit = $this->connection->fetchOne(
95+
"SELECT 1 FROM `$t` WHERE HEX(`$c`) REGEXP 'F[0-4]' LIMIT 1"
96+
);
97+
}
98+
99+
if ($hit) {
100+
$bad[] = "$t.$c";
101+
}
102+
}
103+
104+
return $bad;
105+
}
106+
107+
/**
108+
* @return string[]
109+
*/
110+
private function getTablesToConvert(): array
111+
{
112+
return [
113+
// Main
114+
'access_url',
115+
'agenda_reminder',
116+
'block',
117+
'branch_sync',
118+
'branch_transaction',
119+
'branch_transaction_status',
120+
'career',
121+
'chat',
122+
'chat_video',
123+
'course',
124+
'course_category',
125+
'course_rel_class',
126+
'course_request',
127+
'course_type',
128+
129+
// Course tables (single DB schema in C2)
130+
'c_announcement',
131+
'c_announcement_attachment',
132+
'c_attendance',
133+
'c_attendance_sheet',
134+
'c_attendance_sheet_log',
135+
'c_blog',
136+
'c_blog_attachment',
137+
'c_blog_comment',
138+
'c_blog_post',
139+
'c_blog_rating',
140+
'c_blog_task',
141+
'c_calendar_event',
142+
'c_calendar_event_attachment',
143+
'c_calendar_event_repeat',
144+
'c_course_description',
145+
'c_course_setting',
146+
'c_document',
147+
'c_dropbox_category',
148+
'c_dropbox_feedback',
149+
'c_dropbox_file',
150+
'c_dropbox_post',
151+
'c_forum_attachment',
152+
'c_forum_category',
153+
'c_forum_forum',
154+
'c_forum_post',
155+
'c_forum_thread',
156+
'c_glossary',
157+
'c_group_category',
158+
'c_group_info',
159+
'c_group_rel_user',
160+
'c_link',
161+
'c_link_category',
162+
'c_lp',
163+
'c_lp_category',
164+
'c_lp_item',
165+
'c_lp_item_view',
166+
'c_lp_iv_interaction',
167+
'c_lp_iv_objective',
168+
'c_notebook',
169+
'c_quiz',
170+
'c_quiz_answer',
171+
'c_quiz_category',
172+
'c_quiz_question',
173+
'c_quiz_question_category',
174+
'c_quiz_question_option',
175+
'c_quiz_rel_question',
176+
'c_student_publication',
177+
'c_student_publication_comment',
178+
'c_survey',
179+
'c_survey_answer',
180+
'c_survey_invitation',
181+
'c_survey_question',
182+
'c_survey_question_option',
183+
'c_thematic',
184+
'c_thematic_advance',
185+
'c_thematic_plan',
186+
'c_tool',
187+
'c_tool_intro',
188+
'c_wiki',
189+
'c_wiki_conf',
190+
'c_wiki_discuss',
191+
'c_wiki_mailcue',
192+
193+
// Extra fields / settings
194+
'extra_field',
195+
'extra_field_options',
196+
'extra_field_saved_search',
197+
'extra_field_values',
198+
'ext_log_entries',
199+
'fos_group',
200+
201+
// Gradebook
202+
'gradebook_category',
203+
'gradebook_certificate',
204+
'gradebook_evaluation',
205+
'gradebook_link',
206+
'gradebook_linkeval_log',
207+
'gradebook_score_display',
208+
'grade_components',
209+
'grade_model',
210+
211+
// Misc
212+
'language',
213+
'legal',
214+
'message_attachment',
215+
'portfolio',
216+
'portfolio_category',
217+
'promotion',
218+
'room',
219+
'scheduled_announcements',
220+
'search_engine_ref',
221+
222+
// Sequence
223+
'sequence',
224+
'sequence_condition',
225+
'sequence_method',
226+
'sequence_row_entity',
227+
'sequence_rule',
228+
'sequence_type_entity',
229+
'sequence_variable',
230+
231+
// Sessions / settings
232+
'session',
233+
'session_category',
234+
'settings',
235+
'settings_options',
236+
237+
// Skills
238+
'skill',
239+
'skill_level',
240+
'skill_level_profile',
241+
'skill_profile',
242+
'skill_rel_gradebook',
243+
'skill_rel_item',
244+
'skill_rel_user',
245+
'skill_rel_user_comment',
246+
247+
// Specific fields
248+
'specific_field',
249+
'specific_field_values',
250+
251+
// Templates / announcements
252+
'system_template',
253+
'sys_announcement',
254+
'tag',
255+
'templates',
256+
257+
// Tickets
258+
'ticket_category',
259+
'ticket_message',
260+
'ticket_message_attachments',
261+
'ticket_priority',
262+
'ticket_project',
263+
'ticket_status',
264+
'ticket_ticket',
265+
266+
// Tool rights
267+
'tool',
268+
'tool_resource_right',
269+
270+
// Tracking
271+
'track_e_access',
272+
'track_e_attempt',
273+
'track_e_course_access',
274+
'track_e_default',
275+
'track_e_downloads',
276+
'track_e_exercises',
277+
'track_e_hotpotatoes',
278+
'track_e_hotspot',
279+
'track_e_lastaccess',
280+
'track_e_login',
281+
'track_e_online',
282+
283+
// Users
284+
'user',
285+
'usergroup',
286+
'user_api_key',
287+
'user_course_category',
288+
'user_friend_relation_type',
289+
];
290+
}
291+
292+
private function convertTable(string $table, string $charset, string $collation): void
293+
{
294+
if (!$this->tableExists($table)) {
295+
// Table might not exist depending on the edition / previous migrations.
296+
return;
297+
}
298+
299+
// CONVERT changes all textual columns (CHAR/VARCHAR/TEXT/ENUM/SET) and the table default collation.
300+
$this->addSql(sprintf(
301+
'ALTER TABLE `%s` CONVERT TO CHARACTER SET %s COLLATE %s',
302+
$table,
303+
$charset,
304+
$collation
305+
));
306+
}
307+
308+
private function tableExists(string $table): bool
309+
{
310+
try {
311+
return $this->connection->createSchemaManager()->tablesExist([$table]);
312+
} catch (\Throwable $e) {
313+
// Keep the migration resilient if schema introspection fails.
314+
return false;
315+
}
316+
}
317+
}

0 commit comments

Comments
 (0)