@oml/owl 0.13.0 → 0.14.1
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/out/owl/owl-abox.js +25 -34
- package/out/owl/owl-abox.js.map +1 -1
- package/out/owl/owl-interfaces.d.ts +1 -1
- package/out/owl/owl-service.js +9 -9
- package/out/owl/owl-service.js.map +1 -1
- package/out/owl/owl-shacl.d.ts +3 -3
- package/out/owl/owl-shacl.js +15 -15
- package/out/owl/owl-shacl.js.map +1 -1
- package/out/owl/owl-sparql.d.ts +6 -1
- package/out/owl/owl-sparql.js +135 -16
- package/out/owl/owl-sparql.js.map +1 -1
- package/out/owl/owl-store.d.ts +0 -3
- package/out/owl/owl-store.js +11 -35
- package/out/owl/owl-store.js.map +1 -1
- package/package.json +2 -2
- package/src/owl/owl-abox.ts +25 -37
- package/src/owl/owl-interfaces.ts +1 -1
- package/src/owl/owl-service.ts +9 -8
- package/src/owl/owl-shacl.ts +18 -19
- package/src/owl/owl-sparql.ts +156 -17
- package/src/owl/owl-store.ts +11 -37
package/src/owl/owl-sparql.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// allowing Comunica to initialize and execute queries successfully (without parallel execution).
|
|
7
7
|
import { QueryEngine } from '@comunica/query-sparql';
|
|
8
8
|
import type { Bindings } from '@comunica/types';
|
|
9
|
-
import { Store } from 'n3';
|
|
9
|
+
import { DataFactory, Store } from 'n3';
|
|
10
10
|
import type * as RDF from '@rdfjs/types';
|
|
11
11
|
import type { NamedNode } from 'n3';
|
|
12
12
|
import type { OwlABoxEntailmentState, OwlImportGraph, OwlInferenceRunner, OwlStore } from './owl-interfaces.js';
|
|
@@ -42,8 +42,106 @@ export interface AskResult {
|
|
|
42
42
|
error?: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export type SparqlQueryKind = 'select' | 'ask' | 'construct' | 'describe' | 'unknown';
|
|
46
|
+
|
|
47
|
+
export function detectSparqlKind(sparql: string): SparqlQueryKind {
|
|
48
|
+
const cleaned = sparql
|
|
49
|
+
.replace(/#[^\r\n]*/g, ' ')
|
|
50
|
+
.trim()
|
|
51
|
+
.toUpperCase();
|
|
52
|
+
const match = cleaned.match(/\b(SELECT|ASK|CONSTRUCT|DESCRIBE)\b/);
|
|
53
|
+
const keyword = match?.[1];
|
|
54
|
+
if (keyword === 'SELECT') {
|
|
55
|
+
return 'select';
|
|
56
|
+
}
|
|
57
|
+
if (keyword === 'ASK') {
|
|
58
|
+
return 'ask';
|
|
59
|
+
}
|
|
60
|
+
if (keyword === 'CONSTRUCT') {
|
|
61
|
+
return 'construct';
|
|
62
|
+
}
|
|
63
|
+
if (keyword === 'DESCRIBE') {
|
|
64
|
+
return 'describe';
|
|
65
|
+
}
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extractWhereBodyAndTail(sparql: string): { body: string; tail: string } | undefined {
|
|
70
|
+
const whereMatch = /\bWHERE\b/i.exec(sparql);
|
|
71
|
+
if (!whereMatch || whereMatch.index < 0) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
let cursor = whereMatch.index + whereMatch[0].length;
|
|
75
|
+
while (cursor < sparql.length && /\s/.test(sparql[cursor])) {
|
|
76
|
+
cursor += 1;
|
|
77
|
+
}
|
|
78
|
+
if (sparql[cursor] !== '{') {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const bodyStart = cursor + 1;
|
|
82
|
+
let depth = 1;
|
|
83
|
+
cursor += 1;
|
|
84
|
+
while (cursor < sparql.length && depth > 0) {
|
|
85
|
+
const ch = sparql[cursor];
|
|
86
|
+
if (ch === '{') {
|
|
87
|
+
depth += 1;
|
|
88
|
+
} else if (ch === '}') {
|
|
89
|
+
depth -= 1;
|
|
90
|
+
}
|
|
91
|
+
cursor += 1;
|
|
92
|
+
}
|
|
93
|
+
if (depth !== 0) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const bodyEnd = cursor - 1;
|
|
97
|
+
const body = sparql.slice(bodyStart, bodyEnd).trim();
|
|
98
|
+
const tail = sparql.slice(cursor).trim();
|
|
99
|
+
return { body, tail };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function rewriteDescribeToConstruct(sparql: string): string {
|
|
103
|
+
const normalized = sparql.trim().replace(/\s+/g, ' ');
|
|
104
|
+
const describeMatch = /^DESCRIBE\s+(.+?)\s+(WHERE\b.*)$/is.exec(normalized);
|
|
105
|
+
if (!describeMatch) {
|
|
106
|
+
return sparql;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const describeVarsRaw = describeMatch[1].trim();
|
|
110
|
+
const whereAndTail = describeMatch[2];
|
|
111
|
+
const parsedWhere = extractWhereBodyAndTail(whereAndTail);
|
|
112
|
+
if (!parsedWhere) {
|
|
113
|
+
return sparql;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const variables = describeVarsRaw === '*'
|
|
117
|
+
? ['?s']
|
|
118
|
+
: describeVarsRaw
|
|
119
|
+
.split(/\s+/)
|
|
120
|
+
.filter((token) => token.startsWith('?'));
|
|
121
|
+
|
|
122
|
+
if (variables.length === 0) {
|
|
123
|
+
return sparql;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const constructTriples = variables
|
|
127
|
+
.map((variable, index) => `${variable} ?__p${index} ?__o${index} .`)
|
|
128
|
+
.join(' ');
|
|
129
|
+
|
|
130
|
+
const whereTriples = variables
|
|
131
|
+
.map((variable, index) => `OPTIONAL { ${variable} ?__p${index} ?__o${index} . }`)
|
|
132
|
+
.join(' ');
|
|
133
|
+
|
|
134
|
+
const tail = parsedWhere.tail ? ` ${parsedWhere.tail}` : '';
|
|
135
|
+
return `CONSTRUCT { ${constructTriples} } WHERE { ${parsedWhere.body} ${whereTriples} }${tail}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
45
138
|
interface QueryContext {
|
|
46
|
-
|
|
139
|
+
mappings: {
|
|
140
|
+
ownGraph: NamedNode;
|
|
141
|
+
entailmentsGraph: NamedNode;
|
|
142
|
+
targetOwnGraph: NamedNode;
|
|
143
|
+
targetEntailmentsGraph: NamedNode;
|
|
144
|
+
}[];
|
|
47
145
|
missingModels: string[];
|
|
48
146
|
}
|
|
49
147
|
|
|
@@ -54,13 +152,21 @@ export class SparqlService {
|
|
|
54
152
|
private readonly engine = new QueryEngine();
|
|
55
153
|
private readonly filteredStoreCache = new Map<string, Store>();
|
|
56
154
|
private readonly dirtyFilteredStores = new Set<string>();
|
|
155
|
+
private ontologyIriResolver: (modelUri: string) => string;
|
|
57
156
|
|
|
58
157
|
constructor(
|
|
59
158
|
private readonly reasoningStore: OwlStore,
|
|
60
159
|
private readonly importGraph: OwlImportGraph,
|
|
61
160
|
private readonly aboxEntailmentCache: OwlABoxEntailmentState,
|
|
62
161
|
private readonly reasoningService: OwlInferenceRunner,
|
|
63
|
-
|
|
162
|
+
resolveOntologyIriForModelUri?: (modelUri: string) => string,
|
|
163
|
+
) {
|
|
164
|
+
this.ontologyIriResolver = resolveOntologyIriForModelUri ?? ((modelUri) => modelUri);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setOntologyIriResolver(resolver: (modelUri: string) => string): void {
|
|
168
|
+
this.ontologyIriResolver = resolver;
|
|
169
|
+
}
|
|
64
170
|
|
|
65
171
|
/**
|
|
66
172
|
* Executes a SPARQL query over the model's reasoned context.
|
|
@@ -106,7 +212,8 @@ export class SparqlService {
|
|
|
106
212
|
const warnings = this.composeWarnings(modelUri, queryContext.missingModels);
|
|
107
213
|
const filteredStore = this.getOrCreateFilteredStore(modelUri, queryContext);
|
|
108
214
|
const baseIRI = this.resolveBaseIri(modelUri);
|
|
109
|
-
const
|
|
215
|
+
const rewritten = detectSparqlKind(sparql) === 'describe' ? rewriteDescribeToConstruct(sparql) : sparql;
|
|
216
|
+
const quadsStream = await this.engine.queryQuads(rewritten, {
|
|
110
217
|
sources: [filteredStore],
|
|
111
218
|
unionDefaultGraph: true,
|
|
112
219
|
baseIRI,
|
|
@@ -160,7 +267,18 @@ export class SparqlService {
|
|
|
160
267
|
}
|
|
161
268
|
|
|
162
269
|
getAvailableGraphs(modelUri: string): NamedNode[] {
|
|
163
|
-
|
|
270
|
+
const seen = new Set<string>();
|
|
271
|
+
const graphs: NamedNode[] = [];
|
|
272
|
+
for (const mapping of this.getQueryContext(modelUri).mappings) {
|
|
273
|
+
for (const graph of [mapping.targetOwnGraph, mapping.targetEntailmentsGraph]) {
|
|
274
|
+
if (seen.has(graph.value)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
seen.add(graph.value);
|
|
278
|
+
graphs.push(graph);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return graphs;
|
|
164
282
|
}
|
|
165
283
|
|
|
166
284
|
invalidateFilteredStore(modelUri: string): void {
|
|
@@ -234,7 +352,7 @@ export class SparqlService {
|
|
|
234
352
|
private getQueryContext(modelUri: string): QueryContext {
|
|
235
353
|
const allUris = [modelUri, ...this.importGraph.dependenciesOf(modelUri)];
|
|
236
354
|
const seen = new Set<string>();
|
|
237
|
-
const
|
|
355
|
+
const mappings: QueryContext['mappings'] = [];
|
|
238
356
|
const missingModels: string[] = [];
|
|
239
357
|
for (const uri of allUris) {
|
|
240
358
|
let own: NamedNode;
|
|
@@ -245,15 +363,30 @@ export class SparqlService {
|
|
|
245
363
|
missingModels.push(uri);
|
|
246
364
|
continue;
|
|
247
365
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
366
|
+
if (seen.has(uri)) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
seen.add(uri);
|
|
370
|
+
const resolved = this.ontologyIriResolver(uri);
|
|
371
|
+
if (!resolved || resolved === uri) {
|
|
372
|
+
throw new Error(`Missing ontology IRI mapping for model '${uri}'.`);
|
|
254
373
|
}
|
|
374
|
+
const ontologyIri = this.toOntologyGraphIri(resolved);
|
|
375
|
+
mappings.push({
|
|
376
|
+
ownGraph: own,
|
|
377
|
+
entailmentsGraph: entailments,
|
|
378
|
+
targetOwnGraph: DataFactory.namedNode(ontologyIri),
|
|
379
|
+
targetEntailmentsGraph: DataFactory.namedNode(`${ontologyIri}__entailments`),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return { mappings, missingModels };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private toOntologyGraphIri(iri: string): string {
|
|
386
|
+
if (iri.endsWith('#')) {
|
|
387
|
+
return iri.slice(0, -1);
|
|
255
388
|
}
|
|
256
|
-
return
|
|
389
|
+
return iri;
|
|
257
390
|
}
|
|
258
391
|
|
|
259
392
|
private toRdfTerm(term: RDF.Term): RDFTerm {
|
|
@@ -286,12 +419,18 @@ export class SparqlService {
|
|
|
286
419
|
return [...new Set([...this.toMissingModelsWarnings(missingModels), ...closureWarnings])];
|
|
287
420
|
}
|
|
288
421
|
|
|
289
|
-
private createFilteredStore(
|
|
422
|
+
private createFilteredStore(mappings: QueryContext['mappings']): Store {
|
|
290
423
|
const filteredStore = new Store();
|
|
291
424
|
const mainStore = this.reasoningStore.getStore();
|
|
292
425
|
const seenTriples = new Set<string>();
|
|
293
|
-
for (const
|
|
294
|
-
const
|
|
426
|
+
for (const mapping of mappings) {
|
|
427
|
+
const quads = [
|
|
428
|
+
...mainStore.getQuads(null, null, null, mapping.ownGraph)
|
|
429
|
+
.map((quad) => DataFactory.quad(quad.subject, quad.predicate, quad.object, mapping.targetOwnGraph)),
|
|
430
|
+
...mainStore.getQuads(null, null, null, mapping.entailmentsGraph)
|
|
431
|
+
.map((quad) => DataFactory.quad(quad.subject, quad.predicate, quad.object, mapping.targetEntailmentsGraph)),
|
|
432
|
+
];
|
|
433
|
+
const uniqueQuads = quads.filter((quad) => {
|
|
295
434
|
const key = `${quad.subject.id}|${quad.predicate.id}|${quad.object.id}`;
|
|
296
435
|
if (seenTriples.has(key)) {
|
|
297
436
|
return false;
|
|
@@ -309,7 +448,7 @@ export class SparqlService {
|
|
|
309
448
|
if (cached && !this.dirtyFilteredStores.has(modelUri)) {
|
|
310
449
|
return cached;
|
|
311
450
|
}
|
|
312
|
-
const store = this.createFilteredStore(queryContext.
|
|
451
|
+
const store = this.createFilteredStore(queryContext.mappings);
|
|
313
452
|
this.filteredStoreCache.set(modelUri, store);
|
|
314
453
|
this.dirtyFilteredStores.delete(modelUri);
|
|
315
454
|
return store;
|
package/src/owl/owl-store.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { DataFactory, Store } from 'n3';
|
|
4
4
|
import type { NamedNode, Quad } from 'n3';
|
|
5
5
|
|
|
6
|
-
const OML_NAMESPACE = 'http://opencaesar.io/oml#namespace';
|
|
7
6
|
const { namedNode, quad } = DataFactory;
|
|
8
7
|
|
|
9
8
|
export interface ModelDiff {
|
|
@@ -23,9 +22,9 @@ export class ReasoningStore {
|
|
|
23
22
|
|
|
24
23
|
// Load a model's quads into its named graph.
|
|
25
24
|
// Retracts any existing quads for that model first.
|
|
26
|
-
// Graph IRI is derived from the
|
|
25
|
+
// Graph IRI is derived from the model URI to keep model snapshots isolated.
|
|
27
26
|
loadModel(modelUri: string, quads: Quad[]): void {
|
|
28
|
-
const graphs = this.resolveGraphs(modelUri
|
|
27
|
+
const graphs = this.resolveGraphs(modelUri);
|
|
29
28
|
this.retractModel(modelUri);
|
|
30
29
|
this.modelGraphs.set(modelUri, graphs);
|
|
31
30
|
this.store.addQuads(quads.map((q) => this.withGraph(q, graphs.own)));
|
|
@@ -41,7 +40,7 @@ export class ReasoningStore {
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
applyModelDelta(modelUri: string, quads: Quad[], retracted: Quad[], asserted: Quad[]): void {
|
|
44
|
-
const graphs = this.resolveGraphs(modelUri
|
|
43
|
+
const graphs = this.resolveGraphs(modelUri);
|
|
45
44
|
this.modelGraphs.set(modelUri, graphs);
|
|
46
45
|
if (retracted.length > 0) {
|
|
47
46
|
this.store.removeQuads(retracted.map((q) => this.withGraph(q, graphs.own)));
|
|
@@ -65,7 +64,7 @@ export class ReasoningStore {
|
|
|
65
64
|
// Returns the semantic difference — what was added and what was removed.
|
|
66
65
|
// If the diff is empty, nothing downstream needs to run.
|
|
67
66
|
diffModel(modelUri: string, newQuads: Quad[]): ModelDiff {
|
|
68
|
-
const graphs = this.resolveGraphs(modelUri
|
|
67
|
+
const graphs = this.resolveGraphs(modelUri);
|
|
69
68
|
const existing = this.store.getQuads(null, null, null, graphs.own);
|
|
70
69
|
const next = newQuads.map((q) => this.withGraph(q, graphs.own));
|
|
71
70
|
|
|
@@ -109,30 +108,14 @@ export class ReasoningStore {
|
|
|
109
108
|
return this.store;
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
private resolveGraphs(modelUri: string
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
this.modelGraphs.set(modelUri, graphs);
|
|
117
|
-
return graphs;
|
|
111
|
+
private resolveGraphs(modelUri: string): ModelGraphs {
|
|
112
|
+
const existing = this.modelGraphs.get(modelUri);
|
|
113
|
+
if (existing) {
|
|
114
|
+
return existing;
|
|
118
115
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
// Keep reasoning resilient during transient states (e.g. non-mappable documents):
|
|
124
|
-
// if namespace is missing, fall back to a stable graph derived from the model URI.
|
|
125
|
-
const fallback = this.graphsFromModelUri(modelUri);
|
|
126
|
-
this.modelGraphs.set(modelUri, fallback);
|
|
127
|
-
return fallback;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
private graphsFromNamespace(namespace: string): ModelGraphs {
|
|
131
|
-
const ontologyIri = this.ontologyIriFromNamespace(namespace);
|
|
132
|
-
return {
|
|
133
|
-
own: namedNode(ontologyIri),
|
|
134
|
-
entailments: namedNode(`${ontologyIri}__entailments`),
|
|
135
|
-
};
|
|
116
|
+
const graphs = this.graphsFromModelUri(modelUri);
|
|
117
|
+
this.modelGraphs.set(modelUri, graphs);
|
|
118
|
+
return graphs;
|
|
136
119
|
}
|
|
137
120
|
|
|
138
121
|
private graphsFromModelUri(modelUri: string): ModelGraphs {
|
|
@@ -142,15 +125,6 @@ export class ReasoningStore {
|
|
|
142
125
|
};
|
|
143
126
|
}
|
|
144
127
|
|
|
145
|
-
private namespaceFromQuads(quads: Quad[]): string | undefined {
|
|
146
|
-
const namespaceQuad = quads.find((q) => q.predicate.value === OML_NAMESPACE && q.object.termType === 'NamedNode');
|
|
147
|
-
return namespaceQuad?.object.value;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private ontologyIriFromNamespace(namespace: string): string {
|
|
151
|
-
return namespace.replace(/[\/#]+$/, '');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
128
|
private requireGraphs(modelUri: string): ModelGraphs {
|
|
155
129
|
const graphs = this.modelGraphs.get(modelUri);
|
|
156
130
|
if (!graphs) {
|