331 lines
12 KiB
PHP
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;
|
|
}
|
|
}
|