first commit
This commit is contained in:
330
app/Services/SchemaComparisonService.php
Normal file
330
app/Services/SchemaComparisonService.php
Normal file
@@ -0,0 +1,330 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user