You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Split a large delimited file into row chunks with a repeated header #793
Part of #782 (port GeoNames RDF to LDE), which lists chunking as unhomed: #511 deliberately left it to the caller, so SparqlAnythingConverter consumes pre-split chunks that nothing in LDE produces.
Why chunking exists
It is not a convenience – it is what bounds the conversion’s memory. SPARQL Anything materialises a chunk’s whole result graph before writing it, and each chunk runs in its own JVM that frees it on exit. A single process over the full GeoNames dataset needs >14 GB and OOMs; at 1M rows per chunk a result graph is ~1.2 GB, which is why map.sh pairs CHUNK_SIZE with -Xmx2g per worker. Chunk size and heap are sized together, so whatever we build has to keep that pairing visible.
rm -rf $prefix* – drop a previous run’s chunks, so stale data cannot reach the output.
split -l $CHUNK_SIZE "$input" "$prefix".
For each part, write a header row in front of it (cat "$header" > "$f.csv"; cat "$f" >> "$f.csv"; rm "$f").
Fail loudly if split produced nothing, rather than letting an unmatched glob become the “filename”.
It is called twice: on the main table (~13.5M rows, after the awk pass that synthesises the adm1/adm2 foreign keys) and on the scoped alternate-names table.
What the LDE version needs
Stream.allCountries.txt is ~1.9 GB and alternateNamesV2 is comparable; neither may be read into memory.
A caller-supplied header. The GeoNames dumps carry no header row – it comes from config/headers-*.csv, and SPARQL Anything’s queries depend on those column names. So the header is a line the caller provides, not one lifted off the input. Taking the first line of the input instead is a different mode; if we support both, they must be explicit rather than inferred.
Return the chunk paths.map.sh globs for them afterwards and needs a guard for the unmatched-glob case; returning an ordered array removes that class of bug outright. Order matters, because convert() concatenates in the order it is given.
A fresh output directory per run, cleaned up by the caller – the same stale-output hazard as item 8 in Port GeoNames RDF to LDE #782, which we fixed in the converter by chunking into a per-run directory.
Fail on an empty input, rather than emitting one header-only chunk. A run that produced no rows has failed upstream, and a header-only chunk hides that – the same reasoning as the empty-chunkPaths guard already in convert().
Chunk on physical lines. That is only correct when no field contains a newline; the GeoNames dumps satisfy this, and the shell version already assumes it. Worth stating in the API docs rather than silently assuming, since it does not hold for CSV in general.
Where it lives – open question
Splitting a large delimited file is not SPARQL Anything-specific: an importer or a downloader could want it just as much. But a package for one function is heavy.
My inclination is to start it inside @lde/sparql-anything next to the converter that consumes its output, and to extract it only when a second consumer appears – the alternative is committing to a package boundary before we know whether anything else wants it. Worth a second opinion before implementation.
Not in scope
The GeoNames-specific preparation stays in geonames-rdf: the awk synthesis of the adm1/adm2 foreign keys, and dropping alternate names belonging to out-of-scope features. Those are domain policy, not plumbing.
Shipped in #799, as chunk() in @lde/sparql-anything (0.1.2).
Every requirement above is met, and the open question resolved the way this issue inclined:
Streams – readline over a read stream, one pass, with backpressure. The file is never held.
Caller-supplied header – header?: string, repeated at the top of each chunk. Taking the first line of the input instead is left unbuilt rather than inferred: header?: string widens to a union when a caller actually needs it, and the docs say the input must hold data only.
Returns the chunk paths, in order, which is what a job's chunks takes – so nothing globs, and the unmatched-glob class of bug is gone.
No stale chunks – chunks of the same input from an earlier call are removed first. Files only, and only ones matching this input's own chunk names: the directory is the caller's. That covers the hazard without requiring a fresh directory per run.
Fails on an empty input, rather than emitting a header-only chunk.
Chunks on physical lines, stated in the API docs rather than assumed.
Where it lives: inside @lde/sparql-anything, as this issue inclined – and for a sharper reason than "a package for one function is heavy". LDE's pipeline streams, so it has no use for a file in pieces; chunking exists only because SPARQL Anything materialises a chunk's whole result graph before writing it. That makes it part of working around this tool rather than general LDE plumbing. Nothing in it touches the converter, so extracting it later is a file move.
One thing measured along the way
I benchmarked before choosing an implementation, on a 220 MB / 2 M-row TSV:
time
cat copy (raw I/O floor, macOS)
0.18 s
GNU split -l (Linux, in Docker)
0.30 s
this implementation
1.18 s
BSD split -l (macOS)
3.60 s
csv-parse with raw
7.50 s
Two conclusions worth recording. Shelling out to split is the fastest option on Linux and the slowest on a Mac – BSD split spends 3.3 s of system time moving bytes a copy handles in 0.07 s – so a TaskRunner-based chunker would have made CI fast and development slow, for a step that is seconds inside a two-minute download.sh. And parsing the CSV properly costs about 6×, to guard against a quoted field carrying a newline: a case the GeoNames dumps cannot produce, since the format documents no quoting mechanism, and one the current split -l pipeline does not guard against either.
Part of #782 (port GeoNames RDF to LDE), which lists chunking as unhomed: #511 deliberately left it to the caller, so
SparqlAnythingConverterconsumes pre-split chunks that nothing in LDE produces.Why chunking exists
It is not a convenience – it is what bounds the conversion’s memory. SPARQL Anything materialises a chunk’s whole result graph before writing it, and each chunk runs in its own JVM that frees it on exit. A single process over the full GeoNames dataset needs >14 GB and OOMs; at 1M rows per chunk a result graph is ~1.2 GB, which is why
map.shpairsCHUNK_SIZEwith-Xmx2gper worker. Chunk size and heap are sized together, so whatever we build has to keep that pairing visible.What
geonames-rdfdoes todaydownload.sh’schunk_with_header:rm -rf $prefix*– drop a previous run’s chunks, so stale data cannot reach the output.split -l $CHUNK_SIZE "$input" "$prefix".cat "$header" > "$f.csv"; cat "$f" >> "$f.csv"; rm "$f").splitproduced nothing, rather than letting an unmatched glob become the “filename”.It is called twice: on the main table (~13.5M rows, after the
awkpass that synthesises theadm1/adm2foreign keys) and on the scoped alternate-names table.What the LDE version needs
allCountries.txtis ~1.9 GB andalternateNamesV2is comparable; neither may be read into memory.config/headers-*.csv, and SPARQL Anything’s queries depend on those column names. So the header is a line the caller provides, not one lifted off the input. Taking the first line of the input instead is a different mode; if we support both, they must be explicit rather than inferred.map.shglobs for them afterwards and needs a guard for the unmatched-glob case; returning an ordered array removes that class of bug outright. Order matters, becauseconvert()concatenates in the order it is given.chunkPathsguard already inconvert().Where it lives – open question
Splitting a large delimited file is not SPARQL Anything-specific: an importer or a downloader could want it just as much. But a package for one function is heavy.
My inclination is to start it inside
@lde/sparql-anythingnext to the converter that consumes its output, and to extract it only when a second consumer appears – the alternative is committing to a package boundary before we know whether anything else wants it. Worth a second opinion before implementation.Not in scope
The GeoNames-specific preparation stays in
geonames-rdf: theawksynthesis of theadm1/adm2foreign keys, and dropping alternate names belonging to out-of-scope features. Those are domain policy, not plumbing.