This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
DataMigration/files/custom/Espo/Modules/DataMigration/Services/ImportService.php
T
chaim fd279b6c8b fix: skip columns missing in target DB during raw SQL import
When importing entities with autoincrement (Case, Charge, Invoice),
validate each column exists in the target table before inserting.
Columns from modules not installed on the target are silently skipped
and reported in the import log summary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:25:54 +00:00

206 lines
6.4 KiB
PHP

<?php
namespace Espo\Modules\DataMigration\Services;
use Espo\ORM\EntityManager;
use Espo\Core\Utils\Log;
use Espo\Core\Console\IO;
class ImportService
{
/**
* Import order: entities must be imported in dependency order.
*/
private const IMPORT_ORDER = [
'User',
'Team',
'FixedActivityRate',
'Account',
'Contact',
'PricingAgreement',
'PricingAgreementRate',
'ClientRate',
'Case',
'CaseActivity',
'Invoice',
'Document',
'NhDocument',
'NhMeeting',
'NhDecision',
'NhProcess',
'NhPlea',
'NhActivity',
'NhSyncLog',
'CaseMemory',
'SignatureRequest',
'Charge',
'SignatureRequestSigner',
'SmsLog',
'Note',
'Attachment',
];
public function __construct(
private EntityManager $entityManager,
private EntityImporter $entityImporter,
private JunctionTableHandler $junctionTableHandler,
private AttachmentHandler $attachmentHandler,
private ManifestBuilder $manifestBuilder,
private Log $log
) {}
/**
* Run full import from backup directory.
*
* @param string $inputDir Backup directory path
* @param array $options {batchSize: int, preserveIds: bool, skipDuplicates: bool, dryRun: bool, verbose: bool}
* @param IO $io
*/
public function import(string $inputDir, array $options, IO $io): void
{
$batchSize = $options['batchSize'] ?? 500;
$preserveIds = $options['preserveIds'] ?? true;
$skipDuplicates = $options['skipDuplicates'] ?? false;
$dryRun = $options['dryRun'] ?? false;
$noFiles = $options['noFiles'] ?? false;
$verbose = $options['verbose'] ?? false;
$dataDir = rtrim($inputDir, '/') . '/data';
$filesDir = rtrim($inputDir, '/') . '/files';
// Read and display manifest
$manifest = $this->manifestBuilder->read($inputDir);
if ($manifest === null) {
$io->writeLine('ERROR: No manifest.json found in backup directory.');
return;
}
$io->writeLine('=== Backup Manifest ===');
$io->writeLine("Source: " . ($manifest['sourceHost'] ?? 'unknown'));
$io->writeLine("Exported at: " . ($manifest['exportedAt'] ?? 'unknown'));
$io->writeLine("EspoCRM version: " . ($manifest['espoVersion'] ?? 'unknown'));
$io->writeLine('');
if ($dryRun) {
$io->writeLine('*** DRY RUN MODE - No changes will be made ***');
$io->writeLine('');
}
$idMapper = new IdMapper();
$totalImported = 0;
$totalSkipped = 0;
$totalErrors = 0;
$results = [];
// Import entities in order
foreach (self::IMPORT_ORDER as $entityType) {
$jsonFile = $dataDir . '/' . $entityType . '.json';
if (!file_exists($jsonFile)) {
continue;
}
if (!$this->entityTypeExists($entityType)) {
$io->writeLine("Skipping {$entityType} (module not installed)");
continue;
}
$io->writeLine("Importing {$entityType}...");
$result = $this->entityImporter->import(
$entityType,
$jsonFile,
$preserveIds,
$skipDuplicates,
$idMapper,
$batchSize,
$verbose,
$dryRun,
$io
);
$results[$entityType] = $result;
$totalImported += $result['imported'];
$totalSkipped += $result['skipped'];
$totalErrors += $result['errors'];
$io->writeLine(" {$entityType}: imported={$result['imported']}, skipped={$result['skipped']}, errors={$result['errors']}");
}
// Import junction tables
$io->writeLine('');
$io->writeLine('Importing junction tables...');
if (!$dryRun) {
$this->junctionTableHandler->importJunctions($dataDir, $idMapper, $preserveIds, $io);
} else {
$io->writeLine(' (skipped in dry-run mode)');
}
// Restore attachment files
if ($noFiles) {
$io->writeLine('');
$io->writeLine('Attachment files: SKIPPED (--no-files)');
} else if (is_dir($filesDir)) {
$io->writeLine('');
$io->writeLine('Restoring attachment files...');
if (!$dryRun) {
$this->attachmentHandler->importFiles($filesDir, $idMapper, $preserveIds, $verbose, $io);
} else {
$fileCount = count(array_diff(scandir($filesDir), ['.', '..']));
$io->writeLine(" Would restore {$fileCount} files (dry-run)");
}
}
// Save ID mapping for reference
if (!$dryRun && !$preserveIds) {
$mapPath = rtrim($inputDir, '/') . '/id-mapping.json';
$idMapper->saveTo($mapPath);
$io->writeLine('');
$io->writeLine("ID mapping saved to: {$mapPath}");
}
// Summary
$io->writeLine('');
$io->writeLine('=== Import ' . ($dryRun ? 'Dry Run ' : '') . 'Complete ===');
$io->writeLine("Total imported: {$totalImported}");
$io->writeLine("Total skipped: {$totalSkipped}");
$io->writeLine("Total errors: {$totalErrors}");
if ($totalErrors > 0) {
$io->writeLine('');
$io->writeLine('Entities with errors:');
foreach ($results as $type => $r) {
if ($r['errors'] > 0) {
$io->writeLine(" {$type}: {$r['errors']} errors");
}
}
}
// Report skipped columns (missing in target DB)
$skippedColumns = $this->entityImporter->getSkippedColumns();
if (!empty($skippedColumns)) {
$io->writeLine('');
$io->writeLine('WARNING: Skipped columns not found in target database:');
foreach ($skippedColumns as $type => $columns) {
$io->writeLine(" {$type}: " . implode(', ', $columns));
}
}
}
private function entityTypeExists(string $entityType): bool
{
try {
$this->entityManager->getRDBRepository($entityType);
return true;
} catch (\Exception $e) {
return false;
}
}
}