<?php
namespace Illuminate\Database\Console;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Illuminate\Console\Command;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\QueryException;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\SqlServerConnection;
use Illuminate\Support\Arr;
use Illuminate\Support\Composer;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Process;
abstract class DatabaseInspectionCommand extends Command
{
/**
* A map of database column types.
*
* @var array
*/
protected $typeMappings = [
'bit' => 'string',
'citext' => 'string',
'enum' => 'string',
'geometry' => 'string',
'geomcollection' => 'string',
'linestring' => 'string',
'ltree' => 'string',
'multilinestring' => 'string',
'multipoint' => 'string',
'multipolygon' => 'string',
'point' => 'string',
'polygon' => 'string',
'sysname' => 'string',
];
/**
* The Composer instance.
*
* @var \Illuminate\Support\Composer
*/
protected $composer;
/**
* Create a new command instance.
*
* @param \Illuminate\Support\Composer|null $composer
* @return void
*/
public function __construct(Composer $composer = null)
{
parent::__construct();
$this->composer = $composer ?? $this->laravel->make(Composer::class);
}
/**
* Register the custom Doctrine type mappings for inspection commands.
*
* @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
* @return void
*/
protected function registerTypeMappings(AbstractPlatform $platform)
{
foreach ($this->typeMappings as $type => $value) {
$platform->registerDoctrineTypeMapping($type, $value);
}
}
/**
* Get a human-readable platform name for the given platform.
*
* @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
* @param string $database
* @return string
*/
protected function getPlatformName(AbstractPlatform $platform, $database)
{
return match (class_basename($platform)) {
'MySQLPlatform' => 'MySQL <= 5',
'MySQL57Platform' => 'MySQL 5.7',
'MySQL80Platform' => 'MySQL 8',
'PostgreSQL100Platform', 'PostgreSQLPlatform' => 'Postgres',
'SqlitePlatform' => 'SQLite',
'SQLServerPlatform' => 'SQL Server',
'SQLServer2012Platform' => 'SQL Server 2012',
default => $database,
};
}
/**
* Get the size of a table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return int|null
*/
protected function getTableSize(ConnectionInterface $connection, string $table)
{
return match (true) {
$connection instanceof MySqlConnection => $this->getMySQLTableSize($connection, $table),
$connection instanceof PostgresConnection => $this->getPostgresTableSize($connection, $table),
$connection instanceof SQLiteConnection => $this->getSqliteTableSize($connection, $table),
default => null,
};
}
/**
* Get the size of a MySQL table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return mixed
*/
protected function getMySQLTableSize(ConnectionInterface $connection, string $table)
{
$result = $connection->selectOne('SELECT (data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = ? AND table_name = ?', [
$connection->getDatabaseName(),
$table,
]);
return Arr::wrap((array) $result)['size'];
}
/**
* Get the size of a Postgres table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return mixed
*/
protected function getPostgresTableSize(ConnectionInterface $connection, string $table)
{
$result = $connection->selectOne('SELECT pg_total_relation_size(?) AS size;', [
$table,
]);
return Arr::wrap((array) $result)['size'];
}
/**
* Get the size of a SQLite table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return mixed
*/
protected function getSqliteTableSize(ConnectionInterface $connection, string $table)
{
try {
$result = $connection->selectOne('SELECT SUM(pgsize) AS size FROM dbstat WHERE name=?', [
$table,
]);
return Arr::wrap((array) $result)['size'];
} catch (QueryException $e) {
return null;
}
}
/**
* Get the number of open connections for a database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return int|null
*/
protected function getConnectionCount(ConnectionInterface $connection)
{
$result = match (true) {
$connection instanceof MySqlConnection => $connection->selectOne('show status where variable_name = "threads_connected"'),
$connection instanceof PostgresConnection => $connection->selectOne('select count(*) AS "Value" from pg_stat_activity'),
$connection instanceof SqlServerConnection => $connection->selectOne('SELECT COUNT(*) Value FROM sys.dm_exec_sessions WHERE status = ?', ['running']),
default => null,
};
if (! $result) {
return null;
}
return Arr::wrap((array) $result)['Value'];
}
/**
* Get the connection configuration details for the given connection.
*
* @param string $database
* @return array
*/
protected function getConfigFromDatabase($database)
{
$database ??= config('database.default');
return Arr::except(config('database.connections.'.$database), ['password']);
}
/**
* Ensure the dependencies for the database commands are available.
*
* @return bool
*/
protected function ensureDependenciesExist()
{
return tap(interface_exists('Doctrine\DBAL\Driver'), function ($dependenciesExist) {
if (! $dependenciesExist && $this->components->confirm('Inspecting database information requires the Doctrine DBAL (doctrine/dbal) package. Would you like to install it?')) {
$this->installDependencies();
}
});
}
/**
* Install the command's dependencies.
*
* @return void
*
* @throws \Symfony\Component\Process\Exception\ProcessSignaledException
*/
protected function installDependencies()
{
$command = collect($this->composer->findComposer())
->push('require doctrine/dbal')
->implode(' ');
$process = Process::fromShellCommandline($command, null, null, null, null);
if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
try {
$process->setTty(true);
} catch (RuntimeException $e) {
$this->components->warn($e->getMessage());
}
}
try {
$process->run(fn ($type, $line) => $this->output->write($line));
} catch (ProcessSignaledException $e) {
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}
}
}
}