From a8355df12ece4fe615e59f82e85439023208420b Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Thu, 22 Oct 2026 03:36:46 +0200 Subject: [PATCH] Full restore: tables the restore creates get this installation's charset and collation (utf8mb4 on MySQL/MariaDB); test with an emoji --- lib/Db/DbDumper.php | 29 ++++++++++++++++++--- tests/integration/full-restore-roundtrip.sh | 3 +++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/Db/DbDumper.php b/lib/Db/DbDumper.php index b0918f7..6bd3a40 100644 --- a/lib/Db/DbDumper.php +++ b/lib/Db/DbDumper.php @@ -159,7 +159,7 @@ final class DbDumper { // everywhere); tables it does not know are left out, as asked. foreach ($missing as $table) { if (isset($manifest['tables'][$table]['schema'])) { - $this->db->createSchemaManager()->createTable(self::tableFromStructure($table, $manifest['tables'][$table]['schema'])); + $this->db->createSchemaManager()->createTable(self::tableFromStructure($table, $manifest['tables'][$table]['schema'], $this->tableOptions())); $report['created'][] = $table; } else { $report['missing'][] = $table; @@ -319,8 +319,31 @@ final class DbDumper { 'foreignKeys' => $foreignKeys]; } - private static function tableFromStructure(string $name, array $structure): \Doctrine\DBAL\Schema\Table { - $table = new \Doctrine\DBAL\Schema\Table($name); + /** + * Table options for a table the restore creates: those of this installation's own tables. + * On MySQL/MariaDB that is the character set and collation, as Nextcloud's tables have them + * (utf8mb4, so emoji and other 4-byte characters fit); DBAL alone would use 3-byte utf8. + * + * @return array + * @psalm-suppress UndefinedClass the DBAL classes come with Nextcloud, not with this app + */ + private function tableOptions(): array { + $options = $this->db->createSchemaManager()->createSchemaConfig()->getDefaultTableOptions(); + if ($this->db->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform) { + $collation = $this->db->fetchOne('SELECT TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?', + [$this->prefix . 'appconfig']); + if (is_string($collation) && $collation !== '') { + $options['collation'] = $collation; + $options['charset'] = explode('_', $collation, 2)[0]; + unset($options['collate']); + } + } + return $options; + } + + /** @param array $options table options (see tableOptions()) */ + private static function tableFromStructure(string $name, array $structure, array $options = []): \Doctrine\DBAL\Schema\Table { + $table = new \Doctrine\DBAL\Schema\Table($name, [], [], [], [], $options); foreach ($structure['columns'] as $c) { $table->addColumn($c['name'], $c['type'], array_filter([ 'length' => $c['length'], 'precision' => $c['precision'], 'scale' => $c['scale'], diff --git a/tests/integration/full-restore-roundtrip.sh b/tests/integration/full-restore-roundtrip.sh index ab79807..01b46ce 100755 --- a/tests/integration/full-restore-roundtrip.sh +++ b/tests/integration/full-restore-roundtrip.sh @@ -119,6 +119,8 @@ if [ "$DB" != sqlite ]; then || fail "the mail account does not point at its mailboxes" # Deck installed and then switched off: its tables stay in the database and in the backup run_on ngb-frt-origin app:install deck >/dev/null || fail "install deck on the origin" + # a 4-byte character (emoji) in a table the restore has to create: utf8mb4 on MySQL/MariaDB + sql origin "insert into oc_deck_boards (title, owner, color, archived, deleted_at, last_modified) values ('Plan 📌', 'frtest', '0087C5', 0, 0, 0)" >/dev/null run_on ngb-frt-origin app:disable deck >/dev/null ok "origin has a synced Mail account and the tables of a disabled app" fi @@ -202,6 +204,7 @@ if [ "$ORIGIN_DBTYPE" != sqlite ] && [ "$DB" != sqlite ] && [ "$LEGACY" = legacy ok "older backup: tables of the disabled app left out, Mail restored" elif [ "$ORIGIN_DBTYPE" != sqlite ] && [ "$DB" != sqlite ]; then [ "$(sql restore "select count(*) from information_schema.tables where table_name = 'oc_deck_boards'")" = 1 ] || fail "the tables of the disabled app were not created" + [ "$(sql restore "select title from oc_deck_boards")" = "Plan 📌" ] || fail "a 4-byte character did not survive in a created table" [ "$(sql restore "select count(*) from oc_mail_accounts where drafts_mailbox_id is not null")" = 1 ] || fail "the mail account was not restored" [ "$(sql restore "select count(*) from information_schema.table_constraints where constraint_type = 'FOREIGN KEY' and table_name like 'oc_mail_%'")" -gt 0 ] \ || fail "the foreign keys of Mail are missing after the restore"