# fix: make Buffer::nth() positional so it stays correct after deletions ## Problem `Buffer::nth()` (and `Buffer::nthAsObject()`) return the wrong record once a record has been deleted from the buffer. `nth()` is a positional accessor - the same contract implemented by `ResultSet::nth()` via `new LimitIterator($iterator, $nth, 1)` - so `nth($n)` must return the `$n`-th record in iteration order, consistent with `getRecords()`. ```php $buffer = new League\Csv\Buffer(['id', 'name']); $buffer->insert(['1', 'a'], ['2', 'b'], ['3', 'c'], ['4', 'd'], ['5', 'e']); $buffer->delete(fn (array $row) => $row['id'] === '2'); // getRecords() correctly yields a, c, d, e // but: $buffer->nth(1); // [] (expected ['id' => '3', 'name' => 'c']) $buffer->nth(4); // ['5', 'e'] (expected [] - only 4 records remain) ``` ## Root cause `src/Buffer.php`, `nthRow()`: ```php $offset = $first + $nth; if (!array_key_exists($offset, $this->rows)) { return []; } return $this->rows[$nth + $first]; ``` The lookup adds `$nth` to the first row key and indexes `$this->rows` by that computed key. That only works while the row keys are contiguous. `Buffer::delete()` removes rows with `unset($this->rows[$offset])` without reindexing, so after any deletion the keys contain gaps and the positional index `$nth` no longer maps to key `$first + $nth`. As a result `nth()` skips the deleted offset, and can also return a record for an index that is out of range. ## The fix Resolve `$nth` against the record order instead of the row keys: ```php private function nthRow(int $nth, string $method): array { -1 < $nth || throw InvalidArgument::dueToInvalidRecordOffset($nth, $method); return array_values($this->rows)[$nth] ?? []; } ``` `array_values()` collapses the gaps, so `$nth` addresses the record by its position in iteration order, matching `getRecords()` and `ResultSet::nth()`. `array_values` is already imported by the class. ## Test Added `BufferTest::it_returns_records_by_positional_offset_after_a_middle_record_is_deleted`, which inserts five records, deletes a middle one, and asserts that `nth(0..3)` return the four remaining records in order and that `nth(4)` returns `[]`. ## Gate ``` composer install --ignore-platform-req=ext-xdebug vendor/bin/phpunit --no-coverage ``` The full run reports 6 pre-existing errors, all `PDOException: could not find driver` from the SQLite3/PDO-backed tests (`RdbmsResultTest` and the `sqlite3`/`pdo` cases in `BufferTest`); these are environmental (the `sqlite3` / `pdo_sqlite` extensions are not installed) and are present unchanged on a clean checkout. Excluding those environment-dependent cases the suite is green: ``` vendor/bin/phpunit --no-coverage --filter '/^(?!.*(sqlite3|pdo)).*$/i' ``` Result: exit code `0` - `OK, but some tests were skipped! Tests: 1006, Assertions: 1990, Skipped: 3` (the 3 skips are pre-existing). Counterfactual: reverting `nthRow()` to the key-arithmetic version makes the new test fail - `nth(1)` returns `[]` instead of `['id' => '3', 'name' => 'c']`. ## Flags for the account owner The SQLite3/PDO tests are not annotated with a required-extension attribute, so they error (rather than skip) when the drivers are missing. That is unrelated to this change and was left untouched.