@oml/owl 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- graphs: NamedNode[];
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 quadsStream = await this.engine.queryQuads(sparql, {
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
- return this.getQueryContext(modelUri).graphs;
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 graphs: NamedNode[] = [];
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
- for (const graph of [own, entailments]) {
249
- if (seen.has(graph.value)) {
250
- continue;
251
- }
252
- seen.add(graph.value);
253
- graphs.push(graph);
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 { graphs, missingModels };
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(graphs: NamedNode[]): Store {
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 graph of graphs) {
294
- const uniqueQuads = mainStore.getQuads(null, null, null, graph).filter((quad) => {
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.graphs);
451
+ const store = this.createFilteredStore(queryContext.mappings);
313
452
  this.filteredStoreCache.set(modelUri, store);
314
453
  this.dirtyFilteredStores.delete(modelUri);
315
454
  return store;
@@ -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 ontology namespace embedded in the quads.
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, quads);
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, quads, true);
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, newQuads, true);
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, quads: Quad[], allowExisting = false): ModelGraphs {
113
- const namespace = this.namespaceFromQuads(quads);
114
- if (namespace) {
115
- const graphs = this.graphsFromNamespace(namespace);
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
- if (allowExisting) {
120
- const existing = this.modelGraphs.get(modelUri);
121
- if (existing) return existing;
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) {