diff --git a/config/global.ini.php b/config/global.ini.php index e7b7f7405c9..f41e167a7e6 100644 --- a/config/global.ini.php +++ b/config/global.ini.php @@ -45,6 +45,14 @@ ; Matomo should work correctly without this setting but we recommend to have a charset set. charset = utf8 +; In some database setups the collation used for queries and creating tables can have unexpected +; values, or change after a database version upgrade. +; If you encounter "Illegal mix of collation" errors, setting this config to the value matching +; your existing database tables can help. +; This setting will only be used if "charset" is also set. +; Matomo should work correctly without this setting but we recommend to have a collation set. +collation = + ; Database error codes to ignore during updates ; ;ignore_error_codes[] = 1105 @@ -84,6 +92,7 @@ type = InnoDB schema = Mysql charset = utf8mb4 +collation = utf8mb4_general_ci enable_ssl = 0 ssl_ca = ssl_cert = diff --git a/core/Db.php b/core/Db.php index dc7b0a45beb..1732afc0853 100644 --- a/core/Db.php +++ b/core/Db.php @@ -183,6 +183,7 @@ public static function createReaderDatabaseObject($dbConfig = null) $dbConfig['type'] = $masterDbConfig['type']; $dbConfig['tables_prefix'] = $masterDbConfig['tables_prefix']; $dbConfig['charset'] = $masterDbConfig['charset']; + $dbConfig['collation'] = $masterDbConfig['collation'] ?? null; $db = @Adapter::factory($dbConfig['adapter'], $dbConfig); diff --git a/core/Db/Schema.php b/core/Db/Schema.php index 08a044633f2..4818142523d 100644 --- a/core/Db/Schema.php +++ b/core/Db/Schema.php @@ -89,6 +89,27 @@ private function getSchema(): SchemaInterface return $this->schema; } + /** + * Returns the default collation for a charset. + * + * @param string $charset + * @return string + */ + public function getDefaultCollationForCharset(string $charset): string + { + return $this->getSchema()->getDefaultCollationForCharset($charset); + } + + /** + * Get the table options to use for a CREATE TABLE statement. + * + * @return string + */ + public function getTableCreateOptions(): string + { + return $this->getSchema()->getTableCreateOptions(); + } + /** * Get the SQL to create a specific Piwik table * diff --git a/core/Db/Schema/Mysql.php b/core/Db/Schema/Mysql.php index 8d6cf008c11..65bec751364 100644 --- a/core/Db/Schema/Mysql.php +++ b/core/Db/Schema/Mysql.php @@ -39,10 +39,8 @@ class Mysql implements SchemaInterface */ public function getTablesCreateSql() { - $engine = $this->getTableEngine(); $prefixTables = $this->getTablePrefix(); - $dbSettings = new Db\Settings(); - $charset = $dbSettings->getUsedCharset(); + $tableOptions = $this->getTableCreateOptions(); $tables = array( 'user' => "CREATE TABLE {$prefixTables}user ( @@ -62,7 +60,7 @@ public function getTablesCreateSql() ts_changes_shown TIMESTAMP NULL, PRIMARY KEY(login), UNIQUE INDEX `uniq_email` (`email`) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'user_token_auth' => "CREATE TABLE {$prefixTables}user_token_auth ( idusertokenauth BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, @@ -77,7 +75,7 @@ public function getTablesCreateSql() secure_only TINYINT(2) unsigned NOT NULL DEFAULT '0', PRIMARY KEY(idusertokenauth), UNIQUE KEY uniq_password(password) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'twofactor_recovery_code' => "CREATE TABLE {$prefixTables}twofactor_recovery_code ( @@ -85,7 +83,7 @@ public function getTablesCreateSql() login VARCHAR(100) NOT NULL, recovery_code VARCHAR(40) NOT NULL, PRIMARY KEY(idrecoverycode) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'access' => "CREATE TABLE {$prefixTables}access ( @@ -95,7 +93,7 @@ public function getTablesCreateSql() access VARCHAR(50) NULL, PRIMARY KEY(idaccess), INDEX index_loginidsite (login, idsite) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'site' => "CREATE TABLE {$prefixTables}site ( @@ -119,7 +117,7 @@ public function getTablesCreateSql() keep_url_fragment TINYINT NOT NULL DEFAULT 0, creator_login VARCHAR(100) NULL, PRIMARY KEY(idsite) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'plugin_setting' => "CREATE TABLE {$prefixTables}plugin_setting ( @@ -131,7 +129,7 @@ public function getTablesCreateSql() `idplugin_setting` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY (idplugin_setting), INDEX(plugin_name, user_login) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'site_setting' => "CREATE TABLE {$prefixTables}site_setting ( @@ -143,14 +141,14 @@ public function getTablesCreateSql() `idsite_setting` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY (idsite_setting), INDEX(idsite, plugin_name) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'site_url' => "CREATE TABLE {$prefixTables}site_url ( idsite INTEGER(10) UNSIGNED NOT NULL, url VARCHAR(190) NOT NULL, PRIMARY KEY(idsite, url) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'goal' => "CREATE TABLE `{$prefixTables}goal` ( @@ -167,7 +165,7 @@ public function getTablesCreateSql() `deleted` tinyint(4) NOT NULL default '0', `event_value_as_revenue` tinyint(4) NOT NULL default '0', PRIMARY KEY (`idsite`,`idgoal`) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'logger_message' => "CREATE TABLE {$prefixTables}logger_message ( @@ -177,7 +175,7 @@ public function getTablesCreateSql() level VARCHAR(16) NULL, message TEXT NULL, PRIMARY KEY(idlogger_message) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'log_action' => "CREATE TABLE {$prefixTables}log_action ( @@ -188,7 +186,7 @@ public function getTablesCreateSql() url_prefix TINYINT(2) NULL, PRIMARY KEY(idaction), INDEX index_type_hash (type, hash) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'log_visit' => "CREATE TABLE {$prefixTables}log_visit ( @@ -202,7 +200,7 @@ public function getTablesCreateSql() INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time), INDEX index_idsite_datetime (idsite, visit_last_action_time), INDEX index_idsite_idvisitor_time (idsite, idvisitor, visit_last_action_time DESC) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'log_conversion_item' => "CREATE TABLE `{$prefixTables}log_conversion_item` ( @@ -223,7 +221,7 @@ public function getTablesCreateSql() deleted TINYINT(1) UNSIGNED NOT NULL, PRIMARY KEY(idvisit, idorder, idaction_sku), INDEX index_idsite_servertime ( idsite, server_time ) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'log_conversion' => "CREATE TABLE `{$prefixTables}log_conversion` ( @@ -247,7 +245,7 @@ public function getTablesCreateSql() PRIMARY KEY (idvisit, idgoal, buster), UNIQUE KEY unique_idsite_idorder (idsite, idorder), INDEX index_idsite_datetime ( idsite, server_time ) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'log_link_visit_action' => "CREATE TABLE {$prefixTables}log_link_visit_action ( @@ -261,7 +259,7 @@ public function getTablesCreateSql() pageview_position MEDIUMINT UNSIGNED DEFAULT NULL, PRIMARY KEY(idlink_va), INDEX index_idvisit(idvisit) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'log_profiling' => "CREATE TABLE {$prefixTables}log_profiling ( @@ -271,7 +269,7 @@ public function getTablesCreateSql() idprofiling BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY (idprofiling), UNIQUE KEY query(query(100)) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'option' => "CREATE TABLE `{$prefixTables}option` ( @@ -280,7 +278,7 @@ public function getTablesCreateSql() autoload TINYINT NOT NULL DEFAULT '1', PRIMARY KEY ( option_name ), INDEX autoload( autoload ) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'session' => "CREATE TABLE {$prefixTables}session ( @@ -289,7 +287,7 @@ public function getTablesCreateSql() lifetime INTEGER, data MEDIUMTEXT, PRIMARY KEY ( id ) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'archive_numeric' => "CREATE TABLE {$prefixTables}archive_numeric ( @@ -304,7 +302,7 @@ public function getTablesCreateSql() PRIMARY KEY(idarchive, name), INDEX index_idsite_dates_period(idsite, date1, date2, period, name(6)), INDEX index_period_archived(period, ts_archived) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'archive_blob' => "CREATE TABLE {$prefixTables}archive_blob ( @@ -318,7 +316,7 @@ public function getTablesCreateSql() value MEDIUMBLOB NULL, PRIMARY KEY(idarchive, name), INDEX index_period_archived(period, ts_archived) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'archive_invalidations' => "CREATE TABLE `{$prefixTables}archive_invalidations` ( @@ -335,14 +333,14 @@ public function getTablesCreateSql() `report` VARCHAR(255) NULL, PRIMARY KEY(idinvalidation), INDEX index_idsite_dates_period_name(idsite, date1, period) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'sequence' => "CREATE TABLE {$prefixTables}sequence ( `name` VARCHAR(120) NOT NULL, `value` BIGINT(20) UNSIGNED NOT NULL , PRIMARY KEY(`name`) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'brute_force_log' => "CREATE TABLE {$prefixTables}brute_force_log ( @@ -352,7 +350,7 @@ public function getTablesCreateSql() `login` VARCHAR(100) NULL, INDEX index_ip_address(ip_address), PRIMARY KEY(`id_brute_force_log`) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'tracking_failure' => "CREATE TABLE {$prefixTables}tracking_failure ( @@ -361,14 +359,14 @@ public function getTablesCreateSql() `date_first_occurred` DATETIME NOT NULL , `request_url` MEDIUMTEXT NOT NULL , PRIMARY KEY(`idsite`, `idfailure`) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'locks' => "CREATE TABLE `{$prefixTables}locks` ( `key` VARCHAR(" . Lock::MAX_KEY_LEN . ") NOT NULL, `value` VARCHAR(255) NULL DEFAULT NULL, `expiry_time` BIGINT UNSIGNED DEFAULT 9999999999, PRIMARY KEY (`key`) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", 'changes' => "CREATE TABLE `{$prefixTables}changes` ( `idchange` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, @@ -381,7 +379,7 @@ public function getTablesCreateSql() `link` VARCHAR(255) NULL, PRIMARY KEY(`idchange`), UNIQUE KEY unique_plugin_version_title (`plugin_name`, `version`, `title`(100)) - ) ENGINE=$engine DEFAULT CHARSET=$charset + ) $tableOptions ", ); @@ -522,10 +520,10 @@ public function createDatabase($dbName = null) $dbName = $this->getDbName(); } + $createOptions = $this->getDatabaseCreateOptions(); $dbName = str_replace('`', '', $dbName); - $charset = DbHelper::getDefaultCharset(); - Db::exec("CREATE DATABASE IF NOT EXISTS `" . $dbName . "` DEFAULT CHARACTER SET " . $charset); + Db::exec("CREATE DATABASE IF NOT EXISTS `$dbName` $createOptions"); } /** @@ -538,16 +536,11 @@ public function createDatabase($dbName = null) */ public function createTable($nameWithoutPrefix, $createDefinition) { - $dbSettings = new Db\Settings(); - $charset = $dbSettings->getUsedCharset(); - $statement = sprintf( - "CREATE TABLE IF NOT EXISTS `%s` ( %s ) ENGINE=%s DEFAULT CHARSET=%s %s;", + "CREATE TABLE IF NOT EXISTS `%s` ( %s ) %s;", Common::prefixTable($nameWithoutPrefix), $createDefinition, - $this->getTableEngine(), - $charset, - $dbSettings->getRowFormat() + $this->getTableCreateOptions() ); try { @@ -675,21 +668,89 @@ public function supportsComplexColumnUpdates(): bool return true; } + /** + * Returns the default collation for a charset. + * + * @param string $charset + * + * @return string + * @throws Exception + */ + public function getDefaultCollationForCharset(string $charset): string + { + $result = $this->getDb()->fetchRow( + 'SHOW COLLATION WHERE `Default` = "Yes" AND `Charset` = ?', + [$charset] + ); + + if (!isset($result['Collation'])) { + throw new Exception(sprintf( + 'Failed to detect default collation for character set "%s"', + $charset + )); + } + + return $result['Collation']; + } + public function getDefaultPort(): int { return 3306; } - private function getTablePrefix() + public function getTableCreateOptions(): string { - return $this->getDbSettings()->getTablePrefix(); + $engine = $this->getTableEngine(); + $charset = $this->getUsedCharset(); + $collation = $this->getUsedCollation(); + $rowFormat = $this->getTableRowFormat(); + + $options = "ENGINE=$engine DEFAULT CHARSET=$charset"; + + if ('' !== $collation) { + $options .= " COLLATE=$collation"; + } + + if ('' !== $rowFormat) { + $options .= " $rowFormat"; + } + + return $options; + } + + protected function getDatabaseCreateOptions(): string + { + $charset = DbHelper::getDefaultCharset(); + $collation = $this->getDefaultCollationForCharset($charset); + + return "DEFAULT CHARACTER SET $charset COLLATE $collation"; } - private function getTableEngine() + protected function getTableEngine() { return $this->getDbSettings()->getEngine(); } + protected function getTableRowFormat(): string + { + return $this->getDbSettings()->getRowFormat(); + } + + protected function getUsedCharset(): string + { + return $this->getDbSettings()->getUsedCharset(); + } + + protected function getUsedCollation(): string + { + return $this->getDbSettings()->getUsedCollation(); + } + + private function getTablePrefix() + { + return $this->getDbSettings()->getTablePrefix(); + } + private function getDb() { return Db::get(); diff --git a/core/Db/Schema/Tidb.php b/core/Db/Schema/Tidb.php index 6a1f1e29a27..6d1ba32270e 100644 --- a/core/Db/Schema/Tidb.php +++ b/core/Db/Schema/Tidb.php @@ -25,8 +25,44 @@ public function supportsComplexColumnUpdates(): bool return false; } + public function getDefaultCollationForCharset(string $charset): string + { + $collation = parent::getDefaultCollationForCharset($charset); + + if ('utf8mb4' === $charset && 'utf8mb4_bin' === $collation) { + // replace the TiDB default "utf8mb4_bin" with a better default + return 'utf8mb4_0900_ai_ci'; + } + + return $collation; + } + public function getDefaultPort(): int { return 4000; } + + public function getTableCreateOptions(): string + { + $engine = $this->getTableEngine(); + $charset = $this->getUsedCharset(); + $collation = $this->getUsedCollation(); + $rowFormat = $this->getTableRowFormat(); + + if ('utf8mb4' === $charset && '' === $collation) { + $collation = 'utf8mb4_0900_ai_ci'; + } + + $options = "ENGINE=$engine DEFAULT CHARSET=$charset"; + + if ('' !== $collation) { + $options .= " COLLATE=$collation"; + } + + if ('' !== $rowFormat) { + $options .= " $rowFormat"; + } + + return $options; + } } diff --git a/core/Db/SchemaInterface.php b/core/Db/SchemaInterface.php index 6405c1260d2..cdbf194c0f4 100644 --- a/core/Db/SchemaInterface.php +++ b/core/Db/SchemaInterface.php @@ -125,10 +125,26 @@ public function addMaxExecutionTimeHintToQuery(string $sql, float $limit): strin */ public function supportsComplexColumnUpdates(): bool; + /** + * Returns the default collation for a charset used by this database engine. + * + * @param string $charset + * + * @return string + */ + public function getDefaultCollationForCharset(string $charset): string; + /** * Return the default port used by this database engine * * @return int */ public function getDefaultPort(): int; + + /** + * Return the table options to use for a CREATE TABLE statement. + * + * @return string + */ + public function getTableCreateOptions(): string; } diff --git a/core/Db/Settings.php b/core/Db/Settings.php index c9707fcce8c..e18a9dab8b6 100644 --- a/core/Db/Settings.php +++ b/core/Db/Settings.php @@ -33,6 +33,11 @@ public function getUsedCharset() return strtolower($this->getDbSetting('charset')); } + public function getUsedCollation() + { + return strtolower($this->getDbSetting('collation') ?? ''); + } + public function getRowFormat() { return $this->getUsedCharset() === 'utf8mb4' ? 'ROW_FORMAT=DYNAMIC' : ''; diff --git a/core/DbHelper.php b/core/DbHelper.php index 25aecfa99c6..fad8a0af1a5 100644 --- a/core/DbHelper.php +++ b/core/DbHelper.php @@ -210,7 +210,7 @@ public static function tableHasIndex($table, $indexName) * @return string * @throws Tracker\Db\DbException */ - public static function getDefaultCharset() + public static function getDefaultCharset(): string { $result = Db::get()->fetchRow("SHOW CHARACTER SET LIKE 'utf8mb4'"); @@ -233,6 +233,19 @@ public static function getDefaultCharset() return 'utf8mb4'; } + /** + * Returns the default collation for a charset. + * + * @param string $charset + * + * @return string + * @throws Exception + */ + public static function getDefaultCollationForCharset(string $charset): string + { + return Schema::getInstance()->getDefaultCollationForCharset($charset); + } + /** * Returns sql queries to convert all installed tables to utf8mb4 * diff --git a/core/Tracker/Db/Mysqli.php b/core/Tracker/Db/Mysqli.php index b5c769d9864..05ac1843f36 100644 --- a/core/Tracker/Db/Mysqli.php +++ b/core/Tracker/Db/Mysqli.php @@ -26,6 +26,7 @@ class Mysqli extends Db protected $username; protected $password; protected $charset; + protected $collation; protected $activeTransaction = false; protected $enable_ssl; @@ -57,11 +58,12 @@ public function __construct($dbInfo, $driverName = 'mysql') $this->port = (int)$dbInfo['port']; $this->socket = null; } + $this->dbname = $dbInfo['dbname']; $this->username = $dbInfo['username']; $this->password = $dbInfo['password']; - $this->charset = isset($dbInfo['charset']) ? $dbInfo['charset'] : null; - + $this->charset = $dbInfo['charset'] ?? null; + $this->collation = $dbInfo['collation'] ?? null; if (!empty($dbInfo['enable_ssl'])) { $this->enable_ssl = $dbInfo['enable_ssl']; @@ -133,8 +135,17 @@ public function connect() throw new DbException("Connect failed: " . mysqli_connect_error()); } - if ($this->charset && !mysqli_set_charset($this->connection, $this->charset)) { - throw new DbException("Set Charset failed: " . mysqli_error($this->connection)); + if ($this->charset && $this->collation) { + // mysqli_set_charset does not support setting a collation + $query = "SET NAMES '" . $this->charset . "' COLLATE '" . $this->collation . "'"; + + if (!mysqli_query($this->connection, $query)) { + throw new DbException("Set charset/connection collation failed: " . mysqli_error($this->connection)); + } + } elseif ($this->charset) { + if (!mysqli_set_charset($this->connection, $this->charset)) { + throw new DbException("Set Charset failed: " . mysqli_error($this->connection)); + } } $this->password = ''; diff --git a/core/Tracker/Db/Pdo/Mysql.php b/core/Tracker/Db/Pdo/Mysql.php index 3d9a75f2a70..d4e0714c791 100644 --- a/core/Tracker/Db/Pdo/Mysql.php +++ b/core/Tracker/Db/Pdo/Mysql.php @@ -26,13 +26,33 @@ class Mysql extends Db * @var PDO */ protected $connection = null; + + /** + * @var string + */ protected $dsn; + + /** + * @var string + */ private $username; + + /** + * @var string + */ private $password; + + /** + * @var string|null + */ protected $charset; - protected $mysqlOptions = array(); + /** + * @var string|null + */ + private $collation; + protected $mysqlOptions = []; protected $activeTransaction = false; @@ -58,8 +78,11 @@ public function __construct($dbInfo, $driverName = 'mysql') if (isset($dbInfo['charset'])) { $this->charset = $dbInfo['charset']; $this->dsn .= ';charset=' . $this->charset; - } + if (!empty($dbInfo['collation'])) { + $this->collation = $dbInfo['collation']; + } + } if (isset($dbInfo['enable_ssl']) && $dbInfo['enable_ssl']) { if (!empty($dbInfo['ssl_key'])) { @@ -409,6 +432,11 @@ private function establishConnection(): void */ if (!empty($this->charset)) { $sql = "SET NAMES '" . $this->charset . "'"; + + if (!empty($this->collation)) { + $sql .= " COLLATE '" . $this->collation . "'"; + } + $this->connection->exec($sql); } } diff --git a/core/Updater/Migration/Db/CreateTable.php b/core/Updater/Migration/Db/CreateTable.php index 10f211352fe..3d39c4c3919 100644 --- a/core/Updater/Migration/Db/CreateTable.php +++ b/core/Updater/Migration/Db/CreateTable.php @@ -9,7 +9,7 @@ namespace Piwik\Updater\Migration\Db; -use Piwik\Db; +use Piwik\Db\Schema; /** * @see Factory::createTable() @@ -19,12 +19,11 @@ class CreateTable extends Sql { /** * Constructor. - * @param Db\Settings $dbSettings * @param string $table Prefixed table name * @param string|string[] $columnNames array(columnName => columnValue) * @param string|string[] $primaryKey one or multiple columns that define the primary key */ - public function __construct(Db\Settings $dbSettings, $table, $columnNames, $primaryKey) + public function __construct($table, $columnNames, $primaryKey) { $columns = array(); foreach ($columnNames as $column => $type) { @@ -35,15 +34,12 @@ public function __construct(Db\Settings $dbSettings, $table, $columnNames, $prim $columns[] = sprintf('PRIMARY KEY ( `%s` )', implode('`, `', $primaryKey)); } - - $sql = rtrim(sprintf( - 'CREATE TABLE `%s` (%s) ENGINE=%s DEFAULT CHARSET=%s %s', + $sql = sprintf( + 'CREATE TABLE `%s` (%s) %s', $table, implode(', ', $columns), - $dbSettings->getEngine(), - $dbSettings->getUsedCharset(), - $dbSettings->getRowFormat() - )); + Schema::getInstance()->getTableCreateOptions() + ); parent::__construct($sql, static::ERROR_CODE_TABLE_EXISTS); } diff --git a/core/Updates/5.1.2-rc1.php b/core/Updates/5.1.2-rc1.php new file mode 100644 index 00000000000..406cd87e07e --- /dev/null +++ b/core/Updates/5.1.2-rc1.php @@ -0,0 +1,108 @@ +migration = $factory; + } + + public function getMigrations(Updater $updater) + { + $migrations = []; + + $config = Config::getInstance(); + $dbConfig = $config->database; + + // only run migration if config is not set + if (empty($dbConfig['collation'])) { + $collation = $this->detectCollationForMigration(); + + if (null !== $collation) { + $migrations[] = $this->migration->config->set( + 'database', + 'collation', + $collation + ); + } + } + + return $migrations; + } + + public function doUpdate(Updater $updater) + { + $updater->executeMigrations(__FILE__, $this->getMigrations($updater)); + } + + private function detectCollationForMigration(): ?string + { + try { + $db = Db::get(); + $userTable = Common::prefixTable('user'); + $userTableStatus = $db->fetchRow('SHOW TABLE STATUS WHERE Name = ?', [$userTable]); + + if (empty($userTableStatus['Collation'])) { + // if there is no user table, or no collation for it, abort detection + // this table should always exist and something must be wrong in this case + return null; + } + + $userTableCollation = $userTableStatus['Collation']; + $connectionCollation = $db->fetchOne('SELECT @@collation_connection'); + + if ($userTableCollation === $connectionCollation) { + // if the connection is matching the user table + // we should be safe to assume we have already found a config value + return $userTableCollation; + } + + $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled(ArchiveTableCreator::NUMERIC_TABLE); + + if (0 === count($archiveTables)) { + // skip if there is no archive table (yet) + return null; + } + + // sort tables so we have them in order of their date + rsort($archiveTables); + + $archiveTableStatus = $db->fetchRow('SHOW TABLE STATUS WHERE Name = ?', [$archiveTables[0]]); + + if ( + !empty($archiveTableStatus['Collation']) + && $archiveTableStatus['Collation'] === $userTableCollation + ) { + // the most recent numeric archive table is matching the collation + // of the users table, should be a good config value to choose + return $userTableCollation; + } + } catch (\Exception $e) { + // rely on the system check if detection failed + } + + return null; + } +} diff --git a/core/Version.php b/core/Version.php index d3f2b6707af..19a8f4346d7 100644 --- a/core/Version.php +++ b/core/Version.php @@ -22,7 +22,7 @@ final class Version * The current Matomo version. * @var string */ - public const VERSION = '5.1.1-rc1'; + public const VERSION = '5.1.2-rc1'; public const MAJOR_VERSION = 5; diff --git a/libs/Zend/Db/Adapter/Mysqli.php b/libs/Zend/Db/Adapter/Mysqli.php index 2dcdd67813f..50631bd4de0 100644 --- a/libs/Zend/Db/Adapter/Mysqli.php +++ b/libs/Zend/Db/Adapter/Mysqli.php @@ -375,7 +375,12 @@ protected function _connect() throw new Zend_Db_Adapter_Mysqli_Exception(mysqli_connect_error()); } - if (!empty($this->_config['charset'])) { + if (!empty($this->_config['charset']) && !empty($this->_config['collation'])) { + // mysqli_set_charset does not support setting a collation + $query = "SET NAMES '" . $this->_config['charset'] . "' COLLATE '" . $this->_config['collation'] . "'"; + + mysqli_query($this->_connection, $query); + } elseif (!empty($this->_config['charset'])) { mysqli_set_charset($this->_connection, $this->_config['charset']); } } diff --git a/libs/Zend/Db/Adapter/Pdo/Mysql.php b/libs/Zend/Db/Adapter/Pdo/Mysql.php index 363196334fc..ff064e894e2 100644 --- a/libs/Zend/Db/Adapter/Pdo/Mysql.php +++ b/libs/Zend/Db/Adapter/Pdo/Mysql.php @@ -103,6 +103,11 @@ protected function _connect() if (!empty($this->_config['charset'])) { $initCommand = "SET NAMES '" . $this->_config['charset'] . "'"; + + if (!empty($this->_config['collation'])) { + $initCommand .= " COLLATE '" . $this->_config['collation'] . "'"; + } + $this->_config['driver_options'][1002] = $initCommand; // 1002 = PDO::MYSQL_ATTR_INIT_COMMAND } diff --git a/plugins/Diagnostics/Diagnostic/DatabaseAbilitiesCheck.php b/plugins/Diagnostics/Diagnostic/DatabaseAbilitiesCheck.php index a8ac6e7f640..a553c4dbf1b 100644 --- a/plugins/Diagnostics/Diagnostic/DatabaseAbilitiesCheck.php +++ b/plugins/Diagnostics/Diagnostic/DatabaseAbilitiesCheck.php @@ -42,6 +42,7 @@ public function execute() $result = new DiagnosticResult($this->translator->translate('Installation_DatabaseAbilities')); $result->addItem($this->checkUtf8mb4Charset()); + $result->addItem($this->checkCollation()); if (Config::getInstance()->General['enable_load_data_infile']) { $result->addItem($this->checkLoadDataInfile()); @@ -92,6 +93,29 @@ protected function checkUtf8mb4Charset() ); } + protected function checkCollation(): DiagnosticResultItem + { + $dbSettings = new Db\Settings(); + $collation = $dbSettings->getUsedCollation(); + + if ('' !== $collation) { + return new DiagnosticResultItem(DiagnosticResult::STATUS_OK, 'Connection collation'); + } + + $collationConnection = Db::get()->fetchOne('SELECT @@collation_connection'); + $collationCharset = DbHelper::getDefaultCollationForCharset($dbSettings->getUsedCharset()); + + return new DiagnosticResultItem( + DiagnosticResult::STATUS_WARNING, + sprintf( + 'Connection collation

%s

%s
%s
', + $this->translator->translate('Diagnostics_DatabaseCollationNotConfigured'), + $this->translator->translate('Diagnostics_DatabaseCollationConnection', [$collationConnection]), + $this->translator->translate('Diagnostics_DatabaseCollationCharset', [$collationCharset]) + ) + ); + } + protected function checkLoadDataInfile() { $optionTable = Common::prefixTable('option'); diff --git a/plugins/Diagnostics/Diagnostic/DatabaseInformational.php b/plugins/Diagnostics/Diagnostic/DatabaseInformational.php index 1d2dfc81032..1dd4eb6c522 100644 --- a/plugins/Diagnostics/Diagnostic/DatabaseInformational.php +++ b/plugins/Diagnostics/Diagnostic/DatabaseInformational.php @@ -16,7 +16,7 @@ use Piwik\Translation\Translator; /** - * Informatation about the database. + * Information about the database. */ class DatabaseInformational implements Diagnostic { @@ -38,6 +38,7 @@ public function execute() $dbConfig = Config::getInstance()->database; $results[] = DiagnosticResult::informationalResult('DB Prefix', $dbConfig['tables_prefix']); $results[] = DiagnosticResult::informationalResult('DB Charset', $dbConfig['charset']); + $results[] = DiagnosticResult::informationalResult('DB Collation', $dbConfig['collation']); $results[] = DiagnosticResult::informationalResult('DB Adapter', $dbConfig['adapter']); $results[] = DiagnosticResult::informationalResult('MySQL Version', $this->getServerVersion()); $results[] = DiagnosticResult::informationalResult('Num Tables', $this->getNumMatomoTables()); diff --git a/plugins/Diagnostics/lang/en.json b/plugins/Diagnostics/lang/en.json index ee0a651ef8c..70855e531a4 100644 --- a/plugins/Diagnostics/lang/en.json +++ b/plugins/Diagnostics/lang/en.json @@ -12,6 +12,9 @@ "Sections": "Sections", "BrowserAndAutoArchivingEnabledLabel": "Browser and Auto-archiving enabled", "BrowserAndAutoArchivingEnabledComment": "It looks like both browser and auto archiving are enabled. Auto archiving last started %3$s ago. If %1$sauto archiving%2$s is enabled, you should disable browser archiving in \"General Settings\".", + "DatabaseCollationNotConfigured": "You database connection is configured without an explicit collation. Please update [database] collation = '' in the \"config/config.ini.php\" file with the collation to be used, to ensure all database features work as expected.", + "DatabaseCollationConnection": "Your currently used connection collation is: %1$s", + "DatabaseCollationCharset": "The default collation for your configured charset is: %1$s", "DatabaseReaderConnection": "Database Reader Connection", "DatabaseUtf8Requirement": "This is required to be able to store 4-byte UTF8 characters. Unless utf8mb4 is available special characters, such as emojis, less common characters of asian languages, various historic scripts or mathematical symbols will be replaced with %1$s. You can read more details about this topic in %2$sthis FAQ%3$s.", "DatabaseUtf8mb4CharsetRecommended": "Your database doesn't support utf8mb4 charset yet.", diff --git a/plugins/Installation/Controller.php b/plugins/Installation/Controller.php index d7d40e5f277..151494ccb91 100644 --- a/plugins/Installation/Controller.php +++ b/plugins/Installation/Controller.php @@ -609,6 +609,7 @@ private function createConfigFile($dbInfos) $config->database = $dbInfos; $config->database['charset'] = DbHelper::getDefaultCharset(); + $config->database['collation'] = DbHelper::getDefaultCollationForCharset($config->database['charset']); $config->forceSave(); diff --git a/tests/PHPUnit/Integration/Db/Schema/MariadbTest.php b/tests/PHPUnit/Integration/Db/Schema/MariadbTest.php new file mode 100644 index 00000000000..5841299adc8 --- /dev/null +++ b/tests/PHPUnit/Integration/Db/Schema/MariadbTest.php @@ -0,0 +1,58 @@ + $value) { + DatabaseConfig::setConfigValue($name, $value); + } + + $schema = Db\Schema::getInstance(); + + self::assertSame($expected, $schema->getTableCreateOptions()); + } + + public function getTableCreateOptionsTestData(): iterable + { + yield 'default charset, empty collation' => [ + ['collation' => ''], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC' + ]; + + yield 'override charset, empty collation' => [ + ['charset' => 'utf8mb3', 'collation' => ''], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' + ]; + + yield 'default charset, override collation' => [ + ['collation' => 'utf8mb4_swedish_ci'], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_swedish_ci ROW_FORMAT=DYNAMIC' + ]; + + yield 'override charset and collation' => [ + ['charset' => 'utf8mb3', 'collation' => 'utf8mb3_general_ci'], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' + ]; + } +} diff --git a/tests/PHPUnit/Integration/Db/Schema/MysqlTest.php b/tests/PHPUnit/Integration/Db/Schema/MysqlTest.php new file mode 100644 index 00000000000..b26cd0e3ddb --- /dev/null +++ b/tests/PHPUnit/Integration/Db/Schema/MysqlTest.php @@ -0,0 +1,58 @@ + $value) { + DatabaseConfig::setConfigValue($name, $value); + } + + $schema = Db\Schema::getInstance(); + + self::assertSame($expected, $schema->getTableCreateOptions()); + } + + public function getTableCreateOptionsTestData(): iterable + { + yield 'default charset, empty collation' => [ + ['collation' => ''], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC' + ]; + + yield 'override charset, empty collation' => [ + ['charset' => 'utf8mb3', 'collation' => ''], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' + ]; + + yield 'default charset, override collation' => [ + ['collation' => 'utf8mb4_swedish_ci'], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_swedish_ci ROW_FORMAT=DYNAMIC' + ]; + + yield 'override charset and collation' => [ + ['charset' => 'utf8mb3', 'collation' => 'utf8mb3_general_ci'], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' + ]; + } +} diff --git a/tests/PHPUnit/Integration/Db/Schema/TidbTest.php b/tests/PHPUnit/Integration/Db/Schema/TidbTest.php new file mode 100644 index 00000000000..a8d055d4fd1 --- /dev/null +++ b/tests/PHPUnit/Integration/Db/Schema/TidbTest.php @@ -0,0 +1,72 @@ +getDefaultCollationForCharset('utf8mb4') + ); + } + + /** + * @dataProvider getTableCreateOptionsTestData + */ + public function testTableCreateOptions(array $optionOverrides, string $expected): void + { + if (DatabaseConfig::getConfigValue('schema') !== 'Tidb') { + self::markTestSkipped('Tidb is not available'); + } + + foreach ($optionOverrides as $name => $value) { + DatabaseConfig::setConfigValue($name, $value); + } + + $schema = Db\Schema::getInstance(); + + self::assertSame($expected, $schema->getTableCreateOptions()); + } + + public function getTableCreateOptionsTestData(): iterable + { + yield 'default charset, empty collation' => [ + ['collation' => ''], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC' + ]; + + yield 'override charset, empty collation' => [ + ['charset' => 'utf8mb3', 'collation' => ''], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' + ]; + + yield 'default charset, override collation' => [ + ['collation' => 'utf8mb4_swedish_ci'], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_swedish_ci ROW_FORMAT=DYNAMIC' + ]; + + yield 'override charset and collation' => [ + ['charset' => 'utf8mb3', 'collation' => 'utf8mb3_general_ci'], + 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' + ]; + } +} diff --git a/tests/PHPUnit/Integration/DbHelperTest.php b/tests/PHPUnit/Integration/DbHelperTest.php index d3f88c76a53..de20af974db 100644 --- a/tests/PHPUnit/Integration/DbHelperTest.php +++ b/tests/PHPUnit/Integration/DbHelperTest.php @@ -119,6 +119,25 @@ public function testAddOriginHintToQuery() self::assertEquals($expected, $result); } + public function testGetDefaultCollationForCharset(): void + { + $charset = 'utf8mb4'; + $collation = DbHelper::getDefaultCollationForCharset($charset); + $expectedPrefix = $charset . '_'; + + // exact collation depends on the database used + // but should always start with the charset + self::assertStringStartsWith($expectedPrefix, $collation); + } + + public function testGetDefaultCollationForCharsetThrowsForInvalidCharset(): void + { + self::expectException(\Exception::class); + self::expectExceptionMessage('Failed to detect default collation for character set "invalid"'); + + DbHelper::getDefaultCollationForCharset('invalid'); + } + private function assertDbExists($dbName) { $dbs = Db::fetchAll("SHOW DATABASES"); diff --git a/tests/PHPUnit/Integration/DbTest.php b/tests/PHPUnit/Integration/DbTest.php index 600482b1f84..c6c3b8a7616 100644 --- a/tests/PHPUnit/Integration/DbTest.php +++ b/tests/PHPUnit/Integration/DbTest.php @@ -266,6 +266,43 @@ public function testGetRowCount($adapter, $expectedClass) $this->assertEquals(1, $db->rowCount($result)); } + /** + * @dataProvider getDbAdapter + */ + public function testConnectionCollationDefault(string $adapter, string $expectedClass): void + { + Db::destroyDatabaseObject(); + + $config = Config::getInstance(); + $config->database['adapter'] = $adapter; + $config->database['collation'] = null; + + $db = Db::get(); + self::assertInstanceOf($expectedClass, $db); + + // exact value depends on database used + $currentCollation = $db->fetchOne('SELECT @@collation_connection'); + self::assertStringStartsWith('utf8', $currentCollation); + } + + /** + * @dataProvider getDbAdapter + */ + public function testConnectionCollationSetInConfig(string $adapter, string $expectedClass): void + { + Db::destroyDatabaseObject(); + + $config = Config::getInstance(); + $config->database['adapter'] = $adapter; + $config->database['collation'] = $config->database['charset'] . '_swedish_ci'; + + $db = Db::get(); + self::assertInstanceOf($expectedClass, $db); + + $currentCollation = $db->fetchOne('SELECT @@collation_connection'); + self::assertSame($config->database['collation'], $currentCollation); + } + public function getDbAdapter() { return array( diff --git a/tests/PHPUnit/Integration/Tracker/Db/MysqliTest.php b/tests/PHPUnit/Integration/Tracker/Db/MysqliTest.php new file mode 100644 index 00000000000..eed5f2baa68 --- /dev/null +++ b/tests/PHPUnit/Integration/Tracker/Db/MysqliTest.php @@ -0,0 +1,54 @@ +database['adapter'] = 'MYSQLI'; + } + + public function testConnectionThrowsOnInvalidCharset(): void + { + self::expectException(Tracker\Db\DbException::class); + self::expectExceptionMessageMatches('/Set Charset failed/'); + + $config = Config::getInstance(); + $config->database['collation'] = null; + $config->database['charset'] = 'something really invalid'; + + Tracker\Db::connectPiwikTrackerDb(); + } + + public function testConnectionThrowsOnInvalidConnectionCollation(): void + { + self::expectException(Tracker\Db\DbException::class); + self::expectExceptionMessageMatches('/Set charset\/connection collation failed/'); + + $config = Config::getInstance(); + $config->database['collation'] = 'something really invalid'; + + Tracker\Db::connectPiwikTrackerDb(); + } +} diff --git a/tests/PHPUnit/Integration/Tracker/DbTest.php b/tests/PHPUnit/Integration/Tracker/DbTest.php index df630f24d98..9db9837bb9a 100644 --- a/tests/PHPUnit/Integration/Tracker/DbTest.php +++ b/tests/PHPUnit/Integration/Tracker/DbTest.php @@ -192,6 +192,27 @@ public function testFetchAllNoMatch() $this->assertEquals(array(), $val); } + public function testConnectionCollationDefault(): void + { + $config = Config::getInstance(); + $config->database['collation'] = null; + $db = Tracker\Db::connectPiwikTrackerDb(); + + // exact value depends on database used + $currentCollation = $db->fetchOne('SELECT @@collation_connection'); + self::assertStringStartsWith('utf8', $currentCollation); + } + + public function testConnectionCollationSetInConfig(): void + { + $config = Config::getInstance(); + $config->database['collation'] = $config->database['charset'] . '_swedish_ci'; + $db = Tracker\Db::connectPiwikTrackerDb(); + + $currentCollation = $db->fetchOne('SELECT @@collation_connection'); + self::assertSame($config->database['collation'], $currentCollation); + } + private function insertRowId($value = '1') { $db = Tracker::getDatabase(); diff --git a/tests/PHPUnit/Integration/Updater/Migration/Db/FactoryTest.php b/tests/PHPUnit/Integration/Updater/Migration/Db/FactoryTest.php index 4a210e384b1..583fbad0d27 100644 --- a/tests/PHPUnit/Integration/Updater/Migration/Db/FactoryTest.php +++ b/tests/PHPUnit/Integration/Updater/Migration/Db/FactoryTest.php @@ -10,6 +10,7 @@ namespace Piwik\Tests\Integration\Updater\Migration\Db; use Piwik\Common; +use Piwik\Db\Schema; use Piwik\Tests\Framework\TestCase\IntegrationTestCase; use Piwik\Updater\Migration\Db\AddColumn; use Piwik\Updater\Migration\Db\AddColumns; @@ -94,7 +95,9 @@ public function testCreateTableForwardsParameters() $migration = $this->createTable(); $table = $this->testTablePrefixed; - $this->assertSame("CREATE TABLE `$table` (`column` INT(10) DEFAULT 0, `column2` VARCHAR(255)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;", '' . $migration); + $createOptions = Schema::getInstance()->getTableCreateOptions(); + self::assertStringContainsString('ROW_FORMAT=DYNAMIC', $createOptions); + $this->assertSame("CREATE TABLE `$table` (`column` INT(10) DEFAULT 0, `column2` VARCHAR(255)) $createOptions;", '' . $migration); } @@ -103,7 +106,9 @@ public function testCreateTableWithPrimaryKey() $migration = $this->createTable('column2'); $table = $this->testTablePrefixed; - $this->assertSame("CREATE TABLE `$table` (`column` INT(10) DEFAULT 0, `column2` VARCHAR(255), PRIMARY KEY ( `column2` )) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;", '' . $migration); + $createOptions = Schema::getInstance()->getTableCreateOptions(); + self::assertStringContainsString('ROW_FORMAT=DYNAMIC', $createOptions); + $this->assertSame("CREATE TABLE `$table` (`column` INT(10) DEFAULT 0, `column2` VARCHAR(255), PRIMARY KEY ( `column2` )) $createOptions;", '' . $migration); } public function testDropTableReturnsDropTableInstance() diff --git a/tests/resources/OmniFixture-dump.sql b/tests/resources/OmniFixture-dump.sql index e0bcb6846e9..6e28bae6bb0 100644 --- a/tests/resources/OmniFixture-dump.sql +++ b/tests/resources/OmniFixture-dump.sql @@ -29,7 +29,7 @@ CREATE TABLE `access` ( `access` varchar(50) DEFAULT NULL, PRIMARY KEY (`idaccess`), KEY `index_loginidsite` (`login`,`idsite`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -161,7 +161,7 @@ CREATE TABLE `archive_invalidations` ( `report` varchar(255) DEFAULT NULL, PRIMARY KEY (`idinvalidation`), KEY `index_idsite_dates_period_name` (`idsite`,`date1`,`period`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -187,7 +187,7 @@ CREATE TABLE `brute_force_log` ( `login` varchar(100) DEFAULT NULL, PRIMARY KEY (`id_brute_force_log`), KEY `index_ip_address` (`ip_address`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -217,7 +217,7 @@ CREATE TABLE `changes` ( `link` varchar(255) DEFAULT NULL, PRIMARY KEY (`idchange`), UNIQUE KEY `unique_plugin_version_title` (`plugin_name`,`version`,`title`(100)) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -281,7 +281,7 @@ CREATE TABLE `goal` ( `deleted` tinyint(4) NOT NULL DEFAULT '0', `event_value_as_revenue` tinyint(4) NOT NULL DEFAULT '0', PRIMARY KEY (`idsite`,`idgoal`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -306,7 +306,7 @@ CREATE TABLE `locks` ( `value` varchar(255) DEFAULT NULL, `expiry_time` bigint(20) unsigned DEFAULT '9999999999', PRIMARY KEY (`key`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -333,7 +333,7 @@ CREATE TABLE `log_action` ( `url_prefix` tinyint(2) DEFAULT NULL, PRIMARY KEY (`idaction`), KEY `index_type_hash` (`type`,`hash`) -) ENGINE=InnoDB AUTO_INCREMENT=2764 DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=2764 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -406,7 +406,7 @@ CREATE TABLE `log_conversion` ( PRIMARY KEY (`idvisit`,`idgoal`,`buster`), UNIQUE KEY `unique_idsite_idorder` (`idsite`,`idorder`), KEY `index_idsite_datetime` (`idsite`,`server_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -444,7 +444,7 @@ CREATE TABLE `log_conversion_item` ( `deleted` tinyint(1) unsigned NOT NULL, PRIMARY KEY (`idvisit`,`idorder`,`idaction_sku`), KEY `index_idsite_servertime` (`idsite`,`server_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -520,7 +520,7 @@ CREATE TABLE `log_link_visit_action` ( PRIMARY KEY (`idlink_va`), KEY `index_idvisit` (`idvisit`), KEY `index_idsite_servertime` (`idsite`,`server_time`) -) ENGINE=InnoDB AUTO_INCREMENT=2833 DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=2833 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -547,7 +547,7 @@ CREATE TABLE `log_profiling` ( `idprofiling` bigint(20) unsigned NOT NULL AUTO_INCREMENT, PRIMARY KEY (`idprofiling`), UNIQUE KEY `query` (`query`(100)) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -641,7 +641,7 @@ CREATE TABLE `log_visit` ( KEY `index_idsite_config_datetime` (`idsite`,`config_id`,`visit_last_action_time`), KEY `index_idsite_datetime` (`idsite`,`visit_last_action_time`), KEY `index_idsite_idvisitor_time` (`idsite`,`idvisitor`,`visit_last_action_time`) -) ENGINE=InnoDB AUTO_INCREMENT=500 DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=500 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -668,7 +668,7 @@ CREATE TABLE `logger_message` ( `level` varchar(16) DEFAULT NULL, `message` text, PRIMARY KEY (`idlogger_message`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -693,7 +693,7 @@ CREATE TABLE `option` ( `autoload` tinyint(4) NOT NULL DEFAULT '1', PRIMARY KEY (`option_name`), KEY `autoload` (`autoload`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -722,7 +722,7 @@ CREATE TABLE `plugin_setting` ( `idplugin_setting` bigint(20) unsigned NOT NULL AUTO_INCREMENT, PRIMARY KEY (`idplugin_setting`), KEY `plugin_name` (`plugin_name`,`user_login`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -904,7 +904,7 @@ CREATE TABLE `sequence` ( `name` varchar(120) NOT NULL, `value` bigint(20) unsigned NOT NULL, PRIMARY KEY (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -929,7 +929,7 @@ CREATE TABLE `session` ( `lifetime` int(11) DEFAULT NULL, `data` mediumtext, PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -969,7 +969,7 @@ CREATE TABLE `site` ( `keep_url_fragment` tinyint(4) NOT NULL DEFAULT '0', `creator_login` varchar(100) DEFAULT NULL, PRIMARY KEY (`idsite`) -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -998,7 +998,7 @@ CREATE TABLE `site_setting` ( `idsite_setting` bigint(20) unsigned NOT NULL AUTO_INCREMENT, PRIMARY KEY (`idsite_setting`), KEY `idsite` (`idsite`,`plugin_name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1021,7 +1021,7 @@ CREATE TABLE `site_url` ( `idsite` int(10) unsigned NOT NULL, `url` varchar(190) NOT NULL, PRIMARY KEY (`idsite`,`url`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1257,7 +1257,7 @@ CREATE TABLE `tracking_failure` ( `date_first_occurred` datetime NOT NULL, `request_url` mediumtext NOT NULL, PRIMARY KEY (`idsite`,`idfailure`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1281,7 +1281,7 @@ CREATE TABLE `twofactor_recovery_code` ( `login` varchar(100) NOT NULL, `recovery_code` varchar(40) NOT NULL, PRIMARY KEY (`idrecoverycode`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1345,7 +1345,7 @@ CREATE TABLE `user` ( `ts_changes_shown` timestamp NULL DEFAULT NULL, PRIMARY KEY (`login`), UNIQUE KEY `uniq_email` (`email`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1428,7 +1428,7 @@ CREATE TABLE `user_token_auth` ( `secure_only` tinyint(2) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`idusertokenauth`), UNIQUE KEY `uniq_password` (`password`) -) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; --