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; } }