phpmyadmin/tests/unit/SqlTest.php
Maurício Meneghini Fauth 2dabc839b3
Extract Config dependency from Template class
Uses DI instead.

Signed-off-by: Maurício Meneghini Fauth <mauricio@mfauth.net>
2026-02-11 22:51:02 -03:00

922 lines
38 KiB
PHP

<?php
declare(strict_types=1);
namespace PhpMyAdmin\Tests;
use PhpMyAdmin\Bookmarks\BookmarkRepository;
use PhpMyAdmin\Config;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\ConfigStorage\RelationCleanup;
use PhpMyAdmin\Current;
use PhpMyAdmin\Dbal\DatabaseInterface;
use PhpMyAdmin\Http\Factory\ServerRequestFactory;
use PhpMyAdmin\Indexes\Index;
use PhpMyAdmin\ParseAnalyze;
use PhpMyAdmin\Sql;
use PhpMyAdmin\Template;
use PhpMyAdmin\Tests\Stubs\DbiDummy;
use PhpMyAdmin\Transformations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use ReflectionMethod;
use ReflectionProperty;
use stdClass;
use const MYSQLI_TYPE_SHORT;
use const MYSQLI_TYPE_TIMESTAMP;
use const MYSQLI_TYPE_VAR_STRING;
#[CoversClass(Sql::class)]
class SqlTest extends AbstractTestCase
{
protected DatabaseInterface $dbi;
protected DbiDummy $dummyDbi;
private Sql $sql;
/**
* Setup for test cases
*/
protected function setUp(): void
{
parent::setUp();
$this->setLanguage();
$this->dummyDbi = $this->createDbiDummy();
$this->dbi = $this->createDatabaseInterface($this->dummyDbi);
DatabaseInterface::$instance = $this->dbi;
Current::$database = 'db';
Current::$table = 'table';
$config = Config::getInstance();
$relation = new Relation($this->dbi);
$this->sql = new Sql(
$this->dbi,
$relation,
new RelationCleanup($this->dbi, $relation),
new Transformations($this->dbi, $relation),
new Template($config),
new BookmarkRepository($this->dbi, $relation),
$config,
);
}
/**
* Test for getSqlWithLimitClause
*/
public function testGetSqlWithLimitClause(): void
{
// Test environment.
$_SESSION['tmpval']['pos'] = 1;
$_SESSION['tmpval']['max_rows'] = 2;
self::assertSame('SELECT * FROM test LIMIT 1, 2', $this->callFunction(
$this->sql,
Sql::class,
'getSqlWithLimitClause',
[ParseAnalyze::sqlQuery('SELECT * FROM test LIMIT 0, 10', Current::$database)[0]],
));
}
/**
* Test for isRememberSortingOrder
*/
public function testIsRememberSortingOrder(): void
{
// Test environment.
Config::getInstance()->settings['RememberSorting'] = true;
self::assertTrue(
$this->callFunction($this->sql, Sql::class, 'isRememberSortingOrder', [
ParseAnalyze::sqlQuery('SELECT * FROM tbl', Current::$database)[0],
]),
);
self::assertFalse(
$this->callFunction($this->sql, Sql::class, 'isRememberSortingOrder', [
ParseAnalyze::sqlQuery('SELECT col FROM tbl', Current::$database)[0],
]),
);
self::assertFalse(
$this->callFunction($this->sql, Sql::class, 'isRememberSortingOrder', [
ParseAnalyze::sqlQuery('SELECT 1', Current::$database)[0],
]),
);
self::assertFalse(
$this->callFunction($this->sql, Sql::class, 'isRememberSortingOrder', [
ParseAnalyze::sqlQuery('SELECT col1, col2 FROM tbl', Current::$database)[0],
]),
);
self::assertFalse(
$this->callFunction($this->sql, Sql::class, 'isRememberSortingOrder', [
ParseAnalyze::sqlQuery('SELECT COUNT(*) from tbl', Current::$database)[0],
]),
);
}
/**
* Test for isAppendLimitClause
*/
public function testIsAppendLimitClause(): void
{
// Test environment.
$_SESSION['tmpval']['max_rows'] = 10;
self::assertTrue(
$this->callFunction($this->sql, Sql::class, 'isAppendLimitClause', [
ParseAnalyze::sqlQuery('SELECT * FROM tbl', Current::$database)[0],
]),
);
self::assertFalse(
$this->callFunction($this->sql, Sql::class, 'isAppendLimitClause', [
ParseAnalyze::sqlQuery('SELECT * from tbl LIMIT 0, 10', Current::$database)[0],
]),
);
}
public function testIsJustBrowsing(): void
{
// Test environment.
$_SESSION['tmpval']['max_rows'] = 10;
self::assertTrue(Sql::isJustBrowsing(
ParseAnalyze::sqlQuery('SELECT * FROM db.tbl', Current::$database)[0],
));
self::assertTrue(Sql::isJustBrowsing(
ParseAnalyze::sqlQuery('SELECT * FROM tbl WHERE 1', Current::$database)[0],
));
self::assertFalse(Sql::isJustBrowsing(
ParseAnalyze::sqlQuery('SELECT * from tbl1, tbl2 LIMIT 0, 10', Current::$database)[0],
));
}
/**
* Test for isDeleteTransformationInfo
*/
public function testIsDeleteTransformationInfo(): void
{
self::assertTrue(
$this->callFunction($this->sql, Sql::class, 'isDeleteTransformationInfo', [
ParseAnalyze::sqlQuery('ALTER TABLE tbl DROP COLUMN col', Current::$database)[0],
]),
);
self::assertTrue(
$this->callFunction($this->sql, Sql::class, 'isDeleteTransformationInfo', [
ParseAnalyze::sqlQuery('DROP TABLE tbl', Current::$database)[0],
]),
);
self::assertFalse(
$this->callFunction($this->sql, Sql::class, 'isDeleteTransformationInfo', [
ParseAnalyze::sqlQuery('SELECT * from tbl', Current::$database)[0],
]),
);
}
/**
* Test for hasNoRightsToDropDatabase
*/
public function testHasNoRightsToDropDatabase(): void
{
$this->dummyDbi->addResult(
'SELECT 1 FROM mysql.user LIMIT 1',
[],
);
self::assertTrue(
$this->sql->hasNoRightsToDropDatabase(
ParseAnalyze::sqlQuery('DROP DATABASE db', Current::$database)[0],
),
);
self::assertFalse(
$this->sql->hasNoRightsToDropDatabase(
ParseAnalyze::sqlQuery('DROP TABLE tbl', Current::$database)[0],
),
);
self::assertFalse(
$this->sql->hasNoRightsToDropDatabase(
ParseAnalyze::sqlQuery('SELECT * from tbl', Current::$database)[0],
),
);
}
/**
* Should return false if all columns are not from the same table
*/
public function testWithMultipleTables(): void
{
$col1 = new stdClass();
$col1->table = 'table1';
$col2 = new stdClass();
$col2->table = 'table1';
$col3 = new stdClass();
$col3->table = 'table3';
$fieldsMeta = [$col1, $col2, $col3];
self::assertFalse(
$this->callFunction($this->sql, Sql::class, 'resultSetHasJustOneTable', [$fieldsMeta]),
);
// should not matter on where the odd column occurs
$fieldsMeta = [$col2, $col3, $col1];
self::assertFalse(
$this->callFunction($this->sql, Sql::class, 'resultSetHasJustOneTable', [$fieldsMeta]),
);
$fieldsMeta = [$col3, $col1, $col2];
self::assertFalse(
$this->callFunction($this->sql, Sql::class, 'resultSetHasJustOneTable', [$fieldsMeta]),
);
}
/**
* Should return true if all the columns are from the same table
*/
public function testWithSameTable(): void
{
$col1 = new stdClass();
$col1->table = 'table1';
$col2 = new stdClass();
$col2->table = 'table1';
$col3 = new stdClass();
$col3->table = 'table1';
$fieldsMeta = [$col1, $col2, $col3];
self::assertTrue(
$this->callFunction($this->sql, Sql::class, 'resultSetHasJustOneTable', [$fieldsMeta]),
);
}
/**
* Should return true even if function columns (table is '') occur when others
* are from the same table.
*/
public function testWithFunctionColumns(): void
{
$col1 = new stdClass();
$col1->table = 'table1';
$col2 = new stdClass();
$col2->table = '';
$col3 = new stdClass();
$col3->table = 'table1';
$fieldsMeta = [$col1, $col2, $col3];
self::assertTrue(
$this->callFunction($this->sql, Sql::class, 'resultSetHasJustOneTable', [$fieldsMeta]),
);
// should not matter on where the function column occurs
$fieldsMeta = [$col2, $col3, $col1];
self::assertTrue(
$this->callFunction($this->sql, Sql::class, 'resultSetHasJustOneTable', [$fieldsMeta]),
);
$fieldsMeta = [$col3, $col1, $col2];
self::assertTrue(
$this->callFunction($this->sql, Sql::class, 'resultSetHasJustOneTable', [$fieldsMeta]),
);
}
/**
* We can not say all the columns are from the same table if all the columns
* are function columns (table is '')
*/
public function testWithOnlyFunctionColumns(): void
{
$col1 = new stdClass();
$col1->table = '';
$col2 = new stdClass();
$col2->table = '';
$col3 = new stdClass();
$col3->table = '';
$fieldsMeta = [$col1, $col2, $col3];
self::assertFalse(
$this->callFunction($this->sql, Sql::class, 'resultSetHasJustOneTable', [$fieldsMeta]),
);
}
/** @return mixed[][] */
public static function dataProviderCountQueryResults(): array
{
// sql query
// session tmpval
// num rows
// result
// just browsing
return [
'join on SELECT results with *' => [
// -- Showing rows 0 - 49 (164056 total, 0 in query, Query took 0.1498 seconds.)
'select * from game_auth_logs l join ('
. ' select al.user_id, max(al.id) as id from game_auth_logs al '
. 'where al.successfull = 1 group by al.user_id ) last_log on last_log.id = l.id;',
['max_rows' => 50, 'pos' => 0],
164056,
50,
false,
'SELECT COUNT(*) FROM (SELECT 1 FROM game_auth_logs AS `l` JOIN ('
. ' select al.user_id, max(al.id) as id from game_auth_logs al '
. 'where al.successfull = 1 group by al.user_id ) AS `last_log` ON last_log.id = l.id'
. ' ) as cnt',
],
'join on SELECT results with alias.*' => [
// -- Showing rows 0 - 24 (267 total, Query took 0.1533 seconds.)
'select l.* from game_auth_logs l join ('
. ' select al.user_id, max(al.id) as id from game_auth_logs al '
. 'where al.successfull = 1 group by al.user_id ) last_log on last_log.id = l.id;',
['max_rows' => 50, 'pos' => 0],
267,
50,
false,
'SELECT COUNT(*) FROM (SELECT 1 FROM game_auth_logs AS `l` JOIN ('
. ' select al.user_id, max(al.id) as id from game_auth_logs al '
. 'where al.successfull = 1 group by al.user_id ) AS `last_log` ON last_log.id = l.id'
. ' ) as cnt',
],
['SELECT * FROM company_users WHERE id != 0 LIMIT 0, 10', ['max_rows' => 250], -1, -1],
['SELECT * FROM company_users WHERE id != 0', ['max_rows' => 250, 'pos' => -1], -1, -2],
['SELECT * FROM company_users WHERE id != 0', ['max_rows' => 250, 'pos' => -1], -1, -2],
['SELECT * FROM company_users WHERE id != 0', ['max_rows' => 250, 'pos' => 250], -1, 249],
['SELECT * FROM company_users WHERE id != 0', ['max_rows' => 250, 'pos' => 4], 2, 6],
['SELECT * FROM company_users WHERE id != 0', ['max_rows' => 'all', 'pos' => 4], 2, 2],
[null, [], 2, 0],
['SELECT * FROM company_users LIMIT 1,4', ['max_rows' => 10, 'pos' => 4], 20, 20],
['SELECT * FROM company_users', ['max_rows' => 10, 'pos' => 4], 20, 4],
['SELECT * FROM company_users WHERE not_working_count != 0', ['max_rows' => 10, 'pos' => 4], 20, 0],
['SELECT * FROM company_users WHERE working_count = 0', ['max_rows' => 10, 'pos' => 4], 20, 15],
['UPDATE company_users SET a=1 WHERE working_count = 0', ['max_rows' => 10, 'pos' => 4], 20, 20],
['UPDATE company_users SET a=1 WHERE working_count = 0', ['max_rows' => 'all', 'pos' => 4], 20, 20],
['UPDATE company_users SET a=1 WHERE working_count = 0', ['max_rows' => 15], 20, 20],
['SELECT * FROM company_users WHERE id != 0', ['max_rows' => 250, 'pos' => 4], 2, 6, true],
[
'SELECT *, (SELECT COUNT(*) FROM tbl1) as c1, (SELECT 1 FROM tbl2) as c2 '
. 'FROM company_users WHERE id != 0',
['max_rows' => 250, 'pos' => 4],
2,
6,
true,
],
['SELECT * FROM company_users', ['max_rows' => 10, 'pos' => 4], 20, 18, true],
[
'SELECT *, 1, (SELECT COUNT(*) FROM tbl1) as c1, '
. '(SELECT 1 FROM tbl2) as c2 FROM company_users WHERE subquery_case = 0',
['max_rows' => 10, 'pos' => 4],
20,
42,
],
[
'SELECT ( as c2 FROM company_users WHERE working_count = 0',// Invalid query
['max_rows' => 10],
20,
20,
],
[
'SELECT DISTINCT country_id FROM city;',
['max_rows' => 25, 'pos' => 0],
25,
109,
false,
'SELECT COUNT(*) FROM (SELECT DISTINCT country_id FROM city ) as cnt',
],
[
'SELECT * FROM t1 UNION SELECT * FROM t2;',
['max_rows' => -1, 'pos' => 0],
25,
109,
false,
'SELECT COUNT(*) FROM (SELECT * FROM t1 UNION SELECT * FROM t2 ) as cnt',
],
[
'SELECT SQL_NO_CACHE * FROM t1 WHERE id <> 0',
['max_rows' => -1, 'pos' => 0],
25,
100,
false,
'SELECT COUNT(*) FROM (SELECT 1 FROM t1 WHERE id <> 0 ) as cnt',
],
];
}
/** @param array<string, int|string> $sessionTmpVal */
#[DataProvider('dataProviderCountQueryResults')]
public function testCountQueryResults(
string|null $sqlQuery,
array $sessionTmpVal,
int $numRows,
int $expectedNumRows,
bool $justBrowsing = false,
string|null $expectedCountQuery = null,
): void {
if ($justBrowsing) {
Config::getInstance()->selectedServer['DisableIS'] = true;
}
$_SESSION['tmpval'] = $sessionTmpVal;
if ($expectedCountQuery !== null) {
$this->dummyDbi->addResult(
$expectedCountQuery,
[[$expectedNumRows]],
[],
[],
);
}
$result = $this->callFunction(
$this->sql,
Sql::class,
'countQueryResults',
[
$numRows,
$justBrowsing,
'my_dataset',// db
'company_users',// table
ParseAnalyze::sqlQuery($sqlQuery ?? '', Current::$database)[0],
],
);
self::assertSame($expectedNumRows, $result);
$this->dummyDbi->assertAllQueriesConsumed();
}
public function testExecuteQueryAndSendQueryResponse(): void
{
$this->dummyDbi->addSelectDb('sakila');
$this->dummyDbi->addResult(
'SELECT * FROM `sakila`.`country` LIMIT 0, 3;',
[
['1', 'Afghanistan', '2006-02-15 04:44:00'],
['2', 'Algeria', '2006-02-15 04:44:00'],
['3', 'American Samoa', '2006-02-15 04:44:00'],
],
['country_id', 'country', 'last_update'],
[
FieldHelper::fromArray(['type' => MYSQLI_TYPE_SHORT, 'length' => 5]),
FieldHelper::fromArray(['type' => MYSQLI_TYPE_VAR_STRING, 'length' => 200]),
FieldHelper::fromArray(['type' => MYSQLI_TYPE_TIMESTAMP, 'length' => 19]),
],
);
$this->dummyDbi->addResult(
'SHOW TABLE STATUS FROM `sakila` WHERE `Name` LIKE \'country%\'',
[
[
'country',
'InnoDB',
'10',
'Dynamic',
'109',
'150',
'16384',
'0',
'0',
'0',
'110',
'2011-12-13 14:15:16',
null,
null,
'utf8mb4_general_ci',
null,
'',
'',
'0',
'N',
],
],
[
'Name',
'Engine',
'Version',
'Row_format',
'Rows',
'Avg_row_length',
'Data_length',
'Max_data_length',
'Index_length',
'Data_free',
'Auto_increment',
'Create_time',
'Update_time',
'Check_time',
'Collation',
'Checksum',
'Create_options',
'Comment',
'Max_index_length',
'Temporary',
],
);
$this->dummyDbi->addResult(
'SHOW CREATE TABLE `sakila`.`country`',
[
[
'country',
'CREATE TABLE `country` (' . "\n"
. ' `country_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,' . "\n"
. ' `country` varchar(50) NOT NULL,' . "\n"
. ' `last_update` timestamp NOT NULL DEFAULT current_timestamp()'
. ' ON UPDATE current_timestamp(),' . "\n"
. ' PRIMARY KEY (`country_id`)' . "\n"
. ') ENGINE=InnoDB AUTO_INCREMENT=110 DEFAULT CHARSET=utf8mb4',
],
],
['Table', 'Create Table'],
);
$this->dummyDbi->addResult('SELECT COUNT(*) FROM `sakila`.`country`', [['109']]);
$this->dummyDbi->addResult(
'SELECT `COLUMN_NAME` AS `Field`, `COLUMN_TYPE` AS `Type`, `COLLATION_NAME` AS `Collation`,'
. ' `IS_NULLABLE` AS `Null`, `COLUMN_KEY` AS `Key`,'
. ' `COLUMN_DEFAULT` AS `Default`, `EXTRA` AS `Extra`, `PRIVILEGES` AS `Privileges`,'
. ' `COLUMN_COMMENT` AS `Comment`'
. ' FROM `information_schema`.`COLUMNS`'
. ' WHERE `TABLE_SCHEMA` COLLATE utf8_bin = \'sakila\' AND'
. ' `TABLE_NAME` COLLATE utf8_bin = \'country\''
. ' ORDER BY `ORDINAL_POSITION`',
[
[
'country_id',
'smallint(5) unsigned',
null,
'NO',
'PRI',
null,
'auto_increment',
'select,insert,update,references',
'',
],
[
'country',
'varchar(50)',
'utf8mb4_general_ci',
'NO',
'',
null,
'',
'select,insert,update,references',
'',
],
[
'last_update',
'timestamp',
null,
'NO',
'',
'current_timestamp()',
'on update current_timestamp()',
'select,insert,update,references',
'',
],
],
['Field', 'Type', 'Collation', 'Null', 'Key', 'Default', 'Extra', 'Privileges', 'Comment'],
);
$this->dummyDbi->addResult(
'SELECT `COLUMN_NAME` AS `Field`, `COLUMN_TYPE` AS `Type`, `COLLATION_NAME` AS `Collation`,'
. ' `IS_NULLABLE` AS `Null`, `COLUMN_KEY` AS `Key`,'
. ' `COLUMN_DEFAULT` AS `Default`, `EXTRA` AS `Extra`, `PRIVILEGES` AS `Privileges`,'
. ' `COLUMN_COMMENT` AS `Comment`'
. ' FROM `information_schema`.`COLUMNS`'
. ' WHERE `TABLE_SCHEMA` COLLATE utf8_bin = \'sakila\' AND'
. ' `TABLE_NAME` COLLATE utf8_bin = \'country\''
. ' ORDER BY `ORDINAL_POSITION`',
[
['country_id', 'smallint(5) unsigned', null, 'NO', 'PRI', null, 'auto_increment', '', ''],
['country', 'varchar(50)', null, 'NO', '', null, '', '', ''],
[
'last_update',
'timestamp',
null,
'NO',
'',
'current_timestamp()',
'on update current_timestamp()',
'',
'',
],
],
['Field', 'Type', 'Collation', 'Null', 'Key', 'Default', 'Extra', 'Privileges', 'Comment'],
);
$this->dummyDbi->addResult(
'SHOW INDEXES FROM `sakila`.`country`',
[['country', '0', 'PRIMARY', 'country_id']],
['Table', 'Non_unique', 'Key_name', 'Column_name'],
);
$this->dummyDbi->addResult(
'SELECT 1 FROM information_schema.VIEWS'
. ' WHERE TABLE_SCHEMA = \'sakila\' AND TABLE_NAME = \'country\' AND IS_UPDATABLE = \'YES\'',
[],
);
$_SESSION['sql_from_query_box'] = true;
Current::$database = 'sakila';
Current::$table = 'country';
Current::$sqlQuery = 'SELECT * FROM `sakila`.`country` LIMIT 0, 3;';
$config = Config::getInstance();
$config->selectedServer['DisableIS'] = true;
$config->selectedServer['user'] = 'user';
$request = ServerRequestFactory::create()->createServerRequest('POST', 'https://example.com');
$actual = $this->sql->executeQueryAndSendQueryResponse(
$request,
null,
false,
'sakila',
'different_table',
'',
'',
'index.php?route=/sql',
null,
'',
'SELECT * FROM `sakila`.`country` LIMIT 0, 3;',
'SELECT * FROM `sakila`.`country` LIMIT 0, 3;',
);
self::assertStringContainsString('Showing rows 0 - 2 (3 total', $actual);
self::assertStringContainsString('SELECT * FROM `sakila`.`country` LIMIT 0, 3;', $actual);
self::assertStringContainsString('Afghanistan', $actual);
self::assertStringContainsString('Algeria', $actual);
self::assertStringContainsString('American Samoa', $actual);
self::assertStringContainsString('data-type="int"', $actual);
self::assertStringContainsString('data-type="string"', $actual);
self::assertStringContainsString('data-type="timestamp"', $actual);
}
public function testExecuteQueryAndSendQueryResponseWithNoResult(): void
{
$config = Config::$instance = new Config();
$dbiDummy = $this->createDbiDummy();
$dbi = DatabaseInterface::$instance = $this->createDatabaseInterface($dbiDummy);
$relation = new Relation($dbi, $config);
$sql = new Sql(
$dbi,
$relation,
new RelationCleanup($this->dbi, $relation),
new Transformations($this->dbi, $relation),
new Template($config),
new BookmarkRepository($this->dbi, $relation),
$config,
);
$dbiDummy->removeDefaultResults();
$dbiDummy->addResult('SELECT DATABASE()', [['sakila']]);
$dbiDummy->addSelectDb('sakila');
$dbiDummy->addResult(
"SHOW SESSION VARIABLES LIKE 'FOREIGN_KEY_CHECKS';",
[['foreign_key_checks', 'ON']],
['Variable_name', 'Value'],
);
$dbiDummy->addResult(
"SELECT * FROM `sakila`.`country` WHERE `last_update` > '2025-02-01' LIMIT 0, 25",
[],
['country_id', 'country', 'last_update'],
[
FieldHelper::fromArray(['type' => MYSQLI_TYPE_SHORT, 'length' => 5]),
FieldHelper::fromArray(['type' => MYSQLI_TYPE_VAR_STRING, 'length' => 200]),
FieldHelper::fromArray(['type' => MYSQLI_TYPE_TIMESTAMP, 'length' => 19]),
],
);
$dbiDummy->addResult('SHOW WARNINGS', []);
$dbiDummy->addResult('SELECT @@have_profiling', [['YES']]);
$dbiDummy->addResult('SELECT @@lower_case_table_names', [['0']]);
// phpcs:disable Generic.Files.LineLength.TooLong
$dbiDummy->addResult(
"SHOW TABLE STATUS FROM `sakila` WHERE `Name` LIKE 'country%'",
[['country', 'InnoDB', '10', 'Dynamic', '109', '150', '16384', '0', '0', '0', '110', '2011-12-13 14:15:16', null, null, 'utf8mb4_general_ci', null, '', '', '0', 'N']],
['Name', 'Engine', 'Version', 'Row_format', 'Rows', 'Avg_row_length', 'Data_length', 'Max_data_length', 'Index_length', 'Data_free', 'Auto_increment', 'Create_time', 'Update_time', 'Check_time', 'Collation', 'Checksum', 'Create_options', 'Comment', 'Max_index_length', 'Temporary'],
);
$dbiDummy->addResult('SELECT COUNT(*) FROM `sakila`.`country`', [['109']]);
$dbiDummy->addResult(
'SHOW CREATE TABLE `sakila`.`country`',
[['country', "CREATE TABLE `country` (\n `country_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,\n `country` varchar(50) NOT NULL,\n `last_update` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),\n PRIMARY KEY (`country_id`)\n) ENGINE=InnoDB AUTO_INCREMENT=110 DEFAULT CHARSET=utf8mb4"]],
['Table', 'Create Table'],
);
$dbiDummy->addResult(
"SELECT `COLUMN_NAME` AS `Field`, `COLUMN_TYPE` AS `Type`, `COLLATION_NAME` AS `Collation`, `IS_NULLABLE` AS `Null`, `COLUMN_KEY` AS `Key`, `COLUMN_DEFAULT` AS `Default`, `EXTRA` AS `Extra`, `PRIVILEGES` AS `Privileges`, `COLUMN_COMMENT` AS `Comment` FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` COLLATE utf8_bin = 'sakila' AND `TABLE_NAME` COLLATE utf8_bin = 'country' ORDER BY `ORDINAL_POSITION`",
[
['country_id', 'smallint(5) unsigned', null, 'NO', 'PRI', null, 'auto_increment', 'select,insert,update,references', ''],
['country', 'varchar(50)', 'utf8mb4_general_ci', 'NO', '', null, '', 'select,insert,update,references', ''],
['last_update', 'timestamp', null, 'NO', '', 'current_timestamp()', 'on update current_timestamp()', 'select,insert,update,references', ''],
],
['Field', 'Type', 'Collation', 'Null', 'Key', 'Default', 'Extra', 'Privileges', 'Comment'],
);
// phpcs:enable
$dbiDummy->addResult(
'SHOW INDEXES FROM `sakila`.`country`',
[['country', '0', 'PRIMARY', 'country_id']],
['Table', 'Non_unique', 'Key_name', 'Column_name'],
);
$dbiDummy->addResult(
"SHOW SESSION VARIABLES LIKE 'FOREIGN_KEY_CHECKS';",
[['foreign_key_checks', 'ON']],
['Variable_name', 'Value'],
);
$_SESSION['tmpval'] = [];
$_SESSION['sql_from_query_box'] = true;
Current::$database = 'sakila';
Current::$table = 'country';
Current::$sqlQuery = "SELECT * FROM `sakila`.`country` WHERE `last_update` > '2025-02-01';";
$config->selectedServer['DisableIS'] = true;
$config->selectedServer['user'] = 'user';
(new ReflectionProperty(Index::class, 'registry'))->setValue(null, []);
$request = ServerRequestFactory::create()->createServerRequest('POST', 'https://example.com');
$actual = $sql->executeQueryAndSendQueryResponse(
$request,
null,
false,
'sakila',
'different_table',
'',
'',
'index.php?route=/sql',
null,
'',
"SELECT * FROM `sakila`.`country` WHERE `last_update` > '2025-02-01';",
"SELECT * FROM `sakila`.`country` WHERE `last_update` > '2025-02-01';",
);
$dbiDummy->assertAllQueriesConsumed();
$dbiDummy->assertAllSelectsConsumed();
self::assertStringContainsString('MySQL returned an empty result set (i.e. zero rows). (Query took', $actual);
self::assertStringContainsString(
"SELECT * FROM `sakila`.`country` WHERE `last_update` &gt; '2025-02-01';",
$actual,
);
self::assertStringContainsString('data-column="country_id"', $actual);
self::assertStringContainsString('data-column="country"', $actual);
self::assertStringContainsString('data-column="last_update"', $actual);
self::assertStringContainsString("<tbody>\n \n </tbody>", $actual);
}
public function testGetDetailedProfilingStatsWithoutData(): void
{
$method = new ReflectionMethod($this->sql, 'getDetailedProfilingStats');
self::assertSame([], $method->invoke($this->sql, []));
}
public function testGetDetailedProfilingStatsWithZeroTotalTime(): void
{
$method = new ReflectionMethod($this->sql, 'getDetailedProfilingStats');
$profiling = [
['Status' => 'Starting', 'Duration' => '0'],
['Status' => 'checking permissions', 'Duration' => '0'],
];
self::assertSame([], $method->invoke($this->sql, $profiling));
}
public function testGetDetailedProfilingStatsWithData(): void
{
$method = new ReflectionMethod($this->sql, 'getDetailedProfilingStats');
$profiling = [
['Status' => 'Starting', 'Duration' => '0.000017'],
['Status' => 'checking permissions', 'Duration' => '0.000003'],
['Status' => 'Opening tables', 'Duration' => '0.000152'],
['Status' => 'After opening tables', 'Duration' => '0.000004'],
['Status' => 'System lock', 'Duration' => '0.000002'],
['Status' => 'table lock', 'Duration' => '0.000003'],
['Status' => 'Opening tables', 'Duration' => '0.000008'],
['Status' => 'After opening tables', 'Duration' => '0.000002'],
['Status' => 'System lock', 'Duration' => '0.000002'],
['Status' => 'table lock', 'Duration' => '0.000012'],
['Status' => 'Unlocking tables', 'Duration' => '0.000003'],
['Status' => 'closing tables', 'Duration' => '0.000005'],
['Status' => 'init', 'Duration' => '0.000007'],
['Status' => 'Optimizing', 'Duration' => '0.000004'],
['Status' => 'Statistics', 'Duration' => '0.000006'],
['Status' => 'Preparing', 'Duration' => '0.000006'],
['Status' => 'Executing', 'Duration' => '0.000002'],
['Status' => 'Sending data', 'Duration' => '0.000029'],
['Status' => 'End of update loop', 'Duration' => '0.000003'],
['Status' => 'Query end', 'Duration' => '0.000002'],
['Status' => 'Commit', 'Duration' => '0.000002'],
['Status' => 'closing tables', 'Duration' => '0.000002'],
['Status' => 'Unlocking tables', 'Duration' => '0.000001'],
['Status' => 'closing tables', 'Duration' => '0.000002'],
['Status' => 'Starting cleanup', 'Duration' => '0.000002'],
['Status' => 'Freeing items', 'Duration' => '0.000002'],
['Status' => 'Updating status', 'Duration' => '0.000007'],
['Status' => 'Reset for next command', 'Duration' => '0.000009'],
];
$expected = [
'total_time' => 0.000299,
'states' => [
'Opening Tables' => ['total_time' => 0.00016, 'calls' => 2],
'Sending Data' => ['total_time' => 0.000029, 'calls' => 1],
'Starting' => ['total_time' => 0.000017, 'calls' => 1],
'Table Lock' => ['total_time' => 0.000015, 'calls' => 2],
'Closing Tables' => ['total_time' => 0.000009, 'calls' => 3],
'Reset For Next Command' => ['total_time' => 0.000009, 'calls' => 1],
'Init' => ['total_time' => 0.000007, 'calls' => 1],
'Updating Status' => ['total_time' => 0.000007, 'calls' => 1],
'After Opening Tables' => ['total_time' => 0.000006, 'calls' => 2],
'Statistics' => ['total_time' => 0.000006, 'calls' => 1],
'Preparing' => ['total_time' => 0.000006, 'calls' => 1],
'System Lock' => ['total_time' => 0.000004, 'calls' => 2],
'Unlocking Tables' => ['total_time' => 0.000004, 'calls' => 2],
'Optimizing' => ['total_time' => 0.000004, 'calls' => 1],
'Checking Permissions' => ['total_time' => 0.000003, 'calls' => 1],
'End Of Update Loop' => ['total_time' => 0.000003, 'calls' => 1],
'Executing' => ['total_time' => 0.000002, 'calls' => 1],
'Query End' => ['total_time' => 0.000002, 'calls' => 1],
'Commit' => ['total_time' => 0.000002, 'calls' => 1],
'Starting Cleanup' => ['total_time' => 0.000002, 'calls' => 1],
'Freeing Items' => ['total_time' => 0.000002, 'calls' => 1],
],
'chart' => [
'labels' => [
'Opening Tables',
'Sending Data',
'Starting',
'Table Lock',
'Closing Tables',
'Reset For Next Command',
'Init',
'Updating Status',
'After Opening Tables',
'Statistics',
'Preparing',
'System Lock',
'Unlocking Tables',
'Optimizing',
'Checking Permissions',
'End Of Update Loop',
'Executing',
'Query End',
'Commit',
'Starting Cleanup',
'Freeing Items',
],
'data' => [
0.00016,
0.000029,
0.000017,
0.000015,
0.000009,
0.000009,
0.000007,
0.000007,
0.000006,
0.000006,
0.000006,
0.000004,
0.000004,
0.000004,
0.000003,
0.000003,
0.000002,
0.000002,
0.000002,
0.000002,
0.000002,
],
],
'profile' => [
['status' => 'Starting', 'duration' => '17 µ', 'duration_raw' => '0.000017'],
['status' => 'Checking Permissions', 'duration' => '3 µ', 'duration_raw' => '0.000003'],
['status' => 'Opening Tables', 'duration' => '152 µ', 'duration_raw' => '0.000152'],
['status' => 'After Opening Tables', 'duration' => '4 µ', 'duration_raw' => '0.000004'],
['status' => 'System Lock', 'duration' => '2 µ', 'duration_raw' => '0.000002'],
['status' => 'Table Lock', 'duration' => '3 µ', 'duration_raw' => '0.000003'],
['status' => 'Opening Tables', 'duration' => '8 µ', 'duration_raw' => '0.000008'],
['status' => 'After Opening Tables', 'duration' => '2 µ', 'duration_raw' => '0.000002'],
['status' => 'System Lock', 'duration' => '2 µ', 'duration_raw' => '0.000002'],
['status' => 'Table Lock', 'duration' => '12 µ', 'duration_raw' => '0.000012'],
['status' => 'Unlocking Tables', 'duration' => '3 µ', 'duration_raw' => '0.000003'],
['status' => 'Closing Tables', 'duration' => '5 µ', 'duration_raw' => '0.000005'],
['status' => 'Init', 'duration' => '7 µ', 'duration_raw' => '0.000007'],
['status' => 'Optimizing', 'duration' => '4 µ', 'duration_raw' => '0.000004'],
['status' => 'Statistics', 'duration' => '6 µ', 'duration_raw' => '0.000006'],
['status' => 'Preparing', 'duration' => '6 µ', 'duration_raw' => '0.000006'],
['status' => 'Executing', 'duration' => '2 µ', 'duration_raw' => '0.000002'],
['status' => 'Sending Data', 'duration' => '29 µ', 'duration_raw' => '0.000029'],
['status' => 'End Of Update Loop', 'duration' => '3 µ', 'duration_raw' => '0.000003'],
['status' => 'Query End', 'duration' => '2 µ', 'duration_raw' => '0.000002'],
['status' => 'Commit', 'duration' => '2 µ', 'duration_raw' => '0.000002'],
['status' => 'Closing Tables', 'duration' => '2 µ', 'duration_raw' => '0.000002'],
['status' => 'Unlocking Tables', 'duration' => '1 µ', 'duration_raw' => '0.000001'],
['status' => 'Closing Tables', 'duration' => '2 µ', 'duration_raw' => '0.000002'],
['status' => 'Starting Cleanup', 'duration' => '2 µ', 'duration_raw' => '0.000002'],
['status' => 'Freeing Items', 'duration' => '2 µ', 'duration_raw' => '0.000002'],
['status' => 'Updating Status', 'duration' => '7 µ', 'duration_raw' => '0.000007'],
['status' => 'Reset For Next Command', 'duration' => '9 µ', 'duration_raw' => '0.000009'],
],
];
self::assertSame($expected, $method->invoke($this->sql, $profiling));
}
}