Node.js CSV version 4 — re-writing and performance

Today, we release a new major version of the Node.js CSV parser project. Version 4 is a complete re-writing of the project focusing on performance. It also comes with new functionalities as well as some cleanup in the option properties and the exported information. The official website is updated and the changelog contains the list of changes for this major release.

A massive undertaking

The Node.js CSV project was started on September 25th of 2010. This is quite old in our evolving tech world. Since then, it has survived multiple Node.js evolutions such as the redesign of the Stream APIs. Over the years, the project was maintained with bug fixes, documentation, and support. With the help of the community, incremental features were provided to fit everyone use cases. The quality of the test suite made us confident to get back to the project and dive into the code. However, there was one task which I never had the courage to initiate: to rewrite the parser from the ground up to take benefit of the Buffer API and its promises of performance. A few days of holidays gave me the opportunity to engage this work.

The re-writing started with a blank new project. While there is probably still room for improvements and further optimizations, I run a few benchmarks to measure the performance impact of multiple implementations. This is how I came up with the Resizable Buffer class which reuse the same internal buffer adjusting to the input data set instead of instantiating a new buffer for each field. When ready, the next step was to write the parser. The process was broken down into multiple iterations, 13 exactly:

  1. Basic Buffer loop
  2. Add __needMoreData
  3. Add __autoDiscoverRowDelimiter
  4. Start working on quote , escape , delimiter, and record_delimiter
  5. Options quote , escape , delimiter and record_delimiter working
  6. Option comment
  7. Options relax_column_count and skip_empty_lines as well as info count , empty_line_count and skipped_line_count
  8. Options skip_lines_with_empty_values , skip_lines_with_error , from , to
  9. Option columns
  10. Option trim
  11. Option relax
  12. Options objname , raw , cast, and cast_date
  13. Rewrite info counters

The implementation no longer uses CoffeeScript and is written directly in JavaScript 6. Don’t get me wrong, I am still a big fan of CoffeeScript and we are still using it in the tests for its expressiveness. However, I needed a fine control on the code and using JavaScript as the main language will hopefully encourage more contributions.

Breaking changes

Overall, there are no major breaking changes. The modules are the same and the API for using it remained unchanged. There are however a few minor breaking changes to take into consideration such as the rowDelimiter option being renamed to record_delimiter , some previously deprecated options being removed and the available counters being regrouped into the new info property:

  • Option rowDelimiter is now record_delimiter
  • count is now info.records
  • Drop the record event
  • Normalise error message as {error type}: {error description}
  • State values are now isolated into the info object
  • count is now info.records
  • lines is now info.lines
  • empty_line_count is now info.empty_lines
  • skipped_line_count is now info.invalid_field_length
  • context.count in the cast function is now context.records
  • Drop support for deprecated options auto_parse and auto_parse_date
  • In raw option, the row property is renamed record
  • Option max_limit_on_data_read is now max_record_size
  • Default value of max_record_size is now 0 (unlimited)
  • Drop emission of the record event, use the readable event and this.read() instead

The most impacting breaking change is probably the renaming of the rowDelimiter option into record_delimiter because of its popular usage. Also, the max_record_size is now unlimited by default and must be explicitly defined if used.

New features

This new version comes also with new features. The new information object is a nice addon. It regroups a few counter properties which were available directly from the parser instance. Those properties have been renamed to be more expressive. The information object is directly available from the parser instance as info . To the callback users, they are exported as the third argument of their callback function. They can also be available for each record by activating the info option with the value true.

There are 3 new options which are info , from_line , and to_line :

  • info : Generate two properties info and record where info is a snapshot of the info object at the time the record was created and record is the parsed array or object; note, it can be used conjointly with the raw option.
  • from_line : Start handling records from the requested line number.
  • to_line : Stop handling records after the requested line number.

The info option is quite useful for debugging or giving to the end users some feedback about their mistake.

The from_line and to_line options respectively filter the first and last lines of a data set. Speaking of lines, previous versions of the parser were surely confused when it comes to count lines mixing row and record delimiters. It was working for most users for the simple reason that they are usually the same. It shall be fixed with this new release.

Here is the new feature list extracted from the changelog:

  • new options info , from_line and to_line
  • trim : respect ltrim and rtrim when defined
  • delimiter : may be a Buffer
  • delimiter : handle multiple bytes/characters
  • callback : export info object as third argument
  • cast : catch error in user functions
  • TypeScript: mark info as readonly with required properties
  • comment_lines : count the number of commented lines with no records

What’s coming next

The source code is backed by an extended test suite. No test has been removed and new tests have appeared to reinforce the guarantees of the parser. It is, however, possible that some behaviors are not covered by the tests and, in the next few weeks, we count on your feedback to fix any coming issues.

While not being a big fan of ES6 Promise in the context of the parser, the request for support has been made multiple time and will come soon. It will also be implemented in the other CSV packages.

Another potential improvement is to extend the error objects with additional information such as a unique code associated to each type of error. While being improved, there is room to better normalize the messages.

I am also planning to support the Flow static type checker. I have never used it before. It seems appropriate to the package and it will give me the occasion to try it on.

Finally, I am considering writing a command line tool which will expose all the available options and provide multiple output formats (JSON, JSON line, YAML, …).


Originally published at Adaltas.