From 8c62b1a1b5a4f490eee380987cbbb66cb312ac46 Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Wed, 30 Apr 2025 17:59:38 +0100 Subject: [PATCH] Use native MySQL paging Signed-off-by: Kamil Tekiela --- phpstan-baseline.neon | 4 +- psalm-baseline.xml | 1 - .../Database/RoutinesController.php | 7 +- src/Database/Routines.php | 55 ++++++---------- src/Query/Generator.php | 40 +++++++++--- .../Database/RoutinesControllerTest.php | 65 +++++++++---------- 6 files changed, 85 insertions(+), 87 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 143a00ecbc..7915a0336c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5205,7 +5205,7 @@ parameters: Use dependency injection instead\.$# ''' identifier: staticMethod.deprecated - count: 3 + count: 2 path: src/Database/Routines.php - @@ -5271,7 +5271,7 @@ parameters: - message: '#^Loose comparison via "\=\=" is not allowed\.$#' identifier: equal.notAllowed - count: 3 + count: 1 path: src/Database/Routines.php - diff --git a/psalm-baseline.xml b/psalm-baseline.xml index d58d7ed25c..98c6a77834 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -3467,7 +3467,6 @@ - diff --git a/src/Controllers/Database/RoutinesController.php b/src/Controllers/Database/RoutinesController.php index 6e0b7c3d66..a0befa26bd 100644 --- a/src/Controllers/Database/RoutinesController.php +++ b/src/Controllers/Database/RoutinesController.php @@ -25,8 +25,6 @@ use PhpMyAdmin\UserPrivilegesFactory; use PhpMyAdmin\Util; use function __; -use function array_slice; -use function count; use function htmlentities; use function htmlspecialchars; use function in_array; @@ -457,8 +455,7 @@ final readonly class RoutinesController implements InvocableController $type = null; } - $items = Routines::getDetails($this->dbi, Current::$database, $type); - $totalNumRoutines = count($items); + $totalNumRoutines = Routines::getRoutineCount($this->dbi, Current::$database, $type); $pageSize = $this->config->settings['MaxRoutineList']; $pos = (int) $request->getParam('pos'); @@ -481,7 +478,7 @@ final readonly class RoutinesController implements InvocableController return $this->response->response(); } - $items = array_slice($items, $pos, $pageSize); + $items = Routines::getDetails($this->dbi, Current::$database, $type, limit: $pageSize, offset: $pos); $isAjax = $request->isAjax() && empty($_REQUEST['ajax_page_request']); diff --git a/src/Database/Routines.php b/src/Database/Routines.php index 57558ffec7..c594954f85 100644 --- a/src/Database/Routines.php +++ b/src/Database/Routines.php @@ -20,9 +20,7 @@ use PhpMyAdmin\Util; use function __; use function _ngettext; -use function array_column; use function array_merge; -use function array_multisort; use function count; use function explode; use function htmlentities; @@ -42,7 +40,6 @@ use function str_starts_with; use function stripos; use const ENT_QUOTES; -use const SORT_ASC; /** * Functions for routine management. @@ -1203,35 +1200,17 @@ class Routines string $db, string|null $which = null, string $name = '', + int $limit = 0, + int $offset = 0, ): array { - if (! Config::getInstance()->selectedServer['DisableIS']) { - $query = QueryGenerator::getInformationSchemaRoutinesRequest( - $dbi->quoteString($db), - in_array($which, ['FUNCTION', 'PROCEDURE'], true) ? $which : null, - $name === '' ? null : $dbi->quoteString($name), - ); - $routines = $dbi->fetchResultSimple($query); - } else { - $routines = []; - - if ($which === 'FUNCTION' || $which == null) { - $query = 'SHOW FUNCTION STATUS WHERE `Db` = ' . $dbi->quoteString($db); - if ($name !== '') { - $query .= ' AND `Name` = ' . $dbi->quoteString($name); - } - - $routines = $dbi->fetchResultSimple($query); - } - - if ($which === 'PROCEDURE' || $which == null) { - $query = 'SHOW PROCEDURE STATUS WHERE `Db` = ' . $dbi->quoteString($db); - if ($name !== '') { - $query .= ' AND `Name` = ' . $dbi->quoteString($name); - } - - $routines = array_merge($routines, $dbi->fetchResultSimple($query)); - } - } + $query = QueryGenerator::getInformationSchemaRoutinesRequest( + $dbi->quoteString($db), + in_array($which, ['FUNCTION', 'PROCEDURE'], true) ? $which : null, + $name === '' ? null : $dbi->quoteString($name), + $limit, + $offset, + ); + $routines = $dbi->fetchResultSimple($query); $ret = []; /** @var array{Name:string, Type:string, Definer:string, DTD_IDENTIFIER:string|null} $routine */ @@ -1244,13 +1223,19 @@ class Routines ); } - // Sort results by name - $name = array_column($ret, 'name'); - array_multisort($name, SORT_ASC, $ret); - return $ret; } + public static function getRoutineCount(DatabaseInterface $dbi, string $db, string|null $which = null): int + { + $query = QueryGenerator::getInformationSchemaRoutinesCountRequest( + $dbi->quoteString($db), + in_array($which, ['FUNCTION', 'PROCEDURE'], true) ? $which : null, + ); + + return (int) $dbi->fetchValue($query); + } + public static function getFunctionDefinition(DatabaseInterface $dbi, string $db, string $name): string|null { $result = $dbi->fetchValue( diff --git a/src/Query/Generator.php b/src/Query/Generator.php index d924de551e..a22ca01995 100644 --- a/src/Query/Generator.php +++ b/src/Query/Generator.php @@ -177,19 +177,13 @@ class Generator string $quotedDbName, string|null $routineType, string|null $quotedRoutineName, + int $limit = 0, + int $offset = 0, ): string { $query = 'SELECT' - . ' `ROUTINE_SCHEMA` AS `Db`,' . ' `SPECIFIC_NAME` AS `Name`,' . ' `ROUTINE_TYPE` AS `Type`,' . ' `DEFINER` AS `Definer`,' - . ' `LAST_ALTERED` AS `Modified`,' - . ' `CREATED` AS `Created`,' - . ' `SECURITY_TYPE` AS `Security_type`,' - . ' `ROUTINE_COMMENT` AS `Comment`,' - . ' `CHARACTER_SET_CLIENT` AS `character_set_client`,' - . ' `COLLATION_CONNECTION` AS `collation_connection`,' - . ' `DATABASE_COLLATION` AS `Database Collation`,' . ' `DTD_IDENTIFIER`' . ' FROM `information_schema`.`ROUTINES`' . ' WHERE `ROUTINE_SCHEMA` ' . Util::getCollateForIS() @@ -202,6 +196,36 @@ class Generator $query .= ' AND `SPECIFIC_NAME` = ' . $quotedRoutineName; } + $query .= ' ORDER BY `SPECIFIC_NAME`'; + + if ($limit > 0) { + $query .= ' LIMIT ' . $limit; + } + + if ($offset > 0) { + $query .= ' OFFSET ' . $offset; + } + + return $query; + } + + public static function getInformationSchemaRoutinesCountRequest( + string $quotedDbName, + string|null $routineType, + string|null $quotedRoutineName = null, + ): string { + $query = 'SELECT COUNT(*) AS `count`' + . ' FROM `information_schema`.`ROUTINES`' + . ' WHERE `ROUTINE_SCHEMA` ' . Util::getCollateForIS() + . ' = ' . $quotedDbName; + if ($routineType !== null) { + $query .= " AND `ROUTINE_TYPE` = '" . $routineType . "'"; + } + + if ($quotedRoutineName !== null) { + $query .= ' AND `SPECIFIC_NAME` = ' . $quotedRoutineName; + } + return $query; } diff --git a/tests/unit/Controllers/Database/RoutinesControllerTest.php b/tests/unit/Controllers/Database/RoutinesControllerTest.php index 7412727ce3..c258396c53 100644 --- a/tests/unit/Controllers/Database/RoutinesControllerTest.php +++ b/tests/unit/Controllers/Database/RoutinesControllerTest.php @@ -39,20 +39,20 @@ final class RoutinesControllerTest extends AbstractTestCase ['Grants for definer@localhost'], ); $dummyDbi->addResult( - 'SHOW FUNCTION STATUS WHERE `Db` = \'test_db\'', - [['test_db', 'test_func', 'FUNCTION', 'definer@localhost']], - ['Db', 'Name', 'Type', 'Definer'], - ); - $dummyDbi->addResult( - 'SHOW PROCEDURE STATUS WHERE `Db` = \'test_db\'', - [['test_db', 'test_proc', 'PROCEDURE', 'definer@localhost']], - ['Db', 'Name', 'Type', 'Definer'], + "SELECT `SPECIFIC_NAME` AS `Name`, `ROUTINE_TYPE` AS `Type`, `DEFINER` AS `Definer`, `DTD_IDENTIFIER` FROM `information_schema`.`ROUTINES` WHERE `ROUTINE_SCHEMA` COLLATE utf8_bin = 'test_db' ORDER BY `SPECIFIC_NAME` LIMIT 250", + [['test_db', 'test_func', 'FUNCTION', 'definer@localhost', null], ['test_db', 'test_proc', 'PROCEDURE', 'definer@localhost', null]], + ['Db', 'Name', 'Type', 'Definer', 'DTD_IDENTIFIER'], ); $dummyDbi->addResult('SELECT @@lower_case_table_names', []); $dummyDbi->addResult( - "SELECT `DEFINER` FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA COLLATE utf8_bin='test_db' AND SPECIFIC_NAME='test_func' AND ROUTINE_TYPE='FUNCTION';", - [['definer@localhost']], - ['DEFINER'], + "SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` WHERE GRANTEE='''definer''@''localhost''' AND PRIVILEGE_TYPE='CREATE ROUTINE'", + [['CREATE ROUTINE']], + ['PRIVILEGE_TYPE'], + ); + $dummyDbi->addResult( + "SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` WHERE GRANTEE='''definer''@''localhost''' AND PRIVILEGE_TYPE='EXECUTE'", + [['EXECUTE']], + ['PRIVILEGE_TYPE'], ); $dummyDbi->addResult( "SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` WHERE GRANTEE='''definer''@''localhost''' AND PRIVILEGE_TYPE='CREATE ROUTINE'", @@ -64,35 +64,15 @@ final class RoutinesControllerTest extends AbstractTestCase [['EXECUTE']], ['PRIVILEGE_TYPE'], ); - $dummyDbi->addResult( - 'SHOW CREATE FUNCTION `test_db`.`test_func`', - [['test_func', 'CREATE FUNCTION `test_func` (p INT) RETURNS int(11) BEGIN END']], - ['Function', 'Create Function'], - ); - $dummyDbi->addResult( - "SELECT `DEFINER` FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA COLLATE utf8_bin='test_db' AND SPECIFIC_NAME='test_proc' AND ROUTINE_TYPE='PROCEDURE';", - [['definer@localhost']], - ['DEFINER'], - ); $dummyDbi->addResult( "SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` WHERE GRANTEE='''definer''@''localhost''' AND PRIVILEGE_TYPE='CREATE ROUTINE'", [['CREATE ROUTINE']], ['PRIVILEGE_TYPE'], ); $dummyDbi->addResult( - "SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` WHERE GRANTEE='''definer''@''localhost''' AND PRIVILEGE_TYPE='EXECUTE'", - [['EXECUTE']], - ['PRIVILEGE_TYPE'], - ); - $dummyDbi->addResult( - 'SHOW CREATE PROCEDURE `test_db`.`test_proc`', - [['test_proc2', 'CREATE PROCEDURE `test_proc2` (p INT) BEGIN END']], - ['Procedure', 'Create Procedure'], - ); - $dummyDbi->addResult( - "SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` WHERE GRANTEE='''definer''@''localhost''' AND PRIVILEGE_TYPE='CREATE ROUTINE'", - [['CREATE ROUTINE']], - ['PRIVILEGE_TYPE'], + "SELECT COUNT(*) AS `count` FROM `information_schema`.`ROUTINES` WHERE `ROUTINE_SCHEMA` COLLATE utf8_bin = 'test_db'", + [[2]], + ['count'], ); // phpcs:enable @@ -293,6 +273,8 @@ HTML; // phpcs:enable self::assertSame($expected, $actual); + $dummyDbi->assertAllQueriesConsumed(); + $dummyDbi->assertAllSelectsConsumed(); } public function testWithoutRoutines(): void @@ -312,13 +294,22 @@ HTML; [['GRANT ALL PRIVILEGES ON *.* TO `definer`@`localhost`']], ['Grants for definer@localhost'], ); - $dummyDbi->addResult('SHOW FUNCTION STATUS WHERE `Db` = \'test_db\'', [], ['Db', 'Name', 'Type', 'Definer']); - $dummyDbi->addResult('SHOW PROCEDURE STATUS WHERE `Db` = \'test_db\'', [], ['Db', 'Name', 'Type', 'Definer']); $dummyDbi->addResult( "SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` WHERE GRANTEE='''definer''@''localhost''' AND PRIVILEGE_TYPE='CREATE ROUTINE'", [['CREATE ROUTINE']], ['PRIVILEGE_TYPE'], ); + $dummyDbi->addResult('SELECT @@lower_case_table_names', []); + $dummyDbi->addResult( + "SELECT COUNT(*) AS `count` FROM `information_schema`.`ROUTINES` WHERE `ROUTINE_SCHEMA` COLLATE utf8_bin = 'test_db'", + [[1]], + ['count'], + ); + $dummyDbi->addResult( + "SELECT `SPECIFIC_NAME` AS `Name`, `ROUTINE_TYPE` AS `Type`, `DEFINER` AS `Definer`, `DTD_IDENTIFIER` FROM `information_schema`.`ROUTINES` WHERE `ROUTINE_SCHEMA` COLLATE utf8_bin = 'test_db' ORDER BY `SPECIFIC_NAME` LIMIT 250", + [], + ['Db', 'Name', 'Type', 'Definer', 'DTD_IDENTIFIER'], + ); // phpcs:enable $dbi = $this->createDatabaseInterface($dummyDbi); @@ -438,5 +429,7 @@ HTML; // phpcs:enable self::assertSame($expected, $actual); + $dummyDbi->assertAllQueriesConsumed(); + $dummyDbi->assertAllSelectsConsumed(); } }