Docs · Data in and out
CSV, JSON lines, Arrow
Load CSV files and text in parallel, read JSON lines, and read and write Apache Arrow IPC for pandas, Polars, DuckDB and pyarrow.
Vectro reads CSV, JSON lines and Arrow into tables, and writes Arrow back out. None of it needs a dependency. CSV is parsed straight from bytes in engine memory, on every core, without making a JavaScript string per row.
CSV
engine.readCSV(path, options) loads a file (Node only). engine.parseCSV(textOrBytes, options) takes a string, a Uint8Array or a Buffer, and works in every runtime.
import { Engine } from 'vectrojs';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const dir = mkdtempSync(join(tmpdir(), 'vectro-csv-'));
const file = join(dir, 'orders.csv');
writeFileSync(file, [
'ts,region,price,note',
'2024-03-01T10:00:00Z,EU,12.5,"hello, world"',
'2024-03-02T11:00:00Z,US,,"she said ""hi"""',
'2024-03-03T12:00:00Z,EU,8,x',
].join('\n'));
const engine = new Engine();
const orders = engine.readCSV(file);
console.log(orders.columnNames, orders.length);
console.log(orders.toRows());
console.log(orders.filter("region == 'EU'").sum('price'));
rmSync(dir, { recursive: true });
await engine.close();
ts was recognised as a date column, price as numbers (the blank one is missing, NaN), and region and note as text. Quoted fields may hold commas, newlines and doubled quotes.
How it works
The file’s bytes go into engine memory once. They are cut into chunks at row boundaries (with quotes in the file, a parallel pass counts quote characters per chunk so a cut never lands inside a quoted field), rows are counted per chunk in parallel, then every chunk is parsed in parallel straight into the final columns. Text columns get a dictionary per chunk, merged at the end.
readCSVAsync(path, options) reads the file with async I/O straight into engine memory and parses on the worker threads; parseCSVAsync is the same for text or bytes.
Options
| Option | Meaning | Default |
|---|---|---|
delimiter |
one character, e.g. ';' or '\t' |
',' |
header |
the first line has column names; with false columns are col0, col1, … |
true |
types |
{ name: type } to override the guess (see below) |
guessed |
columns |
names of the columns to load; the others are skipped | all |
type |
number storage, Float64Array or Float32Array |
Float64Array |
sampleRows |
rows read to guess the column types | 1000 |
compress |
pack the columns after loading (see compressed columns) | false |
A UTF-8 byte-order mark and Windows line endings are handled. Column names that aren’t plain identifiers are cleaned up for use in expressions: Order Date becomes Order_Date.
Column types
Each column is guessed from the first sampleRows rows:
| Type | Chosen when | Can be |
|---|---|---|
number |
every sampled value is a number (blanks allowed) | filtered, grouped, summed |
date |
every sampled value looks like an ISO date or timestamp | filtered, grouped; held as ms since 1970, UTC |
category |
text that repeats (regions, statuses) | filtered, grouped, joined, sorted |
string |
free text: 50 or more sampled values, more than half of them different (emails, notes) | returned in results only |
Set the type yourself with types: 'number', 'date', 'category', 'string', or 'text' (category or string, from the sample). A common case is a code with leading zeros, such as a postcode, which should stay text.
import { Engine } from 'vectrojs';
const engine = new Engine();
let csv = 'id;zip;email;region\n';
for (let i = 0; i < 100; i++) csv += `${i};0${1000 + i};user${i}@example.com;${i % 2 ? 'EU' : 'US'}\n`;
const guessed = engine.parseCSV(csv, { delimiter: ';' });
console.log(guessed.toRows(1)); // zip lost its 0; email is free text
const typed = engine.parseCSV(csv, {
delimiter: ';',
types: { zip: 'category', email: 'category' },
columns: ['zip', 'email', 'region'],
});
console.log(typed.toRows(1));
console.log(typed.filter("email == '[email protected]'").toRows());
await engine.close();
Filtering or grouping on a free-text column gives an error that names the fix: load it with types: { email: 'category' }.
Files bigger than memory
readCSV loads the whole file. For a CSV larger than memory (or than the 4 GB an engine holds), convert it to a dataset with engine.csvToDataset(csvPath, datasetPath), which reads it in pieces. See datasets.
JSON lines
JSON lines (.jsonl, .ndjson) is one flat JSON object per line. Vectro’s parser reads them straight into columns, without making an object per row: only the wanted keys are read, numbers and numeric strings go into number columns, strings into category columns, and null, absent keys and nested values become missing. A line that isn’t a JSON object is skipped.
There are three ways in:
- A data folder.
openTables({ root })fromvectrojs/serveloads every.csv,.tsv,.jsonl,.ndjson,.arrow,.featherand.vectrofile in a folder, each as a table named after its file. The servers andnpx vectro serveuse the same loader. JSON lines whose objects carry an array of numbers get a vector index. - A stream. A split engine’s
streamAsync({ format: 'ndjson', columns })takes JSON lines pushed to it or sent over TCP. See streams. - Objects you already have.
engine.tableFromRows(rows)takes an array of objects, with column types from the first row.nullin a number column loads as missing, andDatevalues make a date column.
import { openTables } from 'vectrojs/serve';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const dir = mkdtempSync(join(tmpdir(), 'vectro-jsonl-'));
writeFileSync(join(dir, 'events.jsonl'), [
'{"user":"a","ms":12,"page":"/home"}',
'{"user":"b","ms":null,"page":"/cart"}',
'not json',
'{"user":"a","ms":30,"page":"/cart","extra":{"x":1}}',
].join('\n'));
const tables = await openTables({ root: dir });
const { events } = tables.named();
console.log(events.toRows());
console.log(events.groupBy('page').agg({ hits: 'count()', avgMs: 'mean(ms)' }).toRows());
await tables.close(); // frees the tables and closes the engine it made
rmSync(dir, { recursive: true });
import { SplitEngine } from 'vectrojs';
const engine = new SplitEngine({ workers: 2 });
const live = await engine.streamAsync({ format: 'ndjson', columns: { user: 'category', ms: 'number' } });
live.push('{"user":"a","ms":12}\n{"user":"b","ms":null}\n{"user":"a","ms":30}\n');
await live.flushAsync();
console.log(await live.aggAsync({ events: 'count()', avgMs: 'mean(ms)' }));
await engine.close();
Apache Arrow
Arrow IPC is the binary table format of pandas, Polars, DuckDB, pyarrow and Arrow Flight. Vectro reads and writes both the stream format and the file format (.arrow / .feather), including LZ4-compressed files.
| Call | Does |
|---|---|
engine.fromArrow(bytes, { columns, type }) |
a table from Arrow bytes; columns picks which to read |
table.toArrow({ file }) |
the table’s rows (after any filter) as Arrow bytes; file: true for the file format |
table.toArrowAsync({ file }) |
the same, without blocking |
readArrow(bytes, { columns }) |
{ rows, columns, dates }: columns ready for engine.table, and the names of date columns |
writeArrow(columns, { file, dates }) |
Arrow bytes from plain columns; dates names columns of ms to write as timestamps |
splitEngine.fromArrowAsync(bytes) |
a split table from Arrow bytes |
import { Engine, readArrow, writeArrow } from 'vectrojs';
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const engine = new Engine();
const sales = engine.table({
ts: [new Date('2024-01-01'), new Date('2024-01-02'), new Date('2024-01-03')],
region: ['EU', 'US', 'EU'],
price: [12.5, NaN, 8],
});
// write a Feather file pandas can open with read_feather
const dir = mkdtempSync(join(tmpdir(), 'vectro-arrow-'));
const file = join(dir, 'eu.feather');
writeFileSync(file, sales.filter("region == 'EU'").toArrow({ file: true }));
// read it back, only two columns
const back = engine.fromArrow(readFileSync(file), { columns: ['ts', 'price'] });
console.log(back.toRows());
// the lower-level pair: plain columns in, plain columns out
const bytes = writeArrow({ id: [1, 2], name: ['Ada', 'Bo'] });
const { rows, columns } = readArrow(bytes);
console.log(rows, columns.name);
rmSync(dir, { recursive: true });
await engine.close();
Types
- Read: ints, floats (16, 32 and 64-bit), bools, dates, times, timestamps and durations become number columns (times in milliseconds); dates and timestamps are marked as date columns. Strings and dictionary-encoded strings become category columns. Nulls load as missing:
NaNfor numbers,''for text. 64-bit integers are exact up to 2^53. - Refused: lists, structs, decimals and binary give an error, unless you leave them out with
columns. ZSTD-compressed files aren’t supported yet. - Written: numbers as float64 (float32 tables as float32), dates as
Timestamp(ms, UTC), categories as dictionary-encoded strings, which pandas opens asCategorical.
Files written by pyarrow (every type above, nulls, delta and replaced dictionaries, LZ4, pandas to_feather) are checked to read back exactly, and pyarrow and pandas read Vectro’s files back exactly.
Speed
From the README (M1 Pro): a 10M-row, 5-column Feather file loads in about 0.43 s uncompressed and 1.1 s with LZ4. pyarrow, with native threads, takes 0.16 s and 0.07 s. Arrow streams sent from Vectro’s Emitter arrive at 71 to 75M rows per second into 7 workers.
For Arrow over a network, ArrowReader and ArrowEncoder read and write it batch by batch; streams take format: 'arrow'. See the Arrow API and streams.