@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 +9 -448
- package/dist/combineReporters.js +1 -0
- package/dist/pipeline.d.ts +15 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +41 -1
- package/dist/progressReporter.d.ts +7 -1
- package/dist/progressReporter.d.ts.map +1 -1
- package/dist/sparql/graph.d.ts +13 -3
- package/dist/sparql/graph.d.ts.map +1 -1
- package/dist/sparql/graph.js +14 -2
- package/dist/sparql/selector.d.ts +8 -4
- package/dist/sparql/selector.d.ts.map +1 -1
- package/dist/sparql/selector.js +15 -4
- package/dist/stage.d.ts +34 -1
- package/dist/stage.d.ts.map +1 -1
- package/dist/stage.js +20 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,456 +1,13 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
package/dist/combineReporters.js
CHANGED
|
@@ -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),
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -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
|
package/dist/pipeline.d.ts.map
CHANGED
|
@@ -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
|
-
|
|
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()`
|
|
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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"}
|
package/dist/sparql/graph.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/sparql/graph.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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;
|
|
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"}
|
package/dist/sparql/selector.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
96
|
-
const paginatedQuery = generator.generate(
|
|
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,
|
|
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
|
package/dist/stage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage.d.ts","sourceRoot":"","sources":["../src/stage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,CAAC,GAAG,IAAI,CAC/B,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,EAC1B,OAAO,EAAE,GAAG,KACT,aAAa,CAAC,IAAI,CAAC,CAAC;AAEzB;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,CAAC,GAAG,IAAI,CAChC,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,OAAO,EAAE,YAAY,KAClB,QAAQ,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;AAExC;;;;;GAKG;AACH,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACvC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,aAAa,CAAC,aAAa,CAAC,EAAE,CAAC;CAC3E;AAED,yEAAyE;AACzE,MAAM,MAAM,YAAY,
|
|
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,
|
|
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.
|
|
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.
|
|
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"
|