Files
michaelschiemer/src/Framework/Database/Browser/Discovery/TableDiscovery.php
2025-11-24 21:28:25 +01:00

244 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Database\Browser\Discovery;
use App\Framework\Database\Browser\ValueObjects\TableMetadata;
use App\Framework\Database\DatabaseManager;
use App\Framework\Database\ValueObjects\SqlQuery;
final readonly class TableDiscovery
{
public function __construct(
private DatabaseManager $databaseManager
) {
}
private function getConnection(): \App\Framework\Database\ConnectionInterface
{
return $this->databaseManager->getConnection();
}
/**
* Discover all tables in the current database
*
* @return array<TableMetadata>
*/
public function discoverAllTables(): array
{
$driver = $this->getDriver();
if ($driver === 'pgsql') {
return $this->discoverPostgreSQLTables();
}
return $this->discoverMySQLTables();
}
/**
* Discover PostgreSQL tables
*/
private function discoverPostgreSQLTables(): array
{
$sql = "
SELECT
t.tablename as table_name,
COALESCE(s.n_live_tup::bigint, 0) as estimated_rows,
ROUND(pg_total_relation_size('public.'||t.tablename) / 1024.0 / 1024.0, 2) as size_mb,
NULL as engine,
NULL as table_collation
FROM pg_tables t
LEFT JOIN pg_stat_user_tables s ON s.relname = t.tablename
WHERE t.schemaname = 'public'
ORDER BY t.tablename
";
$query = SqlQuery::create($sql, []);
$result = $this->getConnection()->query($query);
$tables = $result->fetchAll();
$tableMetadata = [];
foreach ($tables as $tableData) {
$estimatedRows = isset($tableData['estimated_rows']) && $tableData['estimated_rows'] !== null ? (int) $tableData['estimated_rows'] : 0;
// For small tables (< 1000 estimated rows), get exact count
// This ensures accuracy for small tables where pg_stat_user_tables might be outdated
$rowCount = $estimatedRows;
if ($estimatedRows < 1000) {
try {
$countQuery = SqlQuery::create("SELECT COUNT(*) FROM \"{$tableData['table_name']}\"", []);
$exactCount = $this->getConnection()->queryScalar($countQuery);
$rowCount = $exactCount !== null ? (int) $exactCount : $estimatedRows;
} catch (\Throwable $e) {
// If exact count fails, fall back to estimated count
$rowCount = $estimatedRows;
}
}
$tableMetadata[] = new TableMetadata(
name: $tableData['table_name'],
rowCount: $rowCount,
sizeMb: isset($tableData['size_mb']) ? (float) $tableData['size_mb'] : null,
engine: null, // PostgreSQL doesn't have engine concept
collation: null, // PostgreSQL collation is per-column, not per-table
);
}
return $tableMetadata;
}
/**
* Discover MySQL tables
*/
private function discoverMySQLTables(): array
{
$databaseName = $this->getCurrentDatabase();
$sql = "
SELECT
table_name,
table_rows,
ROUND((data_length + index_length) / 1024 / 1024, 2) as size_mb,
engine,
table_collation
FROM information_schema.tables
WHERE table_schema = ?
AND table_type = 'BASE TABLE'
ORDER BY table_name
";
$query = SqlQuery::create($sql, [$databaseName]);
$result = $this->getConnection()->query($query);
$tables = $result->fetchAll();
$tableMetadata = [];
foreach ($tables as $tableData) {
$tableMetadata[] = new TableMetadata(
name: $tableData['table_name'],
rowCount: isset($tableData['table_rows']) ? (int) $tableData['table_rows'] : null,
sizeMb: isset($tableData['size_mb']) ? (float) $tableData['size_mb'] : null,
engine: $tableData['engine'] ?? null,
collation: $tableData['table_collation'] ?? null,
);
}
return $tableMetadata;
}
/**
* Discover a specific table by name
*/
public function discoverTable(string $tableName): ?TableMetadata
{
$driver = $this->getDriver();
if ($driver === 'pgsql') {
return $this->discoverPostgreSQLTable($tableName);
}
return $this->discoverMySQLTable($tableName);
}
/**
* Discover a specific PostgreSQL table
*/
private function discoverPostgreSQLTable(string $tableName): ?TableMetadata
{
$sql = "
SELECT
t.tablename as table_name,
COALESCE(s.n_live_tup::bigint, 0) as table_rows,
ROUND(pg_total_relation_size('public.'||?) / 1024.0 / 1024.0, 2) as size_mb,
NULL as engine,
NULL as table_collation
FROM pg_tables t
LEFT JOIN pg_stat_user_tables s ON s.relname = t.tablename
WHERE t.schemaname = 'public'
AND t.tablename = ?
";
$query = SqlQuery::create($sql, [$tableName, $tableName]);
$result = $this->getConnection()->query($query);
$row = $result->fetch();
if ($row === null) {
return null;
}
return new TableMetadata(
name: $row['table_name'],
rowCount: isset($row['table_rows']) && $row['table_rows'] !== null ? (int) $row['table_rows'] : null,
sizeMb: isset($row['size_mb']) ? (float) $row['size_mb'] : null,
engine: null,
collation: null,
);
}
/**
* Discover a specific MySQL table
*/
private function discoverMySQLTable(string $tableName): ?TableMetadata
{
$databaseName = $this->getCurrentDatabase();
$sql = "
SELECT
table_name,
table_rows,
ROUND((data_length + index_length) / 1024 / 1024, 2) as size_mb,
engine,
table_collation
FROM information_schema.tables
WHERE table_schema = ?
AND table_name = ?
";
$query = SqlQuery::create($sql, [$databaseName, $tableName]);
$result = $this->getConnection()->query($query);
$row = $result->fetch();
if ($row === null) {
return null;
}
return new TableMetadata(
name: $row['table_name'],
rowCount: isset($row['table_rows']) ? (int) $row['table_rows'] : null,
sizeMb: isset($row['size_mb']) ? (float) $row['size_mb'] : null,
engine: $row['engine'] ?? null,
collation: $row['table_collation'] ?? null,
);
}
/**
* Get current database name
*/
private function getCurrentDatabase(): string
{
$driver = $this->getDriver();
if ($driver === 'pgsql') {
$sql = "SELECT current_database() as db";
} else {
$sql = "SELECT DATABASE() as db";
}
$query = SqlQuery::create($sql, []);
$result = $this->getConnection()->query($query);
$row = $result->fetch();
return $row['db'] ?? 'unknown';
}
/**
* Get database driver name
*/
private function getDriver(): string
{
$pdo = $this->getConnection()->getPdo();
return $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
}
}