first commit

This commit is contained in:
brusnitsyn
2026-03-23 00:51:38 +09:00
commit 07854e0a9d
110 changed files with 19528 additions and 0 deletions

75
app/Models/Column.php Normal file
View File

@@ -0,0 +1,75 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Column extends Model
{
use HasFactory;
protected $fillable = [
'table_id',
'column_name',
'data_type',
'udt_name',
'ordinal_position',
'is_nullable',
'column_default',
'character_maximum_length',
'numeric_precision',
'numeric_scale',
'datetime_precision',
'comment',
'is_primary_key',
'is_foreign_key',
'is_indexed',
'target_column_name',
'target_data_type',
'exclude_from_migration',
];
protected $casts = [
'ordinal_position' => 'integer',
'is_nullable' => 'boolean',
'is_primary_key' => 'boolean',
'is_foreign_key' => 'boolean',
'is_indexed' => 'boolean',
'exclude_from_migration' => 'boolean',
'character_maximum_length' => 'integer',
'numeric_precision' => 'integer',
'numeric_scale' => 'integer',
];
public function table(): BelongsTo
{
return $this->belongsTo(Table::class);
}
public function getFullTypeAttribute(): string
{
$type = $this->data_type;
if ($this->character_maximum_length) {
$type .= "({$this->character_maximum_length})";
} elseif ($this->numeric_precision && $this->numeric_scale) {
$type .= "({$this->numeric_precision},{$this->numeric_scale})";
} elseif ($this->numeric_precision) {
$type .= "({$this->numeric_precision})";
}
return $type;
}
public function getEffectiveColumnNameAttribute(): string
{
return $this->target_column_name ?? $this->column_name;
}
public function getEffectiveDataTypeAttribute(): string
{
return $this->target_data_type ?? $this->data_type;
}
}

27
app/Models/ForeignKey.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ForeignKey extends Model
{
use HasFactory;
protected $fillable = [
'table_id',
'constraint_name',
'column_name',
'referenced_table',
'referenced_column',
'on_update',
'on_delete',
];
public function table(): BelongsTo
{
return $this->belongsTo(Table::class);
}
}

35
app/Models/Index.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Index extends Model
{
use HasFactory;
protected $table = 'indexes';
protected $fillable = [
'table_id',
'index_name',
'columns',
'is_unique',
'is_primary',
'index_type',
'definition',
];
protected $casts = [
'columns' => 'array',
'is_unique' => 'boolean',
'is_primary' => 'boolean',
];
public function table(): BelongsTo
{
return $this->belongsTo(Table::class);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MigrationRun extends Model
{
use HasFactory;
protected $fillable = [
'schedule_id',
'status',
'total_tables',
'processed_tables',
'total_rows',
'migrated_rows',
'failed_rows',
'error_message',
'logs',
'started_at',
'completed_at',
];
protected $casts = [
'logs' => 'array',
'total_tables' => 'integer',
'processed_tables' => 'integer',
'total_rows' => 'integer',
'migrated_rows' => 'integer',
'failed_rows' => 'integer',
'started_at' => 'datetime',
'completed_at' => 'datetime',
];
public function schedule(): BelongsTo
{
return $this->belongsTo(MigrationSchedule::class, 'schedule_id');
}
public function getProgressAttribute(): float
{
if ($this->total_tables === 0) {
return 0;
}
return round(($this->processed_tables / $this->total_tables) * 100, 2);
}
public function getDurationAttribute(): ?string
{
if (!$this->started_at) {
return null;
}
$end = $this->completed_at ?? now();
$duration = $this->started_at->diff($end);
return $duration->format('%H:%I:%S');
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Models;
use Database\Factories\MigrationScheduleFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class MigrationSchedule extends Model
{
use HasFactory;
protected $fillable = [
'name',
'source_database_id',
'target_database_id',
'tables',
'cron_expression',
'timezone',
'is_active',
'is_incremental',
'incremental_column',
'use_life_table',
'life_table_name',
'life_id_column',
'base_id_column',
'operation_column',
'datetime_column',
'run_in_parallel',
'batch_size',
'truncate_before_migration',
'create_indexes_after',
'python_script_path',
'python_script_args',
'description',
'last_run_at',
'last_successful_migration_at',
'next_run_at',
];
protected $casts = [
'tables' => 'array',
'is_active' => 'boolean',
'is_incremental' => 'boolean',
'use_life_table' => 'boolean',
'run_in_parallel' => 'boolean',
'batch_size' => 'integer',
'truncate_before_migration' => 'boolean',
'create_indexes_after' => 'boolean',
'python_script_args' => 'array',
'last_run_at' => 'datetime',
'last_successful_migration_at' => 'datetime',
'next_run_at' => 'datetime',
];
protected static function newFactory(): MigrationScheduleFactory
{
return MigrationScheduleFactory::new();
}
public function sourceDatabase(): BelongsTo
{
return $this->belongsTo(SourceDatabase::class);
}
public function targetDatabase(): BelongsTo
{
return $this->belongsTo(TargetDatabase::class);
}
public function migrationRuns(): HasMany
{
return $this->hasMany(MigrationRun::class, 'schedule_id');
}
public function scheduledTables(): BelongsToMany
{
return $this->belongsToMany(Table::class, 'migration_schedule_tables')
->withPivot('order')
->orderByPivot('order');
}
public function getNextRunAttribute(): ?string
{
if (!$this->is_active || !$this->cron_expression) {
return null;
}
$cron = new \Cron\CronExpression($this->cron_expression);
$nextRun = $cron->getNextRunDate(new \DateTime(), 0, true);
return $nextRun->setTimezone(new \DateTimeZone($this->timezone))->format('Y-m-d H:i:s');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SchemaChange extends Model
{
use HasFactory;
protected $fillable = [
'table_id',
'change_type',
'old_value',
'new_value',
'description',
'is_applied',
'detected_at',
];
protected $casts = [
'old_value' => 'array',
'new_value' => 'array',
'is_applied' => 'boolean',
'detected_at' => 'datetime',
];
public function table(): BelongsTo
{
return $this->belongsTo(Table::class);
}
public function scopePending($query)
{
return $query->where('is_applied', false);
}
public function scopeApplied($query)
{
return $query->where('is_applied', true);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Models;
use Database\Factories\SourceDatabaseFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class SourceDatabase extends Model
{
use HasFactory;
protected $fillable = [
'name',
'host',
'port',
'database',
'username',
'password',
'driver',
'description',
'is_active',
'last_synced_at',
];
protected $casts = [
'port' => 'integer',
'is_active' => 'boolean',
'last_synced_at' => 'datetime',
];
protected static function newFactory(): SourceDatabaseFactory
{
return SourceDatabaseFactory::new();
}
public function tables(): HasMany
{
return $this->hasMany(Table::class);
}
public function migrationSchedules(): HasMany
{
return $this->hasMany(MigrationSchedule::class, 'source_database_id');
}
public function getConnectionConfig(): array
{
$config = [
'driver' => $this->driver,
'host' => $this->host,
'port' => $this->port,
'database' => $this->database,
'username' => $this->username,
'password' => $this->password,
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
];
// Add PostgreSQL-specific options
if ($this->driver === 'pgsql') {
$config['search_path'] = 'public';
$config['sslmode'] = 'prefer';
}
return $config;
}
}

63
app/Models/Table.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Table extends Model
{
use HasFactory;
protected $fillable = [
'source_database_id',
'schema_name',
'table_name',
'comment',
'options',
'last_checked_at',
];
protected $casts = [
'options' => 'array',
'last_checked_at' => 'datetime',
];
public function sourceDatabase(): BelongsTo
{
return $this->belongsTo(SourceDatabase::class);
}
public function columns(): HasMany
{
return $this->hasMany(Column::class);
}
public function indexes(): HasMany
{
return $this->hasMany(Index::class);
}
public function foreignKeys(): HasMany
{
return $this->hasMany(ForeignKey::class);
}
public function schemaChanges(): HasMany
{
return $this->hasMany(SchemaChange::class);
}
public function migrationScheduleTables(): HasMany
{
return $this->hasMany(MigrationScheduleTable::class);
}
public function getFullNameAttribute(): string
{
return "{$this->schema_name}.{$this->table_name}";
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Models;
use Database\Factories\TargetDatabaseFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class TargetDatabase extends Model
{
use HasFactory;
protected $fillable = [
'name',
'host',
'port',
'database',
'username',
'password',
'driver',
'description',
'is_active',
];
protected $casts = [
'port' => 'integer',
'is_active' => 'boolean',
];
protected static function newFactory(): TargetDatabaseFactory
{
return TargetDatabaseFactory::new();
}
public function migrationSchedules(): HasMany
{
return $this->hasMany(MigrationSchedule::class, 'target_database_id');
}
public function getConnectionConfig(): array
{
$config = [
'driver' => $this->driver,
'host' => $this->host,
'port' => $this->port,
'database' => $this->database,
'username' => $this->username,
'password' => $this->password,
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
];
// Add PostgreSQL-specific options
if ($this->driver === 'pgsql') {
$config['search_path'] = 'public';
$config['sslmode'] = 'prefer';
}
return $config;
}
}

32
app/Models/User.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}