Skip to content
Merged
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
3 changes: 1 addition & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 17 additions & 19 deletions core/Command/Db/DbIndexUsage.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function __construct(
parent::__construct();
}

#[\Override]
protected function configure(): void {
$this
->setName('db:index-usage')
Expand All @@ -33,39 +34,36 @@ protected function configure(): void {
->addOption('all', null, InputOption::VALUE_NONE, 'Show all indexes, not just unused ones');
}

#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int {
$platform = $this->connection->getDatabasePlatform();
$asJson = $input->getOption('json');
$showAll = $input->getOption('all');
$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
$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
";
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
$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
";
ORDER BY relname, indexrelname";
} else {
$output->writeln('<comment>db:index-usage is not supported for SQLite.</comment>');
$output->writeln('<comment>db:index-usage is not supported for SQLite and Oracle.</comment>');
return Command::SUCCESS;
}

Expand Down Expand Up @@ -105,7 +103,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int

if (!$showAll) {
$output->writeln(sprintf(
'<comment>Found %d unused index(es). Consider removing them to improve write performance.</comment>',
'<comment>Found %d unused index(es). If those were not created by Nextcloud, consider removing them to improve write performance.</comment>',
count($rows)
));
}
Expand Down
198 changes: 100 additions & 98 deletions core/Command/Db/DbInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,102 +20,104 @@

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']],
];
}
public function __construct(
private readonly Connection $connection,
) {
parent::__construct();
}

#[\Override]
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');
}

#[\Override]
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']],
];
}
}
Loading
Loading