@origintrail-official/dkg-query 10.0.13 → 10.0.15
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/dist/caller-abort.d.ts +8 -0
- package/dist/caller-abort.d.ts.map +1 -0
- package/dist/caller-abort.js +21 -0
- package/dist/caller-abort.js.map +1 -0
- package/dist/caller-sparql-error.d.ts +46 -0
- package/dist/caller-sparql-error.d.ts.map +1 -0
- package/dist/caller-sparql-error.js +43 -0
- package/dist/caller-sparql-error.js.map +1 -0
- package/dist/dkg-query-engine.d.ts +15 -47
- package/dist/dkg-query-engine.d.ts.map +1 -1
- package/dist/dkg-query-engine.js +130 -1742
- package/dist/dkg-query-engine.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/query-handler.d.ts.map +1 -1
- package/dist/query-handler.js +68 -29
- package/dist/query-handler.js.map +1 -1
- package/dist/query-types.d.ts +27 -5
- package/dist/query-types.d.ts.map +1 -1
- package/dist/scoped-content-graph-discovery-memo.d.ts +25 -0
- package/dist/scoped-content-graph-discovery-memo.d.ts.map +1 -0
- package/dist/scoped-content-graph-discovery-memo.js +56 -0
- package/dist/scoped-content-graph-discovery-memo.js.map +1 -0
- package/dist/scoped-query-error.d.ts +4 -0
- package/dist/scoped-query-error.d.ts.map +1 -0
- package/dist/scoped-query-error.js +7 -0
- package/dist/scoped-query-error.js.map +1 -0
- package/dist/sparql-graph-scope.d.ts +36 -12
- package/dist/sparql-graph-scope.d.ts.map +1 -1
- package/dist/sparql-graph-scope.js +442 -141
- package/dist/sparql-graph-scope.js.map +1 -1
- package/dist/sparql-guard.d.ts +2 -1
- package/dist/sparql-guard.d.ts.map +1 -1
- package/dist/sparql-guard.js.map +1 -1
- package/dist/sparql-min-trust.d.ts +11 -0
- package/dist/sparql-min-trust.d.ts.map +1 -0
- package/dist/sparql-min-trust.js +181 -0
- package/dist/sparql-min-trust.js.map +1 -0
- package/dist/sparql-rewrite-result.d.ts +16 -0
- package/dist/sparql-rewrite-result.d.ts.map +1 -0
- package/dist/sparql-rewrite-result.js +7 -0
- package/dist/sparql-rewrite-result.js.map +1 -0
- package/package.json +4 -3
- package/dist/sparql-utils.d.ts +0 -18
- package/dist/sparql-utils.d.ts.map +0 -1
- package/dist/sparql-utils.js +0 -216
- package/dist/sparql-utils.js.map +0 -1
package/dist/dkg-query-engine.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { asGraphWriteRevisionSource, isSparqlHttpResponseError, } from '@origintrail-official/dkg-storage';
|
|
1
2
|
import { ExactGraphReadError, GraphManager, readExactGraphPaged, resolveGraphScopedOrLegacyMetadata, } from '@origintrail-official/dkg-storage';
|
|
2
|
-
import {
|
|
3
|
+
import { materializePreparedSparql, prepareSparql, } from '@origintrail-official/dkg-rdf-utils/sparql';
|
|
4
|
+
import { contextGraphDataUri, contextGraphSharedMemoryUri, contextGraphVerifiableMemoryUri, contextGraphAssertionUri, MemoryLayer, contextGraphLayerUriCandidates, contextGraphLayerPrefixCandidates, contextGraphSubGraphUri, contextGraphMetaUri, contextGraphSharedMemoryMetaUri, assertionLifecycleUri, contextGraphSubGraphMetaUri, contextGraphPrivateUri, contextGraphSubGraphPrivateUri, assertSafeIri, escapeSparqlLiteral, validateSubGraphName, ASSERTION_NAMED_GRAPH_PREFIX, isAssertionScopedChildGraph, REMOVED_VIEWS, TrustLevel, GRAPH_KA_CONTENT_SCOPE_VERSION, createGraphKnowledgeAssetScope, knowledgeAssetLayerGraphUri, buildLegacyKnowledgeAssetMetadataQuery, } from '@origintrail-official/dkg-core';
|
|
3
5
|
import { validateReadOnlySparql, emptyResultForSparql, detectSparqlQueryForm, } from './sparql-guard.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
import { assertExplicitGraphIrisAllowed, assertNoCallerDatasetClauses, constrainGraphVariablesToAllowedSet, prepareGraphScope, requireGraphScopeRewrite, rewriteGraphRoute, rewriteGraphSet, transitionGraphScope, wrapWithGraph, } from './sparql-graph-scope.js';
|
|
7
|
+
import { injectMinTrustFilter } from './sparql-min-trust.js';
|
|
8
|
+
import { CallerSparqlRejectedError } from './caller-sparql-error.js';
|
|
9
|
+
import { raceAgainstCallerAbort } from './caller-abort.js';
|
|
10
|
+
import { ScopedContentGraphDiscoveryMemo } from './scoped-content-graph-discovery-memo.js';
|
|
11
|
+
export { ScopedQueryViolationError } from './scoped-query-error.js';
|
|
12
|
+
/** Upstream statuses that mean the SUBMITTED query was malformed. */
|
|
13
|
+
const MALFORMED_CALLER_QUERY_STATUSES = new Set([400, 422]);
|
|
12
14
|
function storeOptions(options) {
|
|
13
15
|
if (!options?.signal && !options?.priority && !options?.source)
|
|
14
16
|
return undefined;
|
|
@@ -49,24 +51,6 @@ function createQueryStoreReadContext(store, queryOptions) {
|
|
|
49
51
|
},
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
|
-
function raceAgainstCallerAbort(work, signal) {
|
|
53
|
-
if (!signal)
|
|
54
|
-
return work;
|
|
55
|
-
if (signal.aborted) {
|
|
56
|
-
const reason = signal.reason;
|
|
57
|
-
return Promise.reject(reason instanceof Error ? reason : new Error(String(reason ?? 'aborted')));
|
|
58
|
-
}
|
|
59
|
-
return new Promise((resolve, reject) => {
|
|
60
|
-
const onAbort = () => {
|
|
61
|
-
const reason = signal.reason;
|
|
62
|
-
reject(reason instanceof Error ? reason : new Error(String(reason ?? 'aborted')));
|
|
63
|
-
};
|
|
64
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
65
|
-
void work.then(resolve, reject).finally(() => {
|
|
66
|
-
signal.removeEventListener('abort', onAbort);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
54
|
/**
|
|
71
55
|
* Resolves a V10 GetView + context graph ID to the named-graph URIs (or
|
|
72
56
|
* prefixes) that the query engine should target.
|
|
@@ -228,23 +212,34 @@ export function resolveViewGraphs(view, contextGraphId, opts) {
|
|
|
228
212
|
export class DKGQueryEngine {
|
|
229
213
|
store;
|
|
230
214
|
graphManager;
|
|
231
|
-
|
|
232
|
-
// a completed allow-list that could miss newly-created assertion graphs.
|
|
233
|
-
scopedContentGraphAllowListInFlight = new Map();
|
|
215
|
+
scopedContentGraphDiscoveryMemo;
|
|
234
216
|
constructor(store) {
|
|
235
217
|
this.store = store;
|
|
236
218
|
this.graphManager = new GraphManager(store);
|
|
219
|
+
this.scopedContentGraphDiscoveryMemo = new ScopedContentGraphDiscoveryMemo(asGraphWriteRevisionSource(store));
|
|
237
220
|
}
|
|
238
221
|
async query(sparql, options) {
|
|
222
|
+
const prepared = prepareSparql(sparql);
|
|
223
|
+
if (prepared.status === 'malformed-uchar') {
|
|
224
|
+
throw new Error('SPARQL rejected: malformed Unicode code-point escape');
|
|
225
|
+
}
|
|
226
|
+
if (prepared.wordTokens.has('SERVICE')) {
|
|
227
|
+
throw new Error('SPARQL rejected: SERVICE clauses are not allowed');
|
|
228
|
+
}
|
|
239
229
|
const reads = createQueryStoreReadContext(this.store, options);
|
|
240
|
-
const guard = validateReadOnlySparql(
|
|
230
|
+
const guard = validateReadOnlySparql(prepared);
|
|
241
231
|
if (!guard.safe) {
|
|
242
232
|
throw new Error(`SPARQL rejected: ${guard.reason}`);
|
|
243
233
|
}
|
|
234
|
+
// Policy checks and rewrites retain the prepared source/logical views.
|
|
235
|
+
// Active UCHAR syntax is materialized once, immediately before the final
|
|
236
|
+
// caller query crosses the store boundary.
|
|
237
|
+
const initialGraphScope = prepareGraphScope(prepared);
|
|
238
|
+
let routedScope = initialGraphScope;
|
|
244
239
|
// ── V10 view-based routing ────────────────────────────────────────
|
|
245
240
|
const effectiveContextGraphId = options?.contextGraphId;
|
|
246
241
|
if (effectiveContextGraphId) {
|
|
247
|
-
assertNoCallerDatasetClauses(
|
|
242
|
+
assertNoCallerDatasetClauses(initialGraphScope);
|
|
248
243
|
}
|
|
249
244
|
if (options?.subGraphName) {
|
|
250
245
|
const v = validateSubGraphName(options.subGraphName);
|
|
@@ -342,7 +337,8 @@ export class DKGQueryEngine {
|
|
|
342
337
|
]
|
|
343
338
|
: [];
|
|
344
339
|
const explicitAllowedGraphs = [...allowedGraphs, ...metaAllowList, ...privateAllowList];
|
|
345
|
-
const shouldExpandGraphVariables = options?.includeContextGraphPartitions === true
|
|
340
|
+
const shouldExpandGraphVariables = options?.includeContextGraphPartitions === true
|
|
341
|
+
&& initialGraphScope.graphVariables.length > 0;
|
|
346
342
|
const variableAllowedGraphs = shouldExpandGraphVariables
|
|
347
343
|
? await this.resolveScopedGraphVariableAllowList(effectiveContextGraphId, explicitAllowedGraphs, { subGraphName, isSwmOnlyRoute }, reads)
|
|
348
344
|
: explicitAllowedGraphs;
|
|
@@ -350,8 +346,8 @@ export class DKGQueryEngine {
|
|
|
350
346
|
// allow-list. GRAPH variables only gain known same-CG content
|
|
351
347
|
// partitions for callers that explicitly opt into broad count scans;
|
|
352
348
|
// legacy scoped routes keep their selected memory-layer contract.
|
|
353
|
-
assertExplicitGraphIrisAllowed(
|
|
354
|
-
|
|
349
|
+
assertExplicitGraphIrisAllowed(initialGraphScope, explicitAllowedGraphs);
|
|
350
|
+
routedScope = requireGraphScopeRewrite(constrainGraphVariablesToAllowedSet(initialGraphScope, variableAllowedGraphs));
|
|
355
351
|
}
|
|
356
352
|
if (options?.view) {
|
|
357
353
|
if (!effectiveContextGraphId) {
|
|
@@ -365,10 +361,10 @@ export class DKGQueryEngine {
|
|
|
365
361
|
if (!v.valid)
|
|
366
362
|
throw new Error(v.reason);
|
|
367
363
|
}
|
|
368
|
-
return this.queryWithView(
|
|
364
|
+
return this.queryWithView(initialGraphScope, options.view, effectiveContextGraphId, options, reads);
|
|
369
365
|
}
|
|
370
366
|
// ── Legacy routing (V9 compat) ────────────────────────────────────
|
|
371
|
-
let
|
|
367
|
+
let effectiveScope = routedScope;
|
|
372
368
|
if (effectiveContextGraphId) {
|
|
373
369
|
const dataGraph = options?.subGraphName
|
|
374
370
|
? contextGraphSubGraphUri(effectiveContextGraphId, options.subGraphName)
|
|
@@ -377,37 +373,31 @@ export class DKGQueryEngine {
|
|
|
377
373
|
if (options?.includeSharedMemory ?? options?.includeWorkspace) {
|
|
378
374
|
// Per-KA VM: read-both the published per-KA …/_verifiable_memory/{addr}/{number} + root.
|
|
379
375
|
const vmGraphsInc = await this.discoverGraphsByPrefix(`${dataGraph}/_verifiable_memory/`, reads);
|
|
380
|
-
const
|
|
381
|
-
? (this.wrapVerifiableMemoryGraphSet(sparql, [dataGraph, ...vmGraphsInc])
|
|
382
|
-
?? wrapWithGraph(sparql, dataGraph))
|
|
383
|
-
: wrapWithGraph(sparql, dataGraph);
|
|
376
|
+
const dataScope = requireGraphScopeRewrite(rewriteGraphRoute(routedScope, vmGraphsInc.length > 0 ? [dataGraph, ...vmGraphsInc] : [], dataGraph, 'deduplicated-values-union'));
|
|
384
377
|
// Per-KA SWM: union the discovered …/_shared_memory/{addr}/{number} graphs.
|
|
385
378
|
const swmGraphs = await this.discoverGraphsByPrefix(`${sharedMemoryGraph}/`, reads);
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
379
|
+
const sharedMemoryScope = requireGraphScopeRewrite(rewriteGraphRoute(routedScope, swmGraphs, sharedMemoryGraph, 'union-only'));
|
|
380
|
+
// Both are graph-wrapped forms of the CALLER's query, so they carry
|
|
381
|
+
// the same provenance as execAndNormalize (PR #2330 review — this
|
|
382
|
+
// branch previously bypassed the marker, leaving the original 500 for
|
|
383
|
+
// any `includeSharedMemory` request with malformed SPARQL).
|
|
384
|
+
const dataResult = await this.execCallerQuery(dataScope, reads);
|
|
385
|
+
const smResult = await this.execCallerQuery(sharedMemoryScope, reads);
|
|
391
386
|
return mergeSharedMemoryAndDataResults(dataResult, smResult);
|
|
392
387
|
}
|
|
393
388
|
if (options?.graphSuffix === '_shared_memory') {
|
|
394
389
|
// Uniform layout: SWM is per-KA …/_shared_memory/{addr}/{number}. Discover the
|
|
395
390
|
// per-KA graphs under the prefix and union them (the legacy bucket is now empty).
|
|
396
391
|
const swmGraphs = await this.discoverGraphsByPrefix(`${sharedMemoryGraph}/`, reads);
|
|
397
|
-
|
|
398
|
-
? (wrapWithGraphUnion(sparql, swmGraphs) ?? wrapWithGraph(sparql, sharedMemoryGraph))
|
|
399
|
-
: wrapWithGraph(sparql, sharedMemoryGraph);
|
|
392
|
+
effectiveScope = requireGraphScopeRewrite(rewriteGraphRoute(routedScope, swmGraphs, sharedMemoryGraph, 'union-only'));
|
|
400
393
|
}
|
|
401
394
|
else {
|
|
402
395
|
// Per-KA VM: read-both the published per-KA …/_verifiable_memory/{addr}/{number} + root.
|
|
403
396
|
const vmGraphs = await this.discoverGraphsByPrefix(`${dataGraph}/_verifiable_memory/`, reads);
|
|
404
|
-
|
|
405
|
-
? (this.wrapVerifiableMemoryGraphSet(sparql, [dataGraph, ...vmGraphs])
|
|
406
|
-
?? wrapWithGraph(sparql, dataGraph))
|
|
407
|
-
: wrapWithGraph(sparql, dataGraph);
|
|
397
|
+
effectiveScope = requireGraphScopeRewrite(rewriteGraphRoute(routedScope, vmGraphs.length > 0 ? [dataGraph, ...vmGraphs] : [], dataGraph, 'deduplicated-values-union'));
|
|
408
398
|
}
|
|
409
399
|
}
|
|
410
|
-
const result = await this.execAndNormalize(
|
|
400
|
+
const result = await this.execAndNormalize(effectiveScope, reads);
|
|
411
401
|
// Strip results originating from excluded graphs (e.g. private CGs).
|
|
412
402
|
if (options?.excludeGraphPrefixes?.length && result.bindings.length > 0) {
|
|
413
403
|
return this.filterExcludedGraphs(result, options.excludeGraphPrefixes);
|
|
@@ -437,7 +427,8 @@ export class DKGQueryEngine {
|
|
|
437
427
|
/**
|
|
438
428
|
* Execute a SPARQL query scoped to a declared V10 state view.
|
|
439
429
|
*/
|
|
440
|
-
async queryWithView(
|
|
430
|
+
async queryWithView(initialScope, view, contextGraphId, options, reads) {
|
|
431
|
+
const sparql = initialScope.source;
|
|
441
432
|
// Uniform layout (rc.17): a by-name working-memory read must target the per-KA
|
|
442
433
|
// graph `…/_working_memory/{addr}/{number}` the data was written to. Resolve the
|
|
443
434
|
// KA number from the `dkg:kaId` stamped on the lifecycle URN in `_meta` and pass
|
|
@@ -523,8 +514,8 @@ export class DKGQueryEngine {
|
|
|
523
514
|
// `quads: []`. Delegate to the shared kind-aware empty-result helper.
|
|
524
515
|
return emptyResultForSparql(sparql);
|
|
525
516
|
}
|
|
526
|
-
assertExplicitGraphIrisAllowed(
|
|
527
|
-
|
|
517
|
+
assertExplicitGraphIrisAllowed(initialScope, allGraphs);
|
|
518
|
+
const constrainedScope = requireGraphScopeRewrite(constrainGraphVariablesToAllowedSet(initialScope, allGraphs));
|
|
528
519
|
// Spec §14 trust-gradient filter — only enforced on verifiable-memory
|
|
529
520
|
// where on-chain-anchored trust metadata is expected to live.
|
|
530
521
|
// When `minTrust` (or legacy `_minTrust`) is set, rewrite the query so
|
|
@@ -541,50 +532,41 @@ export class DKGQueryEngine {
|
|
|
541
532
|
// rewriter MUST succeed or we fail closed — returning an empty result
|
|
542
533
|
// is the correct behaviour for "no subject meets the trust threshold"
|
|
543
534
|
// when we cannot prove the threshold was applied.
|
|
544
|
-
let
|
|
535
|
+
let effectiveScope = constrainedScope;
|
|
545
536
|
const effectiveMinTrust = options.minTrust ?? options._minTrust;
|
|
546
537
|
// `SelfAttested` (0) is the floor and means no trust filter is needed.
|
|
547
538
|
// Endorsed and above require explicit writer-side trust metadata.
|
|
548
539
|
if (view === 'verifiable-memory' &&
|
|
549
540
|
effectiveMinTrust !== undefined &&
|
|
550
541
|
effectiveMinTrust > TrustLevel.SelfAttested) {
|
|
551
|
-
const rewritten = injectMinTrustFilter(
|
|
552
|
-
if (
|
|
542
|
+
const rewritten = injectMinTrustFilter(constrainedScope, effectiveMinTrust);
|
|
543
|
+
if (rewritten.kind === 'unsupported') {
|
|
553
544
|
console.warn(`[DKGQueryEngine] minTrust=${effectiveMinTrust} requested for a query shape ` +
|
|
554
545
|
`injectMinTrustFilter cannot safely rewrite; returning empty result (fail-closed)`);
|
|
555
546
|
// Preserve the query form so CONSTRUCT/DESCRIBE callers see
|
|
556
547
|
// `{ bindings: [], quads: [] }` rather than a shapeless deny, and
|
|
557
548
|
// ASK callers see `{ bindings: [{ result: 'false' }] }`.
|
|
558
|
-
return emptyResultForSparql(
|
|
549
|
+
return emptyResultForSparql(constrainedScope.source);
|
|
559
550
|
}
|
|
560
|
-
|
|
551
|
+
effectiveScope = transitionGraphScope(constrainedScope, rewritten.value);
|
|
561
552
|
}
|
|
562
553
|
if (allGraphs.length === 1) {
|
|
563
|
-
return this.execAndNormalize(wrapWithGraph(
|
|
554
|
+
return this.execAndNormalize(requireGraphScopeRewrite(wrapWithGraph(effectiveScope, allGraphs[0])), reads);
|
|
564
555
|
}
|
|
565
556
|
if (view === 'verifiable-memory') {
|
|
566
|
-
const rewritten =
|
|
567
|
-
if (rewritten
|
|
568
|
-
return this.execAndNormalize(rewritten, reads);
|
|
557
|
+
const rewritten = rewriteGraphSet(effectiveScope, allGraphs, 'deduplicated-values-union');
|
|
558
|
+
if (rewritten.kind === 'ready') {
|
|
559
|
+
return this.execAndNormalize(rewritten.value, reads);
|
|
569
560
|
}
|
|
570
561
|
}
|
|
571
|
-
return this.queryMultipleGraphs(
|
|
572
|
-
}
|
|
573
|
-
/** Canonical graph rewrite for root + per-KA/per-cgId verifiable-memory reads. */
|
|
574
|
-
wrapVerifiableMemoryGraphSet(sparql, graphs) {
|
|
575
|
-
if (graphs.length === 0)
|
|
576
|
-
return sparql;
|
|
577
|
-
if (graphs.length === 1)
|
|
578
|
-
return wrapWithGraph(sparql, graphs[0]);
|
|
579
|
-
return wrapWithDeduplicatedGraphValues(sparql, graphs)
|
|
580
|
-
?? wrapWithGraphValues(sparql, graphs)
|
|
581
|
-
?? wrapWithGraphUnion(sparql, graphs);
|
|
562
|
+
return this.queryMultipleGraphs(effectiveScope, allGraphs, reads);
|
|
582
563
|
}
|
|
583
|
-
async queryMultipleGraphs(
|
|
564
|
+
async queryMultipleGraphs(scope, graphs, reads) {
|
|
565
|
+
const sparql = scope.source;
|
|
584
566
|
if (graphs.length === 0)
|
|
585
567
|
return { bindings: [] };
|
|
586
568
|
if (graphs.length === 1) {
|
|
587
|
-
return this.execAndNormalize(wrapWithGraph(
|
|
569
|
+
return this.execAndNormalize(requireGraphScopeRewrite(wrapWithGraph(scope, graphs[0])), reads);
|
|
588
570
|
}
|
|
589
571
|
// Prefer a single `VALUES ?g { … } GRAPH ?g { … }` query: it scopes the
|
|
590
572
|
// read to the named-graph set as ONE basic graph pattern iterated over a
|
|
@@ -595,18 +577,14 @@ export class DKGQueryEngine {
|
|
|
595
577
|
// VALUES form plans in constant depth. LIMIT/ORDER BY/DISTINCT/aggregate
|
|
596
578
|
// semantics are preserved because those modifiers stay in the outer query
|
|
597
579
|
// around the injected block.
|
|
598
|
-
const
|
|
599
|
-
if (
|
|
600
|
-
return this.execAndNormalize(
|
|
580
|
+
const graphSetRewrite = rewriteGraphSet(scope, graphs, 'values-union');
|
|
581
|
+
if (graphSetRewrite.kind === 'ready') {
|
|
582
|
+
return this.execAndNormalize(graphSetRewrite.value, reads);
|
|
601
583
|
}
|
|
602
|
-
// Residual shapes
|
|
584
|
+
// Residual shapes the graph-set policy declines (an inner top-level
|
|
603
585
|
// UNION, no locatable WHERE block, or a sentinel-variable collision) keep
|
|
604
586
|
// the original union / per-graph fallback so the #789 form-aware
|
|
605
587
|
// cross-graph merge below is preserved unchanged.
|
|
606
|
-
const unionSparql = wrapWithGraphUnion(sparql, graphs);
|
|
607
|
-
if (unionSparql !== null) {
|
|
608
|
-
return this.execAndNormalize(unionSparql, reads);
|
|
609
|
-
}
|
|
610
588
|
// Fallback: the inner body contains a UNION so we cannot safely wrap
|
|
611
589
|
// in a single query without either crashing Blazegraph (nested
|
|
612
590
|
// UnionNode) or leaking a helper variable. Run per-graph and merge
|
|
@@ -623,7 +601,7 @@ export class DKGQueryEngine {
|
|
|
623
601
|
// constructed from multiple source graphs).
|
|
624
602
|
const merged = [];
|
|
625
603
|
for (const g of graphs) {
|
|
626
|
-
const r = await this.execAndNormalize(wrapWithGraph(
|
|
604
|
+
const r = await this.execAndNormalize(requireGraphScopeRewrite(wrapWithGraph(scope, g)), reads);
|
|
627
605
|
if (r.quads)
|
|
628
606
|
merged.push(...r.quads);
|
|
629
607
|
}
|
|
@@ -633,7 +611,7 @@ export class DKGQueryEngine {
|
|
|
633
611
|
// Boolean result: true iff the pattern matches in ANY graph.
|
|
634
612
|
// Short-circuit on the first positive graph.
|
|
635
613
|
for (const g of graphs) {
|
|
636
|
-
const r = await this.execAndNormalize(wrapWithGraph(
|
|
614
|
+
const r = await this.execAndNormalize(requireGraphScopeRewrite(wrapWithGraph(scope, g)), reads);
|
|
637
615
|
if (r.bindings[0]?.result === 'true') {
|
|
638
616
|
return { bindings: [{ result: 'true' }] };
|
|
639
617
|
}
|
|
@@ -648,7 +626,7 @@ export class DKGQueryEngine {
|
|
|
648
626
|
// Rather than silently return duplicate / mis-ordered / over-limit
|
|
649
627
|
// rows, reject the unsupported shape explicitly so the caller gets a
|
|
650
628
|
// clear error instead of wrong data.
|
|
651
|
-
if (hasCrossGraphUnsafeModifier(
|
|
629
|
+
if (hasCrossGraphUnsafeModifier(scope)) {
|
|
652
630
|
throw new Error('Multi-graph query combines an inner UNION with a solution-set ' +
|
|
653
631
|
'modifier (DISTINCT/ORDER BY/LIMIT/OFFSET/GROUP BY/aggregate). ' +
|
|
654
632
|
'This shape cannot be evaluated across graphs without corrupting ' +
|
|
@@ -657,7 +635,7 @@ export class DKGQueryEngine {
|
|
|
657
635
|
}
|
|
658
636
|
const all = [];
|
|
659
637
|
for (const g of graphs) {
|
|
660
|
-
const r = await this.execAndNormalize(wrapWithGraph(
|
|
638
|
+
const r = await this.execAndNormalize(requireGraphScopeRewrite(wrapWithGraph(scope, g)), reads);
|
|
661
639
|
all.push(...r.bindings);
|
|
662
640
|
}
|
|
663
641
|
return { bindings: all };
|
|
@@ -783,23 +761,15 @@ export class DKGQueryEngine {
|
|
|
783
761
|
return [...allowed];
|
|
784
762
|
}
|
|
785
763
|
async resolveScopedContentGraphAllowList(contextGraphId, reads, subGraphName) {
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
const promise = this.discoverScopedContentGraphAllowList(contextGraphId, reads.shared, subGraphName);
|
|
796
|
-
this.scopedContentGraphAllowListInFlight.set(key, promise);
|
|
797
|
-
void promise.finally(() => {
|
|
798
|
-
if (this.scopedContentGraphAllowListInFlight.get(key) === promise) {
|
|
799
|
-
this.scopedContentGraphAllowListInFlight.delete(key);
|
|
800
|
-
}
|
|
801
|
-
}).catch(() => undefined);
|
|
802
|
-
return raceAgainstCallerAbort(promise, reads.signal);
|
|
764
|
+
const contentKey = JSON.stringify([contextGraphId, subGraphName ?? null]);
|
|
765
|
+
const graphPrefix = `did:dkg:context-graph:${contextGraphId}`;
|
|
766
|
+
return this.scopedContentGraphDiscoveryMemo.get({
|
|
767
|
+
contentKey,
|
|
768
|
+
laneKey: reads.shared.cacheKey,
|
|
769
|
+
graphPrefix,
|
|
770
|
+
signal: reads.signal,
|
|
771
|
+
load: () => this.discoverScopedContentGraphAllowList(contextGraphId, reads.shared, subGraphName),
|
|
772
|
+
});
|
|
803
773
|
}
|
|
804
774
|
async discoverScopedContentGraphAllowList(contextGraphId, reads, subGraphName) {
|
|
805
775
|
const allowed = new Set();
|
|
@@ -877,11 +847,35 @@ export class DKGQueryEngine {
|
|
|
877
847
|
}
|
|
878
848
|
return uris;
|
|
879
849
|
}
|
|
880
|
-
|
|
881
|
-
|
|
850
|
+
/**
|
|
851
|
+
* Execute CALLER-derived SPARQL and mark a store rejection with provenance.
|
|
852
|
+
*
|
|
853
|
+
* GH#1758 — every caller-derived store execution must go through here.
|
|
854
|
+
* Engine-generated queries (access checks, graph resolution, metadata scans)
|
|
855
|
+
* deliberately do NOT, so the HTTP boundary can answer 400 for a malformed
|
|
856
|
+
* caller query without blaming the caller when the configured backend
|
|
857
|
+
* rejects an internal one.
|
|
858
|
+
*
|
|
859
|
+
* Only 400/422 are translated: 401/403/404/429 and 5xx mean the store
|
|
860
|
+
* rejected US and stay server faults (PR #2330 review).
|
|
861
|
+
*/
|
|
862
|
+
async execCallerQuery(scope, reads) {
|
|
863
|
+
const sparql = materializePreparedSparql(scope.prepared);
|
|
864
|
+
try {
|
|
865
|
+
return await reads.query(sparql);
|
|
866
|
+
}
|
|
867
|
+
catch (err) {
|
|
868
|
+
if (isSparqlHttpResponseError(err) && MALFORMED_CALLER_QUERY_STATUSES.has(err.status)) {
|
|
869
|
+
throw new CallerSparqlRejectedError(err.message, err.status, { cause: err });
|
|
870
|
+
}
|
|
871
|
+
throw err;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
async execAndNormalize(scope, reads) {
|
|
875
|
+
const result = await this.execCallerQuery(scope, reads);
|
|
882
876
|
if (result.type === 'bindings') {
|
|
883
877
|
if (result.bindings.length === 0) {
|
|
884
|
-
const empty = emptyResultForSparql(
|
|
878
|
+
const empty = emptyResultForSparql(scope.source);
|
|
885
879
|
if (empty.quads !== undefined)
|
|
886
880
|
return empty;
|
|
887
881
|
}
|
|
@@ -1108,11 +1102,6 @@ function parseCanonicalIntegerBinding(value) {
|
|
|
1108
1102
|
return undefined;
|
|
1109
1103
|
}
|
|
1110
1104
|
}
|
|
1111
|
-
function assertNoCallerDatasetClauses(sparql) {
|
|
1112
|
-
if (hasCallerDatasetClause(sparql)) {
|
|
1113
|
-
throw new ScopedQueryViolationError('FROM clauses are not allowed on scoped local queries');
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
1105
|
async function listGraphsByPrefix(store, prefix, options) {
|
|
1117
1106
|
return store.listGraphsByPrefix
|
|
1118
1107
|
? store.listGraphsByPrefix(prefix, options)
|
|
@@ -1125,1610 +1114,6 @@ async function listGraphFamily(store, rootGraph, options) {
|
|
|
1125
1114
|
}
|
|
1126
1115
|
return graphs;
|
|
1127
1116
|
}
|
|
1128
|
-
function hasCallerDatasetClause(sparql) {
|
|
1129
|
-
const n = sparql.length;
|
|
1130
|
-
let i = 0;
|
|
1131
|
-
while (i < n) {
|
|
1132
|
-
const ch = sparql[i];
|
|
1133
|
-
if (ch === '#') {
|
|
1134
|
-
while (i < n && sparql[i] !== '\n')
|
|
1135
|
-
i++;
|
|
1136
|
-
continue;
|
|
1137
|
-
}
|
|
1138
|
-
if (ch === '"' || ch === "'") {
|
|
1139
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1140
|
-
continue;
|
|
1141
|
-
}
|
|
1142
|
-
if (ch === '<') {
|
|
1143
|
-
const end = skipSparqlIriRef(sparql, i);
|
|
1144
|
-
i = end ?? i + 1;
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
if (isKeywordStart(sparql, i)) {
|
|
1148
|
-
let j = i + 1;
|
|
1149
|
-
while (j < n && isWordContinuation(sparql[j]))
|
|
1150
|
-
j++;
|
|
1151
|
-
if (isSparqlKeyword(sparql, i, j, 'FROM')) {
|
|
1152
|
-
return true;
|
|
1153
|
-
}
|
|
1154
|
-
i = j;
|
|
1155
|
-
continue;
|
|
1156
|
-
}
|
|
1157
|
-
i++;
|
|
1158
|
-
}
|
|
1159
|
-
return false;
|
|
1160
|
-
}
|
|
1161
|
-
function assertExplicitGraphIrisAllowed(sparql, allowedGraphs) {
|
|
1162
|
-
const allowed = new Set(allowedGraphs);
|
|
1163
|
-
for (const graphIri of collectExplicitGraphIris(sparql)) {
|
|
1164
|
-
if (!allowed.has(graphIri)) {
|
|
1165
|
-
throw new ScopedQueryViolationError(`GRAPH <${graphIri}> is outside the allowed graph set`);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
function readGraphTarget(sparql, start, prefixes) {
|
|
1170
|
-
const variable = readSparqlVariable(sparql, start);
|
|
1171
|
-
if (variable) {
|
|
1172
|
-
return { kind: 'variable', end: start + variable.length };
|
|
1173
|
-
}
|
|
1174
|
-
if (sparql[start] === '<') {
|
|
1175
|
-
const iriEnd = skipSparqlIriRef(sparql, start);
|
|
1176
|
-
if (!iriEnd) {
|
|
1177
|
-
throw new ScopedQueryViolationError('GRAPH target must be a variable, explicit IRI, or resolvable prefixed name on scoped queries');
|
|
1178
|
-
}
|
|
1179
|
-
return {
|
|
1180
|
-
kind: 'iri',
|
|
1181
|
-
iri: sparql.slice(start + 1, iriEnd - 1),
|
|
1182
|
-
end: iriEnd,
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
const prefixedName = readSparqlPrefixName(sparql, start);
|
|
1186
|
-
if (prefixedName) {
|
|
1187
|
-
const iri = resolveSparqlPrefixedName(prefixedName, prefixes);
|
|
1188
|
-
if (!iri) {
|
|
1189
|
-
throw new ScopedQueryViolationError(`GRAPH prefixed target ${sparql.slice(start, start + prefixedName.length)} cannot be resolved from PREFIX declarations`);
|
|
1190
|
-
}
|
|
1191
|
-
return {
|
|
1192
|
-
kind: 'iri',
|
|
1193
|
-
iri,
|
|
1194
|
-
end: start + prefixedName.length,
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
return null;
|
|
1198
|
-
}
|
|
1199
|
-
function constrainGraphVariablesToAllowedSet(sparql, allowedGraphs) {
|
|
1200
|
-
if (hasNestedSelectWithGraphVariable(sparql)) {
|
|
1201
|
-
throw new ScopedQueryViolationError('GRAPH variables inside nested SELECT subqueries cannot be constrained safely');
|
|
1202
|
-
}
|
|
1203
|
-
const graphVariables = collectGraphVariables(sparql);
|
|
1204
|
-
if (graphVariables.length === 0)
|
|
1205
|
-
return sparql;
|
|
1206
|
-
const braceStart = findWhereBraceStart(sparql);
|
|
1207
|
-
if (braceStart === -1) {
|
|
1208
|
-
throw new ScopedQueryViolationError('GRAPH variables cannot be constrained because the WHERE block could not be located');
|
|
1209
|
-
}
|
|
1210
|
-
assertGraphVariablesAreTopLevel(sparql, braceStart);
|
|
1211
|
-
assertNoTopLevelDefaultGraphPatternsWithGraphVariables(sparql, braceStart);
|
|
1212
|
-
const allowed = new Set(allowedGraphs);
|
|
1213
|
-
const variablesNeedingConstraint = graphVariables.filter((variable) => {
|
|
1214
|
-
// A top-level `VALUES ?g { <g1> ... }` already constrains every use of
|
|
1215
|
-
// `GRAPH ?g` in this group. When its values are all inside the DKG
|
|
1216
|
-
// allow-list, injecting the complete allow-list again is redundant and can
|
|
1217
|
-
// be catastrophic at scale: the Blackbox VM reader supplied five exact
|
|
1218
|
-
// partitions, while DKG added the complete 128-graph allow-list and turned a ~3 KB
|
|
1219
|
-
// query into the stable 24,051-byte planner stall from issue #1989.
|
|
1220
|
-
//
|
|
1221
|
-
// If the VALUES shape is dynamic/unsupported or contains an out-of-scope
|
|
1222
|
-
// IRI, retain the existing intersection rewrite. This preserves the
|
|
1223
|
-
// fail-closed behavior without broadening access.
|
|
1224
|
-
return !callerGraphValuesAreAuthorized(sparql, braceStart, variable, allowed);
|
|
1225
|
-
});
|
|
1226
|
-
if (variablesNeedingConstraint.length === 0)
|
|
1227
|
-
return sparql;
|
|
1228
|
-
const values = allowedGraphs
|
|
1229
|
-
.map((g) => `<${assertSafeIri(g)}>`)
|
|
1230
|
-
.join(' ');
|
|
1231
|
-
const constraints = variablesNeedingConstraint
|
|
1232
|
-
.map((variable) => `VALUES ${variable} { ${values} }`)
|
|
1233
|
-
.join(' ');
|
|
1234
|
-
return `${sparql.slice(0, braceStart + 1)} ${constraints} ${sparql.slice(braceStart + 1)}`;
|
|
1235
|
-
}
|
|
1236
|
-
function assertNoTopLevelDefaultGraphPatternsWithGraphVariables(sparql, braceStart) {
|
|
1237
|
-
if (!hasTopLevelDefaultGraphPattern(sparql, braceStart))
|
|
1238
|
-
return;
|
|
1239
|
-
throw new ScopedQueryViolationError('GRAPH variables cannot be mixed with default-graph triple patterns on scoped local queries');
|
|
1240
|
-
}
|
|
1241
|
-
function hasTopLevelDefaultGraphPattern(sparql, braceStart) {
|
|
1242
|
-
const braceEnd = findMatchingCloseBrace(sparql, braceStart);
|
|
1243
|
-
if (braceEnd === -1)
|
|
1244
|
-
return true;
|
|
1245
|
-
const prefixes = collectPrefixDeclarations(sparql);
|
|
1246
|
-
let depth = 0;
|
|
1247
|
-
let i = braceStart + 1;
|
|
1248
|
-
while (i < braceEnd) {
|
|
1249
|
-
const ch = sparql[i];
|
|
1250
|
-
if (ch === '#') {
|
|
1251
|
-
while (i < braceEnd && sparql[i] !== '\n')
|
|
1252
|
-
i++;
|
|
1253
|
-
continue;
|
|
1254
|
-
}
|
|
1255
|
-
if (ch === '"' || ch === "'") {
|
|
1256
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1257
|
-
continue;
|
|
1258
|
-
}
|
|
1259
|
-
if (ch === '{') {
|
|
1260
|
-
depth++;
|
|
1261
|
-
i++;
|
|
1262
|
-
continue;
|
|
1263
|
-
}
|
|
1264
|
-
if (ch === '}') {
|
|
1265
|
-
depth = Math.max(0, depth - 1);
|
|
1266
|
-
i++;
|
|
1267
|
-
continue;
|
|
1268
|
-
}
|
|
1269
|
-
if (depth !== 0 || /\s/.test(ch) || ch === '.' || ch === ';' || ch === ',') {
|
|
1270
|
-
i++;
|
|
1271
|
-
continue;
|
|
1272
|
-
}
|
|
1273
|
-
if (ch === '<')
|
|
1274
|
-
return !!skipSparqlIriRef(sparql, i);
|
|
1275
|
-
if (ch === '?' || ch === '$' || ch === '[')
|
|
1276
|
-
return true;
|
|
1277
|
-
if (isKeywordStart(sparql, i)) {
|
|
1278
|
-
let j = i + 1;
|
|
1279
|
-
while (j < braceEnd && isWordContinuation(sparql[j]))
|
|
1280
|
-
j++;
|
|
1281
|
-
if (isSparqlKeyword(sparql, i, j, 'GRAPH')) {
|
|
1282
|
-
const operandStart = skipSparqlSpaceAndLineComments(sparql, j);
|
|
1283
|
-
const target = readGraphTarget(sparql, operandStart, prefixes);
|
|
1284
|
-
i = target && target.end <= braceEnd ? target.end : j;
|
|
1285
|
-
continue;
|
|
1286
|
-
}
|
|
1287
|
-
if (isSparqlKeyword(sparql, i, j, 'FILTER') || isSparqlKeyword(sparql, i, j, 'BIND')) {
|
|
1288
|
-
const exprStart = skipSparqlSpaceAndLineComments(sparql, j);
|
|
1289
|
-
if (sparql[exprStart] === '(') {
|
|
1290
|
-
const exprEnd = skipBalancedParentheses(sparql, exprStart, braceEnd);
|
|
1291
|
-
i = exprEnd ?? j;
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
if (isSparqlKeyword(sparql, i, j, 'VALUES')) {
|
|
1296
|
-
i = skipValuesClause(sparql, j, braceEnd);
|
|
1297
|
-
continue;
|
|
1298
|
-
}
|
|
1299
|
-
if (isSparqlKeyword(sparql, i, j, 'OPTIONAL') ||
|
|
1300
|
-
isSparqlKeyword(sparql, i, j, 'MINUS') ||
|
|
1301
|
-
isSparqlKeyword(sparql, i, j, 'SERVICE') ||
|
|
1302
|
-
isSparqlKeyword(sparql, i, j, 'UNION') ||
|
|
1303
|
-
isSparqlKeyword(sparql, i, j, 'SELECT')) {
|
|
1304
|
-
i = j;
|
|
1305
|
-
continue;
|
|
1306
|
-
}
|
|
1307
|
-
return true;
|
|
1308
|
-
}
|
|
1309
|
-
i++;
|
|
1310
|
-
}
|
|
1311
|
-
return false;
|
|
1312
|
-
}
|
|
1313
|
-
function assertGraphVariablesAreTopLevel(sparql, braceStart) {
|
|
1314
|
-
const braceEnd = findMatchingCloseBrace(sparql, braceStart);
|
|
1315
|
-
if (braceEnd === -1) {
|
|
1316
|
-
throw new ScopedQueryViolationError('GRAPH variables cannot be constrained because the WHERE block could not be located');
|
|
1317
|
-
}
|
|
1318
|
-
let depth = 0;
|
|
1319
|
-
let i = braceStart + 1;
|
|
1320
|
-
while (i < braceEnd) {
|
|
1321
|
-
const ch = sparql[i];
|
|
1322
|
-
if (ch === '#') {
|
|
1323
|
-
while (i < braceEnd && sparql[i] !== '\n')
|
|
1324
|
-
i++;
|
|
1325
|
-
continue;
|
|
1326
|
-
}
|
|
1327
|
-
if (ch === '"' || ch === "'") {
|
|
1328
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1329
|
-
continue;
|
|
1330
|
-
}
|
|
1331
|
-
if (ch === '<') {
|
|
1332
|
-
const iriEnd = skipSparqlIriRef(sparql, i);
|
|
1333
|
-
i = iriEnd && iriEnd <= braceEnd ? iriEnd : i + 1;
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
if (ch === '{') {
|
|
1337
|
-
depth++;
|
|
1338
|
-
i++;
|
|
1339
|
-
continue;
|
|
1340
|
-
}
|
|
1341
|
-
if (ch === '}') {
|
|
1342
|
-
depth = Math.max(0, depth - 1);
|
|
1343
|
-
i++;
|
|
1344
|
-
continue;
|
|
1345
|
-
}
|
|
1346
|
-
if (isKeywordStart(sparql, i)) {
|
|
1347
|
-
let j = i + 1;
|
|
1348
|
-
while (j < braceEnd && isWordContinuation(sparql[j]))
|
|
1349
|
-
j++;
|
|
1350
|
-
if (isSparqlKeyword(sparql, i, j, 'GRAPH')) {
|
|
1351
|
-
const operandStart = skipSparqlSpaceAndLineComments(sparql, j);
|
|
1352
|
-
if (operandStart < braceEnd && readSparqlVariable(sparql, operandStart) && depth !== 0) {
|
|
1353
|
-
throw new ScopedQueryViolationError('GRAPH variables must appear at the top level of scoped local queries');
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
i = j;
|
|
1357
|
-
continue;
|
|
1358
|
-
}
|
|
1359
|
-
i++;
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
function hasNestedSelectWithGraphVariable(sparql) {
|
|
1363
|
-
const n = sparql.length;
|
|
1364
|
-
let i = 0;
|
|
1365
|
-
let braceDepth = 0;
|
|
1366
|
-
while (i < n) {
|
|
1367
|
-
const ch = sparql[i];
|
|
1368
|
-
if (ch === '#') {
|
|
1369
|
-
while (i < n && sparql[i] !== '\n')
|
|
1370
|
-
i++;
|
|
1371
|
-
continue;
|
|
1372
|
-
}
|
|
1373
|
-
if (ch === '"' || ch === "'") {
|
|
1374
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1375
|
-
continue;
|
|
1376
|
-
}
|
|
1377
|
-
if (ch === '<') {
|
|
1378
|
-
const end = skipSparqlIriRef(sparql, i);
|
|
1379
|
-
i = end ?? i + 1;
|
|
1380
|
-
continue;
|
|
1381
|
-
}
|
|
1382
|
-
if (ch === '{') {
|
|
1383
|
-
braceDepth++;
|
|
1384
|
-
i++;
|
|
1385
|
-
continue;
|
|
1386
|
-
}
|
|
1387
|
-
if (ch === '}') {
|
|
1388
|
-
braceDepth = Math.max(0, braceDepth - 1);
|
|
1389
|
-
i++;
|
|
1390
|
-
continue;
|
|
1391
|
-
}
|
|
1392
|
-
if (isKeywordStart(sparql, i)) {
|
|
1393
|
-
let j = i + 1;
|
|
1394
|
-
while (j < n && isWordContinuation(sparql[j]))
|
|
1395
|
-
j++;
|
|
1396
|
-
const word = sparql.slice(i, j);
|
|
1397
|
-
if (isSparqlKeyword(sparql, i, j, 'SELECT') && braceDepth > 0) {
|
|
1398
|
-
const end = findNestedSelectEnd(sparql, j, braceDepth);
|
|
1399
|
-
if (rangeContainsGraphVariable(sparql, j, end === -1 ? n : end)) {
|
|
1400
|
-
return true;
|
|
1401
|
-
}
|
|
1402
|
-
i = end === -1 ? j : end + 1;
|
|
1403
|
-
continue;
|
|
1404
|
-
}
|
|
1405
|
-
i = j;
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
i++;
|
|
1409
|
-
}
|
|
1410
|
-
return false;
|
|
1411
|
-
}
|
|
1412
|
-
function findNestedSelectEnd(sparql, start, startingDepth) {
|
|
1413
|
-
const n = sparql.length;
|
|
1414
|
-
let depth = startingDepth;
|
|
1415
|
-
let i = start;
|
|
1416
|
-
while (i < n) {
|
|
1417
|
-
const ch = sparql[i];
|
|
1418
|
-
if (ch === '#') {
|
|
1419
|
-
while (i < n && sparql[i] !== '\n')
|
|
1420
|
-
i++;
|
|
1421
|
-
continue;
|
|
1422
|
-
}
|
|
1423
|
-
if (ch === '"' || ch === "'") {
|
|
1424
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1425
|
-
continue;
|
|
1426
|
-
}
|
|
1427
|
-
if (ch === '<') {
|
|
1428
|
-
const end = skipSparqlIriRef(sparql, i);
|
|
1429
|
-
i = end ?? i + 1;
|
|
1430
|
-
continue;
|
|
1431
|
-
}
|
|
1432
|
-
if (ch === '{') {
|
|
1433
|
-
depth++;
|
|
1434
|
-
i++;
|
|
1435
|
-
continue;
|
|
1436
|
-
}
|
|
1437
|
-
if (ch === '}') {
|
|
1438
|
-
depth--;
|
|
1439
|
-
if (depth < startingDepth)
|
|
1440
|
-
return i;
|
|
1441
|
-
if (depth < 0)
|
|
1442
|
-
return -1;
|
|
1443
|
-
i++;
|
|
1444
|
-
continue;
|
|
1445
|
-
}
|
|
1446
|
-
i++;
|
|
1447
|
-
}
|
|
1448
|
-
return -1;
|
|
1449
|
-
}
|
|
1450
|
-
function rangeContainsGraphVariable(sparql, start, end) {
|
|
1451
|
-
const n = Math.min(sparql.length, end);
|
|
1452
|
-
let i = start;
|
|
1453
|
-
while (i < n) {
|
|
1454
|
-
const ch = sparql[i];
|
|
1455
|
-
if (ch === '#') {
|
|
1456
|
-
while (i < n && sparql[i] !== '\n')
|
|
1457
|
-
i++;
|
|
1458
|
-
continue;
|
|
1459
|
-
}
|
|
1460
|
-
if (ch === '"' || ch === "'") {
|
|
1461
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1462
|
-
continue;
|
|
1463
|
-
}
|
|
1464
|
-
if (ch === '<') {
|
|
1465
|
-
const iriEnd = skipSparqlIriRef(sparql, i);
|
|
1466
|
-
i = iriEnd && iriEnd <= n ? iriEnd : i + 1;
|
|
1467
|
-
continue;
|
|
1468
|
-
}
|
|
1469
|
-
if (isKeywordStart(sparql, i)) {
|
|
1470
|
-
let j = i + 1;
|
|
1471
|
-
while (j < n && isWordContinuation(sparql[j]))
|
|
1472
|
-
j++;
|
|
1473
|
-
const word = sparql.slice(i, j);
|
|
1474
|
-
if (isSparqlKeyword(sparql, i, j, 'GRAPH')) {
|
|
1475
|
-
const operandStart = skipSparqlSpaceAndLineComments(sparql, j);
|
|
1476
|
-
if (operandStart < n && readSparqlVariable(sparql, operandStart)) {
|
|
1477
|
-
return true;
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
i = j;
|
|
1481
|
-
continue;
|
|
1482
|
-
}
|
|
1483
|
-
i++;
|
|
1484
|
-
}
|
|
1485
|
-
return false;
|
|
1486
|
-
}
|
|
1487
|
-
function collectExplicitGraphIris(sparql) {
|
|
1488
|
-
const iris = [];
|
|
1489
|
-
const prefixes = collectPrefixDeclarations(sparql);
|
|
1490
|
-
const n = sparql.length;
|
|
1491
|
-
let i = 0;
|
|
1492
|
-
while (i < n) {
|
|
1493
|
-
const ch = sparql[i];
|
|
1494
|
-
if (ch === '#') {
|
|
1495
|
-
while (i < n && sparql[i] !== '\n')
|
|
1496
|
-
i++;
|
|
1497
|
-
continue;
|
|
1498
|
-
}
|
|
1499
|
-
if (ch === '"' || ch === "'") {
|
|
1500
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1501
|
-
continue;
|
|
1502
|
-
}
|
|
1503
|
-
if (ch === '<') {
|
|
1504
|
-
const end = skipSparqlIriRef(sparql, i);
|
|
1505
|
-
i = end ?? i + 1;
|
|
1506
|
-
continue;
|
|
1507
|
-
}
|
|
1508
|
-
if (isKeywordStart(sparql, i)) {
|
|
1509
|
-
let j = i + 1;
|
|
1510
|
-
while (j < n && isWordContinuation(sparql[j]))
|
|
1511
|
-
j++;
|
|
1512
|
-
if (isSparqlKeyword(sparql, i, j, 'GRAPH')) {
|
|
1513
|
-
const operandStart = skipSparqlSpaceAndLineComments(sparql, j);
|
|
1514
|
-
const target = readGraphTarget(sparql, operandStart, prefixes);
|
|
1515
|
-
if (!target) {
|
|
1516
|
-
throw new ScopedQueryViolationError('GRAPH target must be a variable, explicit IRI, or resolvable prefixed name on scoped queries');
|
|
1517
|
-
}
|
|
1518
|
-
if (target.kind === 'iri' && target.iri)
|
|
1519
|
-
iris.push(target.iri);
|
|
1520
|
-
i = target.end;
|
|
1521
|
-
continue;
|
|
1522
|
-
}
|
|
1523
|
-
i = j;
|
|
1524
|
-
continue;
|
|
1525
|
-
}
|
|
1526
|
-
i++;
|
|
1527
|
-
}
|
|
1528
|
-
return iris;
|
|
1529
|
-
}
|
|
1530
|
-
function hasGraphClause(sparql) {
|
|
1531
|
-
const n = sparql.length;
|
|
1532
|
-
let i = 0;
|
|
1533
|
-
while (i < n) {
|
|
1534
|
-
const ch = sparql[i];
|
|
1535
|
-
if (ch === '#') {
|
|
1536
|
-
while (i < n && sparql[i] !== '\n')
|
|
1537
|
-
i++;
|
|
1538
|
-
continue;
|
|
1539
|
-
}
|
|
1540
|
-
if (ch === '"' || ch === "'") {
|
|
1541
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1542
|
-
continue;
|
|
1543
|
-
}
|
|
1544
|
-
if (ch === '<') {
|
|
1545
|
-
const end = skipSparqlIriRef(sparql, i);
|
|
1546
|
-
i = end ?? i + 1;
|
|
1547
|
-
continue;
|
|
1548
|
-
}
|
|
1549
|
-
if (isKeywordStart(sparql, i)) {
|
|
1550
|
-
let j = i + 1;
|
|
1551
|
-
while (j < n && isWordContinuation(sparql[j]))
|
|
1552
|
-
j++;
|
|
1553
|
-
if (isSparqlKeyword(sparql, i, j, 'GRAPH')) {
|
|
1554
|
-
return true;
|
|
1555
|
-
}
|
|
1556
|
-
i = j;
|
|
1557
|
-
continue;
|
|
1558
|
-
}
|
|
1559
|
-
i++;
|
|
1560
|
-
}
|
|
1561
|
-
return false;
|
|
1562
|
-
}
|
|
1563
|
-
function collectGraphVariables(sparql) {
|
|
1564
|
-
const variables = [];
|
|
1565
|
-
const seen = new Set();
|
|
1566
|
-
const n = sparql.length;
|
|
1567
|
-
let i = 0;
|
|
1568
|
-
while (i < n) {
|
|
1569
|
-
const ch = sparql[i];
|
|
1570
|
-
if (ch === '#') {
|
|
1571
|
-
while (i < n && sparql[i] !== '\n')
|
|
1572
|
-
i++;
|
|
1573
|
-
continue;
|
|
1574
|
-
}
|
|
1575
|
-
if (ch === '"' || ch === "'") {
|
|
1576
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1577
|
-
continue;
|
|
1578
|
-
}
|
|
1579
|
-
if (ch === '<') {
|
|
1580
|
-
const end = skipSparqlIriRef(sparql, i);
|
|
1581
|
-
i = end ?? i + 1;
|
|
1582
|
-
continue;
|
|
1583
|
-
}
|
|
1584
|
-
if (isKeywordStart(sparql, i)) {
|
|
1585
|
-
let j = i + 1;
|
|
1586
|
-
while (j < n && isWordContinuation(sparql[j]))
|
|
1587
|
-
j++;
|
|
1588
|
-
const word = sparql.slice(i, j);
|
|
1589
|
-
if (isSparqlKeyword(sparql, i, j, 'GRAPH')) {
|
|
1590
|
-
const operandStart = skipSparqlSpaceAndLineComments(sparql, j);
|
|
1591
|
-
const variable = readSparqlVariable(sparql, operandStart);
|
|
1592
|
-
if (variable && !seen.has(variable)) {
|
|
1593
|
-
seen.add(variable);
|
|
1594
|
-
variables.push(variable);
|
|
1595
|
-
}
|
|
1596
|
-
i = operandStart + (variable?.length ?? 0);
|
|
1597
|
-
continue;
|
|
1598
|
-
}
|
|
1599
|
-
i = j;
|
|
1600
|
-
continue;
|
|
1601
|
-
}
|
|
1602
|
-
i++;
|
|
1603
|
-
}
|
|
1604
|
-
return variables;
|
|
1605
|
-
}
|
|
1606
|
-
function skipBalancedParentheses(sparql, start, limit = sparql.length) {
|
|
1607
|
-
if (sparql[start] !== '(')
|
|
1608
|
-
return null;
|
|
1609
|
-
let depth = 1;
|
|
1610
|
-
let i = start + 1;
|
|
1611
|
-
while (i < limit) {
|
|
1612
|
-
const ch = sparql[i];
|
|
1613
|
-
if (ch === '#') {
|
|
1614
|
-
while (i < limit && sparql[i] !== '\n')
|
|
1615
|
-
i++;
|
|
1616
|
-
continue;
|
|
1617
|
-
}
|
|
1618
|
-
if (ch === '"' || ch === "'") {
|
|
1619
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1620
|
-
continue;
|
|
1621
|
-
}
|
|
1622
|
-
if (ch === '<') {
|
|
1623
|
-
const iriEnd = skipSparqlIriRef(sparql, i);
|
|
1624
|
-
i = iriEnd && iriEnd <= limit ? iriEnd : i + 1;
|
|
1625
|
-
continue;
|
|
1626
|
-
}
|
|
1627
|
-
if (ch === '(')
|
|
1628
|
-
depth++;
|
|
1629
|
-
else if (ch === ')') {
|
|
1630
|
-
depth--;
|
|
1631
|
-
if (depth === 0)
|
|
1632
|
-
return i + 1;
|
|
1633
|
-
}
|
|
1634
|
-
i++;
|
|
1635
|
-
}
|
|
1636
|
-
return null;
|
|
1637
|
-
}
|
|
1638
|
-
function skipValuesClause(sparql, start, limit) {
|
|
1639
|
-
let i = start;
|
|
1640
|
-
while (i < limit) {
|
|
1641
|
-
const ch = sparql[i];
|
|
1642
|
-
if (ch === '#') {
|
|
1643
|
-
while (i < limit && sparql[i] !== '\n')
|
|
1644
|
-
i++;
|
|
1645
|
-
continue;
|
|
1646
|
-
}
|
|
1647
|
-
if (ch === '"' || ch === "'") {
|
|
1648
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1649
|
-
continue;
|
|
1650
|
-
}
|
|
1651
|
-
if (ch === '{') {
|
|
1652
|
-
const end = findMatchingCloseBrace(sparql, i);
|
|
1653
|
-
return end === -1 || end > limit ? i : end + 1;
|
|
1654
|
-
}
|
|
1655
|
-
if (ch === '.')
|
|
1656
|
-
return i + 1;
|
|
1657
|
-
i++;
|
|
1658
|
-
}
|
|
1659
|
-
return i;
|
|
1660
|
-
}
|
|
1661
|
-
/**
|
|
1662
|
-
* Skip past a SPARQL string literal starting at `src[i]`, returning the
|
|
1663
|
-
* index immediately AFTER the closing quote.
|
|
1664
|
-
*
|
|
1665
|
-
* Recognises **all four** SPARQL 1.1 literal forms:
|
|
1666
|
-
*
|
|
1667
|
-
* - `"…"` single-line, double-quoted (escape: `\\`, `\"`, `\n`, …)
|
|
1668
|
-
* - `'…'` single-line, single-quoted (same escape grammar)
|
|
1669
|
-
* - `"""…"""` long-form, double-quoted (may span newlines, contains
|
|
1670
|
-
* raw `"`, `'`, `{`, `}`, `#`, `.` without escaping)
|
|
1671
|
-
* - `'''…'''` long-form, single-quoted (same as above)
|
|
1672
|
-
*
|
|
1673
|
-
* **Caller contract:** `src[i]` MUST be `"` or `'`; otherwise the function
|
|
1674
|
-
* returns `i` (no advance). The cursor returned points to the first byte
|
|
1675
|
-
* AFTER the literal, ready for the caller to resume its own scan.
|
|
1676
|
-
*
|
|
1677
|
-
* If a literal is unterminated (truncated input) the function consumes
|
|
1678
|
-
* the remainder of the string and returns `src.length`. Callers treat
|
|
1679
|
-
* unterminated literals as "the rest of the input is opaque payload",
|
|
1680
|
-
* which is the safe choice for structural scans (brace balancing,
|
|
1681
|
-
* keyword detection): we do NOT want a stray `{` near the end of a
|
|
1682
|
-
* truncated query body to confuse the surrounding scanner.
|
|
1683
|
-
*
|
|
1684
|
-
* dkg-query-engine.ts:848). The
|
|
1685
|
-
* previous helpers (`stripSparqlLineComments`, `scrubStringsAndComments`,
|
|
1686
|
-
* `findMatchingCloseBrace`, `findWhereBraceStart`, and
|
|
1687
|
-
* `splitTopLevelTripleStatements`) all had their own copy of the
|
|
1688
|
-
* single-line literal scanner and NONE recognised triple-quoted
|
|
1689
|
-
* literals, so a long-form payload like
|
|
1690
|
-
*
|
|
1691
|
-
* SELECT ?t WHERE { ?s <p> """contains a {brace} and a #comment""" }
|
|
1692
|
-
*
|
|
1693
|
-
* leaked `{`, `}`, `#`, `.`, etc. through the structural scrubber and
|
|
1694
|
-
* the `minTrust` rewriter (and the SPARQL form classifier, and the
|
|
1695
|
-
* triple terminator splitter) misclassified payload as syntax. The
|
|
1696
|
-
* downstream effect was the same fail-closed empty result the
|
|
1697
|
-
* scrubbing was supposed to prevent. Centralising the lex here means
|
|
1698
|
-
* every helper that walks SPARQL source learns triple-quoted handling
|
|
1699
|
-
* in one place.
|
|
1700
|
-
*/
|
|
1701
|
-
export function skipSparqlStringLiteral(src, i) {
|
|
1702
|
-
return scanSparqlStringLiteral(src, i);
|
|
1703
|
-
}
|
|
1704
|
-
/**
|
|
1705
|
-
* Token-aware locator for the explicit `WHERE` keyword at the
|
|
1706
|
-
* top-level of a SPARQL query. Mirrors the lex rules used by
|
|
1707
|
-
* {@link findMatchingCloseBrace} / the fallback path in
|
|
1708
|
-
* {@link findWhereBraceStart}: skips line comments (`# ... \n`),
|
|
1709
|
-
* single/double/triple-quoted string literals (via
|
|
1710
|
-
* {@link skipSparqlStringLiteral}), and IRIREFs (`<...>`) so the
|
|
1711
|
-
* `WHERE` substring can NOT be sourced from inside any of those
|
|
1712
|
-
* payload contexts. The `<` token is disambiguated as IRI-start
|
|
1713
|
-
* vs less-than via the same next-byte allow-list as
|
|
1714
|
-
* {@link findWhereBraceStart}'s fallback.
|
|
1715
|
-
*
|
|
1716
|
-
* Returns the index of the `W` of the `WHERE` keyword, or `-1` if
|
|
1717
|
-
* none is found at top level. Case-insensitive on the keyword
|
|
1718
|
-
* itself, but the surrounding word boundary is enforced (so
|
|
1719
|
-
* identifiers like `WHEREVER` / `aWHERE` do NOT match).
|
|
1720
|
-
*/
|
|
1721
|
-
function findExplicitWhereTokenIdx(sparql) {
|
|
1722
|
-
const n = sparql.length;
|
|
1723
|
-
const isWordStart = (c) => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '_';
|
|
1724
|
-
const isWordCont = (c) => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
1725
|
-
(c >= '0' && c <= '9') || c === '_';
|
|
1726
|
-
const isIriStartFirstByte = (c) => {
|
|
1727
|
-
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
|
|
1728
|
-
return true;
|
|
1729
|
-
return c === '#' || c === '_' || c === '/' || c === '.';
|
|
1730
|
-
};
|
|
1731
|
-
const isIriStart = (idx) => {
|
|
1732
|
-
const next = sparql[idx + 1];
|
|
1733
|
-
if (next === undefined)
|
|
1734
|
-
return false;
|
|
1735
|
-
if (!isIriStartFirstByte(next))
|
|
1736
|
-
return false;
|
|
1737
|
-
for (let j = idx + 1; j < n; j++) {
|
|
1738
|
-
const c = sparql[j];
|
|
1739
|
-
if (c === '>')
|
|
1740
|
-
return true;
|
|
1741
|
-
if (c === '<' || c === '"' || c === '{' || c === '}' ||
|
|
1742
|
-
c === '|' || c === '\\' || c === '^' || c === '`' ||
|
|
1743
|
-
/\s/.test(c))
|
|
1744
|
-
return false;
|
|
1745
|
-
}
|
|
1746
|
-
return false;
|
|
1747
|
-
};
|
|
1748
|
-
let i = 0;
|
|
1749
|
-
let braceDepth = 0;
|
|
1750
|
-
while (i < n) {
|
|
1751
|
-
const ch = sparql[i];
|
|
1752
|
-
if (ch === '#') {
|
|
1753
|
-
while (i < n && sparql[i] !== '\n')
|
|
1754
|
-
i++;
|
|
1755
|
-
continue;
|
|
1756
|
-
}
|
|
1757
|
-
if (ch === '"' || ch === "'") {
|
|
1758
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1759
|
-
continue;
|
|
1760
|
-
}
|
|
1761
|
-
if (ch === '<') {
|
|
1762
|
-
if (isIriStart(i)) {
|
|
1763
|
-
const end = sparql.indexOf('>', i + 1);
|
|
1764
|
-
if (end === -1)
|
|
1765
|
-
return -1;
|
|
1766
|
-
i = end + 1;
|
|
1767
|
-
continue;
|
|
1768
|
-
}
|
|
1769
|
-
i++;
|
|
1770
|
-
continue;
|
|
1771
|
-
}
|
|
1772
|
-
if (ch === '{') {
|
|
1773
|
-
braceDepth++;
|
|
1774
|
-
i++;
|
|
1775
|
-
continue;
|
|
1776
|
-
}
|
|
1777
|
-
if (ch === '}') {
|
|
1778
|
-
braceDepth = Math.max(0, braceDepth - 1);
|
|
1779
|
-
i++;
|
|
1780
|
-
continue;
|
|
1781
|
-
}
|
|
1782
|
-
if (isWordStart(ch)) {
|
|
1783
|
-
// Word boundary check: previous char (if any) must NOT be a
|
|
1784
|
-
// word-continuation byte. The outer lexer already skipped
|
|
1785
|
-
// comments/strings/IRIs, so a non-word predecessor means we're
|
|
1786
|
-
// at a real keyword start.
|
|
1787
|
-
const prev = i > 0 ? sparql[i - 1] : '';
|
|
1788
|
-
if (prev && isWordCont(prev)) {
|
|
1789
|
-
// Mid-identifier — skip the rest of the word.
|
|
1790
|
-
let j = i + 1;
|
|
1791
|
-
while (j < n && isWordCont(sparql[j]))
|
|
1792
|
-
j++;
|
|
1793
|
-
i = j;
|
|
1794
|
-
continue;
|
|
1795
|
-
}
|
|
1796
|
-
let j = i + 1;
|
|
1797
|
-
while (j < n && isWordCont(sparql[j]))
|
|
1798
|
-
j++;
|
|
1799
|
-
const word = sparql.substring(i, j);
|
|
1800
|
-
if (braceDepth === 0 && word.length === 5 && word.toUpperCase() === 'WHERE') {
|
|
1801
|
-
return i;
|
|
1802
|
-
}
|
|
1803
|
-
i = j;
|
|
1804
|
-
continue;
|
|
1805
|
-
}
|
|
1806
|
-
i++;
|
|
1807
|
-
}
|
|
1808
|
-
return -1;
|
|
1809
|
-
}
|
|
1810
|
-
/**
|
|
1811
|
-
* Find the next significant `{` after a given index, skipping
|
|
1812
|
-
* whitespace AND line comments (`# … \n`) but NOT string literals
|
|
1813
|
-
* — SPARQL grammar does not allow a string literal between the
|
|
1814
|
-
* `WHERE` keyword and its opening `{`, so encountering one means
|
|
1815
|
-
* the input is malformed and we should bail (return `-1`).
|
|
1816
|
-
*/
|
|
1817
|
-
function nextSignificantBraceAfter(sparql, startIdx) {
|
|
1818
|
-
const n = sparql.length;
|
|
1819
|
-
let i = startIdx;
|
|
1820
|
-
while (i < n) {
|
|
1821
|
-
const ch = sparql[i];
|
|
1822
|
-
if (ch === '#') {
|
|
1823
|
-
while (i < n && sparql[i] !== '\n')
|
|
1824
|
-
i++;
|
|
1825
|
-
continue;
|
|
1826
|
-
}
|
|
1827
|
-
if (/\s/.test(ch)) {
|
|
1828
|
-
i++;
|
|
1829
|
-
continue;
|
|
1830
|
-
}
|
|
1831
|
-
if (ch === '{')
|
|
1832
|
-
return i;
|
|
1833
|
-
return -1;
|
|
1834
|
-
}
|
|
1835
|
-
return -1;
|
|
1836
|
-
}
|
|
1837
|
-
/**
|
|
1838
|
-
* Locate the opening `{` of the WHERE clause in a SPARQL query.
|
|
1839
|
-
*
|
|
1840
|
-
* SPARQL 1.1 (§16) allows the `WHERE` keyword to be omitted from
|
|
1841
|
-
* `SELECT`, `DESCRIBE`, and `ASK` queries, and from the second
|
|
1842
|
-
* `GroupGraphPattern` of a `CONSTRUCT`. The legacy callers (`wrapWithGraph`,
|
|
1843
|
-
* `wrapWithGraphUnion`, `injectMinTrustFilter`) all matched only
|
|
1844
|
-
* `WHERE\s*\{`, so any of those legitimate shorthand forms left the
|
|
1845
|
-
* query untouched (no GRAPH wrapping, no trust filter injection) and —
|
|
1846
|
-
* on a `verifiable-memory` view whose data lives in a named sub-graph —
|
|
1847
|
-
* silently returned `[]` instead of executing against the right graph.
|
|
1848
|
-
*
|
|
1849
|
-
* Strategy:
|
|
1850
|
-
* 1. Prefer the explicit `WHERE { ... }` form.
|
|
1851
|
-
* 2. Otherwise, walk top-level braces (skipping IRIs / quoted
|
|
1852
|
-
* strings / comments) and use the LAST top-level `{...}`. This
|
|
1853
|
-
* is correct for every form:
|
|
1854
|
-
* - `SELECT ?x { ... }` (1 top-level brace)
|
|
1855
|
-
* - `ASK { ... }` (1)
|
|
1856
|
-
* - `DESCRIBE ?x { ... }` (1)
|
|
1857
|
-
* - `CONSTRUCT { tmpl } { where }`(2 — last is the WHERE)
|
|
1858
|
-
* `CONSTRUCT WHERE { ... }` already matches the primary path.
|
|
1859
|
-
*
|
|
1860
|
-
* Returns `null` when no top-level `{...}` block is balanced.
|
|
1861
|
-
*/
|
|
1862
|
-
function findWhereBraceStart(sparql) {
|
|
1863
|
-
// The earlier fast path used a raw regex `/\bWHERE\s*\{/i` which
|
|
1864
|
-
// matches ANY `WHERE` followed by `{` — including ones embedded inside
|
|
1865
|
-
// string literals or comments. Adversarial / obfuscated input
|
|
1866
|
-
// like
|
|
1867
|
-
// SELECT ("WHERE {" AS ?x) WHERE { ... }
|
|
1868
|
-
// would have the regex hit the literal substring inside the
|
|
1869
|
-
// SELECT projection, then `sparql.indexOf('{', whereIdx)` would
|
|
1870
|
-
// grab the brace just past the literal — and every later
|
|
1871
|
-
// injection (`wrapWithGraph` / `injectMinTrustFilter`) would
|
|
1872
|
-
// rewrite the wrong block, in some cases producing an invalid
|
|
1873
|
-
// query and in others silently filtering against a string-literal
|
|
1874
|
-
// expression rather than the actual WHERE clause.
|
|
1875
|
-
//
|
|
1876
|
-
// Fix: locate the explicit `WHERE` token using the SAME token-
|
|
1877
|
-
// aware scanner the fallback already uses (skips line comments,
|
|
1878
|
-
// single/double/triple-quoted string literals, and IRIREFs;
|
|
1879
|
-
// disambiguates `<` as IRI-start vs less-than via the next-byte
|
|
1880
|
-
// allow-list below). Then advance past inter-keyword whitespace
|
|
1881
|
-
// (and any line comments) before reading the `{`.
|
|
1882
|
-
const whereTokenIdx = findExplicitWhereTokenIdx(sparql);
|
|
1883
|
-
if (whereTokenIdx !== -1) {
|
|
1884
|
-
const idx = nextSignificantBraceAfter(sparql, whereTokenIdx + 'WHERE'.length);
|
|
1885
|
-
return idx;
|
|
1886
|
-
}
|
|
1887
|
-
// Fallback: scan for top-level `{` while honouring SPARQL token
|
|
1888
|
-
// boundaries — IRIs (`<...>`), quoted literals, and `#` comments
|
|
1889
|
-
// can all contain stray `{` chars that the regex would
|
|
1890
|
-
// misinterpret as block openers.
|
|
1891
|
-
//
|
|
1892
|
-
// dkg-query-engine.ts:559). The classifier rejects obvious
|
|
1893
|
-
// comparison shapes after `<` and falls back to a forward scan
|
|
1894
|
-
// that confirms a balanced IRIREF body. The r30 cut only rejected
|
|
1895
|
-
// `=`, `<`, and whitespace — a pure forward scan from `<` in
|
|
1896
|
-
// compact comparison syntax like
|
|
1897
|
-
// `FILTER(?n<10&&?m>5)`
|
|
1898
|
-
// walks `1`, `0`, `&`, `&`, `?`, `m` (none of which are
|
|
1899
|
-
// IRIREF-forbidden per the SPARQL grammar
|
|
1900
|
-
// `[^<>"{}|^`\]-[#x00-#x20]`) and lands on `>`, mis-classifying
|
|
1901
|
-
// the entire `<10&&?m>` as an IRI. The forward scan therefore
|
|
1902
|
-
// CANNOT be trusted alone for compact `<` operators that operate
|
|
1903
|
-
// on numerics / variables / sub-expressions whose body bytes are
|
|
1904
|
-
// all IRIREF-legal.
|
|
1905
|
-
//
|
|
1906
|
-
// r30+ resolution: combine an EXPLICIT next-byte allow-list of
|
|
1907
|
-
// characters that can validly start a real-world SPARQL IRIREF
|
|
1908
|
-
// (ALPHA for absolute IRIs `http:` / `urn:` / `did:` / `file:` /
|
|
1909
|
-
// `_blank-node:`, `#` for fragment-only relatives, `_` for the
|
|
1910
|
-
// legacy blank-node-as-IRI shape, `/` for path-only relatives,
|
|
1911
|
-
// and `.` for path-relative IRIs) with the existing
|
|
1912
|
-
// forbidden-byte forward scan. Anything else after `<` is treated
|
|
1913
|
-
// as a comparison and we advance by ONE byte. This bails fast on
|
|
1914
|
-
// every `<digit`, `<?var`, `<$var`, `<(...)`, `<"lit"`, `<-1`,
|
|
1915
|
-
// `<+1`, `<&`, `<|`, `<!`, `<*`, `<=`, `<<` shape — i.e. the
|
|
1916
|
-
// full set of SPARQL operator contexts in which `<` is overloaded
|
|
1917
|
-
// as less-than.
|
|
1918
|
-
//
|
|
1919
|
-
// Note: this is INTENTIONALLY stricter than the SPARQL grammar
|
|
1920
|
-
// (which technically allows `<10>` as an IRIREF). Real-world
|
|
1921
|
-
// SPARQL queries don't write bare-digit IRIs; falling out of the
|
|
1922
|
-
// IRI branch here just means we treat `<` as a comparison and
|
|
1923
|
-
// advance one byte, which is the safe behaviour for the brace
|
|
1924
|
-
// scan we actually care about.
|
|
1925
|
-
const isIriStartFirstByte = (c) => {
|
|
1926
|
-
// ASCII letter? (covers every absolute IRI scheme — `http:`,
|
|
1927
|
-
// `urn:`, `did:`, `file:`, `mailto:`, `tag:`, `data:`, …).
|
|
1928
|
-
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
|
|
1929
|
-
return true;
|
|
1930
|
-
// `#fragment` (SPARQL allows fragment-only relative IRIREFs
|
|
1931
|
-
// when the base IRI is set by the query environment), `_blah`
|
|
1932
|
-
// (legacy blank-node-as-IRI), `/path` (path-only relative),
|
|
1933
|
-
// `.something` (path-relative). Everything else is a comparison
|
|
1934
|
-
// operator context.
|
|
1935
|
-
return c === '#' || c === '_' || c === '/' || c === '.';
|
|
1936
|
-
};
|
|
1937
|
-
const isIriStart = (idx) => {
|
|
1938
|
-
const next = sparql[idx + 1];
|
|
1939
|
-
if (next === undefined)
|
|
1940
|
-
return false;
|
|
1941
|
-
if (!isIriStartFirstByte(next))
|
|
1942
|
-
return false;
|
|
1943
|
-
for (let j = idx + 1; j < n; j++) {
|
|
1944
|
-
const c = sparql[j];
|
|
1945
|
-
if (c === '>')
|
|
1946
|
-
return true;
|
|
1947
|
-
// Any IRIREF-forbidden character before `>` proves this `<`
|
|
1948
|
-
// is a comparison, not the start of an IRI.
|
|
1949
|
-
if (c === '<' ||
|
|
1950
|
-
c === '"' ||
|
|
1951
|
-
c === '{' ||
|
|
1952
|
-
c === '}' ||
|
|
1953
|
-
c === '|' ||
|
|
1954
|
-
c === '\\' ||
|
|
1955
|
-
c === '^' ||
|
|
1956
|
-
c === '`' ||
|
|
1957
|
-
/\s/.test(c)) {
|
|
1958
|
-
return false;
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
return false;
|
|
1962
|
-
};
|
|
1963
|
-
const n = sparql.length;
|
|
1964
|
-
const opens = [];
|
|
1965
|
-
let depth = 0;
|
|
1966
|
-
let i = 0;
|
|
1967
|
-
while (i < n) {
|
|
1968
|
-
const ch = sparql[i];
|
|
1969
|
-
if (ch === '#') {
|
|
1970
|
-
while (i < n && sparql[i] !== '\n')
|
|
1971
|
-
i++;
|
|
1972
|
-
continue;
|
|
1973
|
-
}
|
|
1974
|
-
if (ch === '<') {
|
|
1975
|
-
if (isIriStart(i)) {
|
|
1976
|
-
const end = sparql.indexOf('>', i + 1);
|
|
1977
|
-
if (end === -1)
|
|
1978
|
-
return -1;
|
|
1979
|
-
i = end + 1;
|
|
1980
|
-
continue;
|
|
1981
|
-
}
|
|
1982
|
-
// Comparison operator — advance one byte and keep scanning.
|
|
1983
|
-
i++;
|
|
1984
|
-
continue;
|
|
1985
|
-
}
|
|
1986
|
-
if (ch === '"' || ch === "'") {
|
|
1987
|
-
// dkg-query-engine.ts:848).
|
|
1988
|
-
// Centralised triple-quoted-aware skip — see skipSparqlStringLiteral.
|
|
1989
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
1990
|
-
continue;
|
|
1991
|
-
}
|
|
1992
|
-
if (ch === '{') {
|
|
1993
|
-
if (depth === 0)
|
|
1994
|
-
opens.push(i);
|
|
1995
|
-
depth++;
|
|
1996
|
-
}
|
|
1997
|
-
else if (ch === '}') {
|
|
1998
|
-
depth--;
|
|
1999
|
-
if (depth < 0)
|
|
2000
|
-
return -1;
|
|
2001
|
-
}
|
|
2002
|
-
i++;
|
|
2003
|
-
}
|
|
2004
|
-
if (depth !== 0 || opens.length === 0)
|
|
2005
|
-
return -1;
|
|
2006
|
-
return opens[opens.length - 1];
|
|
2007
|
-
}
|
|
2008
|
-
/**
|
|
2009
|
-
* Wraps a SELECT query to scope it to a named graph.
|
|
2010
|
-
* If the query already uses GRAPH patterns, returns it unchanged.
|
|
2011
|
-
*/
|
|
2012
|
-
function wrapWithGraph(sparql, graphUri) {
|
|
2013
|
-
if (hasGraphClause(sparql))
|
|
2014
|
-
return sparql;
|
|
2015
|
-
const braceStart = findWhereBraceStart(sparql);
|
|
2016
|
-
if (braceStart === -1)
|
|
2017
|
-
return sparql;
|
|
2018
|
-
// — dkg-query-engine.ts:939). Use the
|
|
2019
|
-
// literal/comment/IRI-aware helper so a `{` or `}` inside a SPARQL
|
|
2020
|
-
// string literal, line comment, or IRI does NOT confuse the depth
|
|
2021
|
-
// counter and we stop wrapping queries with literal-heavy bodies.
|
|
2022
|
-
const braceEnd = findMatchingCloseBrace(sparql, braceStart);
|
|
2023
|
-
if (braceEnd === -1)
|
|
2024
|
-
return sparql;
|
|
2025
|
-
const before = sparql.slice(0, braceStart + 1);
|
|
2026
|
-
const inner = sparql.slice(braceStart + 1, braceEnd);
|
|
2027
|
-
const after = sparql.slice(braceEnd);
|
|
2028
|
-
return `${before} GRAPH <${graphUri}> { ${inner} } ${after}`;
|
|
2029
|
-
}
|
|
2030
|
-
/**
|
|
2031
|
-
* Wrap a query so it runs over a union of named graphs in a single execution,
|
|
2032
|
-
* preserving LIMIT/ORDER BY/DISTINCT/aggregate semantics.
|
|
2033
|
-
*
|
|
2034
|
-
* the previous revision injected
|
|
2035
|
-
* `VALUES ?_viewGraph { <g1> <g2> … } GRAPH ?_viewGraph { inner }` directly
|
|
2036
|
-
* into the caller's WHERE block. Two failure modes:
|
|
2037
|
-
*
|
|
2038
|
-
* 1. Scope leak — `SELECT *` (or any projection that includes the graph
|
|
2039
|
-
* variable) over a multi-graph view emitted an extra `_viewGraph`
|
|
2040
|
-
* column, so downstream consumers saw a mystery binding they didn't
|
|
2041
|
-
* ask for.
|
|
2042
|
-
* 2. Name collision — a user query that legitimately binds
|
|
2043
|
-
* `?_viewGraph` (rare but valid) would silently intersect with the
|
|
2044
|
-
* helper's VALUES list and clamp to the helper's graph URIs.
|
|
2045
|
-
*
|
|
2046
|
-
* The fix is to use an explicit UNION over each graph instead of a
|
|
2047
|
-
* single GRAPH ?var binding. That keeps the inner block's variables
|
|
2048
|
-
* (and only those) in scope — no helper var is introduced at all, so
|
|
2049
|
-
* neither SELECT * leakage nor variable-name collisions can happen.
|
|
2050
|
-
* Single-graph views skip the UNION wrapper entirely and use a plain
|
|
2051
|
-
* `GRAPH <uri>` block.
|
|
2052
|
-
*
|
|
2053
|
-
* Returns `null` when the inner WHERE body contains a UNION — the
|
|
2054
|
-
* UNION-of-GRAPHs wrapper would produce a nested UnionNode that
|
|
2055
|
-
* crashes Blazegraph, and a VALUES+GRAPH fallback leaks a helper
|
|
2056
|
-
* variable into the caller's scope. The caller should fall back to
|
|
2057
|
-
* per-graph execution.
|
|
2058
|
-
*/
|
|
2059
|
-
function wrapWithGraphUnion(sparql, graphUris) {
|
|
2060
|
-
if (hasGraphClause(sparql))
|
|
2061
|
-
return sparql;
|
|
2062
|
-
if (graphUris.length === 0)
|
|
2063
|
-
return sparql;
|
|
2064
|
-
const braceStart = findWhereBraceStart(sparql);
|
|
2065
|
-
if (braceStart === -1)
|
|
2066
|
-
return sparql;
|
|
2067
|
-
// — dkg-query-engine.ts:939). See
|
|
2068
|
-
// `findMatchingCloseBrace` and the `wrapWithGraph` cousin above.
|
|
2069
|
-
const braceEnd = findMatchingCloseBrace(sparql, braceStart);
|
|
2070
|
-
if (braceEnd === -1)
|
|
2071
|
-
return sparql;
|
|
2072
|
-
const before = sparql.slice(0, braceStart + 1);
|
|
2073
|
-
const inner = sparql.slice(braceStart + 1, braceEnd);
|
|
2074
|
-
const after = sparql.slice(braceEnd);
|
|
2075
|
-
if (graphUris.length === 1) {
|
|
2076
|
-
return `${before} GRAPH <${graphUris[0]}> { ${inner} } ${after}`;
|
|
2077
|
-
}
|
|
2078
|
-
// Blazegraph crashes with "Illegal child type for union: UnionNode"
|
|
2079
|
-
// when a UNION appears inside a GRAPH block that is itself a branch
|
|
2080
|
-
// of an outer UNION. We cannot use VALUES+GRAPH either because the
|
|
2081
|
-
// helper variable leaks into caller scope (SELECT *, name collisions).
|
|
2082
|
-
// Signal the caller to fall back to per-graph execution.
|
|
2083
|
-
const innerHasUnion = /\bUNION\b/i.test(inner);
|
|
2084
|
-
if (innerHasUnion) {
|
|
2085
|
-
return null;
|
|
2086
|
-
}
|
|
2087
|
-
const unionBranches = graphUris
|
|
2088
|
-
.map((g) => `{ GRAPH <${g}> { ${inner} } }`)
|
|
2089
|
-
.join(' UNION ');
|
|
2090
|
-
return `${before} ${unionBranches} ${after}`;
|
|
2091
|
-
}
|
|
2092
|
-
/** The graph variable `wrapWithGraphValues` injects. Double-underscore + a
|
|
2093
|
-
* view-specific name so a user query is astronomically unlikely to bind it;
|
|
2094
|
-
* a collision is nonetheless detected and declined (never silently clamped). */
|
|
2095
|
-
const VIEW_GRAPH_SENTINEL = '?__dkgViewGraph';
|
|
2096
|
-
const DEDUP_GRAPH_SENTINEL = '?__dkgDedupGraph';
|
|
2097
|
-
const DEDUP_RANK_SENTINEL = '?__dkgDedupRank';
|
|
2098
|
-
const DEDUP_PRIOR_GRAPH_SENTINEL = '?__dkgDedupPriorGraph';
|
|
2099
|
-
const DEDUP_PRIOR_RANK_SENTINEL = '?__dkgDedupPriorRank';
|
|
2100
|
-
function wrapWithProjectedGraphSubselect(sparql, graphUris, helperVariables, buildGraphPattern, acceptsInner = () => true) {
|
|
2101
|
-
if (hasGraphClause(sparql))
|
|
2102
|
-
return sparql;
|
|
2103
|
-
if (graphUris.length === 0)
|
|
2104
|
-
return sparql;
|
|
2105
|
-
const braceStart = findWhereBraceStart(sparql);
|
|
2106
|
-
if (braceStart === -1)
|
|
2107
|
-
return null;
|
|
2108
|
-
const braceEnd = findMatchingCloseBrace(sparql, braceStart);
|
|
2109
|
-
if (braceEnd === -1)
|
|
2110
|
-
return null;
|
|
2111
|
-
const before = sparql.slice(0, braceStart + 1);
|
|
2112
|
-
const inner = sparql.slice(braceStart + 1, braceEnd);
|
|
2113
|
-
const after = sparql.slice(braceEnd);
|
|
2114
|
-
const graphs = [...new Set(graphUris)];
|
|
2115
|
-
if (graphs.length === 1) {
|
|
2116
|
-
return `${before} GRAPH <${assertSafeIri(graphs[0])}> { ${inner} } ${after}`;
|
|
2117
|
-
}
|
|
2118
|
-
if (/\bUNION\b/i.test(inner) || !acceptsInner(inner))
|
|
2119
|
-
return null;
|
|
2120
|
-
const helperNames = new Set(helperVariables.map((variable) => variable.slice(1)));
|
|
2121
|
-
if (collectQueryVariables(sparql).some((variable) => helperNames.has(variable.slice(1)))) {
|
|
2122
|
-
return null;
|
|
2123
|
-
}
|
|
2124
|
-
const innerVars = collectQueryVariables(inner);
|
|
2125
|
-
if (innerVars.length === 0)
|
|
2126
|
-
return null;
|
|
2127
|
-
const graphPattern = buildGraphPattern(inner, graphs);
|
|
2128
|
-
return `${before} { SELECT ${innerVars.join(' ')} WHERE { ${graphPattern} } } ${after}`;
|
|
2129
|
-
}
|
|
2130
|
-
/**
|
|
2131
|
-
* Run one graph pattern across an ordered graph set while suppressing only a
|
|
2132
|
-
* solution mapping already produced by an earlier graph. The comparison uses
|
|
2133
|
-
* every variable bound by the caller's inner pattern, before the caller's
|
|
2134
|
-
* projection runs. Thus an identical mirrored triple is emitted once, while
|
|
2135
|
-
* distinct triples that both project to the same `?s` still produce two rows.
|
|
2136
|
-
*
|
|
2137
|
-
* Helper graph/rank variables are hidden inside a sub-SELECT, preserving
|
|
2138
|
-
* `SELECT *` and caller DISTINCT semantics. Unsupported/colliding query shapes
|
|
2139
|
-
* return null so the existing generic multi-graph fallback remains available.
|
|
2140
|
-
*/
|
|
2141
|
-
function wrapWithDeduplicatedGraphValues(sparql, graphUris) {
|
|
2142
|
-
return wrapWithProjectedGraphSubselect(sparql, graphUris, [
|
|
2143
|
-
DEDUP_GRAPH_SENTINEL,
|
|
2144
|
-
DEDUP_RANK_SENTINEL,
|
|
2145
|
-
DEDUP_PRIOR_GRAPH_SENTINEL,
|
|
2146
|
-
DEDUP_PRIOR_RANK_SENTINEL,
|
|
2147
|
-
], (inner, graphs) => {
|
|
2148
|
-
const rows = graphs
|
|
2149
|
-
.map((graph, rank) => `(<${assertSafeIri(graph)}> ${rank})`)
|
|
2150
|
-
.join(' ');
|
|
2151
|
-
return [
|
|
2152
|
-
`VALUES (${DEDUP_GRAPH_SENTINEL} ${DEDUP_RANK_SENTINEL}) { ${rows} }`,
|
|
2153
|
-
`GRAPH ${DEDUP_GRAPH_SENTINEL} { ${inner} }`,
|
|
2154
|
-
'FILTER NOT EXISTS {',
|
|
2155
|
-
` VALUES (${DEDUP_PRIOR_GRAPH_SENTINEL} ${DEDUP_PRIOR_RANK_SENTINEL}) { ${rows} }`,
|
|
2156
|
-
` FILTER (${DEDUP_PRIOR_RANK_SENTINEL} < ${DEDUP_RANK_SENTINEL})`,
|
|
2157
|
-
` GRAPH ${DEDUP_PRIOR_GRAPH_SENTINEL} { ${inner} }`,
|
|
2158
|
-
'}',
|
|
2159
|
-
].join(' ');
|
|
2160
|
-
}, isDedupSafeBasicGraphPattern);
|
|
2161
|
-
}
|
|
2162
|
-
/**
|
|
2163
|
-
* Wrap a query so it runs over a set of named graphs in ONE execution using a
|
|
2164
|
-
* single `VALUES ?g { <g1> … } GRAPH ?g { inner }` block, instead of one
|
|
2165
|
-
* `{ GRAPH <g> { inner } } UNION …` branch per graph.
|
|
2166
|
-
*
|
|
2167
|
-
* Why this exists (issue #1596): the per-graph UNION form (`wrapWithGraphUnion`)
|
|
2168
|
-
* emits an N-branch UnionNode. At the ~1,424 SWM graphs of a large public
|
|
2169
|
-
* context graph that is a ~257 KB query whose union tree makes the
|
|
2170
|
-
* oxigraph-server query planner blow up and reset the connection ("fetch
|
|
2171
|
-
* failed"). A single VALUES table is one basic graph pattern iterated over the
|
|
2172
|
-
* graph list — constant plan depth — so it scales.
|
|
2173
|
-
*
|
|
2174
|
-
* Semantics are preserved. LIMIT/ORDER BY/DISTINCT/GROUP BY/aggregate all live
|
|
2175
|
-
* in the outer query (the `before`/`after` slices) and reduce over the merged
|
|
2176
|
-
* solution set exactly as the UNION form did. The one hazard is the injected
|
|
2177
|
-
* graph variable leaking into the caller's projection: a bare `SELECT *` would
|
|
2178
|
-
* expose `?__dkgViewGraph` as a mystery column, and cross-graph DISTINCT of an
|
|
2179
|
-
* identical triple would split on the graph. Both are neutralised by scoping
|
|
2180
|
-
* the VALUES+GRAPH block inside a sub-SELECT that projects ONLY the user's own
|
|
2181
|
-
* variables (collected from `inner`), so the sentinel never escapes.
|
|
2182
|
-
*
|
|
2183
|
-
* Returns `null` (caller falls back to `wrapWithGraphUnion` / per-graph
|
|
2184
|
-
* execution) when the WHERE block cannot be located, when `inner` carries a
|
|
2185
|
-
* top-level UNION (kept on the #789 form-aware fallback so its behaviour and
|
|
2186
|
-
* tests are untouched), when the user query already binds the sentinel, or
|
|
2187
|
-
* when the WHERE body binds no user variable at all (a var-less body offers
|
|
2188
|
-
* nowhere to hide the sentinel from a `SELECT *` projection).
|
|
2189
|
-
*/
|
|
2190
|
-
function wrapWithGraphValues(sparql, graphUris) {
|
|
2191
|
-
return wrapWithProjectedGraphSubselect(sparql, graphUris, [VIEW_GRAPH_SENTINEL], (inner, graphs) => {
|
|
2192
|
-
const values = graphs.map((graph) => `<${assertSafeIri(graph)}>`).join(' ');
|
|
2193
|
-
return `VALUES ${VIEW_GRAPH_SENTINEL} { ${values} } GRAPH ${VIEW_GRAPH_SENTINEL} { ${inner} }`;
|
|
2194
|
-
});
|
|
2195
|
-
}
|
|
2196
|
-
/**
|
|
2197
|
-
* The mirror anti-join is valid only for a flat basic graph pattern (plus
|
|
2198
|
-
* FILTER expressions). Nested graph patterns can differ by boundness, where a
|
|
2199
|
-
* correlated NOT EXISTS compatibility check is not exact mapping equality.
|
|
2200
|
-
*/
|
|
2201
|
-
function isDedupSafeBasicGraphPattern(inner) {
|
|
2202
|
-
const forbidden = ['OPTIONAL', 'MINUS', 'SERVICE', 'VALUES', 'BIND', 'SELECT', 'GRAPH', 'EXISTS'];
|
|
2203
|
-
let i = 0;
|
|
2204
|
-
while (i < inner.length) {
|
|
2205
|
-
const ch = inner[i];
|
|
2206
|
-
if (ch === '#') {
|
|
2207
|
-
while (i < inner.length && inner[i] !== '\n')
|
|
2208
|
-
i++;
|
|
2209
|
-
continue;
|
|
2210
|
-
}
|
|
2211
|
-
if (ch === '"' || ch === "'") {
|
|
2212
|
-
i = skipSparqlStringLiteral(inner, i);
|
|
2213
|
-
continue;
|
|
2214
|
-
}
|
|
2215
|
-
if (ch === '<') {
|
|
2216
|
-
const end = skipSparqlIriRef(inner, i);
|
|
2217
|
-
i = end ?? i + 1;
|
|
2218
|
-
continue;
|
|
2219
|
-
}
|
|
2220
|
-
if (ch === '{' || ch === '}')
|
|
2221
|
-
return false;
|
|
2222
|
-
if (isKeywordStart(inner, i)) {
|
|
2223
|
-
let end = i + 1;
|
|
2224
|
-
while (end < inner.length && isWordContinuation(inner[end]))
|
|
2225
|
-
end++;
|
|
2226
|
-
if (forbidden.some((keyword) => isSparqlKeyword(inner, i, end, keyword))) {
|
|
2227
|
-
return false;
|
|
2228
|
-
}
|
|
2229
|
-
i = end;
|
|
2230
|
-
continue;
|
|
2231
|
-
}
|
|
2232
|
-
i++;
|
|
2233
|
-
}
|
|
2234
|
-
return true;
|
|
2235
|
-
}
|
|
2236
|
-
/**
|
|
2237
|
-
* Collect every distinct SPARQL variable (`?x` / `$x`, in first-seen order,
|
|
2238
|
-
* sigil included) in a query fragment, skipping tokens inside line comments,
|
|
2239
|
-
* string literals, and IRI refs. Reuses the same literal/comment/IRI-aware
|
|
2240
|
-
* primitives as `collectGraphVariables` so a `?`/`$` inside a literal or IRI is
|
|
2241
|
-
* never mistaken for a variable.
|
|
2242
|
-
*/
|
|
2243
|
-
function collectQueryVariables(sparql) {
|
|
2244
|
-
const variables = [];
|
|
2245
|
-
const seen = new Set();
|
|
2246
|
-
const n = sparql.length;
|
|
2247
|
-
let i = 0;
|
|
2248
|
-
while (i < n) {
|
|
2249
|
-
const ch = sparql[i];
|
|
2250
|
-
if (ch === '#') {
|
|
2251
|
-
while (i < n && sparql[i] !== '\n')
|
|
2252
|
-
i++;
|
|
2253
|
-
continue;
|
|
2254
|
-
}
|
|
2255
|
-
if (ch === '"' || ch === "'") {
|
|
2256
|
-
i = skipSparqlStringLiteral(sparql, i);
|
|
2257
|
-
continue;
|
|
2258
|
-
}
|
|
2259
|
-
if (ch === '<') {
|
|
2260
|
-
const end = skipSparqlIriRef(sparql, i);
|
|
2261
|
-
i = end ?? i + 1;
|
|
2262
|
-
continue;
|
|
2263
|
-
}
|
|
2264
|
-
if (ch === '?' || ch === '$') {
|
|
2265
|
-
const variable = readSparqlVariable(sparql, i);
|
|
2266
|
-
if (variable) {
|
|
2267
|
-
if (!seen.has(variable)) {
|
|
2268
|
-
seen.add(variable);
|
|
2269
|
-
variables.push(variable);
|
|
2270
|
-
}
|
|
2271
|
-
i += variable.length;
|
|
2272
|
-
continue;
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
i++;
|
|
2276
|
-
}
|
|
2277
|
-
return variables;
|
|
2278
|
-
}
|
|
2279
|
-
/**
|
|
2280
|
-
* Rewrites a SPARQL query so EVERY subject variable used in its WHERE
|
|
2281
|
-
* block also matches `<http://dkg.io/ontology/trustLevel> ?__trustN`
|
|
2282
|
-
* with an integer value ≥ `minTrust`. Subjects with no trust metadata
|
|
2283
|
-
* are filtered out (the required triple is absent).
|
|
2284
|
-
*
|
|
2285
|
-
* The rewriter scans the WHERE block for top-level triple patterns
|
|
2286
|
-
* and collects every distinct subject variable so multi-subject
|
|
2287
|
-
* queries like `?a <p> ?o . ?b <q> ?r` have BOTH `?a` and `?b`
|
|
2288
|
-
* trust-filtered.
|
|
2289
|
-
*
|
|
2290
|
-
* Returns `null` when:
|
|
2291
|
-
* - no `WHERE { ... }` block can be located;
|
|
2292
|
-
* - braces are unbalanced;
|
|
2293
|
-
* - the WHERE contains nested structure (`{`, `GRAPH`, `OPTIONAL`,
|
|
2294
|
-
* `UNION`, `MINUS`, `SERVICE`, subselect) we cannot safely rewrite;
|
|
2295
|
-
* - the block contains a constant (IRI/literal/blank) subject — we
|
|
2296
|
-
* cannot attach a filter to a constant, and silently ignoring the
|
|
2297
|
-
* constant row would leak sub-threshold data (L1 fail-closed);
|
|
2298
|
-
* - no subject var is found at all.
|
|
2299
|
-
* Callers treat `null` as "refuse to run".
|
|
2300
|
-
*/
|
|
2301
|
-
/**
|
|
2302
|
-
* Strip SPARQL line comments (`# … EOL`) from a fragment of SPARQL
|
|
2303
|
-
* WHERE body while preserving `#` that appears inside an IRI
|
|
2304
|
-
* (`<http://…/rdf-ns#type>`) or inside a string literal (`"…#…"`,
|
|
2305
|
-
* `'…#…'`). Used by `injectMinTrustFilter` where a full parser would
|
|
2306
|
-
* be overkill but a naive line-comment regex mangles `rdf:type` etc.
|
|
2307
|
-
*
|
|
2308
|
-
* This is intentionally small: we handle the three grammar contexts
|
|
2309
|
-
* that can legally contain a bare `#` in SPARQL 1.1 (IRI, quoted
|
|
2310
|
-
* literal, line comment) and treat everything else as ordinary code.
|
|
2311
|
-
* Triple-quoted `"""…"""` / `'''…'''` are NOT recognised because
|
|
2312
|
-
* `injectMinTrustFilter` already bails on any WHERE containing tokens
|
|
2313
|
-
* from the multi-line literal grammar (FILTER EXISTS, SELECT, …).
|
|
2314
|
-
*/
|
|
2315
|
-
function stripSparqlLineComments(src) {
|
|
2316
|
-
let out = '';
|
|
2317
|
-
let i = 0;
|
|
2318
|
-
const n = src.length;
|
|
2319
|
-
while (i < n) {
|
|
2320
|
-
const ch = src[i];
|
|
2321
|
-
if (ch === '<') {
|
|
2322
|
-
const end = src.indexOf('>', i + 1);
|
|
2323
|
-
if (end === -1) {
|
|
2324
|
-
out += src.slice(i);
|
|
2325
|
-
break;
|
|
2326
|
-
}
|
|
2327
|
-
out += src.slice(i, end + 1);
|
|
2328
|
-
i = end + 1;
|
|
2329
|
-
continue;
|
|
2330
|
-
}
|
|
2331
|
-
if (ch === '"' || ch === "'") {
|
|
2332
|
-
// Centralised triple-quoted-aware skip.
|
|
2333
|
-
const j = skipSparqlStringLiteral(src, i);
|
|
2334
|
-
out += src.slice(i, j);
|
|
2335
|
-
i = j;
|
|
2336
|
-
continue;
|
|
2337
|
-
}
|
|
2338
|
-
if (ch === '#') {
|
|
2339
|
-
const nl = src.indexOf('\n', i);
|
|
2340
|
-
if (nl === -1) {
|
|
2341
|
-
break;
|
|
2342
|
-
}
|
|
2343
|
-
i = nl; // leave the newline so dot-accounting still sees line breaks
|
|
2344
|
-
continue;
|
|
2345
|
-
}
|
|
2346
|
-
out += ch;
|
|
2347
|
-
i++;
|
|
2348
|
-
}
|
|
2349
|
-
return out;
|
|
2350
|
-
}
|
|
2351
|
-
/**
|
|
2352
|
-
* Replace every SPARQL string literal and `# …` comment in `src`
|
|
2353
|
-
* with neutral whitespace, preserving overall byte length. IRIs and
|
|
2354
|
-
* code tokens are passed through verbatim. The returned string is
|
|
2355
|
-
* suitable for STRUCTURAL CHECKS (brace balancing, keyword scans)
|
|
2356
|
-
* that must not be confused by user payloads such as
|
|
2357
|
-
* `"{json: 1}"` or `# OPTIONAL: ...`.
|
|
2358
|
-
*
|
|
2359
|
-
* Triple-quoted
|
|
2360
|
-
* (`"""…"""` / `'''…'''`) literals are NOT recognised because
|
|
2361
|
-
* `injectMinTrustFilter`'s outer pipeline already refuses any WHERE
|
|
2362
|
-
* carrying tokens from the multi-line literal grammar (FILTER EXISTS,
|
|
2363
|
-
* SELECT inside, etc.).
|
|
2364
|
-
*/
|
|
2365
|
-
function scrubStringsAndComments(src) {
|
|
2366
|
-
const n = src.length;
|
|
2367
|
-
const buf = new Array(n);
|
|
2368
|
-
let i = 0;
|
|
2369
|
-
while (i < n) {
|
|
2370
|
-
const ch = src[i];
|
|
2371
|
-
if (ch === '<') {
|
|
2372
|
-
const end = src.indexOf('>', i + 1);
|
|
2373
|
-
if (end === -1) {
|
|
2374
|
-
for (let k = i; k < n; k++)
|
|
2375
|
-
buf[k] = src[k];
|
|
2376
|
-
return buf.join('');
|
|
2377
|
-
}
|
|
2378
|
-
for (let k = i; k <= end; k++)
|
|
2379
|
-
buf[k] = src[k];
|
|
2380
|
-
i = end + 1;
|
|
2381
|
-
continue;
|
|
2382
|
-
}
|
|
2383
|
-
if (ch === '"' || ch === "'") {
|
|
2384
|
-
// Centralised triple-quoted-aware skip.
|
|
2385
|
-
const j = skipSparqlStringLiteral(src, i);
|
|
2386
|
-
for (let k = i; k < j; k++)
|
|
2387
|
-
buf[k] = src[k] === '\n' ? '\n' : ' ';
|
|
2388
|
-
i = j;
|
|
2389
|
-
continue;
|
|
2390
|
-
}
|
|
2391
|
-
if (ch === '#') {
|
|
2392
|
-
const nl = src.indexOf('\n', i);
|
|
2393
|
-
const end = nl === -1 ? n : nl;
|
|
2394
|
-
for (let k = i; k < end; k++)
|
|
2395
|
-
buf[k] = ' ';
|
|
2396
|
-
i = end;
|
|
2397
|
-
continue;
|
|
2398
|
-
}
|
|
2399
|
-
buf[i] = ch;
|
|
2400
|
-
i++;
|
|
2401
|
-
}
|
|
2402
|
-
return buf.join('');
|
|
2403
|
-
}
|
|
2404
|
-
/**
|
|
2405
|
-
* Split a SPARQL WHERE body on **top-level** triple terminators, i.e.
|
|
2406
|
-
* dots that live outside quoted literals and outside IRI angle
|
|
2407
|
-
* brackets. The earlier `/\.(?=\s|$)/` regex broke on literal dots
|
|
2408
|
-
* in messages like `?s <p> "hello. world"`, silently fragmenting
|
|
2409
|
-
* the statement so the subject scanner returned garbage and
|
|
2410
|
-
* `_minTrust` fail-closed to `[]` for every text/chat query. This
|
|
2411
|
-
* tokenizer walks the body character
|
|
2412
|
-
* by character, tracks `<…>` and `"…"` / `'…'` scopes (with `\`-escape
|
|
2413
|
-
* handling), and only treats `.` as a separator when it sits at depth
|
|
2414
|
-
* zero and is followed by whitespace or end-of-input. Comments have
|
|
2415
|
-
* already been stripped by {@link stripSparqlLineComments} before we
|
|
2416
|
-
* get here, so `#` is treated as an ordinary character.
|
|
2417
|
-
*
|
|
2418
|
-
* Parentheses and braces would also open top-level scopes in general
|
|
2419
|
-
* SPARQL, but `injectMinTrustFilter` refuses to rewrite any WHERE that
|
|
2420
|
-
* contains `{`, `}`, `FILTER EXISTS`, subselects, or property paths
|
|
2421
|
-
* with grouping (the `/\{|\}/.test(inner)` + token guard above), so
|
|
2422
|
-
* this helper only has to handle the three grammar contexts that can
|
|
2423
|
-
* legally carry a bare `.` in the shapes we rewrite: IRI, string
|
|
2424
|
-
* literal, and top-level statement terminator.
|
|
2425
|
-
*/
|
|
2426
|
-
function splitTopLevelTripleStatements(body) {
|
|
2427
|
-
const out = [];
|
|
2428
|
-
let start = 0;
|
|
2429
|
-
let i = 0;
|
|
2430
|
-
const n = body.length;
|
|
2431
|
-
while (i < n) {
|
|
2432
|
-
const ch = body[i];
|
|
2433
|
-
if (ch === '<') {
|
|
2434
|
-
const end = body.indexOf('>', i + 1);
|
|
2435
|
-
if (end === -1) {
|
|
2436
|
-
i = n;
|
|
2437
|
-
break;
|
|
2438
|
-
}
|
|
2439
|
-
i = end + 1;
|
|
2440
|
-
continue;
|
|
2441
|
-
}
|
|
2442
|
-
if (ch === '"' || ch === "'") {
|
|
2443
|
-
// Centralised triple-quoted-aware skip.
|
|
2444
|
-
i = skipSparqlStringLiteral(body, i);
|
|
2445
|
-
continue;
|
|
2446
|
-
}
|
|
2447
|
-
if (ch === '.') {
|
|
2448
|
-
// Terminator only when followed by whitespace OR end-of-input.
|
|
2449
|
-
// This keeps decimals and prefixed-name dots (rdf:type.foo —
|
|
2450
|
-
// rejected upstream anyway) from accidentally splitting, and
|
|
2451
|
-
// matches the original regex semantics on the top-level cases.
|
|
2452
|
-
const next = i + 1 < n ? body[i + 1] : '';
|
|
2453
|
-
if (next === '' || /\s/.test(next)) {
|
|
2454
|
-
const piece = body.slice(start, i).trim();
|
|
2455
|
-
if (piece)
|
|
2456
|
-
out.push(piece);
|
|
2457
|
-
start = i + 1;
|
|
2458
|
-
i += 1;
|
|
2459
|
-
continue;
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
i++;
|
|
2463
|
-
}
|
|
2464
|
-
const tail = body.slice(start).trim();
|
|
2465
|
-
if (tail)
|
|
2466
|
-
out.push(tail);
|
|
2467
|
-
return out;
|
|
2468
|
-
}
|
|
2469
|
-
function injectMinTrustFilter(sparql, minTrust) {
|
|
2470
|
-
// The
|
|
2471
|
-
// pre-fix rewriter only recognised `WHERE\s*\{`, so SPARQL 1.1
|
|
2472
|
-
// shorthand forms (`SELECT ?x { … }`, `ASK { … }`,
|
|
2473
|
-
// `DESCRIBE ?x { … }`, `CONSTRUCT { tmpl } { where }`) returned
|
|
2474
|
-
// `null` and the `minTrust > Endorsed` caller silently fell
|
|
2475
|
-
// through to an empty result. `findWhereBraceStart` normalises
|
|
2476
|
-
// every shape to the WHERE-clause brace position before we apply
|
|
2477
|
-
// the existing depth-counting pass below.
|
|
2478
|
-
const braceStart = findWhereBraceStart(sparql);
|
|
2479
|
-
if (braceStart === -1)
|
|
2480
|
-
return null;
|
|
2481
|
-
// — dkg-query-engine.ts:939). The
|
|
2482
|
-
// earlier brace-balance loop counted `{`/`}` inside SPARQL string
|
|
2483
|
-
// literals (e.g. `FILTER(STR(?t) = "{")`), so a literal-heavy WHERE
|
|
2484
|
-
// ended at depth 1 and `injectMinTrustFilter` returned `null` —
|
|
2485
|
-
// which the `_minTrust > Endorsed` caller treats as "refuse to run"
|
|
2486
|
-
// and silently fails closed. Use the literal/comment/IRI-aware
|
|
2487
|
-
// helper so the brace boundaries match what SPARQL actually
|
|
2488
|
-
// parses.
|
|
2489
|
-
const braceEnd = findMatchingCloseBrace(sparql, braceStart);
|
|
2490
|
-
if (braceEnd === -1)
|
|
2491
|
-
return null;
|
|
2492
|
-
const inner = sparql.slice(braceStart + 1, braceEnd);
|
|
2493
|
-
// A
|
|
2494
|
-
// leading top-level `VALUES` clause is the canonical SPARQL shape
|
|
2495
|
-
// for batched exact-subject lookups:
|
|
2496
|
-
//
|
|
2497
|
-
// SELECT ?o WHERE {
|
|
2498
|
-
// VALUES ?s { <a> <b> <c> }
|
|
2499
|
-
// ?s <p> ?o .
|
|
2500
|
-
// }
|
|
2501
|
-
//
|
|
2502
|
-
// the forbidden-tokens regex treated any VALUES as
|
|
2503
|
-
// "unsupported" and `_minTrust` fell through to
|
|
2504
|
-
// `emptyResultForForm(...)`, which turns into a silent `[]` / `false`
|
|
2505
|
-
// even when the bound subjects satisfy the threshold. The contract
|
|
2506
|
-
// we need is:
|
|
2507
|
-
// (a) bail loudly on complex VALUES we can't reason about
|
|
2508
|
-
// (multi-var tuples, multi-line, no closing `}`);
|
|
2509
|
-
// (b) for the common single-var VALUES case, peel it off, run
|
|
2510
|
-
// the existing subject analysis on the body, and re-emit
|
|
2511
|
-
// the VALUES binding at the top of the rewritten WHERE so
|
|
2512
|
-
// the trust filter still applies to each bound IRI.
|
|
2513
|
-
//
|
|
2514
|
-
// Any other location (non-leading, multi-var, parenthesised row
|
|
2515
|
-
// syntax `VALUES (?x ?y) { (<a> "b") }`) still bails because the
|
|
2516
|
-
// flat scanner cannot safely rewrite them.
|
|
2517
|
-
const { valuesClause, bodyAfterValues } = peelLeadingValues(inner);
|
|
2518
|
-
const scanTarget = bodyAfterValues ?? inner;
|
|
2519
|
-
// — dkg-query-engine.ts:851).
|
|
2520
|
-
// Pre-fix the unsupported-nesting guard `/\{|\}/.test(scanTarget)`
|
|
2521
|
-
// and the keyword guard below ran on the RAW WHERE body. Any
|
|
2522
|
-
// `{`, `}`, or sensitive keyword that happened to appear inside a
|
|
2523
|
-
// SPARQL string literal (`"{json: 1}"`, `"OPTIONAL field"`,
|
|
2524
|
-
// `"SELECT * FROM x"`) or inside a `# …` line comment caused the
|
|
2525
|
-
// rewriter to bail out and the caller fell through to
|
|
2526
|
-
// `emptyResultForSparql(...)`. That silently fail-closed every
|
|
2527
|
-
// legitimate high-trust query whose payload happened to mention
|
|
2528
|
-
// those tokens — text/JSON/log content is the most common case.
|
|
2529
|
-
//
|
|
2530
|
-
// Scrub literals and comments to neutral spaces BEFORE the
|
|
2531
|
-
// structural / keyword checks so they only see real code tokens.
|
|
2532
|
-
// IRIs are preserved verbatim because IRIREF grammar already
|
|
2533
|
-
// forbids `{`, `}`, `"`, and the keyword tokens we care about.
|
|
2534
|
-
const codeView = scrubStringsAndComments(scanTarget);
|
|
2535
|
-
if (/[{}]/.test(codeView))
|
|
2536
|
-
return null;
|
|
2537
|
-
if (/\b(GRAPH|OPTIONAL|UNION|MINUS|SERVICE|VALUES|FILTER\s+EXISTS|FILTER\s+NOT\s+EXISTS|SELECT)\b/i.test(codeView)) {
|
|
2538
|
-
return null;
|
|
2539
|
-
}
|
|
2540
|
-
// Strip SPARQL line comments (`# … \n`) so the dot accounting below
|
|
2541
|
-
// doesn't misclassify "# foo ." as a terminating triple — BUT leave
|
|
2542
|
-
// `#` fragments inside IRIs (`<…#…>`) and literals (`"…#…"`) alone.
|
|
2543
|
-
// The naive `/#[^\n]*/g` regex used here previously mangled the
|
|
2544
|
-
// extremely common `rdf:type` shape
|
|
2545
|
-
// `<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>` whenever
|
|
2546
|
-
// `_minTrust` was set, which fail-closes the entire query to `[]`
|
|
2547
|
-
// .
|
|
2548
|
-
const innerCodeOnly = stripSparqlLineComments(scanTarget);
|
|
2549
|
-
const trimmedInner = innerCodeOnly.trim();
|
|
2550
|
-
if (trimmedInner.length === 0)
|
|
2551
|
-
return null;
|
|
2552
|
-
// Split on the top-level `.` separator to walk each triple pattern.
|
|
2553
|
-
// use a
|
|
2554
|
-
// quote/IRI-aware tokenizer instead of a naive regex so `?s <p>
|
|
2555
|
-
// "hello. world"` isn't fragmented into broken statements that the
|
|
2556
|
-
// subject scanner then refuses, fail-closing `_minTrust` to `[]`
|
|
2557
|
-
// for every text/chat query. Rejoined dots are preserved for the
|
|
2558
|
-
// emitted query by the clause builder below.
|
|
2559
|
-
const statements = splitTopLevelTripleStatements(trimmedInner);
|
|
2560
|
-
const subjectVars = new Set();
|
|
2561
|
-
const subjectIris = new Set();
|
|
2562
|
-
const subjectPrefixed = new Set();
|
|
2563
|
-
for (const stmt of statements) {
|
|
2564
|
-
// top-level `FILTER(...)` / `BIND(... AS ?x)` clauses share the
|
|
2565
|
-
// statement-list with triple patterns and have no subject token.
|
|
2566
|
-
// Pre-fix the subject regex below didn't match either keyword,
|
|
2567
|
-
// returned `null`, and `injectMinTrustFilter()` propagated `null`
|
|
2568
|
-
// — collapsing every query like
|
|
2569
|
-
// SELECT ?s WHERE { ?s <p> ?o . FILTER(?o > 10) }
|
|
2570
|
-
// into an empty result whenever `minTrust > SelfAttested`.
|
|
2571
|
-
//
|
|
2572
|
-
// Skip these clauses in the subject scan: they don't introduce
|
|
2573
|
-
// new subjects and they survive verbatim because the rewritten
|
|
2574
|
-
// WHERE is built by appending trust-filter triples to the
|
|
2575
|
-
// *original* trimmed inner (see `rewrittenBody` below) — the
|
|
2576
|
-
// FILTER/BIND text stays exactly where the caller put it.
|
|
2577
|
-
//
|
|
2578
|
-
// Anti-recursion: only skip TOP-LEVEL FILTER/BIND. Nested ones
|
|
2579
|
-
// (e.g. `FILTER EXISTS { ... }`) are already rejected by the
|
|
2580
|
-
// `\{|\}` and `FILTER\s+EXISTS` checks at line 753 / 754, so
|
|
2581
|
-
// by the time we reach this loop we're guaranteed to be looking
|
|
2582
|
-
// at a flat FILTER(<expr>) or BIND(<expr> AS ?x).
|
|
2583
|
-
const stmtTrimmed = stmt.trim();
|
|
2584
|
-
if (/^FILTER\s*\(/i.test(stmtTrimmed) || /^BIND\s*\(/i.test(stmtTrimmed)) {
|
|
2585
|
-
continue;
|
|
2586
|
-
}
|
|
2587
|
-
// First non-whitespace token is the subject. Accept:
|
|
2588
|
-
// - variable (`?x`, `$x`)
|
|
2589
|
-
// - absolute IRI (`<urn:x>`)
|
|
2590
|
-
// - blank node (`_:b`)
|
|
2591
|
-
// - RDF literal (`"…"` with optional type/lang tag)
|
|
2592
|
-
// - prefixed name (`ex:item`) — SPARQL `PNAME_LN` / `PNAME_NS`.
|
|
2593
|
-
// Earlier revisions fail-closed `_minTrust` to `[]` for
|
|
2594
|
-
// every query that used standard `PREFIX ex: <urn:> …`
|
|
2595
|
-
// syntax, which is the recommended SPARQL shape for exact
|
|
2596
|
-
// entity lookups.
|
|
2597
|
-
const m = stmt.match(/^\s*([?$]([A-Za-z_]\w*)|<[^>]+>|_:[A-Za-z_]\w*|"[^"]*"(?:\^\^<[^>]+>|@[A-Za-z-]+)?|[A-Za-z][\w-]*:[A-Za-z_][\w-]*|[A-Za-z][\w-]*:)/);
|
|
2598
|
-
if (!m)
|
|
2599
|
-
return null;
|
|
2600
|
-
const subj = m[1];
|
|
2601
|
-
if (subj.startsWith('?') || subj.startsWith('$')) {
|
|
2602
|
-
subjectVars.add(subj);
|
|
2603
|
-
continue;
|
|
2604
|
-
}
|
|
2605
|
-
// exact-entity lookups like `SELECT ?o WHERE { <e> <p> ?o }` are
|
|
2606
|
-
// the most common SPARQL shape in DKG and must NOT fail closed on
|
|
2607
|
-
// `_minTrust`. The threshold is perfectly enforceable against a
|
|
2608
|
-
// concrete IRI: attach `<iri> <trustLevel> ?t . FILTER(?t >= N)`
|
|
2609
|
-
// to the rewritten WHERE. Blank-node and literal subjects remain
|
|
2610
|
-
// refused — neither can carry trust metadata in our ontology.
|
|
2611
|
-
if (subj.startsWith('<') && subj.endsWith('>')) {
|
|
2612
|
-
subjectIris.add(subj);
|
|
2613
|
-
continue;
|
|
2614
|
-
}
|
|
2615
|
-
// Prefixed name — treat like an IRI at the clause-emission stage.
|
|
2616
|
-
// The original query still carries the `PREFIX` declarations, so
|
|
2617
|
-
// emitting `ex:item <trustLevel> ?t . FILTER(...)` is valid SPARQL
|
|
2618
|
-
// at the same scope. Rejects `_:bn` (starts with `_:`) and
|
|
2619
|
-
// string literals (start with `"`) naturally because this branch
|
|
2620
|
-
// only runs when subj starts with a letter.
|
|
2621
|
-
if (/^[A-Za-z]/.test(subj) && subj.includes(':')) {
|
|
2622
|
-
subjectPrefixed.add(subj);
|
|
2623
|
-
continue;
|
|
2624
|
-
}
|
|
2625
|
-
// Blank-node / literal subject — cannot attach a trust filter.
|
|
2626
|
-
return null;
|
|
2627
|
-
}
|
|
2628
|
-
if (subjectVars.size === 0 && subjectIris.size === 0 && subjectPrefixed.size === 0)
|
|
2629
|
-
return null;
|
|
2630
|
-
const extraClauses = [];
|
|
2631
|
-
let i = 0;
|
|
2632
|
-
for (const subjectVar of subjectVars) {
|
|
2633
|
-
const trustVar = `?__dkgTrust${i++}`;
|
|
2634
|
-
extraClauses.push(`${subjectVar} <${TRUST_LEVEL_PREDICATE}> ${trustVar} . ` +
|
|
2635
|
-
`FILTER(<http://www.w3.org/2001/XMLSchema#integer>(STR(${trustVar})) >= ${minTrust})`);
|
|
2636
|
-
}
|
|
2637
|
-
for (const subjectIri of subjectIris) {
|
|
2638
|
-
const trustVar = `?__dkgTrust${i++}`;
|
|
2639
|
-
extraClauses.push(`${subjectIri} <${TRUST_LEVEL_PREDICATE}> ${trustVar} . ` +
|
|
2640
|
-
`FILTER(<http://www.w3.org/2001/XMLSchema#integer>(STR(${trustVar})) >= ${minTrust})`);
|
|
2641
|
-
}
|
|
2642
|
-
for (const subjectPfx of subjectPrefixed) {
|
|
2643
|
-
const trustVar = `?__dkgTrust${i++}`;
|
|
2644
|
-
extraClauses.push(`${subjectPfx} <${TRUST_LEVEL_PREDICATE}> ${trustVar} . ` +
|
|
2645
|
-
`FILTER(<http://www.w3.org/2001/XMLSchema#integer>(STR(${trustVar})) >= ${minTrust})`);
|
|
2646
|
-
}
|
|
2647
|
-
// the previous implementation unconditionally inserted
|
|
2648
|
-
// `" . "` between `inner.trim()` and the injected clauses, which
|
|
2649
|
-
// produced `... . . ?s <trustLevel> ...` when the original WHERE
|
|
2650
|
-
// already ended with a dot (the common case) — a SPARQL syntax error
|
|
2651
|
-
// that every rewritten query hit. Here we emit each rewritten triple
|
|
2652
|
-
// with its OWN dot and join them after the original inner block,
|
|
2653
|
-
// always with exactly one separating dot regardless of whether the
|
|
2654
|
-
// caller terminated their final triple pattern.
|
|
2655
|
-
const endsWithDot = /\.\s*$/.test(trimmedInner);
|
|
2656
|
-
const separator = endsWithDot ? ' ' : ' . ';
|
|
2657
|
-
const rewrittenBody = `${trimmedInner}${separator}${extraClauses.join(' ')}`;
|
|
2658
|
-
// if the WHERE started with a `VALUES ?s { … }` clause the
|
|
2659
|
-
// peeler set aside, re-emit it at the top of the rewritten body so
|
|
2660
|
-
// the bindings it introduces still drive the trust-filtered BGP.
|
|
2661
|
-
const rewrittenInner = valuesClause
|
|
2662
|
-
? `${valuesClause} ${rewrittenBody}`
|
|
2663
|
-
: rewrittenBody;
|
|
2664
|
-
const before = sparql.slice(0, braceStart + 1);
|
|
2665
|
-
const after = sparql.slice(braceEnd);
|
|
2666
|
-
return `${before} ${rewrittenInner} ${after}`;
|
|
2667
|
-
}
|
|
2668
|
-
/**
|
|
2669
|
-
* peel a single leading top-level `VALUES ?var { … }` clause
|
|
2670
|
-
* off the WHERE body. Returns the clause text (verbatim, including the
|
|
2671
|
-
* trailing `}`) and the remainder so the caller can reason about
|
|
2672
|
-
* triples alone. If the WHERE does NOT start with a VALUES clause, or
|
|
2673
|
-
* the VALUES clause is multi-var (`VALUES (?x ?y) { (<a> "b") }`), has
|
|
2674
|
-
* unbalanced braces, or uses nested parentheses for row syntax, returns
|
|
2675
|
-
* `{ valuesClause: null, bodyAfterValues: null }` so the caller falls
|
|
2676
|
-
* back to refusing the query (the forbidden-tokens regex still trips
|
|
2677
|
-
* on `VALUES`).
|
|
2678
|
-
*/
|
|
2679
|
-
function peelLeadingValues(inner) {
|
|
2680
|
-
const withoutComments = stripSparqlLineComments(inner);
|
|
2681
|
-
const m = withoutComments.match(/^\s*VALUES\s+([?$][A-Za-z_]\w*)\s*\{/i);
|
|
2682
|
-
if (!m)
|
|
2683
|
-
return { valuesClause: null, bodyAfterValues: null };
|
|
2684
|
-
const openBraceRel = m[0].length - 1;
|
|
2685
|
-
let depth = 1;
|
|
2686
|
-
let i = openBraceRel + 1;
|
|
2687
|
-
let inString = false;
|
|
2688
|
-
let inIri = false;
|
|
2689
|
-
for (; i < withoutComments.length; i++) {
|
|
2690
|
-
const ch = withoutComments[i];
|
|
2691
|
-
if (inString) {
|
|
2692
|
-
if (ch === '\\') {
|
|
2693
|
-
i++;
|
|
2694
|
-
continue;
|
|
2695
|
-
}
|
|
2696
|
-
if (ch === '"')
|
|
2697
|
-
inString = false;
|
|
2698
|
-
continue;
|
|
2699
|
-
}
|
|
2700
|
-
if (inIri) {
|
|
2701
|
-
if (ch === '>')
|
|
2702
|
-
inIri = false;
|
|
2703
|
-
continue;
|
|
2704
|
-
}
|
|
2705
|
-
if (ch === '"') {
|
|
2706
|
-
inString = true;
|
|
2707
|
-
continue;
|
|
2708
|
-
}
|
|
2709
|
-
if (ch === '<') {
|
|
2710
|
-
inIri = true;
|
|
2711
|
-
continue;
|
|
2712
|
-
}
|
|
2713
|
-
if (ch === '(' || ch === ')') {
|
|
2714
|
-
// Row-tuple syntax — we can't reason about multi-var rows safely.
|
|
2715
|
-
return { valuesClause: null, bodyAfterValues: null };
|
|
2716
|
-
}
|
|
2717
|
-
if (ch === '{')
|
|
2718
|
-
depth++;
|
|
2719
|
-
else if (ch === '}') {
|
|
2720
|
-
depth--;
|
|
2721
|
-
if (depth === 0)
|
|
2722
|
-
break;
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
if (depth !== 0)
|
|
2726
|
-
return { valuesClause: null, bodyAfterValues: null };
|
|
2727
|
-
const closeAbs = i;
|
|
2728
|
-
const valuesClause = withoutComments.slice(0, closeAbs + 1).trim();
|
|
2729
|
-
const bodyAfterValues = withoutComments.slice(closeAbs + 1);
|
|
2730
|
-
return { valuesClause, bodyAfterValues };
|
|
2731
|
-
}
|
|
2732
1117
|
function mergeSharedMemoryAndDataResults(dataResult, smResult) {
|
|
2733
1118
|
if (dataResult.type === 'quads' || smResult.type === 'quads') {
|
|
2734
1119
|
const mergedQuads = dedupeQuads([
|
|
@@ -2784,29 +1169,32 @@ function dedupeQuads(quads) {
|
|
|
2784
1169
|
* inner-UNION case in `queryMultipleGraphs`) to reject shapes that
|
|
2785
1170
|
* would otherwise return duplicate / mis-ordered / over-limit rows.
|
|
2786
1171
|
*
|
|
2787
|
-
*
|
|
2788
|
-
*
|
|
2789
|
-
*
|
|
2790
|
-
*
|
|
2791
|
-
*
|
|
2792
|
-
* because the per-graph partial aggregates can't be combined post-hoc.
|
|
1172
|
+
* Consume the canonical prepared token stream so active UCHAR spellings are
|
|
1173
|
+
* decoded exactly as they will be for execution while strings, comments, and
|
|
1174
|
+
* IRI bodies remain opaque. The aggregate check is intentionally broad — any
|
|
1175
|
+
* standard aggregate invalidates naive concatenation because per-graph
|
|
1176
|
+
* partial aggregates cannot be combined post-hoc.
|
|
2793
1177
|
*/
|
|
2794
|
-
function hasCrossGraphUnsafeModifier(
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
1178
|
+
function hasCrossGraphUnsafeModifier(scope) {
|
|
1179
|
+
const tokens = scope.prepared.tokens;
|
|
1180
|
+
const singleWordModifiers = new Set(['DISTINCT', 'HAVING', 'LIMIT', 'OFFSET']);
|
|
1181
|
+
const aggregates = new Set(['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'SAMPLE', 'GROUP_CONCAT']);
|
|
1182
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
1183
|
+
const token = tokens[index];
|
|
1184
|
+
if (token?.kind !== 'word')
|
|
1185
|
+
continue;
|
|
1186
|
+
if (singleWordModifiers.has(token.upper))
|
|
1187
|
+
return true;
|
|
1188
|
+
const next = tokens[index + 1];
|
|
1189
|
+
if ((token.upper === 'ORDER' || token.upper === 'GROUP')
|
|
1190
|
+
&& next?.kind === 'word'
|
|
1191
|
+
&& next.upper === 'BY')
|
|
1192
|
+
return true;
|
|
1193
|
+
if (aggregates.has(token.upper)
|
|
1194
|
+
&& next?.kind === 'symbol'
|
|
1195
|
+
&& next.logicalValue === '(')
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
2810
1198
|
return false;
|
|
2811
1199
|
}
|
|
2812
1200
|
//# sourceMappingURL=dkg-query-engine.js.map
|