Skip to content

Commit 23fa267

Browse files
claudearclaude
andcommitted
Fix CSV row column mismatch error by handling malformed rows gracefully
Instead of throwing "CSV row does not match the number of header columns" which crashes the entire migration, handle common CSV inconsistencies: - Skip empty/trailing blank rows (fgetcsv returns [''] for these) - Pad short rows with empty strings for missing trailing columns - Truncate rows with extra columns to match header count Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bbdd8ef commit 23fa267

4 files changed

Lines changed: 90 additions & 2 deletions

File tree

src/Migration/Sources/CSV.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,21 @@ private function exportRows(int $batchSize): void
245245

246246
$buffer = [];
247247

248+
$headerCount = \count($headers);
249+
248250
while (($row = \fgetcsv($stream, 0, $delimiter, '"', '"')) !== false) {
249-
if (\count($row) !== \count($headers)) {
250-
throw new \Exception('CSV row does not match the number of header columns.', Exception::CODE_VALIDATION);
251+
$rowCount = \count($row);
252+
253+
// Skip empty rows (e.g. trailing blank lines parsed as [''])
254+
if ($rowCount === 1 && \trim($row[0]) === '') {
255+
continue;
256+
}
257+
258+
// Pad short rows with empty strings
259+
if ($rowCount < $headerCount) {
260+
$row = \array_pad($row, $headerCount, '');
261+
} elseif ($rowCount > $headerCount) {
262+
$row = \array_slice($row, 0, $headerCount);
251263
}
252264

253265
$data = \array_combine($headers, $row);

tests/Migration/Unit/General/CSVTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,74 @@ public function testCSVExportImportCompatibility()
416416
}
417417
}
418418

419+
/**
420+
* Test that CSV parsing handles trailing empty lines gracefully.
421+
* Trailing empty lines in CSV files produce rows like [''] which have
422+
* a different count than headers, previously causing:
423+
* "CSV row does not match the number of header columns."
424+
*/
425+
public function testCSVParsingHandlesTrailingEmptyLines(): void
426+
{
427+
$filepath = self::RESOURCES_DIR . 'trailing_empty_lines.csv';
428+
$stream = fopen($filepath, 'r');
429+
$this->assertNotFalse($stream);
430+
431+
$headers = fgetcsv($stream, 0, ',', '"', '"');
432+
$this->assertSame(['id', 'name', 'age'], $headers);
433+
434+
$rows = [];
435+
while (($row = fgetcsv($stream, 0, ',', '"', '"')) !== false) {
436+
// Simulate the fixed behavior: skip empty rows
437+
if (\count($row) === 1 && \trim($row[0]) === '') {
438+
continue;
439+
}
440+
$rows[] = $row;
441+
}
442+
fclose($stream);
443+
444+
// Should have exactly 2 data rows, trailing empty line should be skipped
445+
$this->assertCount(2, $rows);
446+
$this->assertSame(['1', 'Alice', '23'], $rows[0]);
447+
$this->assertSame(['2', 'Bob', '30'], $rows[1]);
448+
}
449+
450+
/**
451+
* Test that CSV parsing handles rows with fewer columns than headers.
452+
* Short rows should be padded with empty strings rather than throwing.
453+
*/
454+
public function testCSVParsingHandlesShortRows(): void
455+
{
456+
$filepath = self::RESOURCES_DIR . 'short_rows.csv';
457+
$stream = fopen($filepath, 'r');
458+
$this->assertNotFalse($stream);
459+
460+
$headers = fgetcsv($stream, 0, ',', '"', '"');
461+
$this->assertSame(['id', 'name', 'age'], $headers);
462+
$headerCount = \count($headers);
463+
464+
$rows = [];
465+
while (($row = fgetcsv($stream, 0, ',', '"', '"')) !== false) {
466+
if (\count($row) === 1 && \trim($row[0]) === '') {
467+
continue;
468+
}
469+
// Simulate the fixed behavior: pad short rows
470+
if (\count($row) < $headerCount) {
471+
$row = \array_pad($row, $headerCount, '');
472+
}
473+
$rows[] = \array_combine($headers, $row);
474+
}
475+
fclose($stream);
476+
477+
$this->assertCount(3, $rows);
478+
$this->assertSame('Alice', $rows[0]['name']);
479+
$this->assertSame('23', $rows[0]['age']);
480+
// Short row should have been padded
481+
$this->assertSame('Bob', $rows[1]['name']);
482+
$this->assertSame('', $rows[1]['age']); // Padded with empty string
483+
$this->assertSame('Charlie', $rows[2]['name']);
484+
$this->assertSame('25', $rows[2]['age']);
485+
}
486+
419487
private function recursiveDelete(string $dir): void
420488
{
421489
if (is_dir($dir)) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
id,name,age
2+
1,Alice,23
3+
2,Bob
4+
3,Charlie,25
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
id,name,age
2+
1,Alice,23
3+
2,Bob,30
4+

0 commit comments

Comments
 (0)