Skip to content

Commit

Permalink
Add configuration for database connection collation
Browse files Browse the repository at this point in the history
  • Loading branch information
mneudert committed Sep 6, 2024
1 parent 750fdce commit 29a4076
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 7 deletions.
7 changes: 7 additions & 0 deletions config/global.ini.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@
; Matomo should work correctly without this setting but we recommend to have a charset set.
charset = utf8

; In some database setups the collation for the connection changes after a "SET NAMES" statement
; is issued, unless a "COLLATE" argument is added.
; 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.
connection_collation =

; Database error codes to ignore during updates
;
;ignore_error_codes[] = 1105
Expand Down
1 change: 1 addition & 0 deletions core/Db.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ public static function createReaderDatabaseObject($dbConfig = null)
$dbConfig['type'] = $masterDbConfig['type'];
$dbConfig['tables_prefix'] = $masterDbConfig['tables_prefix'];
$dbConfig['charset'] = $masterDbConfig['charset'];
$dbConfig['connection_collation'] = $masterDbConfig['connection_collation'] ?? null;

$db = @Adapter::factory($dbConfig['adapter'], $dbConfig);

Expand Down
19 changes: 15 additions & 4 deletions core/Tracker/Db/Mysqli.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Mysqli extends Db
protected $username;
protected $password;
protected $charset;
protected $connectionCollation;
protected $activeTransaction = false;

protected $enable_ssl;
Expand Down Expand Up @@ -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->connectionCollation = $dbInfo['connection_collation'] ?? null;

if (!empty($dbInfo['enable_ssl'])) {
$this->enable_ssl = $dbInfo['enable_ssl'];
Expand Down Expand Up @@ -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->connectionCollation) {
// mysqli_set_charset does not support setting a collation
$query = "SET NAMES '" . $this->charset . "' COLLATE '" . $this->connectionCollation . "'";

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 = '';
Expand Down
32 changes: 30 additions & 2 deletions core/Tracker/Db/Pdo/Mysql.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 $connectionCollation;

protected $mysqlOptions = [];

protected $activeTransaction = false;

Expand All @@ -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['connection_collation'])) {
$this->connectionCollation = $dbInfo['connection_collation'];
}
}

if (isset($dbInfo['enable_ssl']) && $dbInfo['enable_ssl']) {
if (!empty($dbInfo['ssl_key'])) {
Expand Down Expand Up @@ -409,6 +432,11 @@ private function establishConnection(): void
*/
if (!empty($this->charset)) {
$sql = "SET NAMES '" . $this->charset . "'";

if (!empty($this->connectionCollation)) {
$sql .= " COLLATE '" . $this->connectionCollation . "'";
}

$this->connection->exec($sql);
}
}
Expand Down
7 changes: 6 additions & 1 deletion libs/Zend/Db/Adapter/Mysqli.php
Original file line number Diff line number Diff line change
Expand Up @@ -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['connection_collation'])) {
// mysqli_set_charset does not support setting a collation
$query = "SET NAMES '" . $this->_config['charset'] . "' COLLATE '" . $this->_config['connection_collation'] . "'";

mysqli_query($this->_connection, $query);
} elseif (!empty($this->_config['charset'])) {
mysqli_set_charset($this->_connection, $this->_config['charset']);
}
}
Expand Down
5 changes: 5 additions & 0 deletions libs/Zend/Db/Adapter/Pdo/Mysql.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ protected function _connect()

if (!empty($this->_config['charset'])) {
$initCommand = "SET NAMES '" . $this->_config['charset'] . "'";

if (!empty($this->_config['connection_collation'])) {
$initCommand .= " COLLATE '" . $this->_config['connection_collation'] . "'";
}

$this->_config['driver_options'][1002] = $initCommand; // 1002 = PDO::MYSQL_ATTR_INIT_COMMAND
}

Expand Down
37 changes: 37 additions & 0 deletions tests/PHPUnit/Integration/DbTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,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['connection_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['connection_collation'] = $config->database['charset'] . '_swedish_ci';

$db = Db::get();
self::assertInstanceOf($expectedClass, $db);

$currentCollation = $db->fetchOne('SELECT @@collation_connection');
self::assertSame($config->database['connection_collation'], $currentCollation);
}

public function getDbAdapter()
{
return array(
Expand Down
53 changes: 53 additions & 0 deletions tests/PHPUnit/Integration/Tracker/Db/MysqliTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace PHPUnit\Integration\Tracker\Db;

use Piwik\Config;
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
use Piwik\Tracker;

/**
* Tracker DB test
*
* @group Core
* @group TrackerDbTest
*/
class MysqliTest extends IntegrationTestCase
{
public function setUp(): void
{
parent::setUp();

$config = Config::getInstance();
$config->database['adapter'] = 'MYSQLI';
}

public function testConnectionThrowsOnInvalidCharset(): void
{
self::expectException(Tracker\Db\DbException::class);
self::expectExceptionMessageMatches('/Set Charset failed/');

$config = Config::getInstance();
$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['connection_collation'] = 'something really invalid';

Tracker\Db::connectPiwikTrackerDb();
}
}
21 changes: 21 additions & 0 deletions tests/PHPUnit/Integration/Tracker/DbTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,27 @@ public function testFetchAllNoMatch()
$this->assertEquals(array(), $val);
}

public function testConnectionCollationDefault(): void
{
$config = Config::getInstance();
$config->database['connection_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['connection_collation'] = $config->database['charset'] . '_swedish_ci';
$db = Tracker\Db::connectPiwikTrackerDb();

$currentCollation = $db->fetchOne('SELECT @@collation_connection');
self::assertSame($config->database['connection_collation'], $currentCollation);
}

private function insertRowId($value = '1')
{
$db = Tracker::getDatabase();
Expand Down

0 comments on commit 29a4076

Please sign in to comment.