From 264829cf4c5d8eb43d2a6cf3d678a2596d329431 Mon Sep 17 00:00:00 2001 From: Sskift <163287349+Sskift@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:40:21 +0800 Subject: [PATCH] fix(csv-parse): retain the last emitted record byte offset --- packages/csv-parse/lib/api/index.js | 2 +- packages/csv-parse/test/api.info.ts | 6 +-- .../test/info.bytes_records.offset.js | 53 +++++++++++++++++++ packages/csv-parse/test/info.bytes_records.ts | 2 +- 4 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 packages/csv-parse/test/info.bytes_records.offset.js diff --git a/packages/csv-parse/lib/api/index.js b/packages/csv-parse/lib/api/index.js index ca97838cd..b62bfe36c 100644 --- a/packages/csv-parse/lib/api/index.js +++ b/packages/csv-parse/lib/api/index.js @@ -749,7 +749,7 @@ const transform = function (original_options = {}) { return; } } - this.info.bytes_records += this.info.bytes; + this.info.bytes_records = this.info.bytes; push(record); }, // Return a tuple with the error and the casted value diff --git a/packages/csv-parse/test/api.info.ts b/packages/csv-parse/test/api.info.ts index f18b7f9c8..bffa85816 100644 --- a/packages/csv-parse/test/api.info.ts +++ b/packages/csv-parse/test/api.info.ts @@ -7,7 +7,7 @@ describe("API info", function () { if (!info) return next(Error("Invalid assessment")); info.should.eql({ bytes: 10, - bytes_records: 16, + bytes_records: 10, columns: false, comment_lines: 0, empty_lines: 0, @@ -25,7 +25,7 @@ describe("API info", function () { if (!info) return next(Error("Invalid assessment")); info.should.eql({ bytes: 11, - bytes_records: 17, + bytes_records: 11, columns: false, comment_lines: 0, empty_lines: 0, @@ -61,7 +61,7 @@ describe("API info", function () { if (!info) return next(Error("Invalid assessment")); info.should.eql({ bytes: 20, - bytes_records: 41, + bytes_records: 20, columns: false, comment_lines: 0, empty_lines: 0, diff --git a/packages/csv-parse/test/info.bytes_records.offset.js b/packages/csv-parse/test/info.bytes_records.offset.js new file mode 100644 index 000000000..965f20dd4 --- /dev/null +++ b/packages/csv-parse/test/info.bytes_records.offset.js @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import { parse } from "../lib/index.js"; + +describe("info bytes_records as the last emitted record offset", function () { + it("keeps the previous record offset throughout field casting", function (next) { + const fieldOffsets = []; + parse( + "a,b\nc,d\ne,f", + { + cast(value, info) { + fieldOffsets.push(info.bytes_records); + return value; + }, + }, + (err, records, info) => { + if (err) return next(err); + assert.deepEqual(fieldOffsets, [0, 0, 4, 4, 8, 8]); + assert.equal(info.bytes, 11); + assert.equal(info.bytes_records, 11); + next(); + }, + ); + }); + + for (const [name, input, options, expected] of [ + [ + "skipped middle record", + "a,b\nc,d\ne,f", + { on_record: (record) => (record[0] === "c" ? null : record) }, + 11, + ], + [ + "skipped final record", + "a,b\nc,d\ne,f", + { on_record: (record) => (record[0] === "e" ? null : record) }, + 8, + ], + ["column headers", "x,y\na,b\nc,d", { columns: true }, 11], + ["from_line", "a,b\nc,d\ne,f", { from_line: 2 }, 11], + ["UTF8 BOM", "\ufeffa,b\nc,d", { bom: true }, 10], + ["no emitted records", "a,b\nc,d", { on_record: () => null }, 0], + ["one emitted record", "a,b", {}, 3], + ]) { + it(`reports an absolute offset with ${name}`, function (next) { + parse(input, options, (err, records, info) => { + if (err) return next(err); + assert.equal(info.bytes_records, expected); + assert.ok(info.bytes_records <= Buffer.byteLength(input)); + next(); + }); + }); + } +}); diff --git a/packages/csv-parse/test/info.bytes_records.ts b/packages/csv-parse/test/info.bytes_records.ts index 1f1b53005..7fe8ff470 100644 --- a/packages/csv-parse/test/info.bytes_records.ts +++ b/packages/csv-parse/test/info.bytes_records.ts @@ -60,7 +60,7 @@ describe("info bytes_records", function () { records.should.eql([ [0, 0], [18, 18], - [46, 46], + [28, 28], ]); }); });