You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
5.1 KiB
153 lines
5.1 KiB
<?php |
|
|
|
namespace App\Services; |
|
|
|
use App\Contracts\DatabaseListerInterface; |
|
use Illuminate\Support\Facades\DB; |
|
use InvalidArgumentException; |
|
use Throwable; |
|
|
|
class SqlServerDatabaseService implements DatabaseListerInterface |
|
{ |
|
private const PROTECTED_DATABASES = ['master', 'model', 'msdb', 'tempdb']; |
|
|
|
private const QUERY = <<<'SQL' |
|
SELECT |
|
d.name AS db_name, |
|
CONVERT(varchar(19), d.create_date, 120) AS created, |
|
DATEDIFF(day, d.create_date, GETDATE()) AS active_days, |
|
d.state_desc AS state, |
|
CONVERT(varchar(19), act.last_activity, 120) AS last_index_activity, |
|
CONVERT(varchar(19), bak.last_backup, 120) AS last_backup |
|
FROM sys.databases d |
|
OUTER APPLY ( |
|
SELECT MAX(v) AS last_activity |
|
FROM ( |
|
SELECT last_user_seek AS v FROM sys.dm_db_index_usage_stats WHERE database_id = d.database_id |
|
UNION ALL |
|
SELECT last_user_scan FROM sys.dm_db_index_usage_stats WHERE database_id = d.database_id |
|
UNION ALL |
|
SELECT last_user_lookup FROM sys.dm_db_index_usage_stats WHERE database_id = d.database_id |
|
UNION ALL |
|
SELECT last_user_update FROM sys.dm_db_index_usage_stats WHERE database_id = d.database_id |
|
) x |
|
) act |
|
OUTER APPLY ( |
|
SELECT MAX(backup_finish_date) AS last_backup |
|
FROM msdb.dbo.backupset |
|
WHERE database_name = d.name AND type = 'D' |
|
) bak |
|
SQL; |
|
|
|
private string $connection = 'sqlsrv'; |
|
|
|
public function using(string $connection): self |
|
{ |
|
$clone = clone $this; |
|
$clone->connection = $connection; |
|
|
|
return $clone; |
|
} |
|
|
|
public function listDatabases(string $sort, string $dir, string $search = ''): array |
|
{ |
|
try { |
|
$rows = collect(DB::connection($this->connection)->select(self::QUERY)) |
|
->map(fn ($row) => (array) $row); |
|
|
|
if ($search !== '') { |
|
$needle = mb_strtolower($search); |
|
$rows = $rows->filter(function (array $row) use ($needle) { |
|
return str_contains(mb_strtolower($row['db_name'] ?? ''), $needle) |
|
|| str_contains(mb_strtolower($row['state'] ?? ''), $needle); |
|
}); |
|
} |
|
|
|
$rows = $rows->sortBy(function (array $row) use ($sort) { |
|
if ($sort === 'active_days') { |
|
return (int) ($row['active_days'] ?? 0); |
|
} |
|
|
|
return $row[$sort] ?? ''; |
|
}, SORT_REGULAR, $dir === 'desc'); |
|
|
|
$uptime = DB::connection($this->connection)->selectOne( |
|
'SELECT CONVERT(varchar(19), sqlserver_start_time, 120) AS started, DATEDIFF(day, sqlserver_start_time, GETDATE()) AS uptime_days FROM sys.dm_os_sys_info' |
|
); |
|
|
|
$host = config('database.connections.' . $this->connection . '.host'); |
|
|
|
return [ |
|
'rows' => $rows->values()->all(), |
|
'meta' => [ |
|
'total' => $rows->count(), |
|
'server' => $host, |
|
'driver' => 'sqlsrv', |
|
'sql_started' => $uptime->started ?? null, |
|
'uptime_days' => $uptime->uptime_days ?? null, |
|
], |
|
'error' => null, |
|
]; |
|
} catch (Throwable) { |
|
return [ |
|
'rows' => [], |
|
'meta' => [ |
|
'total' => 0, |
|
'server' => config('database.connections.' . $this->connection . '.host'), |
|
'driver' => 'sqlsrv', |
|
'sql_started' => null, |
|
'uptime_days' => null, |
|
], |
|
'error' => 'Unable to load databases. Please check SQL Server connectivity.', |
|
]; |
|
} |
|
} |
|
|
|
public function makeOffline(string $databaseName): void |
|
{ |
|
$databaseName = $this->validateName($databaseName); |
|
|
|
if ($this->isProtected($databaseName)) { |
|
throw new InvalidArgumentException('System databases cannot be taken offline.'); |
|
} |
|
|
|
$exists = DB::connection($this->connection)->selectOne( |
|
'SELECT name, state_desc FROM sys.databases WHERE name = ?', |
|
[$databaseName] |
|
); |
|
|
|
if (! $exists) { |
|
throw new InvalidArgumentException('Database not found.'); |
|
} |
|
|
|
if (($exists->state_desc ?? '') === 'OFFLINE') { |
|
throw new InvalidArgumentException('Database is already offline.'); |
|
} |
|
|
|
$quoted = str_replace(']', ']]', $databaseName); |
|
DB::connection($this->connection)->statement( |
|
"ALTER DATABASE [{$quoted}] SET OFFLINE WITH ROLLBACK IMMEDIATE" |
|
); |
|
} |
|
|
|
public function isProtected(string $databaseName): bool |
|
{ |
|
return in_array(mb_strtolower($databaseName), self::PROTECTED_DATABASES, true); |
|
} |
|
|
|
public function supportsOffline(): bool |
|
{ |
|
return true; |
|
} |
|
|
|
private function validateName(string $databaseName): string |
|
{ |
|
$databaseName = trim($databaseName); |
|
|
|
if (! preg_match('/^[a-zA-Z0-9_\-]+$/', $databaseName)) { |
|
throw new InvalidArgumentException('Invalid database name.'); |
|
} |
|
|
|
return $databaseName; |
|
} |
|
}
|
|
|