Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core/Db/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,16 @@ public function supportsSortingInSubquery(): bool
return $this->getSchema()->supportsSortingInSubquery();
}

/**
* Returns if the database engine supports window functions.
*
* @return bool
*/
public function supportsWindowFunctions(): bool
{
return $this->getSchema()->supportsWindowFunctions();
}

/**
* Returns the supported read isolation transaction level
*
Expand Down
7 changes: 7 additions & 0 deletions core/Db/Schema/Mariadb.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ public function supportsRankingRollupWithoutExtraSorting(): bool
return false;
}

public function supportsWindowFunctions(): bool
{
$version = strtolower($this->getVersion());

return version_compare($version, '10.2', '>=');
}

public function hasReachedEOL(): bool
{
$currentVersion = $this->getVersion();
Expand Down
23 changes: 22 additions & 1 deletion core/Db/Schema/Mysql.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ class Mysql implements SchemaInterface
public const OPTION_NAME_MATOMO_INSTALL_VERSION = 'install_version';
public const MAX_TABLE_NAME_LENGTH = 64;

/**
* @var string|null
*/
private $databaseVersion = null;

private $tablesInstalled = null;

public function getDatabaseType(): string
Expand Down Expand Up @@ -786,6 +791,18 @@ public function supportsSortingInSubquery(): bool
return true;
}

public function supportsWindowFunctions(): bool
{
$version = strtolower($this->getVersion());

// If MySQL is configured but MariaDb used don't take chances
if (str_contains($version, 'mariadb')) {
return false;
}

return version_compare($version, '8.0', '>=');
}

public function getSupportedReadIsolationTransactionLevel(): string
{
return 'READ UNCOMMITTED';
Expand Down Expand Up @@ -871,7 +888,11 @@ private function getTablePrefix()

public function getVersion(): string
{
return Db::fetchOne("SELECT VERSION()");
if (null === $this->databaseVersion) {
$this->databaseVersion = Db::fetchOne("SELECT VERSION()");
}

return $this->databaseVersion;
}

protected function getTableStatus()
Expand Down
5 changes: 5 additions & 0 deletions core/Db/Schema/Tidb.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ public function supportsSortingInSubquery(): bool
return false;
}

public function supportsWindowFunctions(): bool
{
return true;
}

public function getSupportedReadIsolationTransactionLevel(): string
{
// TiDB doesn't support READ UNCOMMITTED
Expand Down
7 changes: 7 additions & 0 deletions core/Db/SchemaInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,13 @@ public function supportsRankingRollupWithoutExtraSorting(): bool;
*/
public function supportsSortingInSubquery(): bool;

/**
* Returns if the database engine supports window functions.
*
* @return bool
*/
public function supportsWindowFunctions(): bool;

/**
* Returns the version of the database server
* @return string
Expand Down
98 changes: 87 additions & 11 deletions core/DbHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,26 +430,59 @@ public static function addOptimizerHintToQuery(string $sql, string $hint): strin
}

/**
* Extracts the "ORDER BY" clause from a query.
* Extracts the "GROUP BY" clause from a query.
*
* Will return null if no clause found or the extraction failed,
* e.g. parentheses in the extracted clause are not balanced.
*/
public static function extractOrderByFromQuery(string $sql): ?string
public static function extractGroupByFromQuery(string $sql, bool $stripTableNames = false): ?string
{
$pattern = '/.*ORDER\s+BY\s+(.*?)(?:\s+LIMIT|\s*;|\s*$)/is';
$groupBy = self::extractClauseFromQuery(
$sql,
'/.*GROUP\s+BY\s+(.*?)(?:\s+(?:WITH|HAVING|WINDOW|ORDER|LIMIT)|\s*;|\s*$)(.*)/is'
);

if (preg_match($pattern, $sql, $matches)) {
$orderBy = $matches[1];
$openParentheses = substr_count($orderBy, '(');
$closeParentheses = substr_count($orderBy, ')');
if ($stripTableNames && null !== $groupBy) {
return self::stripTableNamesFromQueryClause($groupBy);
} else {
return $groupBy;
}
}

if ($openParentheses === $closeParentheses) {
return trim($orderBy);
}
/**
* Extracts the "ORDER BY" clause from a query.
*
* Will return null if no clause found or the extraction failed,
* e.g. parentheses in the extracted clause are not balanced.
*/
public static function extractOrderByFromQuery(string $sql, bool $stripTableNames = false): ?string
{
$orderBy = self::extractClauseFromQuery(
$sql,
'/.*ORDER\s+BY\s+(.*?)(?:\s+LIMIT|\s*;|\s*$)(.*)/is'
);

if ($stripTableNames && null !== $orderBy) {
return self::stripTableNamesFromQueryClause($orderBy);
} else {
return $orderBy;
}
}

return null;
/**
* Extracts the "SELECT" columns from a query.
*
* Will return null if no columns found or the extraction failed.
*
* Will skip comments and optimizer hints between the SELECT and the
* first column, but not between individual columns.
*/
public static function extractSelectFromQuery(string $sql): ?string
{
return self::extractClauseFromQuery(
$sql,
'/^\s*SELECT\s+(?:\/\*.*?\*\/\s*)*(.*?)(?:\s+FROM|\s*;|\s*$)/is'
);
}

/**
Expand All @@ -465,4 +498,47 @@ public static function isValidDbname($dbname)
{
return (0 !== preg_match('/(^[a-zA-Z0-9]+([a-zA-Z0-9\_\.\-\+]*))$/D', $dbname));
}

private static function extractClauseFromQuery(string $query, string $pattern): ?string
{
preg_match($pattern, $query, $matches);

if (empty($matches[1])) {
return null;
}

$clause = trim($matches[1]);
$openParentheses = substr_count($clause, '(');
$closeParentheses = substr_count($clause, ')');

if ($openParentheses !== $closeParentheses) {
return null;
}

// secondary match is after optional keywords
// check for balanced parentheses to avoid matching
// clause from a nested query
if (!empty($matches[2])) {
$postMatch = $matches[2];
$openParentheses = substr_count($postMatch, '(');
$closeParentheses = substr_count($postMatch, ')');

if ($openParentheses !== $closeParentheses) {
return null;
}
}

return $clause;
}

private static function stripTableNamesFromQueryClause(string $clause): string
{
return preg_replace_callback(
'/`?\w+`?\.`?(\w+)`?/',
function (array $matches): string {
return '`' . $matches[1] . '`';
},
$clause
);
}
}
Loading