New_Micro_Learning/backend/app/Console/Commands/ImportSqliteDatabase.php

133 خطوط
4.5 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
class ImportSqliteDatabase extends Command
{
protected $signature = 'db:import-sqlite
{path=database/database.sqlite : Path to the legacy SQLite database}
{--chunk=500 : Rows copied per batch}';
protected $description = 'Import all compatible data from the legacy SQLite database into an empty MySQL database';
public function handle(): int
{
if (DB::getDefaultConnection() !== 'mysql') {
$this->error('The default DB_CONNECTION must be mysql.');
return self::FAILURE;
}
$path = $this->absolutePath((string) $this->argument('path'));
if (! is_file($path)) {
$this->error("SQLite database not found: {$path}");
return self::FAILURE;
}
$chunkSize = max(1, (int) $this->option('chunk'));
config(['database.connections.sqlite_import.database' => $path]);
DB::purge('sqlite_import');
$migrationExitCode = Artisan::call('migrate', ['--force' => true]);
$this->output->write(Artisan::output());
if ($migrationExitCode !== self::SUCCESS) {
$this->error('MySQL migrations failed; no SQLite data was copied.');
return self::FAILURE;
}
$sourceTables = array_map(
static fn (object $table): string => $table->name,
DB::connection('sqlite_import')->select(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
)
);
$targetTables = array_map(
static fn (object $table): string => $table->name,
DB::connection('mysql')->select(
"SELECT table_name AS name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'"
)
);
$tables = array_values(array_intersect($sourceTables, $targetTables));
$tables = array_values(array_diff($tables, ['migrations', 'sqlite_sequence']));
$this->assertTargetIsEmpty($tables);
DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=0');
try {
foreach ($tables as $table) {
$copied = $this->copyTable($table, $chunkSize);
$this->line("{$table}: {$copied} rows");
}
} finally {
DB::connection('mysql')->statement('SET FOREIGN_KEY_CHECKS=1');
}
$this->newLine();
$this->info('SQLite data imported into MySQL successfully. The SQLite file was left unchanged.');
return self::SUCCESS;
}
private function assertTargetIsEmpty(array $tables): void
{
foreach ($tables as $table) {
if (DB::connection('mysql')->table($table)->exists()) {
throw new RuntimeException(
"MySQL table [{$table}] is not empty. Import into a new database to prevent duplicate or overwritten data."
);
}
}
}
private function copyTable(string $table, int $chunkSize): int
{
$sourceColumns = Schema::connection('sqlite_import')->getColumnListing($table);
$targetColumns = Schema::connection('mysql')->getColumnListing($table);
$columns = array_values(array_intersect($sourceColumns, $targetColumns));
if ($columns === []) {
$this->warn("{$table}: skipped because it has no compatible columns");
return 0;
}
$copied = 0;
DB::connection('sqlite_import')->table($table)->select($columns)->orderBy($columns[0])->chunk(
$chunkSize,
function ($rows) use ($table, $columns, &$copied): void {
$payload = $rows->map(static function ($row) use ($columns): array {
$values = (array) $row;
return array_intersect_key($values, array_flip($columns));
})->all();
if ($payload !== []) {
DB::connection('mysql')->table($table)->insert($payload);
$copied += count($payload);
}
}
);
return $copied;
}
private function absolutePath(string $path): string
{
if (preg_match('/^(?:[A-Za-z]:[\\\\\/]|\/)/', $path) === 1) {
return $path;
}
return base_path($path);
}
}