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