@lde/sparql-qlever 0.15.3 → 0.15.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,45 +1,13 @@
1
- # SPARQL QLever
1
+ # @lde/sparql-qlever
2
2
 
3
3
  An adapter for the [QLever](https://github.com/ad-freiburg/qlever) SPARQL server.
4
4
 
5
- ## Index caching
5
+ ## Installation
6
6
 
7
- Building a QLever index is slow. To avoid rebuilding it on every pipeline run, the importer caches a single index and reuses it when the source data hasn't changed. On subsequent runs, indexing is skipped when the source file matches and hasn't been re-downloaded.
8
-
9
- Only **one** index is cached at a time. In a multi-dataset pipeline, each dataset overwrites the previous index. On re-run, the last-indexed dataset gets a cache hit while the others rebuild.
10
-
11
- Caching is enabled by default. Disable it by passing `cacheIndex: false` to `createQlever()` or the `Importer` constructor (e.g. driven by a `QLEVER_CACHE_INDEX=false` environment variable).
12
-
13
- ## Networking (Docker mode)
14
-
15
- By default `createQlever({ mode: 'docker' })` publishes QLever’s port on the host and addresses the query endpoint as `localhost` – correct when the consumer runs on the host, or shares the QLever container’s network namespace. A consumer that itself runs in a container on a bridge network cannot reach a host-published port; pass `network` (a Docker network the consumer is attached to) together with `containerName`, and QLever joins that network with the endpoint addressed by container name instead:
16
-
17
- ```ts
18
- const { importer, server } = createQlever({
19
- mode: 'docker',
20
- image: 'adfreiburg/qlever:latest',
21
- network: 'app_default',
22
- containerName: 'qlever',
23
- });
24
- server.queryEndpoint.toString(); // 'http://qlever:7001/sparql'
7
+ ```sh
8
+ npm install @lde/sparql-qlever
25
9
  ```
26
10
 
27
- ## Configuration
28
-
29
- `createQlever()` accepts `indexOptions` and `serverOptions` to tune QLever's index builder and server respectively.
30
-
31
- ### Server options (`serverOptions`)
32
-
33
- Passed to `qlever-server` at startup.
34
-
35
- | Option | Description | Default |
36
- | ----------------------- | ------------------------------------------------ | ------- |
37
- | `memory-max-size` | Maximum memory for query processing and caching. | `'4G'` |
38
- | `default-query-timeout` | Default query timeout. | `'30s'` |
39
- | `cache-max-size` | Maximum cache size for query results. | – |
40
-
41
- Example:
42
-
43
11
  ```ts
44
12
  const { importer, server } = createQlever({
45
13
  mode: 'docker',
@@ -51,14 +19,6 @@ const { importer, server } = createQlever({
51
19
  });
52
20
  ```
53
21
 
54
- ### Index options (`indexOptions`)
55
-
56
- Passed to `qlever-index` during import.
22
+ ## Documentation
57
23
 
58
- | Option | Description | Default |
59
- | ------------------------------- | --------------------------------------------------------------------------------------------- | ----------- |
60
- | `ascii-prefixes-only` | Enable faster parsing for well-behaved TTL files. | `true` |
61
- | `num-triples-per-batch` | Triples per batch; lower values reduce memory usage. | `3_000_000` |
62
- | `stxxl-memory` | Memory budget for sorting during the index build. | `'10G'` |
63
- | `parallel-parsing` | Parse input in parallel. | `true` |
64
- | `only-pso-and-pos-permutations` | Build only PSO and POS permutations. Faster, but queries with predicate variables won't work. | `false` |
24
+ See the [full documentation](https://ldelements.org/reference/sparql-qlever).
@@ -12,8 +12,8 @@ export interface PreprocessResult {
12
12
  * JSON-LD, RDF/XML and TriG distributions return `true`: `qlever-index` cannot
13
13
  * parse any of them, so we stream them through `rdf-parse` into N-Quads first.
14
14
  *
15
- * Native RDF formats (`nt`, `nq`, `ttl`) — including when wrapped in
16
- * `application/gzip` or `application/zip` — go straight through the shell
15
+ * Native RDF formats (`nt`, `nq`, `ttl`) – including when wrapped in
16
+ * `application/gzip` or `application/zip` – go straight through the shell
17
17
  * pipeline in `index()`, which uses `gunzip -c` or `unzip -p` as appropriate.
18
18
  * Standalone `mediaType=application/zip` is rejected upstream: the inner
19
19
  * format must be declared.
@@ -25,8 +25,8 @@ export declare function needsPreprocessing(distribution: Distribution): boolean;
25
25
  *
26
26
  * Streams the source through `rdf-parse` → `rdf-serialize` so memory use
27
27
  * stays bounded regardless of input size. Handles gzip transparently
28
- * (declared `compressFormat` or `.gz` filename) and zip containers (folds
29
- * every parseable entry into the output stream in order).
28
+ * (detected from the file’s magic bytes, not from declared metadata) and zip
29
+ * containers (folds every parseable entry into the output stream in order).
30
30
  *
31
31
  * Cached: if the output is newer than the input, it is reused as-is.
32
32
  */
@@ -1 +1 @@
1
- {"version":3,"file":"preprocess.d.ts","sourceRoot":"","sources":["../src/preprocess.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AA6B5C,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAKtE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,gBAAgB,CAAC,CAyB3B"}
1
+ {"version":3,"file":"preprocess.d.ts","sourceRoot":"","sources":["../src/preprocess.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AA2B5C,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAKtE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,gBAAgB,CAAC,CAyB3B"}
@@ -2,7 +2,7 @@ import { rdfParser } from 'rdf-parse';
2
2
  import { rdfSerializer } from 'rdf-serialize';
3
3
  import { createGunzip } from 'node:zlib';
4
4
  import { createReadStream, createWriteStream } from 'node:fs';
5
- import { rm, stat } from 'node:fs/promises';
5
+ import { open, rm, stat } from 'node:fs/promises';
6
6
  import { finished } from 'node:stream/promises';
7
7
  import { promisify } from 'node:util';
8
8
  import yauzl from 'yauzl';
@@ -10,8 +10,6 @@ const JSONLD_MIME = 'application/ld+json';
10
10
  const RDFXML_MIME = 'application/rdf+xml';
11
11
  const TRIG_MIME = 'application/trig';
12
12
  const ZIP_MIME = 'application/zip';
13
- const GZIP_MIME = 'application/gzip';
14
- const GZIP_MIME_LEGACY = 'application/x-gzip';
15
13
  /**
16
14
  * RDF media types `qlever-index` cannot read natively, keyed by `rdf-parse`
17
15
  * `contentType` with a label for warnings. TriG belongs here too: its
@@ -29,8 +27,8 @@ const preprocessFormats = new Map([
29
27
  * JSON-LD, RDF/XML and TriG distributions return `true`: `qlever-index` cannot
30
28
  * parse any of them, so we stream them through `rdf-parse` into N-Quads first.
31
29
  *
32
- * Native RDF formats (`nt`, `nq`, `ttl`) — including when wrapped in
33
- * `application/gzip` or `application/zip` — go straight through the shell
30
+ * Native RDF formats (`nt`, `nq`, `ttl`) – including when wrapped in
31
+ * `application/gzip` or `application/zip` – go straight through the shell
34
32
  * pipeline in `index()`, which uses `gunzip -c` or `unzip -p` as appropriate.
35
33
  * Standalone `mediaType=application/zip` is rejected upstream: the inner
36
34
  * format must be declared.
@@ -45,8 +43,8 @@ export function needsPreprocessing(distribution) {
45
43
  *
46
44
  * Streams the source through `rdf-parse` → `rdf-serialize` so memory use
47
45
  * stays bounded regardless of input size. Handles gzip transparently
48
- * (declared `compressFormat` or `.gz` filename) and zip containers (folds
49
- * every parseable entry into the output stream in order).
46
+ * (detected from the file’s magic bytes, not from declared metadata) and zip
47
+ * containers (folds every parseable entry into the output stream in order).
50
48
  *
51
49
  * Cached: if the output is newer than the input, it is reused as-is.
52
50
  */
@@ -66,7 +64,7 @@ export async function preprocess(localFile, distribution) {
66
64
  await streamRdfZip(localFile, outputFile, contentType, label, warnings);
67
65
  }
68
66
  else {
69
- await streamRdfFile(localFile, outputFile, contentType, distribution);
67
+ await streamRdfFile(localFile, outputFile, contentType);
70
68
  }
71
69
  return { path: outputFile, format: 'nq', warnings };
72
70
  }
@@ -102,12 +100,11 @@ async function closeWritable(output) {
102
100
  output.end();
103
101
  });
104
102
  }
105
- async function streamRdfFile(localFile, outputFile, contentType, distribution) {
106
- const isGzipped = distribution.compressMimeType === GZIP_MIME ||
107
- distribution.compressMimeType === GZIP_MIME_LEGACY ||
108
- localFile.toLowerCase().endsWith('.gz');
103
+ async function streamRdfFile(localFile, outputFile, contentType) {
109
104
  const source = createReadStream(localFile);
110
- const input = isGzipped ? source.pipe(createGunzip()) : source;
105
+ const input = (await isGzipFile(localFile))
106
+ ? source.pipe(createGunzip())
107
+ : source;
111
108
  const output = createWriteStream(outputFile);
112
109
  try {
113
110
  await pipeRdfToWritable(input, output, contentType);
@@ -116,6 +113,25 @@ async function streamRdfFile(localFile, outputFile, contentType, distribution) {
116
113
  await closeWritable(output);
117
114
  }
118
115
  }
116
+ /**
117
+ * Whether the file begins with the gzip magic number (RFC 1952 §2.3.1).
118
+ *
119
+ * Sniffing the bytes is the only reliable signal for a downloaded dump.
120
+ * `dcat:compressFormat` is frequently absent, and the local filename is derived
121
+ * from the access URL, so a dump behind a query string (`…/download.trig.gz?graph=…`)
122
+ * carries no `.gz` suffix. Conversely, a `.gz` name says nothing once the server
123
+ * declared a real `Content-Encoding: gzip` and `fetch` already inflated the body.
124
+ */
125
+ async function isGzipFile(path) {
126
+ const file = await open(path);
127
+ try {
128
+ const { buffer, bytesRead } = await file.read(Buffer.alloc(2), 0, 2, 0);
129
+ return bytesRead === 2 && buffer[0] === 0x1f && buffer[1] === 0x8b;
130
+ }
131
+ finally {
132
+ await file.close();
133
+ }
134
+ }
119
135
  const openZip = promisify(yauzl.open);
120
136
  /**
121
137
  * Fold every parseable entry of a zip into the N-Quads output, in order. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/sparql-qlever",
3
- "version": "0.15.3",
3
+ "version": "0.15.5",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/ldelements/lde.git",
6
6
  "directory": "packages/sparql-qlever"
@@ -31,7 +31,7 @@
31
31
  "@lde/task-runner": "^0.2.13",
32
32
  "@lde/task-runner-docker": "^0.2.16",
33
33
  "@lde/task-runner-native": "^0.2.16",
34
- "@lde/wait-for-sparql": "^0.2.16",
34
+ "@lde/wait-for-sparql": "^0.2.17",
35
35
  "rdf-parse": "^5.0.0",
36
36
  "rdf-serialize": "^5.1.0",
37
37
  "tslib": "^2.3.0",