@lde/pipeline 0.35.4 → 0.35.6

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,456 +1,13 @@
1
- # Pipeline
1
+ # @lde/pipeline
2
2
 
3
- A framework for transforming large RDF datasets, primarily using [SPARQL](https://www.w3.org/TR/sparql11-query/) queries with TypeScript for the parts that are hard to express in SPARQL alone.
3
+ A framework for transforming large RDF datasets, primarily using [SPARQL](https://www.w3.org/TR/sparql11-query/) queries with TypeScript for the parts that are hard to express in SPARQL alone. Transformations are plain SPARQL query files – portable, transparent, testable and version-controlled – and memory stays bounded no matter how large the dataset.
4
4
 
5
- - **SPARQL-native.** Data transformations are plain SPARQL query files – portable, transparent, testable and version-controlled.
6
- - **Composable.** Executors are an interface: wrap a SPARQL reader with custom TypeScript to handle edge cases like date parsing or string normalisation (see [Reader](#reader)).
7
- - **Extensible.** A plugin system lets packages like [@lde/pipeline-void](../pipeline-void) (or your own plugins) hook into the pipeline lifecycle.
5
+ ## Installation
8
6
 
9
- ## Components
10
-
11
- A **Pipeline** consists of:
12
-
13
- - a **Dataset Selector** that selects which datasets to process
14
- - a **Distribution Resolver** that resolves each dataset to a usable SPARQL endpoint
15
- - one or more **Stages**, each consisting of:
16
- - an optional **Item Selector** that selects resources (as variable bindings) for fan-out
17
- - one or more **Executors** that generate triples
18
-
19
- ### Dataset Selector
20
-
21
- Selects datasets, either manually or by querying a DCAT Dataset Registry:
22
-
23
- ```typescript
24
- // From a registry
25
- const selector = new RegistrySelector({
26
- registry: new Client(new URL('https://example.com/sparql')),
27
- });
28
-
29
- // Manual
30
- const selector = new ManualDatasetSelection([dataset]);
31
- ```
32
-
33
- ### Distribution Resolver
34
-
35
- Resolves each dataset to a usable SPARQL endpoint. `SparqlDistributionResolver` probes a dataset’s own endpoint; wrap it in `ImportResolver` to add the ability to import a data dump into a local SPARQL server.
36
-
37
- `ImportResolver`’s `strategy` controls how the source is chosen, ordered by how eagerly a dump is imported:
38
-
39
- ```typescript
40
- const resolver = new ImportResolver(new SparqlDistributionResolver(), {
41
- importer,
42
- server,
43
- strategy: 'sparqlWithImportFallback',
44
- });
45
- ```
46
-
47
- - `'sparql'` (default) – use the dataset’s own SPARQL endpoint when one is available; import a data dump only when no endpoint responds.
48
- - `'sparqlWithImportFallback'` – like `'sparql'`, but also fall back to the data dump when the endpoint passes probing yet a stage fails against it at runtime. The pipeline discards the endpoint-sourced partial output and re-runs all stages against the import. Use this when endpoints are present but unreliable for heavy aggregate queries.
49
- - `'import'` – always import the data dump, even when a working endpoint is advertised.
50
-
51
- ### Stage
52
-
53
- A stage groups an item selector, one or more readers, and configuration:
54
-
55
- ```typescript
56
- new Stage({
57
- name: 'per-class',
58
- itemSelector: new SparqlItemSelector({
59
- query: 'SELECT DISTINCT ?class WHERE { ?s a ?class }',
60
- }),
61
- readers: reader,
62
- batchSize: 100,
63
- maxConcurrency: 5,
64
- });
65
- ```
66
-
67
- #### Batch size
68
-
69
- `batchSize` (default: 10) controls how many variable bindings are passed to each reader call as a `VALUES` clause. It also sets the page size for the item selector's SPARQL requests, so that each paginated request fills exactly one reader batch.
70
-
71
- Some SPARQL endpoints enforce different result limits for SELECT and CONSTRUCT queries. Since the selector uses SELECT and the reader uses CONSTRUCT, a `LIMIT` clause in the selector query overrides `batchSize` as the page size. Use this when the endpoint caps SELECT results below your desired batch size:
72
-
73
- ```typescript
74
- // Endpoint caps SELECT results at 500, but each CONSTRUCT can handle 1000 bindings.
75
- new Stage({
76
- name: 'per-class',
77
- itemSelector: new SparqlItemSelector({
78
- query: 'SELECT DISTINCT ?class WHERE { ?s a ?class } LIMIT 500',
79
- }),
80
- readers: reader,
81
- batchSize: 1000, // Two SELECT pages fill one CONSTRUCT batch.
82
- });
7
+ ```sh
8
+ npm install @lde/pipeline
83
9
  ```
84
10
 
85
- #### Concurrency
86
-
87
- `maxConcurrency` (default: 10) limits the total number of concurrent SPARQL queries. Within each batch, all readers run in parallel; the number of concurrent batches is automatically reduced to `⌊maxConcurrency / executorCount⌋` so the total query pressure stays within the limit. For example, with `maxConcurrency: 10` and two readers per stage, up to 5 batches run concurrently (10 SPARQL queries total).
88
-
89
- #### Expecting output
90
-
91
- `expectsOutput` (default: `false`) marks a stage whose query must yield at least one quad. A supported stage that produces none is then treated as a hard failure rather than a legitimately empty result.
92
-
93
- Set it for scalar aggregates such as `SELECT (COUNT(*) AS ?n)`, which always return exactly one row – so zero output can only mean the endpoint truncated or aborted the response (e.g. a timeout surfaced as an empty `HTTP 200`). The failure flows through like any other hard stage failure, triggering the [reactive dump fallback](#distribution-resolver) when `strategy: 'sparqlWithImportFallback'` is configured. Leave it `false` for stages that may legitimately be empty, such as class or property partitions of a dataset that lacks that structure.
94
-
95
- ### Item Selector
96
-
97
- Selects resources from the distribution and fans out reader calls per batch of results. Implements the `ItemSelector` interface:
98
-
99
- ```typescript
100
- interface ItemSelector {
101
- select(
102
- distribution: Distribution,
103
- batchSize?: number,
104
- ): AsyncIterable<VariableBindings>;
105
- }
106
- ```
107
-
108
- The distribution is received at run time, so selectors don't need the endpoint URL at construction time. The `batchSize` parameter is set by the stage. Use `SparqlItemSelector` for SPARQL-based selection with automatic pagination:
109
-
110
- ```typescript
111
- new SparqlItemSelector({
112
- query: 'SELECT DISTINCT ?class WHERE { ?s a ?class }',
113
- });
114
- ```
115
-
116
- A row is yielded only when **every** projected variable binds a `NamedNode` – binding values double as stable item identities downstream, which a blank-node label or literal cannot provide. Other rows are dropped client-side but still count toward pagination, so they are fetched first; to skip them at the endpoint, filter in the query itself (e.g. `FILTER(isIRI(?s))`, as `selectByClass` in `@lde/search-pipeline` does with `FILTER(!isBlank(?root))`).
117
-
118
- #### Capping total results with `maxResults`
119
-
120
- By default, `SparqlItemSelector` paginates through **all** matching rows: any `LIMIT` clause in the query is interpreted as the page size, then it walks pages with `OFFSET` until the source is exhausted. To cap the total bindings yielded across all pages – for sampling, testing, prototyping, or just safety – set `maxResults`:
121
-
122
- ```typescript
123
- new SparqlItemSelector({
124
- query: 'SELECT DISTINCT ?s WHERE { ?s a <http://example.com/Class> }',
125
- maxResults: 50,
126
- });
127
- ```
128
-
129
- When `maxResults` is set:
130
-
131
- - Pagination stops as soon as `maxResults` bindings have been yielded – no wasted page request after the cap is hit.
132
- - As long as no row has been dropped, the last (partial) page's `LIMIT` is shrunk to the remaining cap so the endpoint doesn't over-fetch on the remainder (e.g. with `maxResults: 85` and `pageSize: 10`, the 9th page request is `LIMIT 5`, not `LIMIT 10`). Once rows have been dropped, pages stay at full size – shrinking to the yielded remainder would crawl a dropped-row region one row per request.
133
- - The first page uses the configured page size as-is; `maxResults` and page size stay orthogonal. If `maxResults < pageSize`, the first page may return a few rows that aren't yielded.
134
- - `maxResults: 0` is a valid no-op; the selector yields nothing without issuing any SPARQL request.
135
- - `maxResults` is independent of any `LIMIT` clause in the query, which still controls page size when the cap is larger than one page.
136
-
137
- For dynamic queries that depend on the distribution, implement `ItemSelector` directly:
138
-
139
- ```typescript
140
- const itemSelector: ItemSelector = {
141
- select: (distribution, batchSize) => {
142
- const query = buildQuery(distribution);
143
- return new SparqlItemSelector({ query }).select(distribution, batchSize);
144
- },
145
- };
146
- ```
147
-
148
- ### Reader
149
-
150
- Generates RDF triples. The built-in `SparqlConstructReader` runs a SPARQL CONSTRUCT query with template substitution and variable bindings:
151
-
152
- ```typescript
153
- const reader = new SparqlConstructReader({
154
- query: 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }',
155
- });
156
- ```
157
-
158
- When querying endpoints that return line-oriented formats like N-Triples (e.g. QLever), enable `lineBuffer` to work around an [N3.js chunk-splitting bug](https://github.com/rdfjs/N3.js/issues/578) that causes intermittent parse errors on large responses:
159
-
160
- ```typescript
161
- const reader = new SparqlConstructReader({
162
- query: 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }',
163
- lineBuffer: true,
164
- });
165
- ```
166
-
167
- SPARQL CONSTRUCT queries can produce duplicate triples – for example, constant triples (like `?dataset a edm:ProvidedCHO`) are emitted for every solution row. Enable `deduplicate` to remove duplicates inline on the stream using a string-based identity set (inspired by [Comunica's `distinctConstruct`](https://comunica.dev/docs/query/advanced/context/#14--distinct-construct)):
168
-
169
- ```typescript
170
- const reader = new SparqlConstructReader({
171
- query: 'CONSTRUCT { ?s a edm:ProvidedCHO . ?s ?p ?o } WHERE { ?s ?p ?o }',
172
- deduplicate: true,
173
- });
174
- ```
175
-
176
- The dedup set is scoped to each `read()` call, so memory stays bounded to the number of unique quads per batch. A standalone `deduplicateQuads()` function is also exported for use outside the reader.
177
-
178
- ### Extending a stage with a quad transform
179
-
180
- Some logic is hard to express in pure SPARQL – cleaning up messy date notations, converting locale-specific dates to ISO 8601, or sampling a reader’s output and firing follow-up queries. Rather than subclass `Reader`, attach a `QuadTransform` to it as data: a plain function `(quads, context) => quads` that post-processes one reader’s output before the stage merges it with its siblings. This is extension point 1 of [ADR 2](../../docs/decisions/0002-unify-pipeline-extension-on-quad-transforms.md).
181
-
182
- A transform receives an `ReaderContext` – the `dataset`, the `distribution` (so it can fire its own SPARQL queries), and the `stage` name. It runs once per reader call, so **write it to accept being called more than once**: a global stage calls it once over the reader’s complete output, but a per-class stage with batching enabled calls it once per batch (one class at `batchSize: 1`). Accumulate within an invocation, not across invocations – or keep the transform per-quad, where the number of calls makes no difference.
183
-
184
- ```typescript
185
- import { DataFactory } from 'n3';
186
- import {
187
- Stage,
188
- SparqlConstructReader,
189
- type QuadTransform,
190
- type ReaderContext,
191
- } from '@lde/pipeline';
192
-
193
- const cleanDates: QuadTransform<ReaderContext> = async function* (quads) {
194
- for await (const quad of quads) {
195
- if (quad.object.termType === 'Literal' && isMessyDate(quad.object)) {
196
- yield DataFactory.quad(
197
- quad.subject,
198
- quad.predicate,
199
- DataFactory.literal(
200
- parseDutchDate(quad.object.value),
201
- DataFactory.namedNode('http://www.w3.org/2001/XMLSchema#date'),
202
- ),
203
- );
204
- } else {
205
- yield quad;
206
- }
207
- }
208
- };
209
-
210
- new Stage({
211
- name: 'dates',
212
- readers: {
213
- reader: await SparqlConstructReader.fromFile('dates.rq'),
214
- transform: cleanDates,
215
- },
216
- });
217
- ```
218
-
219
- `transform` accepts a single transform or an array applied in order, so a stage can compose several. This keeps SPARQL doing the heavy lifting while TypeScript handles the edge cases. See [@lde/pipeline-void](../pipeline-void)'s `withVocabularies` for a real-world example of this pattern.
220
-
221
- #### Adaptive timeouts
222
-
223
- By default, every SPARQL request uses the same 5-minute budget. When a pipeline runs against many third-party endpoints, that fixed budget can cost ~80 minutes on a single dataset whose endpoint times out repeatedly on heavy queries – light stages on the same endpoint then sit behind the heavy ones that will never succeed.
224
-
225
- A `TimeoutPolicy` decides the budget for each SPARQL request and observes the outcome. Two are built in:
226
-
227
- - **`ConstantTimeoutPolicy(timeoutMs)`** – returns the same budget for every request. The implicit default when `PipelineOptions.timeout` is omitted (`constantTimeoutPolicy(300_000)`).
228
- - **`AdaptiveTimeoutPolicy({ defaultMs, tightenedMs, tightenAfterTimeouts })`** – per-endpoint state machine. Each endpoint is either _healthy_ (use `defaultMs`) or _tightened_ (use `tightenedMs`). After `tightenAfterTimeouts` consecutive `timeout` outcomes the endpoint flips to _tightened_; a single `ok` flips it back to _healthy_.
229
-
230
- `PipelineOptions.timeout` accepts a `() => TimeoutPolicy` factory. The pipeline invokes it once per dataset, so policy state resets between datasets and one bad dataset can’t poison the next:
231
-
232
- ```typescript
233
- import { adaptiveTimeoutPolicy } from '@lde/pipeline';
234
-
235
- new Pipeline({
236
- // …
237
- timeout: adaptiveTimeoutPolicy({
238
- defaultMs: 300_000, // 5 min while the endpoint is healthy
239
- tightenedMs: 10_000, // 10 s once the endpoint is tightened
240
- tightenAfterTimeouts: 2, // flip to tightened after 2 consecutive timeouts
241
- }),
242
- });
243
- ```
244
-
245
- Outcomes are classified as:
246
-
247
- | outcome | source |
248
- | --------- | ------------------------------------------------------------------------ |
249
- | `ok` | the request resolved |
250
- | `timeout` | client-side `AbortSignal.timeout()` fired, or upstream returned HTTP 504 |
251
- | `error` | anything else (other HTTP errors, parse errors, …) – neutral |
252
-
253
- Transitions are forwarded to the `ProgressReporter` via `timeoutTightened` / `timeoutRelaxed`; `ConsoleReporter` prints them as `↘ Tightened` / `↗ Relaxed` lines so operators can tell a fast-failed stage from an unexpected speedup.
254
-
255
- Implement `TimeoutPolicy` directly for custom strategies (closing over shared state in the factory if you want it to span datasets).
256
-
257
- Timeouts live at the pipeline level – neither `SparqlConstructReader` nor `SparqlItemSelector` accept their own `timeout` option. Per-endpoint state belongs in the adaptive policy, and per-stage budgets aren’t supported. Reusable stage facades (`@lde/pipeline-void`, `@lde/pipeline-shacl-sampler`) follow the same convention.
258
-
259
- ### Validation
260
-
261
- Stages can optionally validate their output quads against a `Validator`. Validation operates on the **combined output of all readers per batch**, not on individual quads or per-reader output. A batch produces a complete result set – a self-contained cluster of linked resources – that can be meaningfully matched against SHACL shapes. Even with a single reader, each batch is a complete unit; with multiple readers, shapes that reference triples from different readers are validated correctly.
262
-
263
- Validating individual quads would be meaningless, since a single quad carries no structural context for shape matching. Validating the full pipeline output would also be problematic: because the pipeline streams results in batches, it doesn’t know where resource cluster boundaries fall. Batching the output could split a valid cluster across two batches, causing partial resources to fail validation even though the complete cluster is valid.
264
-
265
- Quads are buffered, validated, and then written or discarded based on the `onInvalid` policy. When no validator is configured, quads stream directly with zero overhead.
266
-
267
- ```typescript
268
- import { ShaclValidator } from '@lde/pipeline-shacl-validator';
269
- import { FileWriter } from '@lde/pipeline';
270
-
271
- new Stage({
272
- name: 'transform',
273
- readers: await SparqlConstructReader.fromFile('transform.rq'),
274
- validation: {
275
- validator: new ShaclValidator({
276
- shapesFile: './shapes.ttl',
277
- reportWriters: [
278
- new FileWriter({ outputDir: './validation', format: 'turtle' }),
279
- ],
280
- }),
281
- onInvalid: 'write', // 'write' (default) | 'skip' | 'halt'
282
- },
283
- });
284
- ```
285
-
286
- | `onInvalid` | Behaviour |
287
- | ----------- | -------------------------------------------------- |
288
- | `'write'` | Write quads even if validation fails **(default)** |
289
- | `'skip'` | Discard the batch silently |
290
- | `'halt'` | Throw an error, stopping the pipeline |
291
-
292
- `Validator` is an interface, so you can implement your own validation strategy. See [@lde/pipeline-shacl-validator](../pipeline-shacl-validator) for the SHACL implementation.
293
-
294
- #### Per-dataset reporting
295
-
296
- After all stages for a dataset have run, the pipeline calls `validator.report(dataset)` once for each distinct validator attached to any stage and emits a `datasetValidated(dataset, report)` event on the reporter. The call happens **regardless of whether any stage actually invoked `validate()`** – for SHACL that means a dataset whose stages produced no input typically reports `quadsValidated: 0` and `conforms: true` (the SHACL vacuous-truth default). Consumers that want to distinguish ‘not tested’ from ‘tested and passed’ can read `quadsValidated`.
297
-
298
- ### Writer
299
-
300
- Writes generated quads to a destination. A `Writer` is transactional: each pipeline run opens one run on it (`openRun(context)`), writes every dataset through the resulting `RunWriter`, and ends with exactly one `commit()` (on success) or `abort(error)` (on failure). The run lifecycle is the home of destination-level concerns such as atomic swaps, deletion sweeps and cross-pod locks; the pipeline drives `openRun → write* → commit/abort` uniformly and never branches on the writer’s update mode. After a dataset’s stages complete, its `flush(dataset, outcome)` says whether the dataset succeeded, so a writer can gate destructive finalization (e.g. an In-place stale-document sweep) on `'success'`.
301
-
302
- - `SparqlUpdateWriter` – writes to a SPARQL endpoint via UPDATE queries; writes are visible as they land, so `commit`/`abort` are no-ops
303
- - `FileWriter` – writes to local files; each file is streamed to a sibling temp file and atomically renamed on flush, `commit` finalizes any files still open, and `abort` discards temp output. In `n-quads` format it skolemises blank nodes to dataset-scoped IRIs, because such files are typically `cat`-indexed into one served graph where document-scoped blank-node labels from different datasets would otherwise fuse (see `blankNodes`/`failOnBlankNodes` and ldelements/lde#474); Turtle/N-Triples output is left unchanged
304
-
305
- The `RunContext` handed to `openRun` carries the run’s identity (`runId`, `startedAt`), the full selection (`selectedSources()`, complete by commit time – including datasets skipped as unchanged) and the pipeline’s provenance store, when configured.
306
-
307
- A destination without run-level state implements the same contract with no-op `commit`/`abort` – `SparqlUpdateWriter` shows the pattern:
308
-
309
- ```typescript
310
- async openRun(): Promise<RunWriter> {
311
- return {
312
- write: async (dataset, quads) => {
313
- // send quads somewhere
314
- },
315
- commit: () => Promise.resolve(),
316
- abort: () => Promise.resolve(),
317
- };
318
- }
319
- ```
320
-
321
- ### Reporter
322
-
323
- A `ProgressReporter` observes the run, receiving lifecycle events such as `pipelineStart`, `stageComplete`, `datasetValidated` and `pipelineComplete`. Every method is optional, so a reporter implements only the events it cares about.
324
-
325
- Pass a single reporter, or an array to have several observe the same run – for example a console reporter alongside one that collects validation verdicts:
326
-
327
- ```typescript
328
- new Pipeline({
329
- // …
330
- reporter: [new ConsoleReporter(), verdictCollector],
331
- });
332
- ```
333
-
334
- Each reporter receives every event, in array order; a reporter that does not implement a given event is skipped for it.
335
-
336
- ### Provenance store
337
-
338
- A `ProvenanceStore` gives the pipeline a small per-dataset memory, so a future run can skip datasets that are genuinely unchanged. It is purely a storage seam: the framework owns the skip decision (see [`sourceFingerprint`](#source-change-fingerprint) and `shouldReprocess`), the store owns only how each record is persisted.
339
-
340
- ```typescript
341
- interface ProvenanceStore {
342
- check?(): Promise<void>;
343
- get(datasetUri: URL): Promise<ProcessingRecord | null>;
344
- set(datasetUri: URL, record: ProcessingRecord): Promise<void>;
345
- }
346
- ```
347
-
348
- The optional `check` verifies the store can persist records; the pipeline calls it once at the start of a run, so a store that would fail every write fails the run immediately instead of silently disabling skip-unchanged. A record write that fails mid-run is reported through the reporter (`stageFailed` with stage `'provenance'`) and does not abort the run.
349
-
350
- A `ProcessingRecord` holds the two opaque change fields – `sourceFingerprint` (derived automatically from source metadata) and `pipelineVersion` (consumer-declared) – plus `generatedAt` and a `status` of `'success'` or `'failed'`. The two change fields are compared only for equality, never parsed or ordered.
351
-
352
- #### `FileLoadedSparqlProvenanceStore`
353
-
354
- The reference implementation targets a triplestore that is served read-only and rebuilt by bulk-loading files (e.g. [QLever](https://github.com/ad-freiburg/qlever)). It reads through SPARQL queries against the live endpoint, and writes records as files for the next bulk-load – because the endpoint accepts no SPARQL UPDATE.
355
-
356
- ```typescript
357
- import { FileLoadedSparqlProvenanceStore } from '@lde/pipeline';
358
-
359
- const store = new FileLoadedSparqlProvenanceStore({
360
- queryEndpoint: new URL('http://localhost:7001/sparql'),
361
- pipelineIri: new URL('https://example.org/pipelines/dkg'),
362
- outputDir: './provenance',
363
- });
364
- ```
365
-
366
- - `get` runs a named-graph-scoped SPARQL `SELECT` against `queryEndpoint`, reading the records a previous run loaded.
367
- - `set` writes one flat [PROV-O](https://www.w3.org/TR/prov-o/) N-Quads file per dataset into `outputDir`, in the pipeline-scoped named graph, to be bulk-loaded after the run.
368
-
369
- Each record is stored as flat PROV-O on the dataset entity – `prov:generatedAtTime` plus `sourceFingerprint`, `pipelineVersion` and `status` under the `https://w3id.org/lde/provenance#` namespace. Scoping every record by `pipelineIri` (used as the named graph) lets multiple pipelines share one triplestore without colliding.
370
-
371
- #### `FileProvenanceStore`
372
-
373
- For pipelines that run without a triplestore, `FileProvenanceStore` persists all records to a single JSON file, keyed by dataset URI:
374
-
375
- ```typescript
376
- import { FileProvenanceStore } from '@lde/pipeline';
377
-
378
- const store = new FileProvenanceStore({ path: './state/provenance.json' });
379
- ```
380
-
381
- Writes are atomic (temp file + rename), so a run killed mid-write cannot corrupt the next run’s skip decisions. Safe for a single writer: concurrent pipeline processes writing the same file lose each other’s updates – use the SPARQL-backed store (or file locking) instead. The file must sit on a durable volume to survive across runs, the same requirement the SPARQL-backed store already has, and its directory must be writable by the user the pipeline runs as – `check` probes this with a real write at the start of the run, so a read-only or root-owned mount fails loudly instead of silently disabling skipping.
382
-
383
- #### Enabling skipping
384
-
385
- Skipping is opt-in. Pass a `provenanceStore` and a `pipelineVersion` to the `Pipeline`:
386
-
387
- ```typescript
388
- new Pipeline({
389
- // …
390
- provenanceStore: store,
391
- pipelineVersion: 'v3', // rotate only on releases that change output
392
- });
393
- ```
394
-
395
- For each dataset the pipeline probes its distributions, derives the source-change fingerprint, reads the stored record, and **skips before importing** when both change fields match:
396
-
397
- ```
398
- skip iff recorded.sourceFingerprint === current.sourceFingerprint
399
- AND recorded.pipelineVersion === current.pipelineVersion
400
- ```
401
-
402
- Otherwise it imports (if needed), runs the stages, and writes an updated record. `pipelineVersion` is consumer-owned and opaque: rotate it only on releases that change output, and every dataset reprocesses on the next run. It is **required** when a `provenanceStore` is configured (a skip-enabled pipeline with no version would silently freeze); when no store is configured, every dataset is reprocessed – today’s behaviour. A dataset that failed but whose source is unchanged is recorded as `'failed'` and skipped on later runs until its source changes or the version rotates, so a deterministically failing import is not retried every run.
403
-
404
- ### Source-change fingerprint
405
-
406
- `sourceFingerprint(distribution, probeResult)` derives a cheap, opaque change signal for a distribution from metadata the probe already collected – no body download. For a data dump it combines the most recent of the register’s `dct:modified` and the artifact’s HTTP `Last-Modified` with the byte size (the probe’s `Content-Length`, falling back to the declared `dcat:byteSize`). It returns `null` for a live SPARQL endpoint, or when no date and no size can be established – a `null` fingerprint never compares equal, so such a distribution is always reprocessed.
407
-
408
- ### Plugins
409
-
410
- Plugins hook into the pipeline lifecycle via the `PipelinePlugin` interface. Register them in the `plugins` array when constructing a `Pipeline`. A plugin can implement either or both of two hooks, each a `QuadTransform`:
411
-
412
- - **`beforeStageWrite`** runs over each stage’s output as it is written – scoped to one stage’s quads for one dataset.
413
- - **`beforeDatasetWrite`** runs once over a dataset’s combined cross-stage output, just before it is written – scoped to the whole dataset, so it can reconcile quads that different stages produced. The output is streamed through a single queue rather than materialized, so memory stays bounded by whatever the transform itself buffers; it honors the writer’s run lifecycle (`flush`/`reset`/`commit`/`abort`), and a failure is isolated to the dataset being written.
414
-
415
- Both hooks are domain-agnostic – dedup, roll-ups, provenance stamping, or namespace rewriting can use them. This package ships `provenancePlugin()`, `namespaceNormalizationPlugin()`, and `schemaOrgNormalizationPlugin()` (all `beforeStageWrite` plugins). Plugins that must understand a particular RDF shape live with that vocabulary: for example, [@lde/pipeline-void](../pipeline-void) ships `schemaOrgPartitionMergePlugin()`, a `beforeDatasetWrite` plugin that merges the VoID partition _nodes_ that Schema.org `http`/`https` variants produce.
416
-
417
- #### `provenancePlugin()`
418
-
419
- Appends [PROV-O](https://www.w3.org/TR/prov-o/) provenance quads (`prov:Entity`, `prov:Activity`, `prov:startedAtTime`, `prov:endedAtTime`) to every stage’s output. The `prov:Activity` is a stable IRI keyed on `(dataset, stage)`, not a blank node, so activities stay distinct – and a re-run stays idempotent – when per-dataset outputs are merged into one graph (blank-node labels are not unique across separately serialised documents and would fuse unrelated activities).
420
-
421
- #### `namespaceNormalizationPlugin(options)`
422
-
423
- Rewrites every IRI in the `from` namespace to the `to` namespace, in subject, predicate and object position alike. A blanket, vocabulary-agnostic rewrite for standardizing a namespace across a dataset’s own quads – for example when mapping instance data to an application profile.
424
-
425
- ```typescript
426
- import { namespaceNormalizationPlugin } from '@lde/pipeline';
427
-
428
- new Pipeline({
429
- // ...
430
- plugins: [
431
- namespaceNormalizationPlugin({
432
- from: 'http://example.org/',
433
- to: 'https://example.org/',
434
- }),
435
- ],
436
- });
437
- ```
438
-
439
- #### `schemaOrgNormalizationPlugin(options?)`
440
-
441
- Convenience wrapper around `namespaceNormalizationPlugin` for the common case: rewrites `http://schema.org/` to `https://schema.org/` (pass `{ reverse: true }` for the opposite direction).
442
-
443
- ```typescript
444
- import { schemaOrgNormalizationPlugin, provenancePlugin } from '@lde/pipeline';
445
-
446
- new Pipeline({
447
- // ...
448
- plugins: [schemaOrgNormalizationPlugin(), provenancePlugin()],
449
- });
450
- ```
451
-
452
- This is a plain rewrite and knows nothing about VoID. If your pipeline emits VoID partitions and a dataset mixes both schema.org namespaces, rewriting the `void:class` objects alone still leaves two `void:classPartition` nodes for one class – use [`schemaOrgPartitionMergePlugin`](../pipeline-void) from `@lde/pipeline-void`, which also merges those nodes.
453
-
454
11
  ## Usage
455
12
 
456
13
  ```typescript
@@ -484,3 +41,7 @@ const pipeline = new Pipeline({
484
41
 
485
42
  await pipeline.run();
486
43
  ```
44
+
45
+ ## Documentation
46
+
47
+ See the [full documentation](https://ldelements.org/reference/pipeline).
@@ -32,6 +32,7 @@ export function combineReporters(reporters) {
32
32
  const combined = {
33
33
  pipelineStart: (...args) => forward('pipelineStart', ...args),
34
34
  datasetsSelected: (...args) => forward('datasetsSelected', ...args),
35
+ selectionEmpty: (...args) => forward('selectionEmpty', ...args),
35
36
  datasetStart: (...args) => forward('datasetStart', ...args),
36
37
  distributionProbed: (...args) => forward('distributionProbed', ...args),
37
38
  importStarted: (...args) => forward('importStarted', ...args),
@@ -9,6 +9,21 @@ import type { ProvenanceStore } from './provenance/store.js';
9
9
  import type { StageOutputResolver } from './stageOutputResolver.js';
10
10
  import type { ProgressReporter } from './progressReporter.js';
11
11
  import { type TimeoutPolicy } from './sparql/timeoutPolicy.js';
12
+ /**
13
+ * Why a run was abandoned rather than committed: its selector produced no
14
+ * datasets, so the run holds no membership a writer could safely act on.
15
+ *
16
+ * Every writer reads the run’s selection as the authoritative membership of its
17
+ * destination – that is what lets an In-place writer sweep departed sources and
18
+ * a Blue/green writer ship a fresh collection. An empty selection would make
19
+ * that reading catastrophic (delete everything / swap in an empty collection),
20
+ * and it is indistinguishable from a registry outage or a criteria typo, so the
21
+ * pipeline aborts instead. Handed to {@link RunWriter.abort}, whose contract –
22
+ * leave the live destination as it was – is exactly the wanted behaviour.
23
+ */
24
+ export declare class EmptySelection extends Error {
25
+ constructor();
26
+ }
12
27
  /**
13
28
  * Context handed to a {@link PipelinePlugin.beforeStageWrite} transform: the
14
29
  * `dataset` whose merged output is being written and the `stage` that produced
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
package/dist/pipeline.js CHANGED
@@ -13,6 +13,24 @@ import { importOutcomeToVerdict, probeResultToVerdict, } from '@lde/distribution
13
13
  import { NotSupported } from './sparql/reader.js';
14
14
  import { combineReporters } from './combineReporters.js';
15
15
  import { ConstantTimeoutPolicy, } from './sparql/timeoutPolicy.js';
16
+ /**
17
+ * Why a run was abandoned rather than committed: its selector produced no
18
+ * datasets, so the run holds no membership a writer could safely act on.
19
+ *
20
+ * Every writer reads the run’s selection as the authoritative membership of its
21
+ * destination – that is what lets an In-place writer sweep departed sources and
22
+ * a Blue/green writer ship a fresh collection. An empty selection would make
23
+ * that reading catastrophic (delete everything / swap in an empty collection),
24
+ * and it is indistinguishable from a registry outage or a criteria typo, so the
25
+ * pipeline aborts instead. Handed to {@link RunWriter.abort}, whose contract –
26
+ * leave the live destination as it was – is exactly the wanted behaviour.
27
+ */
28
+ export class EmptySelection extends Error {
29
+ constructor() {
30
+ super('Selection produced no datasets; abandoning the run rather than treating an empty selection as membership. The destination is unchanged.');
31
+ this.name = 'EmptySelection';
32
+ }
33
+ }
16
34
  /**
17
35
  * Split an async iterable into `count` branches that can be consumed
18
36
  * independently. Backpressure is enforced by the slowest consumer –
@@ -301,17 +319,39 @@ export class Pipeline {
301
319
  const runWriter = this.beforeDatasetWrite
302
320
  ? new DatasetTransformWriter(openedWriter, this.beforeDatasetWrite)
303
321
  : openedWriter;
322
+ let abandoned = false;
304
323
  try {
305
324
  for await (const dataset of datasets) {
306
325
  selectedSources.push(dataset.iri.toString());
307
326
  await this.processDataset(dataset, runWriter, context);
308
327
  }
309
- await runWriter.commit();
328
+ // Committing an empty selection would hand every writer an empty
329
+ // membership as if it were the truth: an In-place writer’s sweep reads
330
+ // every indexed source as departed and deletes the lot, and a Blue/green
331
+ // writer swaps an empty collection live and drops the one that held the
332
+ // data. A selector returning nothing is indistinguishable from a registry
333
+ // outage or a criteria typo, so the run is abandoned instead – leaving
334
+ // the destination exactly as it was, which is what `abort` already
335
+ // guarantees for both writers. To empty a destination on purpose, drop it.
336
+ abandoned = selectedSources.length === 0;
337
+ if (!abandoned) {
338
+ await runWriter.commit();
339
+ }
310
340
  }
311
341
  catch (error) {
312
342
  await runWriter.abort(error);
313
343
  throw error;
314
344
  }
345
+ // Outside the try, so the run is aborted exactly once. Inside it, an abort
346
+ // that rejected (a writer whose lock release fails, a fan-out branch that
347
+ // rethrows) would fall into the catch and be aborted a second time, and
348
+ // would make an abandoned run throw – which is the one thing abandoning is
349
+ // meant not to do.
350
+ if (abandoned) {
351
+ const reason = new EmptySelection();
352
+ this.reporter?.selectionEmpty?.(reason.message);
353
+ await runWriter.abort(reason);
354
+ }
315
355
  const finalMemory = process.memoryUsage();
316
356
  this.reporter?.pipelineComplete?.({
317
357
  duration: Date.now() - start,
@@ -21,6 +21,12 @@ export interface DistributionAnalysisResult {
21
21
  export interface ProgressReporter {
22
22
  pipelineStart?(name: string): void;
23
23
  datasetsSelected?(count: number, duration: number): void;
24
+ /**
25
+ * Called when the run is abandoned because the selection was empty – no
26
+ * commit, destination unchanged. Distinct from a completed run that indexed
27
+ * nothing: the writers were told to abort, so nothing was swept or swapped.
28
+ */
29
+ selectionEmpty?(reason: string): void;
24
30
  datasetStart?(dataset: Dataset): void;
25
31
  /** Called each time a single distribution probe completes. */
26
32
  distributionProbed?(result: DistributionAnalysisResult): void;
@@ -53,7 +59,7 @@ export interface ProgressReporter {
53
59
  /**
54
60
  * Called once per (dataset, validator) pair after all stages for a dataset
55
61
  * have run. Fires regardless of whether any stage actually invoked
56
- * `validate()` — the report reflects the validator’s accumulated state.
62
+ * `validate()` – the report reflects the validator’s accumulated state.
57
63
  * When no stage produced data, the report typically carries
58
64
  * `quadsValidated: 0` and `conforms: true` (the SHACL vacuous-truth
59
65
  * default); consumers that want to distinguish ‘not tested’ from ‘tested
@@ -1 +1 @@
1
- {"version":3,"file":"progressReporter.d.ts","sourceRoot":"","sources":["../src/progressReporter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"progressReporter.d.ts","sourceRoot":"","sources":["../src/progressReporter.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,18 @@
1
- import { type QueryConstruct } from '@traqula/rules-sparql-1-1';
1
+ import { type QueryConstruct, type QuerySelect } from '@traqula/rules-sparql-1-1';
2
2
  /**
3
- * Set the default graph (FROM clause) on a parsed CONSTRUCT query.
3
+ * Set the default graph (FROM clause) on a parsed CONSTRUCT or SELECT query.
4
4
  *
5
5
  * Mutates the query in place, replacing any existing FROM clause.
6
+ *
7
+ * Both query forms scope the same way, and a stage reading a
8
+ * {@link Distribution} with a `namedGraph` must scope **both**: were only the
9
+ * reader to honour it, an item selector would pick its roots from the whole
10
+ * endpoint and the reader would then find nothing for most of them.
11
+ *
12
+ * The graph IRI is {@link assertSafeIri}-checked before it becomes an `<…>`
13
+ * reference: a `namedGraph` is a plain string, typically carried in from
14
+ * third-party registry data, and one containing an angle bracket or whitespace
15
+ * would break out of the IRI reference and rewrite the query around it.
6
16
  */
7
- export declare function withDefaultGraph(query: QueryConstruct, graphIri: string): void;
17
+ export declare function withDefaultGraph(query: QueryConstruct | QuerySelect, graphIri: string): void;
8
18
  //# sourceMappingURL=graph.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/sparql/graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAI5E;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,MAAM,GACf,IAAI,CAKN"}
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/sparql/graph.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAInC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,GAAG,WAAW,EACnC,QAAQ,EAAE,MAAM,GACf,IAAI,CAMN"}
@@ -1,10 +1,22 @@
1
- import { AstFactory } from '@traqula/rules-sparql-1-1';
1
+ import { assertSafeIri } from '@lde/dataset';
2
+ import { AstFactory, } from '@traqula/rules-sparql-1-1';
2
3
  const F = new AstFactory();
3
4
  /**
4
- * Set the default graph (FROM clause) on a parsed CONSTRUCT query.
5
+ * Set the default graph (FROM clause) on a parsed CONSTRUCT or SELECT query.
5
6
  *
6
7
  * Mutates the query in place, replacing any existing FROM clause.
8
+ *
9
+ * Both query forms scope the same way, and a stage reading a
10
+ * {@link Distribution} with a `namedGraph` must scope **both**: were only the
11
+ * reader to honour it, an item selector would pick its roots from the whole
12
+ * endpoint and the reader would then find nothing for most of them.
13
+ *
14
+ * The graph IRI is {@link assertSafeIri}-checked before it becomes an `<…>`
15
+ * reference: a `namedGraph` is a plain string, typically carried in from
16
+ * third-party registry data, and one containing an angle bracket or whitespace
17
+ * would break out of the IRI reference and rewrite the query around it.
7
18
  */
8
19
  export function withDefaultGraph(query, graphIri) {
20
+ assertSafeIri(graphIri);
9
21
  query.datasets = F.datasetClauses([{ clauseType: 'default', value: F.termNamed(F.gen(), graphIri) }], F.gen());
10
22
  }
@@ -7,7 +7,7 @@ export interface SparqlItemSelectorOptions {
7
7
  * SELECT query projecting at least one named variable.
8
8
  *
9
9
  * A `LIMIT` clause in the query overrides the stage's `batchSize` as the
10
- * page size — use this when the SPARQL endpoint enforces a result limit.
10
+ * page size – use this when the SPARQL endpoint enforces a result limit.
11
11
  * It does **not** cap the total number of bindings the selector yields;
12
12
  * pagination continues with `OFFSET` until the source is exhausted. Use
13
13
  * {@link maxResults} to cap the total.
@@ -15,7 +15,7 @@ export interface SparqlItemSelectorOptions {
15
15
  query: string;
16
16
  /**
17
17
  * Maximum number of bindings the selector yields across all pages.
18
- * Use this for sampling — “give me at most N items, don’t walk the full
18
+ * Use this for sampling – “give me at most N items, don’t walk the full
19
19
  * source”. Independent of {@link query}’s `LIMIT`, which controls page
20
20
  * size. Pagination stops as soon as `maxResults` bindings have been
21
21
  * yielded. Must not be negative.
@@ -36,8 +36,12 @@ export interface SparqlItemSelectorOptions {
36
36
  * pagination early. Dropped rows are still fetched; to skip them at the
37
37
  * endpoint, filter in the query itself (e.g. `FILTER(isIRI(?s))`).
38
38
  *
39
- * The endpoint URL comes from the {@link Distribution} passed to {@link select}.
40
- * Pagination is an internal detail — consumers iterate binding rows directly.
39
+ * The endpoint URL comes from the {@link Distribution} passed to {@link select},
40
+ * as does the graph to scope to: a distribution declaring a `namedGraph` selects
41
+ * within that graph (a `FROM` clause), the same scoping `SparqlConstructReader`
42
+ * applies to the extraction. The two must scope alike, or the roots come from a
43
+ * wider source than the quads read for them.
44
+ * Pagination is an internal detail – consumers iterate binding rows directly.
41
45
  *
42
46
  * The page size (results per SPARQL request) is determined by, in order:
43
47
  * 1. A `LIMIT` clause in the selector query (for endpoints with hard result limits)
@@ -1 +1 @@
1
- {"version":3,"file":"selector.d.ts","sourceRoot":"","sources":["../../src/sparql/selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAQ9D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAoBpD,MAAM,WAAW,yBAAyB;IACxC;;;;;;;;OAQG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAwB;gBAEzC,OAAO,EAAE,yBAAyB;IA0BvC,MAAM,CACX,YAAY,EAAE,YAAY,EAC1B,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,aAAa,GACtB,qBAAqB,CAAC,gBAAgB,CAAC;IAwE1C;;;;OAIG;YACW,uBAAuB;CA8BtC"}
1
+ {"version":3,"file":"selector.d.ts","sourceRoot":"","sources":["../../src/sparql/selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAQ9D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAoBpD,MAAM,WAAW,yBAAyB;IACxC;;;;;;;;OAQG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAwB;gBAEzC,OAAO,EAAE,yBAAyB;IA0BvC,MAAM,CACX,YAAY,EAAE,YAAY,EAC1B,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,aAAa,GACtB,qBAAqB,CAAC,gBAAgB,CAAC;IA8E1C;;;;OAIG;YACW,uBAAuB;CA8BtC"}
@@ -2,6 +2,7 @@ import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint';
2
2
  import { Parser } from '@traqula/parser-sparql-1-1';
3
3
  import { Generator } from '@traqula/generator-sparql-1-1';
4
4
  import { AstFactory, } from '@traqula/rules-sparql-1-1';
5
+ import { withDefaultGraph } from './graph.js';
5
6
  import { ConstantTimeoutPolicy, } from './timeoutPolicy.js';
6
7
  const transientStatusPattern = /HTTP status (\d+)/;
7
8
  /**
@@ -25,8 +26,12 @@ const F = new AstFactory();
25
26
  * pagination early. Dropped rows are still fetched; to skip them at the
26
27
  * endpoint, filter in the query itself (e.g. `FILTER(isIRI(?s))`).
27
28
  *
28
- * The endpoint URL comes from the {@link Distribution} passed to {@link select}.
29
- * Pagination is an internal detail — consumers iterate binding rows directly.
29
+ * The endpoint URL comes from the {@link Distribution} passed to {@link select},
30
+ * as does the graph to scope to: a distribution declaring a `namedGraph` selects
31
+ * within that graph (a `FROM` clause), the same scoping `SparqlConstructReader`
32
+ * applies to the extraction. The two must scope alike, or the roots come from a
33
+ * wider source than the quads read for them.
34
+ * Pagination is an internal detail – consumers iterate binding rows directly.
30
35
  *
31
36
  * The page size (results per SPARQL request) is determined by, in order:
32
37
  * 1. A `LIMIT` clause in the selector query (for endpoints with hard result limits)
@@ -76,6 +81,12 @@ export class SparqlItemSelector {
76
81
  throw new Error(`Page size must be positive; got ${basePageSize} (from the query’s LIMIT or the stage’s batchSize)`);
77
82
  }
78
83
  const endpoint = distribution.accessUrl;
84
+ // Per call, so concurrent selections over different datasets cannot see
85
+ // each other's graph scope or page window.
86
+ const query = structuredClone(this.parsed);
87
+ if (distribution.namedGraph) {
88
+ withDefaultGraph(query, distribution.namedGraph);
89
+ }
79
90
  const policy = options?.timeout ?? defaultTimeoutPolicy;
80
91
  let offset = 0;
81
92
  let totalFetched = 0;
@@ -92,8 +103,8 @@ export class SparqlItemSelector {
92
103
  const effectivePageSize = offset === 0 || totalFetched > totalYielded
93
104
  ? basePageSize
94
105
  : Math.min(basePageSize, remaining);
95
- this.parsed.solutionModifiers.limitOffset = F.solutionModifierLimitOffset(effectivePageSize, offset, F.gen());
96
- const paginatedQuery = generator.generate(this.parsed);
106
+ query.solutionModifiers.limitOffset = F.solutionModifierLimitOffset(effectivePageSize, offset, F.gen());
107
+ const paginatedQuery = generator.generate(query);
97
108
  const stream = await this.fetchBindingsWithPolicy(endpoint, paginatedQuery, policy);
98
109
  // Fetched rows drive pagination, yielded rows only `maxResults` – see
99
110
  // the class JSDoc for why dropped rows must keep their page slot.
package/dist/stage.d.ts CHANGED
@@ -102,6 +102,35 @@ export interface StageOptions<Out = Quad> {
102
102
  * @default 10
103
103
  */
104
104
  maxConcurrency?: number;
105
+ /**
106
+ * Where this stage reads, when that is **not** the dataset’s own
107
+ * distribution: given the dataset and the distribution the pipeline resolved
108
+ * for it, return the {@link Distribution} this stage should use instead.
109
+ *
110
+ * One hook covers the whole stage – the item selector and every reader are
111
+ * handed the same substitute – because a stage that reads elsewhere must
112
+ * *select* there too, or its roots and its quads come from different sources.
113
+ *
114
+ * The dataset is passed so the substitute can be derived per dataset. The
115
+ * motivating case is a stage sourced from the dataset registry rather than
116
+ * from the dataset’s data: the registry endpoint is fixed, but the graph to
117
+ * scope to is the dataset in hand.
118
+ *
119
+ * ```ts
120
+ * sourceFor: (dataset) => {
121
+ * const source = new Distribution(registryEndpoint, SPARQL_MEDIA_TYPE);
122
+ * source.namedGraph = dataset.iri.toString();
123
+ * return source;
124
+ * }
125
+ * ```
126
+ *
127
+ * Omit it – the default – to read the dataset’s own distribution.
128
+ *
129
+ * A **chained child** stage may not declare one: a child is handed its
130
+ * parent’s output as its distribution, so substituting a source would discard
131
+ * the chain it exists to continue. The parent’s constructor rejects that.
132
+ */
133
+ sourceFor?: (dataset: Dataset, distribution: Distribution) => Distribution;
105
134
  /** Child stages that chain off this stage's output. */
106
135
  stages?: Stage[];
107
136
  /**
@@ -148,6 +177,9 @@ export declare class Stage<Out = Quad> {
148
177
  readonly stages: readonly Stage[];
149
178
  /** Whether an empty result is treated as a hard failure. @see {@link StageOptions.expectsOutput} */
150
179
  readonly expectsOutput: boolean;
180
+ /** Whether this stage reads a source of its own rather than the dataset’s
181
+ * resolved distribution. @see {@link StageOptions.sourceFor} */
182
+ readonly sourcesOwnData: boolean;
151
183
  private readonly readers;
152
184
  private readonly itemSelector?;
153
185
  private readonly batchSize;
@@ -155,10 +187,11 @@ export declare class Stage<Out = Quad> {
155
187
  private readonly validation?;
156
188
  private readonly project?;
157
189
  private readonly queueCapacity?;
190
+ private readonly sourceFor?;
158
191
  constructor(options: StageOptions<Out>);
159
192
  /** The validator for this stage, if configured. */
160
193
  get validator(): Validator | undefined;
161
- run(dataset: Dataset, distribution: Distribution, writer: DatasetWriter<Out>, options?: RunOptions): Promise<NotSupported | void>;
194
+ run(dataset: Dataset, resolved: Distribution, writer: DatasetWriter<Out>, options?: RunOptions): Promise<NotSupported | void>;
162
195
  /**
163
196
  * Throw when {@link StageOptions.expectsOutput} is set but the stage produced
164
197
  * no quads – a supported-but-empty result that signals a truncated or aborted
@@ -1 +1 @@
1
- {"version":3,"file":"stage.d.ts","sourceRoot":"","sources":["../src/stage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stage.d.ts","sourceRoot":"","sources":["../src/stage.ts"],"names":[],"mappings":"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"}
package/dist/stage.js CHANGED
@@ -6,6 +6,9 @@ export class Stage {
6
6
  stages;
7
7
  /** Whether an empty result is treated as a hard failure. @see {@link StageOptions.expectsOutput} */
8
8
  expectsOutput;
9
+ /** Whether this stage reads a source of its own rather than the dataset’s
10
+ * resolved distribution. @see {@link StageOptions.sourceFor} */
11
+ sourcesOwnData;
9
12
  readers;
10
13
  itemSelector;
11
14
  batchSize;
@@ -13,6 +16,7 @@ export class Stage {
13
16
  validation;
14
17
  project;
15
18
  queueCapacity;
19
+ sourceFor;
16
20
  constructor(options) {
17
21
  if (options.project && !options.itemSelector) {
18
22
  throw new Error(`Stage '${options.name}': 'project' requires an 'itemSelector' – without one there is no batch to project, only the readers' whole output.`);
@@ -20,6 +24,14 @@ export class Stage {
20
24
  if (options.project && (options.stages?.length ?? 0) > 0) {
21
25
  throw new Error(`Stage '${options.name}': 'project' cannot combine with chained 'stages' – a chained stage serializes to N-Triples, which a projected item cannot.`);
22
26
  }
27
+ // A child is handed its parent's output as its distribution; a `sourceFor`
28
+ // would substitute that away, silently reading elsewhere instead of
29
+ // continuing the chain. Caught here rather than at run time, where it looks
30
+ // like a chain that mysteriously produced nothing.
31
+ const sourcedChild = options.stages?.find((child) => child.sourcesOwnData);
32
+ if (sourcedChild) {
33
+ throw new Error(`Stage '${options.name}': chained stage '${sourcedChild.name}' declares 'sourceFor' – a chained stage reads its parent's output, so it cannot source its own data.`);
34
+ }
23
35
  this.name = options.name;
24
36
  this.stages = options.stages ?? [];
25
37
  this.readers = normalizeReaders(options.readers);
@@ -30,13 +42,20 @@ export class Stage {
30
42
  this.expectsOutput = options.expectsOutput ?? false;
31
43
  this.project = options.project;
32
44
  this.queueCapacity = options.queueCapacity;
45
+ this.sourceFor = options.sourceFor;
46
+ this.sourcesOwnData = options.sourceFor !== undefined;
33
47
  }
34
48
  /** The validator for this stage, if configured. */
35
49
  get validator() {
36
50
  return this.validation?.validator;
37
51
  }
38
- async run(dataset, distribution, writer, options) {
52
+ async run(dataset, resolved, writer, options) {
39
53
  const timeout = options?.timeout;
54
+ // Substituted once, before anything reads: the selector and the readers
55
+ // must agree on where this stage's data comes from.
56
+ const distribution = this.sourceFor
57
+ ? this.sourceFor(dataset, resolved)
58
+ : resolved;
40
59
  if (this.itemSelector) {
41
60
  return this.runWithSelector(this.itemSelector.select(distribution, this.batchSize, {
42
61
  timeout,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/pipeline",
3
- "version": "0.35.4",
3
+ "version": "0.35.6",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/ldelements/lde.git",
6
6
  "directory": "packages/pipeline"
@@ -39,7 +39,7 @@
39
39
  "fetch-sparql-endpoint": "^7.1.1",
40
40
  "filenamify-url": "^4.0.0",
41
41
  "is-network-error": "^1.3.2",
42
- "n3": "^2.1.1",
42
+ "n3": "^2.1.2",
43
43
  "p-retry": "^8.0.0",
44
44
  "rdf-string": "^2.0.1",
45
45
  "tslib": "^2.3.0"