Some checks failed
Deploy Application / deploy (push) Has been cancelled
244 lines
7.5 KiB
PHP
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);
|
|
}
|
|
}
|
|
|