mirror of
https://github.com/nextcloud/server.git
synced 2026-06-14 19:20:35 -04:00
feat(db): add occ db:info, db:size, db:index-usage and db:locks
Implements RFC #59422. Adds four read-only diagnostic commands to the occ CLI for administrators to inspect database health without needing external tools: - db:info: shows engine version and key config variables with health check against recommended values - db:size: lists all tables ordered by total disk usage - db:index-usage: reports unused indexes via performance_schema (MySQL) or pg_stat_user_indexes (PostgreSQL) - db:locks: detects active blocking transactions and deadlocks All commands support MySQL/MariaDB and PostgreSQL. A --json flag is available for automated parsing. Includes 31 unit tests. Closes #59422 Signed-off-by: Rodrigo Correia <rodrigo.mendes.correia@tecnico.ulisboa.pt> Signed-off-by: Carolina Quinteiro <carolinafquinteiro@tecnico.ulisboa.pt> Co-authored-by: Carolina Quinteiro <carolinafquinteiro@tecnico.ulisboa.pt>
This commit is contained in:
parent
6f7961f5ce
commit
729e1e6920
9 changed files with 1044 additions and 0 deletions
115
core/Command/Db/DbIndexUsage.php
Normal file
115
core/Command/Db/DbIndexUsage.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OC\Core\Command\Db;
|
||||
|
||||
use OC\DB\Connection;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
|
||||
class DbIndexUsage extends Command {
|
||||
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('db:index-usage')
|
||||
->setDescription('Report unused database indexes (indexes that slow writes but are never read)')
|
||||
->addOption('json', null, InputOption::VALUE_NONE, 'Output in JSON format')
|
||||
->addOption('all', null, InputOption::VALUE_NONE, 'Show all indexes, not just unused ones');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$platform = $this->connection->getDatabasePlatform();
|
||||
$asJson = $input->getOption('json');
|
||||
$showAll = $input->getOption('all');
|
||||
|
||||
if ($platform instanceof MySQLPlatform) {
|
||||
// Requires performance_schema to be enabled (default in MySQL 5.6+/MariaDB 10.0+)
|
||||
$unused_filter = $showAll ? '' : "WHERE s.count_read = 0 AND s.index_name IS NOT NULL AND s.index_name != 'PRIMARY'";
|
||||
$sql = "
|
||||
SELECT s.object_name AS `table`,
|
||||
s.index_name AS `index`,
|
||||
s.count_read AS reads,
|
||||
s.count_write AS writes
|
||||
FROM performance_schema.table_io_waits_summary_by_index_usage s
|
||||
{$unused_filter}
|
||||
ORDER BY s.object_name, s.index_name
|
||||
";
|
||||
} elseif ($platform instanceof PostgreSQLPlatform) {
|
||||
$unused_filter = $showAll ? '' : 'AND idx_scan = 0';
|
||||
$sql = "
|
||||
SELECT relname AS table,
|
||||
indexrelname AS index,
|
||||
idx_scan AS reads,
|
||||
idx_tup_read AS tuples_read,
|
||||
idx_tup_fetch AS tuples_fetched
|
||||
FROM pg_stat_user_indexes
|
||||
JOIN pg_index USING (indexrelid)
|
||||
WHERE indisunique IS FALSE
|
||||
{$unused_filter}
|
||||
ORDER BY relname, indexrelname
|
||||
";
|
||||
} else {
|
||||
$output->writeln('<comment>db:index-usage is not supported for SQLite.</comment>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
try {
|
||||
$rows = $this->connection->executeQuery($sql)->fetchAllAssociative();
|
||||
} catch (\Doctrine\DBAL\Exception $e) {
|
||||
$output->writeln('<error>Failed to query index usage statistics. The required performance tables may not be available on this database version.</error>');
|
||||
$output->writeln('<comment>Details: ' . $e->getMessage() . '</comment>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if (empty($rows)) {
|
||||
$output->writeln('<info>No unused indexes found. Great!</info>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if ($asJson) {
|
||||
$output->writeln(json_encode($rows, JSON_PRETTY_PRINT));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$table = new Table($output);
|
||||
|
||||
if ($platform instanceof MySQLPlatform) {
|
||||
$table->setHeaders(['Table', 'Index', 'Reads', 'Writes']);
|
||||
foreach ($rows as $row) {
|
||||
$table->addRow([$row['table'], $row['index'], $row['reads'], $row['writes']]);
|
||||
}
|
||||
} else {
|
||||
$table->setHeaders(['Table', 'Index', 'Scans', 'Tuples Read', 'Tuples Fetched']);
|
||||
foreach ($rows as $row) {
|
||||
$table->addRow([$row['table'], $row['index'], $row['reads'], $row['tuples_read'], $row['tuples_fetched']]);
|
||||
}
|
||||
}
|
||||
|
||||
$table->render();
|
||||
|
||||
if (!$showAll) {
|
||||
$output->writeln(sprintf(
|
||||
'<comment>Found %d unused index(es). Consider removing them to improve write performance.</comment>',
|
||||
count($rows)
|
||||
));
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
121
core/Command/Db/DbInfo.php
Normal file
121
core/Command/Db/DbInfo.php
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OC\Core\Command\Db;
|
||||
|
||||
use OC\DB\Connection;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
|
||||
class DbInfo extends Command {
|
||||
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('db:info')
|
||||
->setDescription('Show database server information and configuration health check')
|
||||
->addOption('json', null, InputOption::VALUE_NONE, 'Output in JSON format');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$platform = $this->connection->getDatabasePlatform();
|
||||
$asJson = $input->getOption('json');
|
||||
|
||||
if ($platform instanceof MySQLPlatform) {
|
||||
$rows = $this->getMySQLInfo();
|
||||
} elseif ($platform instanceof PostgreSQLPlatform) {
|
||||
$rows = $this->getPostgreSQLInfo();
|
||||
} elseif ($platform instanceof SqlitePlatform) {
|
||||
$rows = $this->getSQLiteInfo();
|
||||
} else {
|
||||
$output->writeln('<error>Unsupported database platform.</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if ($asJson) {
|
||||
$output->writeln(json_encode($rows, JSON_PRETTY_PRINT));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setHeaders(['Setting', 'Value', 'Recommended', 'Status']);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$status = isset($row['recommended'])
|
||||
? ($row['ok'] ? '<info>OK</info>' : '<comment>CHECK</comment>')
|
||||
: '';
|
||||
$table->addRow([
|
||||
$row['setting'],
|
||||
$row['value'],
|
||||
$row['recommended'] ?? '—',
|
||||
$status,
|
||||
]);
|
||||
}
|
||||
|
||||
$table->render();
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function getMySQLInfo(): array {
|
||||
$result = $this->connection->executeQuery(
|
||||
"SELECT VERSION() AS version, @@innodb_buffer_pool_size AS buffer_pool,
|
||||
@@max_connections AS max_conn, @@character_set_database AS charset,
|
||||
@@transaction_isolation AS tx_isolation"
|
||||
);
|
||||
$info = $result->fetchAssociative();
|
||||
|
||||
$bufferPoolGB = round(($info['buffer_pool'] / 1024 / 1024 / 1024), 2);
|
||||
|
||||
return [
|
||||
['setting' => 'Engine', 'value' => 'MySQL/MariaDB'],
|
||||
['setting' => 'Version', 'value' => $info['version']],
|
||||
['setting' => 'Character Set', 'value' => $info['charset'], 'recommended' => 'utf8mb4', 'ok' => str_contains($info['charset'], 'utf8mb4')],
|
||||
['setting' => 'Max Connections', 'value' => $info['max_conn'], 'recommended' => '≥ 150', 'ok' => (int)$info['max_conn'] >= 150],
|
||||
['setting' => 'InnoDB Buffer Pool (GB)','value' => $bufferPoolGB, 'recommended' => '≥ 1 GB', 'ok' => $bufferPoolGB >= 1],
|
||||
['setting' => 'Transaction Isolation', 'value' => $info['tx_isolation'], 'recommended' => 'READ-COMMITTED', 'ok' => $info['tx_isolation'] === 'READ-COMMITTED'],
|
||||
];
|
||||
}
|
||||
|
||||
private function getPostgreSQLInfo(): array {
|
||||
$result = $this->connection->executeQuery(
|
||||
"SELECT version(),
|
||||
current_setting('max_connections') AS max_conn,
|
||||
current_setting('shared_buffers') AS shared_buffers,
|
||||
current_setting('work_mem') AS work_mem"
|
||||
);
|
||||
$info = $result->fetchAssociative();
|
||||
|
||||
return [
|
||||
['setting' => 'Engine', 'value' => 'PostgreSQL'],
|
||||
['setting' => 'Version', 'value' => $info['version']],
|
||||
['setting' => 'Max Connections', 'value' => $info['max_conn'], 'recommended' => '≥ 100', 'ok' => (int)$info['max_conn'] >= 100],
|
||||
['setting' => 'Shared Buffers', 'value' => $info['shared_buffers'],'recommended' => '128MB+', 'ok' => true],
|
||||
['setting' => 'Work Mem', 'value' => $info['work_mem'], 'recommended' => '4MB+', 'ok' => true],
|
||||
];
|
||||
}
|
||||
|
||||
private function getSQLiteInfo(): array {
|
||||
$result = $this->connection->executeQuery('SELECT sqlite_version() AS version');
|
||||
$info = $result->fetchAssociative();
|
||||
return [
|
||||
['setting' => 'Engine', 'value' => 'SQLite'],
|
||||
['setting' => 'Version', 'value' => $info['version']],
|
||||
];
|
||||
}
|
||||
}
|
||||
103
core/Command/Db/DbLocks.php
Normal file
103
core/Command/Db/DbLocks.php
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OC\Core\Command\Db;
|
||||
|
||||
use OC\DB\Connection;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
|
||||
class DbLocks extends Command {
|
||||
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('db:locks')
|
||||
->setDescription('Show active database locks, deadlocks, and long-running transactions')
|
||||
->addOption('json', null, InputOption::VALUE_NONE, 'Output in JSON format');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$platform = $this->connection->getDatabasePlatform();
|
||||
$asJson = $input->getOption('json');
|
||||
|
||||
if ($platform instanceof MySQLPlatform) {
|
||||
$sql = "
|
||||
SELECT r.trx_id AS waiting_trx_id,
|
||||
r.trx_mysql_thread_id AS waiting_thread,
|
||||
r.trx_query AS waiting_query,
|
||||
b.trx_id AS blocking_trx_id,
|
||||
b.trx_mysql_thread_id AS blocking_thread,
|
||||
b.trx_query AS blocking_query
|
||||
FROM information_schema.innodb_lock_waits w
|
||||
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
|
||||
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
|
||||
";
|
||||
$headers = ['Waiting TRX', 'Waiting Thread', 'Waiting Query', 'Blocking TRX', 'Blocking Thread', 'Blocking Query'];
|
||||
$cols = ['waiting_trx_id', 'waiting_thread', 'waiting_query', 'blocking_trx_id', 'blocking_thread', 'blocking_query'];
|
||||
} elseif ($platform instanceof PostgreSQLPlatform) {
|
||||
$sql = "
|
||||
SELECT blocked_locks.pid AS blocked_pid,
|
||||
blocked_activity.usename AS blocked_user,
|
||||
blocking_locks.pid AS blocking_pid,
|
||||
blocking_activity.usename AS blocking_user,
|
||||
blocked_activity.query AS blocked_query,
|
||||
blocking_activity.query AS blocking_query,
|
||||
now() - blocked_activity.query_start AS blocked_duration
|
||||
FROM pg_catalog.pg_locks blocked_locks
|
||||
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
|
||||
JOIN pg_catalog.pg_locks blocking_locks
|
||||
ON blocking_locks.locktype = blocked_locks.locktype
|
||||
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
|
||||
AND blocking_locks.pid != blocked_locks.pid
|
||||
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
|
||||
WHERE NOT blocked_locks.granted
|
||||
";
|
||||
$headers = ['Blocked PID', 'Blocked User', 'Blocking PID', 'Blocking User', 'Blocked Query', 'Duration'];
|
||||
$cols = ['blocked_pid', 'blocked_user', 'blocking_pid', 'blocking_user', 'blocked_query', 'blocked_duration'];
|
||||
} else {
|
||||
$output->writeln('<comment>db:locks is not supported for SQLite (SQLite uses file-level locking).</comment>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$rows = $this->connection->executeQuery($sql)->fetchAllAssociative();
|
||||
|
||||
if (empty($rows)) {
|
||||
$output->writeln('<info>No active locks or blocking transactions detected.</info>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if ($asJson) {
|
||||
$output->writeln(json_encode($rows, JSON_PRETTY_PRINT));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$output->writeln(sprintf('<error>Found %d blocking transaction(s)!</error>', count($rows)));
|
||||
$output->writeln('');
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setHeaders($headers);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$table->addRow(array_map(fn($col) => $row[$col] ?? '—', $cols));
|
||||
}
|
||||
|
||||
$table->render();
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
94
core/Command/Db/DbSize.php
Normal file
94
core/Command/Db/DbSize.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OC\Core\Command\Db;
|
||||
|
||||
use OC\DB\Connection;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
|
||||
class DbSize extends Command {
|
||||
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('db:size')
|
||||
->setDescription('Show disk usage of all Nextcloud database tables, ordered by size')
|
||||
->addOption('json', null, InputOption::VALUE_NONE, 'Output in JSON format');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$platform = $this->connection->getDatabasePlatform();
|
||||
$asJson = $input->getOption('json');
|
||||
|
||||
if ($platform instanceof MySQLPlatform) {
|
||||
$sql = "
|
||||
SELECT table_name AS `table`,
|
||||
ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_mb,
|
||||
ROUND(data_length / 1024 / 1024, 2) AS data_mb,
|
||||
ROUND(index_length / 1024 / 1024, 2) AS index_mb,
|
||||
table_rows AS rows,
|
||||
IF(table_rows > 0, ROUND((data_length + index_length) / table_rows, 0), 0) AS avg_row_bytes
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
ORDER BY (data_length + index_length) DESC
|
||||
";
|
||||
$headers = ['Table', 'Total (MB)', 'Data (MB)', 'Index (MB)', 'Rows', 'Avg Row (bytes)'];
|
||||
$cols = ['table', 'total_mb', 'data_mb', 'index_mb', 'rows', 'avg_row_bytes'];
|
||||
} elseif ($platform instanceof PostgreSQLPlatform) {
|
||||
$sql = "
|
||||
SELECT relname AS table,
|
||||
ROUND(pg_total_relation_size(c.oid) / 1024.0 / 1024.0, 2) AS total_mb,
|
||||
ROUND(pg_relation_size(c.oid) / 1024.0 / 1024.0, 2) AS data_mb,
|
||||
ROUND(pg_indexes_size(c.oid) / 1024.0 / 1024.0, 2) AS index_mb,
|
||||
c.reltuples::bigint AS rows,
|
||||
CASE WHEN c.reltuples > 0 THEN ROUND(pg_total_relation_size(c.oid) / c.reltuples) ELSE 0 END AS avg_row_bytes
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relkind = 'r' AND n.nspname = 'public'
|
||||
ORDER BY pg_total_relation_size(c.oid) DESC
|
||||
";
|
||||
$headers = ['Table', 'Total (MB)', 'Data (MB)', 'Index (MB)', 'Rows (est.)', 'Avg Row (bytes)'];
|
||||
$cols = ['table', 'total_mb', 'data_mb', 'index_mb', 'rows', 'avg_row_bytes'];
|
||||
} else {
|
||||
$output->writeln('<comment>db:size is not supported for SQLite.</comment>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$rows = $this->connection->executeQuery($sql)->fetchAllAssociative();
|
||||
|
||||
if ($asJson) {
|
||||
$output->writeln(json_encode($rows, JSON_PRETTY_PRINT));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setHeaders($headers);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$table->addRow(array_map(fn($col) => $row[$col], $cols));
|
||||
}
|
||||
|
||||
$table->render();
|
||||
|
||||
$totalMB = array_sum(array_column($rows, 'total_mb'));
|
||||
$output->writeln(sprintf('<info>Total database size: %.2f MB</info>', $totalMB));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,10 @@ use OC\Core\Command\Db\AddMissingPrimaryKeys;
|
|||
use OC\Core\Command\Db\ConvertFilecacheBigInt;
|
||||
use OC\Core\Command\Db\ConvertMysqlToMB4;
|
||||
use OC\Core\Command\Db\ConvertType;
|
||||
use OC\Core\Command\Db\DbIndexUsage;
|
||||
use OC\Core\Command\Db\DbInfo;
|
||||
use OC\Core\Command\Db\DbLocks;
|
||||
use OC\Core\Command\Db\DbSize;
|
||||
use OC\Core\Command\Db\ExpectedSchema;
|
||||
use OC\Core\Command\Db\ExportSchema;
|
||||
use OC\Core\Command\Db\Migrations\ExecuteCommand;
|
||||
|
|
@ -168,6 +172,10 @@ if ($config->getSystemValueBool('installed', false)) {
|
|||
$application->add(Server::get(AddMissingColumns::class));
|
||||
$application->add(Server::get(AddMissingIndices::class));
|
||||
$application->add(Server::get(AddMissingPrimaryKeys::class));
|
||||
$application->add(Server::get(DbInfo::class));
|
||||
$application->add(Server::get(DbSize::class));
|
||||
$application->add(Server::get(DbIndexUsage::class));
|
||||
$application->add(Server::get(DbLocks::class));
|
||||
$application->add(Server::get(ExpectedSchema::class));
|
||||
$application->add(Server::get(ExportSchema::class));
|
||||
|
||||
|
|
|
|||
164
tests/Core/Command/Db/DbIndexUsageTest.php
Normal file
164
tests/Core/Command/Db/DbIndexUsageTest.php
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Core\Command\Db;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use OC\Core\Command\Db\DbIndexUsage;
|
||||
use OC\DB\Connection;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Test\TestCase;
|
||||
|
||||
class DbIndexUsageTest extends TestCase {
|
||||
|
||||
private Connection&MockObject $connection;
|
||||
private InputInterface&MockObject $input;
|
||||
private DbIndexUsage $command;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->input = $this->createMock(InputInterface::class);
|
||||
$this->command = new DbIndexUsage($this->connection);
|
||||
}
|
||||
|
||||
private function mockMySQLRows(): array {
|
||||
return [
|
||||
['table' => 'oc_filecache', 'index' => 'idx_fc_name', 'reads' => 0, 'writes' => 150],
|
||||
['table' => 'oc_share', 'index' => 'idx_sh_par', 'reads' => 0, 'writes' => 42],
|
||||
];
|
||||
}
|
||||
|
||||
private function mockPostgreSQLRows(): array {
|
||||
return [
|
||||
['table' => 'oc_filecache', 'index' => 'idx_fc_name', 'reads' => 0, 'tuples_read' => 0, 'tuples_fetched' => 0],
|
||||
];
|
||||
}
|
||||
|
||||
private function mockResult(array $rows): Result&MockObject {
|
||||
$result = $this->createMock(Result::class);
|
||||
$result->method('fetchAllAssociative')->willReturn($rows);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function testNoUnusedIndexesPrintsSuccessMessage(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult([]));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false], ['all', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('No unused indexes found', $output->fetch());
|
||||
}
|
||||
|
||||
public function testMySQLUnusedIndexesRendersTable(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockMySQLRows()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false], ['all', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$content = $output->fetch();
|
||||
$this->assertStringContainsString('Reads', $content);
|
||||
$this->assertStringContainsString('Writes', $content);
|
||||
$this->assertStringContainsString('idx_fc_name', $content);
|
||||
$this->assertStringContainsString('Found 2 unused index(es)', $content);
|
||||
}
|
||||
|
||||
public function testPostgreSQLUnusedIndexesRendersTable(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(PostgreSQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockPostgreSQLRows()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false], ['all', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$content = $output->fetch();
|
||||
$this->assertStringContainsString('Tuples Read', $content);
|
||||
$this->assertStringContainsString('Tuples Fetched', $content);
|
||||
}
|
||||
|
||||
public function testAllFlagSuppressesCountMessage(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockMySQLRows()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false], ['all', true]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertStringNotContainsString('Found', $output->fetch());
|
||||
}
|
||||
|
||||
public function testDefaultFilterIncludedInQuery(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->with($this->stringContains('count_read = 0'))
|
||||
->willReturn($this->mockResult([]));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false], ['all', false]]);
|
||||
|
||||
self::invokePrivate($this->command, 'execute', [$this->input, new BufferedOutput()]);
|
||||
}
|
||||
|
||||
public function testAllFlagRemovesFilterFromQuery(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->expects($this->once())
|
||||
->method('executeQuery')
|
||||
->with($this->logicalNot($this->stringContains('count_read = 0')))
|
||||
->willReturn($this->mockResult([]));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false], ['all', true]]);
|
||||
|
||||
self::invokePrivate($this->command, 'execute', [$this->input, new BufferedOutput()]);
|
||||
}
|
||||
|
||||
public function testJsonOutputWhenRowsExist(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockMySQLRows()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', true], ['all', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$data = json_decode($output->fetch(), true);
|
||||
$this->assertIsArray($data);
|
||||
$this->assertCount(2, $data);
|
||||
$this->assertArrayHasKey('table', $data[0]);
|
||||
$this->assertArrayHasKey('index', $data[0]);
|
||||
}
|
||||
|
||||
public function testSQLiteReturnsSuccessWithMessage(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(SqlitePlatform::class));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false], ['all', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('not supported for SQLite', $output->fetch());
|
||||
}
|
||||
}
|
||||
166
tests/Core/Command/Db/DbInfoTest.php
Normal file
166
tests/Core/Command/Db/DbInfoTest.php
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Tests\Core\Command\Db;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use OC\Core\Command\Db\DbInfo;
|
||||
use OC\DB\Connection;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Test\TestCase;
|
||||
|
||||
class DbInfoTest extends TestCase {
|
||||
|
||||
private Connection&MockObject $connection;
|
||||
private InputInterface&MockObject $input;
|
||||
private DbInfo $command;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->input = $this->createMock(InputInterface::class);
|
||||
$this->command = new DbInfo($this->connection);
|
||||
}
|
||||
|
||||
private function mockMySQLResult(array $overrides = []): Result&MockObject {
|
||||
$result = $this->createMock(Result::class);
|
||||
$result->method('fetchAssociative')->willReturn(array_merge([
|
||||
'version' => '8.0.30',
|
||||
'buffer_pool' => 1073741824, // 1 GB
|
||||
'max_conn' => '200',
|
||||
'charset' => 'utf8mb4',
|
||||
'tx_isolation' => 'READ-COMMITTED',
|
||||
], $overrides));
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function mockPostgreSQLResult(): Result&MockObject {
|
||||
$result = $this->createMock(Result::class);
|
||||
$result->method('fetchAssociative')->willReturn([
|
||||
'version' => 'PostgreSQL 15.2 on x86_64',
|
||||
'max_conn' => '100',
|
||||
'shared_buffers' => '128MB',
|
||||
'work_mem' => '4MB',
|
||||
]);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function testMySQLTableOutput(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockMySQLResult());
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$content = $output->fetch();
|
||||
$this->assertStringContainsString('Setting', $content);
|
||||
$this->assertStringContainsString('MySQL/MariaDB', $content);
|
||||
}
|
||||
|
||||
public function testPostgreSQLTableOutput(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(PostgreSQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockPostgreSQLResult());
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('PostgreSQL', $output->fetch());
|
||||
}
|
||||
|
||||
public function testSQLiteTableOutput(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(SqlitePlatform::class));
|
||||
$result = $this->createMock(Result::class);
|
||||
$result->method('fetchAssociative')->willReturn(['version' => '3.43.0']);
|
||||
$this->connection->method('executeQuery')->willReturn($result);
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('SQLite', $output->fetch());
|
||||
}
|
||||
|
||||
public function testUnsupportedPlatformReturnsFailure(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(AbstractPlatform::class));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(1, $exit);
|
||||
$this->assertStringContainsString('Unsupported', $output->fetch());
|
||||
}
|
||||
|
||||
public function testJsonOutputContainsSettingKeys(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockMySQLResult());
|
||||
$this->input->method('getOption')->willReturnMap([['json', true]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$data = json_decode($output->fetch(), true);
|
||||
$this->assertIsArray($data);
|
||||
$this->assertArrayHasKey('setting', $data[0]);
|
||||
$this->assertArrayHasKey('value', $data[0]);
|
||||
}
|
||||
|
||||
public static function dataMySQLHealthChecks(): array {
|
||||
return [
|
||||
'charset utf8mb4 → OK' => ['charset', 'utf8mb4', true, 'Character Set'],
|
||||
'charset latin1 → CHECK' => ['charset', 'latin1', false, 'Character Set'],
|
||||
'max_conn 200 → OK' => ['max_conn', '200', true, 'Max Connections'],
|
||||
'max_conn 50 → CHECK' => ['max_conn', '50', false, 'Max Connections'],
|
||||
'tx_isolation READ-COMMITTED → OK' => ['tx_isolation', 'READ-COMMITTED', true, 'Transaction Isolation'],
|
||||
'tx_isolation REPEATABLE-READ → CHECK' => ['tx_isolation', 'REPEATABLE-READ', false, 'Transaction Isolation'],
|
||||
];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataMySQLHealthChecks')]
|
||||
public function testMySQLHealthCheckStatus(
|
||||
string $field,
|
||||
string $value,
|
||||
bool $expectedOk,
|
||||
string $settingLabel,
|
||||
): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockMySQLResult([$field => $value]));
|
||||
$this->input->method('getOption')->willReturnMap([['json', true]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$data = json_decode($output->fetch(), true);
|
||||
$rows = array_values(array_filter($data, fn($r) => $r['setting'] === $settingLabel));
|
||||
$this->assertNotEmpty($rows, "Setting '{$settingLabel}' not found in JSON output");
|
||||
$this->assertSame($expectedOk, $rows[0]['ok']);
|
||||
}
|
||||
}
|
||||
156
tests/Core/Command/Db/DbLocksTest.php
Normal file
156
tests/Core/Command/Db/DbLocksTest.php
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Core\Command\Db;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use OC\Core\Command\Db\DbLocks;
|
||||
use OC\DB\Connection;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Test\TestCase;
|
||||
|
||||
class DbLocksTest extends TestCase {
|
||||
|
||||
private Connection&MockObject $connection;
|
||||
private InputInterface&MockObject $input;
|
||||
private DbLocks $command;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->input = $this->createMock(InputInterface::class);
|
||||
$this->command = new DbLocks($this->connection);
|
||||
}
|
||||
|
||||
private function mockMySQLLocks(): array {
|
||||
return [[
|
||||
'waiting_trx_id' => '12345',
|
||||
'waiting_thread' => '42',
|
||||
'waiting_query' => 'UPDATE oc_filecache SET path_hash = ?',
|
||||
'blocking_trx_id' => '12344',
|
||||
'blocking_thread' => '41',
|
||||
'blocking_query' => null, // NULL — deve ser renderizado como '—'
|
||||
]];
|
||||
}
|
||||
|
||||
private function mockPostgreSQLLocks(): array {
|
||||
return [[
|
||||
'blocked_pid' => 1234,
|
||||
'blocked_user' => 'nextcloud',
|
||||
'blocking_pid' => 1233,
|
||||
'blocking_user' => 'nextcloud',
|
||||
'blocked_query' => 'SELECT * FROM oc_filecache WHERE parent = ?',
|
||||
'blocked_duration' => '00:00:05.123456',
|
||||
]];
|
||||
}
|
||||
|
||||
private function mockResult(array $rows): Result&MockObject {
|
||||
$result = $this->createMock(Result::class);
|
||||
$result->method('fetchAllAssociative')->willReturn($rows);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function testMySQLNoLocksShowsInfoMessage(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult([]));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('No active locks', $output->fetch());
|
||||
}
|
||||
|
||||
public function testPostgreSQLNoLocksShowsInfoMessage(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(PostgreSQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult([]));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('No active locks', $output->fetch());
|
||||
}
|
||||
|
||||
public function testMySQLLocksFoundShowsErrorMessage(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockMySQLLocks()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('Found 1 blocking transaction(s)', $output->fetch());
|
||||
}
|
||||
|
||||
public function testPostgreSQLLocksFoundShowsErrorMessage(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(PostgreSQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockPostgreSQLLocks()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('Found 1 blocking transaction(s)', $output->fetch());
|
||||
}
|
||||
|
||||
public function testJsonOutputWhenLocksExist(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockMySQLLocks()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', true]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$data = json_decode($output->fetch(), true);
|
||||
$this->assertIsArray($data);
|
||||
$this->assertCount(1, $data);
|
||||
$this->assertArrayHasKey('waiting_trx_id', $data[0]);
|
||||
}
|
||||
|
||||
public function testSQLiteReturnsSuccessWithMessage(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(SqlitePlatform::class));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('file-level locking', $output->fetch());
|
||||
}
|
||||
|
||||
public function testNullColumnRenderedAsDash(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockMySQLLocks())); // blocking_query = null
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertStringContainsString('—', $output->fetch());
|
||||
}
|
||||
}
|
||||
117
tests/Core/Command/Db/DbSizeTest.php
Normal file
117
tests/Core/Command/Db/DbSizeTest.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Core\Command\Db;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use OC\Core\Command\Db\DbSize;
|
||||
use OC\DB\Connection;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Test\TestCase;
|
||||
|
||||
class DbSizeTest extends TestCase {
|
||||
|
||||
private Connection&MockObject $connection;
|
||||
private InputInterface&MockObject $input;
|
||||
private DbSize $command;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->input = $this->createMock(InputInterface::class);
|
||||
$this->command = new DbSize($this->connection);
|
||||
}
|
||||
|
||||
private function mockRows(): array {
|
||||
return [
|
||||
['table' => 'oc_filecache', 'total_mb' => 12.50, 'data_mb' => 10.00, 'index_mb' => 2.50, 'rows' => 5000, 'avg_row_bytes' => 2560],
|
||||
['table' => 'oc_share', 'total_mb' => 3.25, 'data_mb' => 2.00, 'index_mb' => 1.25, 'rows' => 200, 'avg_row_bytes' => 16384],
|
||||
];
|
||||
}
|
||||
|
||||
private function mockResult(array $rows): Result&MockObject {
|
||||
$result = $this->createMock(Result::class);
|
||||
$result->method('fetchAllAssociative')->willReturn($rows);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function testMySQLOutputContainsTableAndTotal(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockRows()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$content = $output->fetch();
|
||||
$this->assertStringContainsString('oc_filecache', $content);
|
||||
$this->assertStringContainsString('Total database size', $content);
|
||||
}
|
||||
|
||||
public function testPostgreSQLOutputContainsTableAndTotal(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(PostgreSQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockRows()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('Total database size', $output->fetch());
|
||||
}
|
||||
|
||||
public function testSQLiteReturnsSuccessWithMessage(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(SqlitePlatform::class));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$this->assertStringContainsString('not supported for SQLite', $output->fetch());
|
||||
}
|
||||
|
||||
public function testJsonOutputIsValidArray(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockRows()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', true]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$exit = self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
$this->assertSame(0, $exit);
|
||||
$data = json_decode($output->fetch(), true);
|
||||
$this->assertIsArray($data);
|
||||
$this->assertCount(2, $data);
|
||||
$this->assertArrayHasKey('table', $data[0]);
|
||||
$this->assertArrayHasKey('total_mb', $data[0]);
|
||||
}
|
||||
|
||||
public function testTotalSizeCalculation(): void {
|
||||
$this->connection->method('getDatabasePlatform')
|
||||
->willReturn($this->createMock(MySQLPlatform::class));
|
||||
$this->connection->method('executeQuery')
|
||||
->willReturn($this->mockResult($this->mockRows()));
|
||||
$this->input->method('getOption')->willReturnMap([['json', false]]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
self::invokePrivate($this->command, 'execute', [$this->input, $output]);
|
||||
|
||||
// 12.50 + 3.25 = 15.75
|
||||
$this->assertStringContainsString('15.75', $output->fetch());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue