@lde/pipeline 0.31.5 → 0.33.0
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 +45 -29
- package/dist/pipeline.d.ts +2 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +114 -41
- package/dist/provenance/fileLoadedSparqlProvenanceStore.d.ts.map +1 -1
- package/dist/provenance/fileLoadedSparqlProvenanceStore.js +5 -2
- package/dist/sparql/index.d.ts +1 -1
- package/dist/sparql/index.d.ts.map +1 -1
- package/dist/sparql/index.js +1 -1
- package/dist/sparql/{executor.d.ts → reader.d.ts} +28 -27
- package/dist/sparql/reader.d.ts.map +1 -0
- package/dist/sparql/{executor.js → reader.js} +18 -17
- package/dist/sparql/selector.d.ts +1 -1
- package/dist/sparql/selector.d.ts.map +1 -1
- package/dist/sparql/values.d.ts +1 -1
- package/dist/sparql/values.d.ts.map +1 -1
- package/dist/stage.d.ts +31 -31
- package/dist/stage.d.ts.map +1 -1
- package/dist/stage.js +27 -27
- package/dist/writer/fileWriter.d.ts +34 -7
- package/dist/writer/fileWriter.d.ts.map +1 -1
- package/dist/writer/fileWriter.js +71 -39
- package/dist/writer/sparqlUpdateWriter.d.ts +19 -8
- package/dist/writer/sparqlUpdateWriter.d.ts.map +1 -1
- package/dist/writer/sparqlUpdateWriter.js +25 -12
- package/dist/writer/writer.d.ts +103 -16
- package/dist/writer/writer.d.ts.map +1 -1
- package/package.json +7 -7
- package/dist/sparql/executor.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
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.
|
|
4
4
|
|
|
5
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
|
|
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
7
|
- **Extensible.** A plugin system lets packages like [@lde/pipeline-void](../pipeline-void) (or your own plugins) hook into the pipeline lifecycle.
|
|
8
8
|
|
|
9
9
|
## Components
|
|
@@ -50,7 +50,7 @@ const resolver = new ImportResolver(new SparqlDistributionResolver(), {
|
|
|
50
50
|
|
|
51
51
|
### Stage
|
|
52
52
|
|
|
53
|
-
A stage groups an item selector, one or more
|
|
53
|
+
A stage groups an item selector, one or more readers, and configuration:
|
|
54
54
|
|
|
55
55
|
```typescript
|
|
56
56
|
new Stage({
|
|
@@ -58,7 +58,7 @@ new Stage({
|
|
|
58
58
|
itemSelector: new SparqlItemSelector({
|
|
59
59
|
query: 'SELECT DISTINCT ?class WHERE { ?s a ?class }',
|
|
60
60
|
}),
|
|
61
|
-
|
|
61
|
+
readers: reader,
|
|
62
62
|
batchSize: 100,
|
|
63
63
|
maxConcurrency: 5,
|
|
64
64
|
});
|
|
@@ -66,9 +66,9 @@ new Stage({
|
|
|
66
66
|
|
|
67
67
|
#### Batch size
|
|
68
68
|
|
|
69
|
-
`batchSize` (default: 10) controls how many variable bindings are passed to each
|
|
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
70
|
|
|
71
|
-
Some SPARQL endpoints enforce different result limits for SELECT and CONSTRUCT queries. Since the selector uses SELECT and the
|
|
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
72
|
|
|
73
73
|
```typescript
|
|
74
74
|
// Endpoint caps SELECT results at 500, but each CONSTRUCT can handle 1000 bindings.
|
|
@@ -77,14 +77,14 @@ new Stage({
|
|
|
77
77
|
itemSelector: new SparqlItemSelector({
|
|
78
78
|
query: 'SELECT DISTINCT ?class WHERE { ?s a ?class } LIMIT 500',
|
|
79
79
|
}),
|
|
80
|
-
|
|
80
|
+
readers: reader,
|
|
81
81
|
batchSize: 1000, // Two SELECT pages fill one CONSTRUCT batch.
|
|
82
82
|
});
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
#### Concurrency
|
|
86
86
|
|
|
87
|
-
`maxConcurrency` (default: 10) limits the total number of concurrent SPARQL queries. Within each batch, all
|
|
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
88
|
|
|
89
89
|
#### Expecting output
|
|
90
90
|
|
|
@@ -94,7 +94,7 @@ Set it for scalar aggregates such as `SELECT (COUNT(*) AS ?n)`, which always ret
|
|
|
94
94
|
|
|
95
95
|
### Item Selector
|
|
96
96
|
|
|
97
|
-
Selects resources from the distribution and fans out
|
|
97
|
+
Selects resources from the distribution and fans out reader calls per batch of results. Implements the `ItemSelector` interface:
|
|
98
98
|
|
|
99
99
|
```typescript
|
|
100
100
|
interface ItemSelector {
|
|
@@ -143,12 +143,12 @@ const itemSelector: ItemSelector = {
|
|
|
143
143
|
};
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
###
|
|
146
|
+
### Reader
|
|
147
147
|
|
|
148
|
-
Generates RDF triples. The built-in `
|
|
148
|
+
Generates RDF triples. The built-in `SparqlConstructReader` runs a SPARQL CONSTRUCT query with template substitution and variable bindings:
|
|
149
149
|
|
|
150
150
|
```typescript
|
|
151
|
-
const
|
|
151
|
+
const reader = new SparqlConstructReader({
|
|
152
152
|
query: 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }',
|
|
153
153
|
});
|
|
154
154
|
```
|
|
@@ -156,7 +156,7 @@ const executor = new SparqlConstructExecutor({
|
|
|
156
156
|
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:
|
|
157
157
|
|
|
158
158
|
```typescript
|
|
159
|
-
const
|
|
159
|
+
const reader = new SparqlConstructReader({
|
|
160
160
|
query: 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }',
|
|
161
161
|
lineBuffer: true,
|
|
162
162
|
});
|
|
@@ -165,30 +165,30 @@ const executor = new SparqlConstructExecutor({
|
|
|
165
165
|
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)):
|
|
166
166
|
|
|
167
167
|
```typescript
|
|
168
|
-
const
|
|
168
|
+
const reader = new SparqlConstructReader({
|
|
169
169
|
query: 'CONSTRUCT { ?s a edm:ProvidedCHO . ?s ?p ?o } WHERE { ?s ?p ?o }',
|
|
170
170
|
deduplicate: true,
|
|
171
171
|
});
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
-
The dedup set is scoped to each `
|
|
174
|
+
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.
|
|
175
175
|
|
|
176
176
|
### Extending a stage with a quad transform
|
|
177
177
|
|
|
178
|
-
Some logic is hard to express in pure SPARQL — cleaning up messy date notations, converting locale-specific dates to ISO 8601, or sampling
|
|
178
|
+
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).
|
|
179
179
|
|
|
180
|
-
A transform receives an `
|
|
180
|
+
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.
|
|
181
181
|
|
|
182
182
|
```typescript
|
|
183
183
|
import { DataFactory } from 'n3';
|
|
184
184
|
import {
|
|
185
185
|
Stage,
|
|
186
|
-
|
|
186
|
+
SparqlConstructReader,
|
|
187
187
|
type QuadTransform,
|
|
188
|
-
type
|
|
188
|
+
type ReaderContext,
|
|
189
189
|
} from '@lde/pipeline';
|
|
190
190
|
|
|
191
|
-
const cleanDates: QuadTransform<
|
|
191
|
+
const cleanDates: QuadTransform<ReaderContext> = async function* (quads) {
|
|
192
192
|
for await (const quad of quads) {
|
|
193
193
|
if (quad.object.termType === 'Literal' && isMessyDate(quad.object)) {
|
|
194
194
|
yield DataFactory.quad(
|
|
@@ -207,8 +207,8 @@ const cleanDates: QuadTransform<ExecutorContext> = async function* (quads) {
|
|
|
207
207
|
|
|
208
208
|
new Stage({
|
|
209
209
|
name: 'dates',
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
readers: {
|
|
211
|
+
reader: await SparqlConstructReader.fromFile('dates.rq'),
|
|
212
212
|
transform: cleanDates,
|
|
213
213
|
},
|
|
214
214
|
});
|
|
@@ -252,11 +252,11 @@ Transitions are forwarded to the `ProgressReporter` via `timeoutTightened` / `ti
|
|
|
252
252
|
|
|
253
253
|
Implement `TimeoutPolicy` directly for custom strategies (closing over shared state in the factory if you want it to span datasets).
|
|
254
254
|
|
|
255
|
-
Timeouts live at the pipeline level — neither `
|
|
255
|
+
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.
|
|
256
256
|
|
|
257
257
|
### Validation
|
|
258
258
|
|
|
259
|
-
Stages can optionally validate their output quads against a `Validator`. Validation operates on the **combined output of all
|
|
259
|
+
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.
|
|
260
260
|
|
|
261
261
|
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.
|
|
262
262
|
|
|
@@ -268,7 +268,7 @@ import { FileWriter } from '@lde/pipeline';
|
|
|
268
268
|
|
|
269
269
|
new Stage({
|
|
270
270
|
name: 'transform',
|
|
271
|
-
|
|
271
|
+
readers: await SparqlConstructReader.fromFile('transform.rq'),
|
|
272
272
|
validation: {
|
|
273
273
|
validator: new ShaclValidator({
|
|
274
274
|
shapesFile: './shapes.ttl',
|
|
@@ -295,10 +295,26 @@ After all stages for a dataset have run, the pipeline calls `validator.report(da
|
|
|
295
295
|
|
|
296
296
|
### Writer
|
|
297
297
|
|
|
298
|
-
Writes generated quads to a destination:
|
|
298
|
+
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'`.
|
|
299
299
|
|
|
300
|
-
- `SparqlUpdateWriter` — writes to a SPARQL endpoint via UPDATE queries
|
|
301
|
-
- `FileWriter` — writes to local files
|
|
300
|
+
- `SparqlUpdateWriter` — writes to a SPARQL endpoint via UPDATE queries; writes are visible as they land, so `commit`/`abort` are no-ops
|
|
301
|
+
- `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
|
|
302
|
+
|
|
303
|
+
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.
|
|
304
|
+
|
|
305
|
+
A destination without run-level state implements the same contract with no-op `commit`/`abort` – `SparqlUpdateWriter` shows the pattern:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
async openRun(): Promise<RunWriter> {
|
|
309
|
+
return {
|
|
310
|
+
write: async (dataset, quads) => {
|
|
311
|
+
// send quads somewhere
|
|
312
|
+
},
|
|
313
|
+
commit: () => Promise.resolve(),
|
|
314
|
+
abort: () => Promise.resolve(),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
```
|
|
302
318
|
|
|
303
319
|
### Reporter
|
|
304
320
|
|
|
@@ -425,7 +441,7 @@ new Pipeline({
|
|
|
425
441
|
import {
|
|
426
442
|
Pipeline,
|
|
427
443
|
Stage,
|
|
428
|
-
|
|
444
|
+
SparqlConstructReader,
|
|
429
445
|
SparqlItemSelector,
|
|
430
446
|
SparqlUpdateWriter,
|
|
431
447
|
ManualDatasetSelection,
|
|
@@ -439,7 +455,7 @@ const pipeline = new Pipeline({
|
|
|
439
455
|
itemSelector: new SparqlItemSelector({
|
|
440
456
|
query: 'SELECT DISTINCT ?class WHERE { ?s a ?class }',
|
|
441
457
|
}),
|
|
442
|
-
|
|
458
|
+
readers: new SparqlConstructReader({
|
|
443
459
|
query:
|
|
444
460
|
'CONSTRUCT { ?class a <http://example.org/Class> } WHERE { ?s a ?class }',
|
|
445
461
|
}),
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface PipelinePlugin {
|
|
|
27
27
|
* Transform the merged, post-stage quad stream before writing (extension
|
|
28
28
|
* point 2: pipeline-wide, post-merge). The home of cross-cutting concerns
|
|
29
29
|
* – provenance, namespace normalisation – that apply regardless of which
|
|
30
|
-
*
|
|
30
|
+
* reader produced a quad.
|
|
31
31
|
*/
|
|
32
32
|
beforeStageWrite?: QuadTransform<BeforeStageWriteContext>;
|
|
33
33
|
}
|
|
@@ -97,7 +97,7 @@ export declare class Pipeline {
|
|
|
97
97
|
private reportValidators;
|
|
98
98
|
private collectStages;
|
|
99
99
|
/**
|
|
100
|
-
* The writer a stage's merged output is written through: the
|
|
100
|
+
* The writer a stage's merged output is written through: the open run
|
|
101
101
|
* wrapped with the plugins' {@link PipelinePlugin.beforeStageWrite}
|
|
102
102
|
* transforms, carrying this `stage`'s identity so a transform can mint stable
|
|
103
103
|
* per-`(dataset, stage)` IRIs rather than blank nodes.
|
package/dist/pipeline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAgB,MAAM,cAAc,CAAC;AAGrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAKV,MAAM,EACP,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,oBAAoB,EAI1B,MAAM,4BAA4B,CAAC;AAKpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAY7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,EAAE;QACT,mBAAmB,EAAE,mBAAmB,CAAC;QACzC,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,gBAAgB,GAAG,SAAS,gBAAgB,EAAE,CAAC;IAC1D;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,aAAa,CAAC;CAC/B;AAqID,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAyC;IAC3E,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAuB;IAC5D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAkB;IACnD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAS;gBAE9B,OAAO,EAAE,eAAe;IA+C9B,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;YAyCZ,cAAc;IAqN5B,+EAA+E;YACjE,aAAa;YAmBb,gBAAgB;IAW9B,OAAO,CAAE,aAAa;IAOtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAMnB;;;;;;;;OAQG;IACH,OAAO,CAAC,0BAA0B;IA4BlC;;;;;OAKG;YACW,SAAS;IAuCvB;;;OAGG;YACW,QAAQ;IA0CtB,2EAA2E;YAC7D,eAAe;YAqBf,QAAQ;YAuEP,SAAS;CAczB"}
|
package/dist/pipeline.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { StreamParser } from 'n3';
|
|
3
4
|
import { FileWriter } from './writer/fileWriter.js';
|
|
4
5
|
import { NoDistributionAvailable, ResolvedDistribution, } from './distribution/resolver.js';
|
|
@@ -8,7 +9,7 @@ import { shouldReprocess } from './provenance/reprocessDecision.js';
|
|
|
8
9
|
import { NetworkError, SparqlProbeResult, } from '@lde/distribution-probe';
|
|
9
10
|
import { ImportSuccessful } from '@lde/sparql-importer';
|
|
10
11
|
import { importOutcomeToVerdict, probeResultToVerdict, } from '@lde/distribution-health';
|
|
11
|
-
import { NotSupported } from './sparql/
|
|
12
|
+
import { NotSupported } from './sparql/reader.js';
|
|
12
13
|
import { combineReporters } from './combineReporters.js';
|
|
13
14
|
import { ConstantTimeoutPolicy, } from './sparql/timeoutPolicy.js';
|
|
14
15
|
/**
|
|
@@ -54,17 +55,57 @@ class FanOutWriter {
|
|
|
54
55
|
constructor(writers) {
|
|
55
56
|
this.writers = writers;
|
|
56
57
|
}
|
|
58
|
+
async openRun(context) {
|
|
59
|
+
// Opening a writer's run can have side effects (a cross-pod lock, a fresh
|
|
60
|
+
// collection). If one writer opens but a sibling then fails, the pipeline
|
|
61
|
+
// never gets a RunWriter to abort, so roll the opened ones back here
|
|
62
|
+
// before rethrowing — otherwise their locks and collections leak.
|
|
63
|
+
const results = await Promise.allSettled(this.writers.map((writer) => writer.openRun(context)));
|
|
64
|
+
const opened = results.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
|
|
65
|
+
const failure = results.find((result) => result.status === 'rejected');
|
|
66
|
+
if (failure) {
|
|
67
|
+
await Promise.allSettled(opened.map((run) => run.abort(failure.reason)));
|
|
68
|
+
throw failure.reason;
|
|
69
|
+
}
|
|
70
|
+
return new FanOutRunWriter(opened);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Run independent branch operations concurrently, giving every branch its
|
|
75
|
+
* chance even when one fails; the first failure is rethrown afterwards.
|
|
76
|
+
*/
|
|
77
|
+
async function settleBranches(tasks) {
|
|
78
|
+
const results = await Promise.allSettled(tasks);
|
|
79
|
+
const failed = results.find((result) => result.status === 'rejected');
|
|
80
|
+
if (failed)
|
|
81
|
+
throw failed.reason;
|
|
82
|
+
}
|
|
83
|
+
class FanOutRunWriter {
|
|
84
|
+
runs;
|
|
85
|
+
constructor(runs) {
|
|
86
|
+
this.runs = runs;
|
|
87
|
+
}
|
|
57
88
|
async write(dataset, quads) {
|
|
58
|
-
const branches = tee(quads, this.
|
|
59
|
-
await Promise.all(this.
|
|
89
|
+
const branches = tee(quads, this.runs.length);
|
|
90
|
+
await Promise.all(this.runs.map((run, index) => run.write(dataset, branches[index])));
|
|
60
91
|
}
|
|
61
|
-
async flush(dataset) {
|
|
62
|
-
for (const
|
|
63
|
-
await
|
|
92
|
+
async flush(dataset, outcome) {
|
|
93
|
+
for (const run of this.runs)
|
|
94
|
+
await run.flush?.(dataset, outcome);
|
|
64
95
|
}
|
|
65
96
|
async reset(dataset) {
|
|
66
|
-
for (const
|
|
67
|
-
await
|
|
97
|
+
for (const run of this.runs)
|
|
98
|
+
await run.reset?.(dataset);
|
|
99
|
+
}
|
|
100
|
+
async commit() {
|
|
101
|
+
// Destinations are independent, so their commits (alias swaps, sweeps –
|
|
102
|
+
// the slowest part of a run) need not queue behind each other.
|
|
103
|
+
await settleBranches(this.runs.map((run) => run.commit()));
|
|
104
|
+
}
|
|
105
|
+
async abort(error) {
|
|
106
|
+
// Abort every branch even when one abort fails: each destination must get
|
|
107
|
+
// its chance to clean up.
|
|
108
|
+
await settleBranches(this.runs.map((run) => run.abort(error)));
|
|
68
109
|
}
|
|
69
110
|
}
|
|
70
111
|
class TransformWriter {
|
|
@@ -134,8 +175,28 @@ export class Pipeline {
|
|
|
134
175
|
const selectStart = Date.now();
|
|
135
176
|
const datasets = await this.datasetSelector.select();
|
|
136
177
|
this.reporter?.datasetsSelected?.(datasets.total, Date.now() - selectStart);
|
|
137
|
-
|
|
138
|
-
|
|
178
|
+
// The run transaction: one openRun → write* → commit/abort per pipeline
|
|
179
|
+
// run. `selectedSources` accumulates every selected dataset – including
|
|
180
|
+
// ones skipped as unchanged – so a writer's commit can sweep by registry
|
|
181
|
+
// membership.
|
|
182
|
+
const selectedSources = [];
|
|
183
|
+
const context = {
|
|
184
|
+
runId: randomUUID(),
|
|
185
|
+
startedAt: new Date().toISOString(),
|
|
186
|
+
selectedSources: () => selectedSources,
|
|
187
|
+
provenance: this.provenanceStore,
|
|
188
|
+
};
|
|
189
|
+
const runWriter = await this.writer.openRun(context);
|
|
190
|
+
try {
|
|
191
|
+
for await (const dataset of datasets) {
|
|
192
|
+
selectedSources.push(dataset.iri.toString());
|
|
193
|
+
await this.processDataset(dataset, runWriter, context);
|
|
194
|
+
}
|
|
195
|
+
await runWriter.commit();
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
await runWriter.abort(error);
|
|
199
|
+
throw error;
|
|
139
200
|
}
|
|
140
201
|
const finalMemory = process.memoryUsage();
|
|
141
202
|
this.reporter?.pipelineComplete?.({
|
|
@@ -144,7 +205,7 @@ export class Pipeline {
|
|
|
144
205
|
heapUsedBytes: finalMemory.heapUsed,
|
|
145
206
|
});
|
|
146
207
|
}
|
|
147
|
-
async processDataset(dataset) {
|
|
208
|
+
async processDataset(dataset, runWriter, context) {
|
|
148
209
|
this.reporter?.datasetStart?.(dataset);
|
|
149
210
|
// Probe phase: gather probe results and the source-to-be, without importing.
|
|
150
211
|
let probed;
|
|
@@ -232,7 +293,7 @@ export class Pipeline {
|
|
|
232
293
|
});
|
|
233
294
|
let stageFailed = false;
|
|
234
295
|
try {
|
|
235
|
-
stageFailed = await this.runStages(dataset, resolved.distribution, timeout);
|
|
296
|
+
stageFailed = await this.runStages(dataset, resolved.distribution, timeout, runWriter, context);
|
|
236
297
|
// Reactive fallback: an endpoint that passed probing but could not serve
|
|
237
298
|
// the analysis stages is empirically incapable. Switch to the dataset’s
|
|
238
299
|
// data dump and re-run all stages locally, discarding the
|
|
@@ -265,8 +326,8 @@ export class Pipeline {
|
|
|
265
326
|
this.reportSelectedDistribution(dataset, fallback, fingerprint);
|
|
266
327
|
// Discard the endpoint-sourced partial output before the re-run so
|
|
267
328
|
// the dump-sourced stats replace it rather than appending to it.
|
|
268
|
-
await
|
|
269
|
-
stageFailed = await this.runStages(dataset, fallback.distribution, timeout);
|
|
329
|
+
await runWriter.reset?.(dataset);
|
|
330
|
+
stageFailed = await this.runStages(dataset, fallback.distribution, timeout, runWriter, context);
|
|
270
331
|
}
|
|
271
332
|
else if (fallback.importFailed) {
|
|
272
333
|
// A failed dump import is a deep validity verdict on that dump –
|
|
@@ -284,7 +345,7 @@ export class Pipeline {
|
|
|
284
345
|
await this.distributionResolver.cleanup?.();
|
|
285
346
|
unsubscribe?.();
|
|
286
347
|
}
|
|
287
|
-
await
|
|
348
|
+
await runWriter.flush?.(dataset, stageFailed ? 'failed' : 'success');
|
|
288
349
|
await this.reportValidators(dataset);
|
|
289
350
|
// A dataset whose stages threw produced incomplete output; record it as
|
|
290
351
|
// ‘failed’ rather than freezing a broken result under a ‘success’ record.
|
|
@@ -331,15 +392,15 @@ export class Pipeline {
|
|
|
331
392
|
}
|
|
332
393
|
}
|
|
333
394
|
/**
|
|
334
|
-
* The writer a stage's merged output is written through: the
|
|
395
|
+
* The writer a stage's merged output is written through: the open run
|
|
335
396
|
* wrapped with the plugins' {@link PipelinePlugin.beforeStageWrite}
|
|
336
397
|
* transforms, carrying this `stage`'s identity so a transform can mint stable
|
|
337
398
|
* per-`(dataset, stage)` IRIs rather than blank nodes.
|
|
338
399
|
*/
|
|
339
|
-
stageWriter(stage) {
|
|
400
|
+
stageWriter(runWriter, stage) {
|
|
340
401
|
return this.beforeStageWrite
|
|
341
|
-
? new TransformWriter(
|
|
342
|
-
:
|
|
402
|
+
? new TransformWriter(runWriter, this.beforeStageWrite, stage)
|
|
403
|
+
: runWriter;
|
|
343
404
|
}
|
|
344
405
|
/**
|
|
345
406
|
* Report a resolved distribution as the dataset's selected source, plus its
|
|
@@ -362,15 +423,15 @@ export class Pipeline {
|
|
|
362
423
|
* whether any stage hard-failed – the signal the reactive dump fallback
|
|
363
424
|
* reacts to.
|
|
364
425
|
*/
|
|
365
|
-
async runStages(dataset, distribution, timeout) {
|
|
426
|
+
async runStages(dataset, distribution, timeout, runWriter, context) {
|
|
366
427
|
let stageFailed = false;
|
|
367
428
|
for (const stage of this.stages) {
|
|
368
429
|
try {
|
|
369
430
|
if (stage.stages.length > 0) {
|
|
370
|
-
await this.runChain(dataset, distribution, stage, timeout);
|
|
431
|
+
await this.runChain(dataset, distribution, stage, runWriter, context, timeout);
|
|
371
432
|
}
|
|
372
433
|
else {
|
|
373
|
-
await this.runStage(dataset, distribution, stage, this.stageWriter(stage.name), timeout);
|
|
434
|
+
await this.runStage(dataset, distribution, stage, this.stageWriter(runWriter, stage.name), timeout);
|
|
374
435
|
}
|
|
375
436
|
}
|
|
376
437
|
catch (error) {
|
|
@@ -384,7 +445,7 @@ export class Pipeline {
|
|
|
384
445
|
* Run a stage with reporting and return whether it was supported.
|
|
385
446
|
* Returns `true` if the stage produced results, `false` if NotSupported.
|
|
386
447
|
*/
|
|
387
|
-
async runStage(dataset, distribution, stage, writer
|
|
448
|
+
async runStage(dataset, distribution, stage, writer, timeout) {
|
|
388
449
|
this.reporter?.stageStart?.(stage.name);
|
|
389
450
|
const stageStart = Date.now();
|
|
390
451
|
let itemsProcessed = 0;
|
|
@@ -421,34 +482,46 @@ export class Pipeline {
|
|
|
421
482
|
throw new Error(`Stage '${stage.name}' returned NotSupported in chained mode`);
|
|
422
483
|
}
|
|
423
484
|
}
|
|
424
|
-
async runChain(dataset, distribution, stage, timeout) {
|
|
485
|
+
async runChain(dataset, distribution, stage, runWriter, context, timeout) {
|
|
425
486
|
const { stageOutputResolver, outputDir } = this.chaining;
|
|
426
487
|
const outputFiles = [];
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
488
|
+
// Run one chained stage into its scratch FileWriter and flush it, so the
|
|
489
|
+
// output file exists on its final path before the resolver reads it. The
|
|
490
|
+
// scratch run is bracketed like any other: committed on success, aborted
|
|
491
|
+
// on failure so no half-written temp file is left behind.
|
|
492
|
+
const runScratchStage = async (chainedStage, stageDistribution) => {
|
|
493
|
+
const scratchWriter = new FileWriter({
|
|
494
|
+
outputDir: `${outputDir}/${chainedStage.name}`,
|
|
431
495
|
format: 'n-triples',
|
|
432
496
|
});
|
|
433
|
-
await
|
|
434
|
-
|
|
497
|
+
const scratchRun = await scratchWriter.openRun(context);
|
|
498
|
+
try {
|
|
499
|
+
await this.runChainedStage(dataset, stageDistribution, chainedStage, scratchRun, timeout);
|
|
500
|
+
await scratchRun.flush(dataset, 'success');
|
|
501
|
+
await scratchRun.commit();
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
await scratchRun.abort(error);
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
return scratchWriter.getOutputPath(dataset);
|
|
508
|
+
};
|
|
509
|
+
try {
|
|
510
|
+
// 1. Run parent stage → scratch FileWriter.
|
|
511
|
+
const parentOutput = await runScratchStage(stage, distribution);
|
|
512
|
+
outputFiles.push(parentOutput);
|
|
435
513
|
// 2. Chain through children.
|
|
436
|
-
let currentDistribution = await stageOutputResolver.resolve(
|
|
514
|
+
let currentDistribution = await stageOutputResolver.resolve(parentOutput);
|
|
437
515
|
for (let i = 0; i < stage.stages.length; i++) {
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
outputDir: `${outputDir}/${child.name}`,
|
|
441
|
-
format: 'n-triples',
|
|
442
|
-
});
|
|
443
|
-
await this.runChainedStage(dataset, currentDistribution, child, childWriter, timeout);
|
|
444
|
-
outputFiles.push(childWriter.getOutputPath(dataset));
|
|
516
|
+
const childOutput = await runScratchStage(stage.stages[i], currentDistribution);
|
|
517
|
+
outputFiles.push(childOutput);
|
|
445
518
|
if (i < stage.stages.length - 1) {
|
|
446
|
-
currentDistribution = await stageOutputResolver.resolve(
|
|
519
|
+
currentDistribution = await stageOutputResolver.resolve(childOutput);
|
|
447
520
|
}
|
|
448
521
|
}
|
|
449
|
-
// 3. Concatenate all output files →
|
|
522
|
+
// 3. Concatenate all output files → the open run, applying the plugins'
|
|
450
523
|
// beforeStageWrite transforms once for the chain under the parent stage.
|
|
451
|
-
await this.stageWriter(stage.name).write(dataset, this.readFiles(outputFiles));
|
|
524
|
+
await this.stageWriter(runWriter, stage.name).write(dataset, this.readFiles(outputFiles));
|
|
452
525
|
}
|
|
453
526
|
finally {
|
|
454
527
|
await stageOutputResolver.cleanup();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileLoadedSparqlProvenanceStore.d.ts","sourceRoot":"","sources":["../../src/provenance/fileLoadedSparqlProvenanceStore.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAI9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAgBlD,MAAM,WAAW,sCAAsC;IACrD,wEAAwE;IACxE,aAAa,EAAE,GAAG,CAAC;IACnB;;;OAGG;IACH,WAAW,EAAE,GAAG,CAAC;IACjB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,qBAAa,+BAAgC,YAAW,eAAe;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAM;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAM;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;gBAEpC,OAAO,EAAE,sCAAsC;IAWrD,GAAG,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAoB5D,OAAO,CAAC,WAAW;IAmBb,GAAG,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"fileLoadedSparqlProvenanceStore.d.ts","sourceRoot":"","sources":["../../src/provenance/fileLoadedSparqlProvenanceStore.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAI9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAgBlD,MAAM,WAAW,sCAAsC;IACrD,wEAAwE;IACxE,aAAa,EAAE,GAAG,CAAC;IACnB;;;OAGG;IACH,WAAW,EAAE,GAAG,CAAC;IACjB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,qBAAa,+BAAgC,YAAW,eAAe;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAM;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAM;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;gBAEpC,OAAO,EAAE,sCAAsC;IAWrD,GAAG,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAoB5D,OAAO,CAAC,WAAW;IAmBb,GAAG,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;YASpD,OAAO;CAsBvB"}
|
|
@@ -68,8 +68,11 @@ export class FileLoadedSparqlProvenanceStore {
|
|
|
68
68
|
}
|
|
69
69
|
async set(datasetUri, record) {
|
|
70
70
|
const dataset = new Dataset({ iri: datasetUri, distributions: [] });
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// Each record is complete in itself, so it gets its own mini run:
|
|
72
|
+
// write + flush materializes the record file atomically.
|
|
73
|
+
const run = await this.writer.openRun();
|
|
74
|
+
await run.write(dataset, this.toQuads(datasetUri, record));
|
|
75
|
+
await run.flush(dataset);
|
|
73
76
|
}
|
|
74
77
|
async *toQuads(datasetUri, record) {
|
|
75
78
|
const subject = namedNode(datasetUri.toString());
|
package/dist/sparql/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { deduplicateQuads,
|
|
1
|
+
export { deduplicateQuads, SparqlConstructReader, LineBufferTransform, NotSupported, readQueryFile, type ReadOptions, type Reader, type SparqlConstructReaderOptions, type VariableBindings, } from './reader.js';
|
|
2
2
|
export { SparqlItemSelector, type SparqlItemSelectorOptions, } from './selector.js';
|
|
3
3
|
export { injectValues } from './values.js';
|
|
4
4
|
export { withDefaultGraph } from './graph.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sparql/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sparql/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,KAAK,4BAA4B,EACjC,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,4BAA4B,EACjC,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,oBAAoB,CAAC"}
|
package/dist/sparql/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { deduplicateQuads,
|
|
1
|
+
export { deduplicateQuads, SparqlConstructReader, LineBufferTransform, NotSupported, readQueryFile, } from './reader.js';
|
|
2
2
|
export { SparqlItemSelector, } from './selector.js';
|
|
3
3
|
export { injectValues } from './values.js';
|
|
4
4
|
export { withDefaultGraph } from './graph.js';
|