Files
project-replica/app/Services/SchemaComparisonService.php
2026-03-23 00:51:38 +09:00

331 lines
12 KiB
PHP

<?php
namespace App\Services;
use App\Models\Table;
use App\Models\Column;
use App\Models\Index;
use App\Models\ForeignKey;
use App\Models\SchemaChange;
use App\Models\SourceDatabase;
use Illuminate\Support\Collection;
class SchemaComparisonService
{
public function __construct(
private SchemaExtractionService $extractionService
) {}
/**
* Check for schema changes in a source database
*/
public function checkForChanges(SourceDatabase $database): Collection
{
$currentTables = $this->extractionService->extractTables($database);
$changes = collect();
foreach ($currentTables as $tableData) {
$table = Table::where('source_database_id', $database->id)
->where('schema_name', $tableData->table_schema)
->where('table_name', $tableData->table_name)
->first();
if (!$table) {
// New table detected
$changes->push([
'type' => 'table_added',
'table_schema' => $tableData->table_schema,
'table_name' => $tableData->table_name,
'description' => "New table detected: {$tableData->table_schema}.{$tableData->table_name}",
]);
continue;
}
// Check for column changes
$columnChanges = $this->checkColumnChanges($table, $database, $tableData->table_schema, $tableData->table_name);
$changes = $changes->merge($columnChanges);
// Check for index changes
$indexChanges = $this->checkIndexChanges($table, $database, $tableData->table_schema, $tableData->table_name);
$changes = $changes->merge($indexChanges);
// Check for foreign key changes
$fkChanges = $this->checkForeignKeyChanges($table, $database, $tableData->table_schema, $tableData->table_name);
$changes = $changes->merge($fkChanges);
$table->update(['last_checked_at' => now()]);
}
// Check for deleted tables
$this->checkDeletedTables($database, $currentTables);
return $changes;
}
/**
* Check for column changes
*/
private function checkColumnChanges(Table $table, SourceDatabase $database, string $schema, string $tableName): Collection
{
$changes = collect();
$currentColumns = $this->extractionService->extractColumns($database, $schema, $tableName);
$storedColumns = $table->columns;
$currentColumnMap = $currentColumns->keyBy('column_name');
$storedColumnMap = $storedColumns->keyBy('column_name');
// Check for new columns
foreach ($currentColumnMap as $columnName => $columnData) {
if (!isset($storedColumnMap[$columnName])) {
$change = SchemaChange::create([
'table_id' => $table->id,
'change_type' => 'column_added',
'old_value' => null,
'new_value' => (array) $columnData,
'description' => "New column added: {$columnName}",
'detected_at' => now(),
]);
$changes->push([
'type' => 'column_added',
'table_name' => $table->table_name,
'column_name' => $columnName,
'description' => "New column added: {$table->table_name}.{$columnName}",
'change' => $change,
]);
}
}
// Check for dropped columns
foreach ($storedColumnMap as $columnName => $column) {
if (!isset($currentColumnMap[$columnName])) {
$change = SchemaChange::create([
'table_id' => $table->id,
'change_type' => 'column_dropped',
'old_value' => $column->toArray(),
'new_value' => null,
'description' => "Column dropped: {$columnName}",
'detected_at' => now(),
]);
$changes->push([
'type' => 'column_dropped',
'table_name' => $table->table_name,
'column_name' => $columnName,
'description' => "Column dropped: {$table->table_name}.{$columnName}",
'change' => $change,
]);
}
}
// Check for modified columns
foreach ($currentColumnMap as $columnName => $currentData) {
if (isset($storedColumnMap[$columnName])) {
$storedColumn = $storedColumnMap[$columnName];
$currentArray = (array) $currentData;
$fieldsToCompare = ['data_type', 'is_nullable', 'column_default', 'character_maximum_length', 'numeric_precision', 'numeric_scale'];
foreach ($fieldsToCompare as $field) {
if ($currentArray[$field] != $storedColumn->$field) {
$change = SchemaChange::create([
'table_id' => $table->id,
'change_type' => 'column_modified',
'old_value' => [$field => $storedColumn->$field],
'new_value' => [$field => $currentArray[$field]],
'description' => "Column modified: {$columnName}.{$field} changed from {$storedColumn->$field} to {$currentArray[$field]}",
'detected_at' => now(),
]);
$changes->push([
'type' => 'column_modified',
'table_name' => $table->table_name,
'column_name' => $columnName,
'field' => $field,
'description' => "Column modified: {$table->table_name}.{$columnName}.{$field}",
'change' => $change,
]);
break;
}
}
}
}
return $changes;
}
/**
* Check for index changes
*/
private function checkIndexChanges(Table $table, SourceDatabase $database, string $schema, string $tableName): Collection
{
$changes = collect();
$currentIndexes = $this->extractionService->extractIndexes($database, $schema, $tableName);
$storedIndexes = $table->indexes;
$currentIndexMap = $currentIndexes->keyBy('index_name');
$storedIndexMap = $storedIndexes->keyBy('index_name');
// Check for new indexes
foreach ($currentIndexMap as $indexName => $indexData) {
if (!isset($storedIndexMap[$indexName])) {
$change = SchemaChange::create([
'table_id' => $table->id,
'change_type' => 'index_added',
'old_value' => null,
'new_value' => (array) $indexData,
'description' => "New index added: {$indexName}",
'detected_at' => now(),
]);
$changes->push([
'type' => 'index_added',
'table_name' => $table->table_name,
'index_name' => $indexName,
'description' => "New index added: {$table->table_name}.{$indexName}",
'change' => $change,
]);
}
}
// Check for dropped indexes
foreach ($storedIndexMap as $indexName => $index) {
if (!isset($currentIndexMap[$indexName])) {
$change = SchemaChange::create([
'table_id' => $table->id,
'change_type' => 'index_dropped',
'old_value' => $index->toArray(),
'new_value' => null,
'description' => "Index dropped: {$indexName}",
'detected_at' => now(),
]);
$changes->push([
'type' => 'index_dropped',
'table_name' => $table->table_name,
'index_name' => $indexName,
'description' => "Index dropped: {$table->table_name}.{$indexName}",
'change' => $change,
]);
}
}
return $changes;
}
/**
* Check for foreign key changes
*/
private function checkForeignKeyChanges(Table $table, SourceDatabase $database, string $schema, string $tableName): Collection
{
$changes = collect();
$currentFKs = $this->extractionService->extractForeignKeys($database, $schema, $tableName);
$storedFKs = $table->foreignKeys;
$currentFKMap = $currentFKs->keyBy('constraint_name');
$storedFKMap = $storedFKs->keyBy('constraint_name');
// Check for new foreign keys
foreach ($currentFKMap as $constraintName => $fkData) {
if (!isset($storedFKMap[$constraintName])) {
$change = SchemaChange::create([
'table_id' => $table->id,
'change_type' => 'foreign_key_added',
'old_value' => null,
'new_value' => (array) $fkData,
'description' => "New foreign key added: {$constraintName}",
'detected_at' => now(),
]);
$changes->push([
'type' => 'foreign_key_added',
'table_name' => $table->table_name,
'constraint_name' => $constraintName,
'description' => "New foreign key added: {$table->table_name}.{$constraintName}",
'change' => $change,
]);
}
}
// Check for dropped foreign keys
foreach ($storedFKMap as $constraintName => $fk) {
if (!isset($currentFKMap[$constraintName])) {
$change = SchemaChange::create([
'table_id' => $table->id,
'change_type' => 'foreign_key_dropped',
'old_value' => $fk->toArray(),
'new_value' => null,
'description' => "Foreign key dropped: {$constraintName}",
'detected_at' => now(),
]);
$changes->push([
'type' => 'foreign_key_dropped',
'table_name' => $table->table_name,
'constraint_name' => $constraintName,
'description' => "Foreign key dropped: {$table->table_name}.{$constraintName}",
'change' => $change,
]);
}
}
return $changes;
}
/**
* Check for deleted tables
*/
private function checkDeletedTables(SourceDatabase $database, Collection $currentTables): void
{
$currentTableNames = $currentTables->map(fn($t) => "{$t->table_schema}.{$t->table_name}");
$storedTables = Table::where('source_database_id', $database->id)->get();
foreach ($storedTables as $table) {
$fullName = "{$table->schema_name}.{$table->table_name}";
if (!$currentTableNames->contains($fullName)) {
SchemaChange::create([
'table_id' => $table->id,
'change_type' => 'table_dropped',
'old_value' => [
'schema_name' => $table->schema_name,
'table_name' => $table->table_name,
],
'new_value' => null,
'description' => "Table dropped: {$fullName}",
'detected_at' => now(),
]);
}
}
}
/**
* Get pending schema changes
*/
public function getPendingChanges(?int $tableId = null): Collection
{
$query = SchemaChange::pending();
if ($tableId) {
$query->where('table_id', $tableId);
}
return $query->with('table')->get();
}
/**
* Mark schema change as applied
*/
public function markAsApplied(int $changeId): bool
{
$change = SchemaChange::find($changeId);
if ($change) {
return $change->update(['is_applied' => true]);
}
return false;
}
}