From 3dbd979c6fa50465de83da00db2564e4ae4c49f3 Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Sat, 25 Feb 2023 17:46:15 +0300 Subject: [PATCH] pkp/pkp-lib#8700 Updated DAO::countRecords() to optimize the query before enclosing it with a SELECT --- classes/db/DAO.inc.php | 65 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/classes/db/DAO.inc.php b/classes/db/DAO.inc.php index 47f68502bd2..5aa48b0bbfe 100644 --- a/classes/db/DAO.inc.php +++ b/classes/db/DAO.inc.php @@ -103,8 +103,15 @@ function retrieveRange($sql, $params = [], $dbResultRange = null, $callHooks = t * @return int */ public function countRecords($sql, $params = []) { - $result = $this->retrieve('SELECT COUNT(*) AS row_count FROM (' . $sql . ') AS count_subquery', $params); - return $result->current()->row_count; + foreach ([$this->_optimizeCountQuery($sql, $params), [$sql, $params]] as [$sql, $params]) { + try { + $result = $this->retrieve("SELECT COUNT(*) AS row_count FROM ({$sql}) AS count_subquery", $params); + return $result->current()->row_count; + } catch (Exception $e) { + error_log($e); + } + } + throw $e; } /** @@ -583,4 +590,58 @@ function formatDateToDB($date, $defaultNumWeeks = null, $acceptPastDate = true) return null; } } + + /** + * Retrieves a SELECT statement without the SELECT and ORDER BY clauses for optimization purposes + * @return array The SQL query at the index 0 and the updated parameters at the index 1 + */ + private static function _optimizeCountQuery(string $s, array $params): array { + $findTopLevelExpression = static function (string $s, string $expression, int $index, ?int &$foundParams = null): int { + static + $beginLevel = '(', + $endLevel = ')', + $delimiters = ["'" => 0, '`' => 0, '"' => 0], + $escape = '\\'; + + if ($index < 0) { + return -1; + } + $levels = 0; + $delimiter = null; + for ($l = strlen($s), $i = $index; $i < $l; ) { + $c = $s[$i]; + if ($c === $beginLevel) { + ++$levels; + } elseif ($c === $endLevel) { + if (!$levels--) { + return -1; + } + } elseif (($newDelimiter = $delimiters[$c] ?? null)) { + if ($delimiter === $newDelimiter) { + $delimiter = null; + } elseif (!$delimiter) { + $delimiter = $c; + } + } else if ($c === $escape && $delimiter) { + $i += 2; + continue; + } elseif ($c === '?') { + ++$foundParams; + } elseif (!$delimiter && !$levels && preg_match("/\G{$expression}/i", $s, $m, 0, $i)) { + return $i; + } + ++$i; + } + return -1; + }; + + $selectParams = 0; + // Abort if there's a UNION clause or if there's no "FROM" + if (~$findTopLevelExpression($s, 'UNION', 0) || !~($from = $findTopLevelExpression($s, '\bFROM\b', 0, $selectParams))) { + return [$s, $params]; + } + // Slice the statement up to the ORDER BY clause + $order = ~($order = $findTopLevelExpression($s, '\bORDER\s+BY\b', $from)) ? $order - $from : null; + return ['SELECT 0 ' . substr($s, $from, $order), array_slice($params, $selectParams)]; + } }