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
geonames-rdf converts the GeoNames data dumps to a ~13.5M-feature geonames.nt with a set of shell scripts (download.sh, map.sh, test.sh) driving the SPARQL Anything CLI. We want to port it to LDE. #511 is the first piece: @lde/sparql-anything with a SparqlAnythingConverter.
This issue tracks what has to change before the migration can start, split into musts (the port cannot run correctly or finish without them) and shoulds (worth fixing, but a migration could ship around them).
Status
Every must and should is done. @lde/sparql-anything is at 0.1.0.
Four of them landed differently from the description below, and two follow-ups came out of the work; see the status comment and the one after it.
Still open: the Scope beyond #511 section – chunking, the ontology step, publication, and keeping test.sh's golden-file diff.
What map.sh actually does
It makes four different SPARQL Anything invocations, not one:
#
invocation
query
--load
{SOURCE}
1
admin codes
admin-codes.rq
–
– (paths inline in the query)
2
places chunks
places.rq
admin-codes.ttl
yes
3
alternate-names chunks
alternate-names.rq
–
yes
4
ontology
ontology.rq
ontology.rdf (as the input)
–
SparqlAnythingConverter as first proposed expressed only #2.
Musts
1. Parallelism.convert() is a sequential for loop with an await inside. map.sh runs an xargs -P worker pool sized from nproc, capped by the cgroup memory limit (or /proc/meminfo on a host), budgeting ~3 GB per worker. On allCountries that is ~14 places chunks plus ~4 alternate-names chunks; running them one at a time multiplies the conversion, which takes ~17 min of the ~40 min workflow (measured, see below).
Needs a concurrency option; the memory-aware default is generic enough to belong in LDE rather than in the consumer. This is the one that decides whether the port is viable – everything else is correctness. Do it after 6+7, so the pool is not reworked when its unit of work changes shape.
2. JVM heap control, and a CLI escape hatch.map.sh passes -Xmx2g per worker precisely because SPARQL Anything materialises a chunk’s whole result graph before writing it. The converter hardcodes java -jar … with no -Xmx, so each JVM takes a quarter of host RAM by default – which over-subscribes the moment concurrency lands. Also needs a passthrough for flags the converter does not model, such as -ad on SPARQL Anything 1.2-dev for #624/#625. In review: feat(sparql-anything): cap the JVM heap, and pass through CLI arguments #789 (javaOptions, cliArgs).
4. Guard against an empty chunk output. SPARQL Anything exits 0 when it cannot read or parse an input: it logs, writes an empty --output and stops, which is how we once shipped a geonames.nt missing every ontology triple while the run stayed green. assertNonEmpty now fails the conversion, and an empty chunkPaths is rejected outright. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)
5. convert() resolving before the output was flushed.pipeline(…, {end: false}) resolves when the source ends; output.end() ran un-awaited in finally, so a caller could read a truncated file. Now await finished(output), plus a newline between concatenated files so two triples can never share a line. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)
Shoulds
6+7. One call over jobs, not one converter per query. Originally two separate points – a single worker pool, and returning the chunk outputs; 8’s workDir change merged them and made them more pressing rather than less.
map.sh deliberately feeds both chunk sets through a single pool: an alternate-names chunk is roughly a quarter of the work of a places chunk (one triple per row, no admin-codes join), so the long jobs are listed first and the short ones fill the tail. One SparqlAnythingConverter per query means two sequential drains, which loses that packing.
And the port has to get places + alternate names + ontology.nt into one file. convert() returns void, and now that the per-chunk outputs live in a run directory deleted in finally, the caller can no longer concatenate them itself – which it could when they sat next to the inputs.
Both want the same change: convert() takes jobs of {chunk, query, load} in a single call, or gains an explicit append. Settle this before 1.
8. Chunk outputs beside the inputs, never cleaned. Outputs and generated queries now go to a fresh per-run directory under a caller-supplied workDir, removed in finally, and are referred to by runner-relative paths so the same command works on the host and under DockerTaskRunner. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)
The ontology step. Fetch a pinned ontology_v3.3.rdf and convert RDF/XML to N-Triples.
Publication.gzip -9 plus zip -9, uploaded to DigitalOcean Spaces. Probably stays in the consumer’s CI.
The consumption bridge, on the geonames-rdf side: how a shell-and-Docker repo invokes the TypeScript converter.
What should not move into LDE, because it is GeoNames domain policy rather than plumbing: the awk synthesis of the adm1/adm2 foreign keys, the filtering of alternate names belonging to out-of-scope features, and the pinned ontology version.
Finally, test.sh is a golden-file test that runs the real map.sh over fixtures and diffs the result against test/expected/geonames.nt. Whatever replaces it has to keep that end-to-end diff: reviewing that file line by line is the actual test.
geonames-rdf converts the GeoNames data dumps to a ~13.5M-feature
geonames.ntwith a set of shell scripts (download.sh,map.sh,test.sh) driving the SPARQL Anything CLI. We want to port it to LDE. #511 is the first piece:@lde/sparql-anythingwith aSparqlAnythingConverter.This issue tracks what has to change before the migration can start, split into musts (the port cannot run correctly or finish without them) and shoulds (worth fixing, but a migration could ship around them).
Status
Every must and should is done.
@lde/sparql-anythingis at0.1.0.adminCodesFile→ a generic, optionalload— feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511convert()can resolve before the output is flushed — feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511convert()should return the chunk outputs — feat(sparql-anything)!: convert a list of jobs, not a list of chunks #791, by removing the needwait()race — feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511 (quoting) and fix(task-runner-native): settle wait() for a process that already closed #787 (the race)Four of them landed differently from the description below, and two follow-ups came out of the work; see the status comment and the one after it.
Still open: the Scope beyond #511 section – chunking, the ontology step, publication, and keeping
test.sh's golden-file diff.What
map.shactually doesIt makes four different SPARQL Anything invocations, not one:
--load{SOURCE}admin-codes.rqplaces.rqadmin-codes.ttlalternate-names.rqontology.rqontology.rdf(as the input)SparqlAnythingConverteras first proposed expressed only #2.Musts
1. Parallelism.
convert()is a sequentialforloop with anawaitinside.map.shruns anxargs -Pworker pool sized fromnproc, capped by the cgroup memory limit (or/proc/meminfoon a host), budgeting ~3 GB per worker. OnallCountriesthat is ~14 places chunks plus ~4 alternate-names chunks; running them one at a time multiplies the conversion, which takes ~17 min of the ~40 min workflow (measured, see below).Needs a
concurrencyoption; the memory-aware default is generic enough to belong in LDE rather than in the consumer. This is the one that decides whether the port is viable – everything else is correctness. Do it after 6+7, so the pool is not reworked when its unit of work changes shape.2. JVM heap control, and a CLI escape hatch.
map.shpasses-Xmx2gper worker precisely because SPARQL Anything materialises a chunk’s whole result graph before writing it. The converter hardcodesjava -jar …with no-Xmx, so each JVM takes a quarter of host RAM by default – which over-subscribes the moment concurrency lands. Also needs a passthrough for flags the converter does not model, such as-adon SPARQL Anything 1.2-dev for #624/#625. In review: feat(sparql-anything): cap the JVM heap, and pass through CLI arguments #789 (javaOptions,cliArgs).3. A generic, optional
load.adminCodesFilewas required, single, and named after a GeoNames concept; it could not express invocation test: collect coverage #3 (no--load) or feat: add dataset-registry-client #4 (--loadis the input). Nowload?: string. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)4. Guard against an empty chunk output. SPARQL Anything exits 0 when it cannot read or parse an input: it logs, writes an empty
--outputand stops, which is how we once shipped ageonames.ntmissing every ontology triple while the run stayed green.assertNonEmptynow fails the conversion, and an emptychunkPathsis rejected outright. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)5.
convert()resolving before the output was flushed.pipeline(…, {end: false})resolves when the source ends;output.end()ran un-awaited infinally, so a caller could read a truncated file. Nowawait finished(output), plus a newline between concatenated files so two triples can never share a line. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)Shoulds
6+7. One call over jobs, not one converter per query. Originally two separate points – a single worker pool, and returning the chunk outputs; 8’s
workDirchange merged them and made them more pressing rather than less.map.shdeliberately feeds both chunk sets through a single pool: an alternate-names chunk is roughly a quarter of the work of a places chunk (one triple per row, no admin-codes join), so the long jobs are listed first and the short ones fill the tail. OneSparqlAnythingConverterper query means two sequential drains, which loses that packing.And the port has to get places + alternate names +
ontology.ntinto one file.convert()returnsvoid, and now that the per-chunk outputs live in a run directory deleted infinally, the caller can no longer concatenate them itself – which it could when they sat next to the inputs.Both want the same change:
convert()takes jobs of{chunk, query, load}in a single call, or gains an explicitappend. Settle this before 1.8. Chunk outputs beside the inputs, never cleaned. Outputs and generated queries now go to a fresh per-run directory under a caller-supplied
workDir, removed infinally, and are referred to by runner-relative paths so the same command works on the host and underDockerTaskRunner. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)9. Shell interpolation, and the
wait()race. Interpolated values go throughshellQuote(feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511).wait()attached itscloselistener only afterrun()returned, so a process that failed fast could close first andwait()would never settle – invisible while the converter was sequential, fatal under a pool. Fixed in@lde/task-runner-native(fix(task-runner-native): settle wait() for a process that already closed #787).Scope beyond the converter
Conversion is roughly a third of the migration. Still unhomed:
download.sh’schunk_with_headersplits a TSV intoCHUNK_SIZE-row chunks, each prefixed with the header row the queries expect. Generic; a good LDE candidate. feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511 explicitly left chunking to the caller. Tracked in Split a large delimited file into row chunks with a repeated header #793.ontology_v3.3.rdfand convert RDF/XML to N-Triples.gzip -9pluszip -9, uploaded to DigitalOcean Spaces. Probably stays in the consumer’s CI.What should not move into LDE, because it is GeoNames domain policy rather than plumbing: the
awksynthesis of theadm1/adm2foreign keys, the filtering of alternate names belonging to out-of-scope features, and the pinned ontology version.Finally,
test.shis a golden-file test that runs the realmap.shover fixtures and diffs the result againsttest/expected/geonames.nt. Whatever replaces it has to keep that end-to-end diff: reviewing that file line by line is the actual test.