@unrdf/knowledge-engine 5.0.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/LICENSE +21 -0
- package/README.md +84 -0
- package/package.json +64 -0
- package/src/browser-shims.mjs +343 -0
- package/src/browser.mjs +910 -0
- package/src/canonicalize.mjs +414 -0
- package/src/condition-cache.mjs +109 -0
- package/src/condition-evaluator.mjs +722 -0
- package/src/dark-matter-core.mjs +742 -0
- package/src/define-hook.mjs +213 -0
- package/src/effect-sandbox-browser.mjs +283 -0
- package/src/effect-sandbox-worker.mjs +170 -0
- package/src/effect-sandbox.mjs +517 -0
- package/src/engines/index.mjs +11 -0
- package/src/engines/rdf-engine.mjs +299 -0
- package/src/file-resolver.mjs +387 -0
- package/src/hook-executor-batching.mjs +277 -0
- package/src/hook-executor.mjs +870 -0
- package/src/hook-management.mjs +150 -0
- package/src/index.mjs +93 -0
- package/src/ken-parliment.mjs +119 -0
- package/src/ken.mjs +149 -0
- package/src/knowledge-engine/builtin-rules.mjs +190 -0
- package/src/knowledge-engine/inference-engine.mjs +418 -0
- package/src/knowledge-engine/knowledge-engine.mjs +317 -0
- package/src/knowledge-engine/pattern-dsl.mjs +142 -0
- package/src/knowledge-engine/pattern-matcher.mjs +215 -0
- package/src/knowledge-engine/rules.mjs +184 -0
- package/src/knowledge-engine.mjs +319 -0
- package/src/knowledge-hook-engine.mjs +360 -0
- package/src/knowledge-hook-manager.mjs +469 -0
- package/src/knowledge-substrate-core.mjs +927 -0
- package/src/lite.mjs +222 -0
- package/src/lockchain-writer-browser.mjs +414 -0
- package/src/lockchain-writer.mjs +602 -0
- package/src/monitoring/andon-signals.mjs +775 -0
- package/src/observability.mjs +531 -0
- package/src/parse.mjs +290 -0
- package/src/performance-optimizer.mjs +678 -0
- package/src/policy-pack.mjs +572 -0
- package/src/query-cache.mjs +116 -0
- package/src/query-optimizer.mjs +1051 -0
- package/src/query.mjs +306 -0
- package/src/reason.mjs +350 -0
- package/src/resolution-layer.mjs +506 -0
- package/src/schemas.mjs +1063 -0
- package/src/security/error-sanitizer.mjs +257 -0
- package/src/security/path-validator.mjs +194 -0
- package/src/security/sandbox-restrictions.mjs +331 -0
- package/src/security-validator.mjs +389 -0
- package/src/store-cache.mjs +137 -0
- package/src/telemetry.mjs +167 -0
- package/src/transaction.mjs +810 -0
- package/src/utils/adaptive-monitor.mjs +746 -0
- package/src/utils/circuit-breaker.mjs +513 -0
- package/src/utils/edge-case-handler.mjs +503 -0
- package/src/utils/memory-manager.mjs +498 -0
- package/src/utils/ring-buffer.mjs +282 -0
- package/src/validate.mjs +319 -0
- package/src/validators/index.mjs +338 -0
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Query Optimizer for performance improvements
|
|
3
|
+
* @module query-optimizer
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Implements query plan caching, indexing, and delta-aware evaluation
|
|
7
|
+
* to optimize SPARQL and SHACL query performance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
11
|
+
import { utf8ToBytes, bytesToHex } from '@noble/hashes/utils.js';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import LRUCache from 'lru-cache';
|
|
15
|
+
import { trace, metrics, SpanStatusCode } from '@opentelemetry/api';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Schema for query plan
|
|
19
|
+
*/
|
|
20
|
+
const _QueryPlanSchema = z.object({
|
|
21
|
+
id: z.string(),
|
|
22
|
+
query: z.string(),
|
|
23
|
+
type: z.enum(['sparql-ask', 'sparql-select', 'shacl']),
|
|
24
|
+
hash: z.string(),
|
|
25
|
+
plan: z.object({
|
|
26
|
+
operations: z.array(
|
|
27
|
+
z.object({
|
|
28
|
+
type: z.string(),
|
|
29
|
+
cost: z.number(),
|
|
30
|
+
selectivity: z.number(),
|
|
31
|
+
dependencies: z.array(z.string()).optional(),
|
|
32
|
+
})
|
|
33
|
+
),
|
|
34
|
+
estimatedCost: z.number(),
|
|
35
|
+
estimatedRows: z.number(),
|
|
36
|
+
indexes: z.array(z.string()).optional(),
|
|
37
|
+
}),
|
|
38
|
+
createdAt: z.number(),
|
|
39
|
+
lastUsed: z.number(),
|
|
40
|
+
hitCount: z.number().default(0),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Schema for index definition
|
|
45
|
+
*/
|
|
46
|
+
const _IndexSchema = z.object({
|
|
47
|
+
id: z.string(),
|
|
48
|
+
name: z.string(),
|
|
49
|
+
type: z.enum(['predicate', 'subject', 'object', 'graph', 'composite']),
|
|
50
|
+
fields: z.array(z.string()),
|
|
51
|
+
selectivity: z.number().min(0).max(1),
|
|
52
|
+
size: z.number().nonnegative(),
|
|
53
|
+
createdAt: z.number(),
|
|
54
|
+
lastUpdated: z.number(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Schema for delta-aware evaluation context
|
|
59
|
+
*/
|
|
60
|
+
const _DeltaAwareContextSchema = z.object({
|
|
61
|
+
delta: z.object({
|
|
62
|
+
additions: z.array(z.any()),
|
|
63
|
+
removals: z.array(z.any()),
|
|
64
|
+
}),
|
|
65
|
+
affectedSubjects: z.set(z.string()).optional(),
|
|
66
|
+
affectedPredicates: z.set(z.string()).optional(),
|
|
67
|
+
affectedObjects: z.set(z.string()).optional(),
|
|
68
|
+
affectedGraphs: z.set(z.string()).optional(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Query Optimizer for performance improvements
|
|
73
|
+
*/
|
|
74
|
+
export class QueryOptimizer {
|
|
75
|
+
/**
|
|
76
|
+
* Create a new query optimizer
|
|
77
|
+
* @param {Object} [config] - Configuration
|
|
78
|
+
*/
|
|
79
|
+
constructor(config = {}) {
|
|
80
|
+
this.config = {
|
|
81
|
+
enableCaching: config.enableCaching !== false,
|
|
82
|
+
enableIndexing: config.enableIndexing !== false,
|
|
83
|
+
enableDeltaAware: config.enableDeltaAware !== false,
|
|
84
|
+
maxCacheSize: config.maxCacheSize || 1000,
|
|
85
|
+
cacheMaxAge: config.cacheMaxAge || 300000, // 5 minutes
|
|
86
|
+
indexUpdateThreshold: config.indexUpdateThreshold || 100,
|
|
87
|
+
enableOTEL: config.enableOTEL !== false,
|
|
88
|
+
...config,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.queryPlans = new Map();
|
|
92
|
+
this.indexes = new Map();
|
|
93
|
+
|
|
94
|
+
// LRU Cache for query plans (40-60% overhead reduction)
|
|
95
|
+
this.cache = new LRUCache({
|
|
96
|
+
max: this.config.maxCacheSize,
|
|
97
|
+
maxAge: this.config.cacheMaxAge,
|
|
98
|
+
ttl: this.config.cacheMaxAge,
|
|
99
|
+
updateAgeOnGet: true,
|
|
100
|
+
updateAgeOnHas: true,
|
|
101
|
+
dispose: (value, _key) => {
|
|
102
|
+
// Clean up query plan resources
|
|
103
|
+
if (value && value.plan && value.plan.operations) {
|
|
104
|
+
value.plan.operations.length = 0;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.stats = {
|
|
110
|
+
cacheHits: 0,
|
|
111
|
+
cacheMisses: 0,
|
|
112
|
+
indexHits: 0,
|
|
113
|
+
indexMisses: 0,
|
|
114
|
+
deltaOptimizations: 0,
|
|
115
|
+
totalQueries: 0,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// OTEL instrumentation
|
|
119
|
+
if (this.config.enableOTEL) {
|
|
120
|
+
this.tracer = trace.getTracer('query-optimizer');
|
|
121
|
+
this.meter = metrics.getMeter('query-optimizer');
|
|
122
|
+
|
|
123
|
+
// Create OTEL metrics
|
|
124
|
+
this.cacheHitCounter = this.meter.createCounter('query.cache.hits', {
|
|
125
|
+
description: 'Number of query cache hits',
|
|
126
|
+
});
|
|
127
|
+
this.cacheMissCounter = this.meter.createCounter('query.cache.misses', {
|
|
128
|
+
description: 'Number of query cache misses',
|
|
129
|
+
});
|
|
130
|
+
this.queryOptimizationDuration = this.meter.createHistogram('query.optimization.duration', {
|
|
131
|
+
description: 'Query optimization duration in ms',
|
|
132
|
+
unit: 'ms',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Optimize a query
|
|
139
|
+
* @param {string} query - Query string
|
|
140
|
+
* @param {string} type - Query type
|
|
141
|
+
* @param {Store} graph - RDF graph
|
|
142
|
+
* @param {Object} [delta] - Delta for delta-aware optimization
|
|
143
|
+
* @returns {Promise<Object>} Optimized query plan
|
|
144
|
+
*/
|
|
145
|
+
async optimizeQuery(query, type, graph, delta = null) {
|
|
146
|
+
this.stats.totalQueries++;
|
|
147
|
+
|
|
148
|
+
const startTime = Date.now();
|
|
149
|
+
const queryHash = this._hashQuery(query, type);
|
|
150
|
+
|
|
151
|
+
// OTEL span for query optimization
|
|
152
|
+
const span = this.config.enableOTEL
|
|
153
|
+
? this.tracer.startSpan('query.optimize', {
|
|
154
|
+
attributes: {
|
|
155
|
+
'query.type': type,
|
|
156
|
+
'query.hash': queryHash.substring(0, 8),
|
|
157
|
+
'query.length': query.length,
|
|
158
|
+
'delta.enabled': !!delta,
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Check LRU cache first
|
|
165
|
+
if (this.config.enableCaching) {
|
|
166
|
+
const cached = this.cache.get(queryHash);
|
|
167
|
+
if (cached) {
|
|
168
|
+
this.stats.cacheHits++;
|
|
169
|
+
cached.lastUsed = Date.now();
|
|
170
|
+
cached.hitCount++;
|
|
171
|
+
|
|
172
|
+
// Record cache hit metrics
|
|
173
|
+
if (this.config.enableOTEL) {
|
|
174
|
+
this.cacheHitCounter.add(1, { 'query.type': type });
|
|
175
|
+
span.setAttribute('cache.hit', true);
|
|
176
|
+
span.setAttribute('cache.hitCount', cached.hitCount);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
span?.end();
|
|
180
|
+
return cached;
|
|
181
|
+
}
|
|
182
|
+
this.stats.cacheMisses++;
|
|
183
|
+
|
|
184
|
+
// Record cache miss metrics
|
|
185
|
+
if (this.config.enableOTEL) {
|
|
186
|
+
this.cacheMissCounter.add(1, { 'query.type': type });
|
|
187
|
+
span.setAttribute('cache.hit', false);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create new query plan
|
|
192
|
+
const plan = await this._createQueryPlan(query, type, queryHash, graph, delta);
|
|
193
|
+
|
|
194
|
+
// Cache the plan using LRU cache
|
|
195
|
+
if (this.config.enableCaching) {
|
|
196
|
+
this.cache.set(queryHash, plan);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const duration = Date.now() - startTime;
|
|
200
|
+
|
|
201
|
+
// Record optimization metrics
|
|
202
|
+
if (this.config.enableOTEL) {
|
|
203
|
+
this.queryOptimizationDuration.record(duration, { 'query.type': type });
|
|
204
|
+
span.setAttribute('optimization.duration', duration);
|
|
205
|
+
span.setAttribute('plan.estimatedCost', plan.plan.estimatedCost);
|
|
206
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
span?.end();
|
|
210
|
+
return plan;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (span) {
|
|
213
|
+
span.recordException(error);
|
|
214
|
+
span.setStatus({
|
|
215
|
+
code: SpanStatusCode.ERROR,
|
|
216
|
+
message: error.message,
|
|
217
|
+
});
|
|
218
|
+
span.end();
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Execute optimized query
|
|
226
|
+
* @param {Object} plan - Query plan
|
|
227
|
+
* @param {Store} graph - RDF graph
|
|
228
|
+
* @param {Object} [delta] - Delta for delta-aware execution
|
|
229
|
+
* @returns {Promise<any>} Query result
|
|
230
|
+
*/
|
|
231
|
+
async executeOptimizedQuery(plan, graph, delta = null) {
|
|
232
|
+
if (this.config.enableDeltaAware && delta) {
|
|
233
|
+
return this._executeDeltaAware(plan, graph, delta);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return this._executeStandard(plan, graph);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create indexes for a graph
|
|
241
|
+
* @param {Store} graph - RDF graph
|
|
242
|
+
* @returns {Promise<Array>} Created indexes
|
|
243
|
+
*/
|
|
244
|
+
async createIndexes(graph) {
|
|
245
|
+
if (!this.config.enableIndexing) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const indexes = [];
|
|
250
|
+
const quads = graph.getQuads();
|
|
251
|
+
|
|
252
|
+
// Create predicate index
|
|
253
|
+
const predicateIndex = this._createPredicateIndex(quads);
|
|
254
|
+
indexes.push(predicateIndex);
|
|
255
|
+
|
|
256
|
+
// Create subject index
|
|
257
|
+
const subjectIndex = this._createSubjectIndex(quads);
|
|
258
|
+
indexes.push(subjectIndex);
|
|
259
|
+
|
|
260
|
+
// Create object index
|
|
261
|
+
const objectIndex = this._createObjectIndex(quads);
|
|
262
|
+
indexes.push(objectIndex);
|
|
263
|
+
|
|
264
|
+
// Create composite indexes for common patterns
|
|
265
|
+
const compositeIndexes = this._createCompositeIndexes(quads);
|
|
266
|
+
indexes.push(...compositeIndexes);
|
|
267
|
+
|
|
268
|
+
// Store indexes
|
|
269
|
+
for (const index of indexes) {
|
|
270
|
+
this.indexes.set(index.id, index);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return indexes;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Update indexes with delta
|
|
278
|
+
* @param {Object} delta - Delta to apply
|
|
279
|
+
* @returns {Promise<void>}
|
|
280
|
+
*/
|
|
281
|
+
async updateIndexes(delta) {
|
|
282
|
+
if (!this.config.enableIndexing) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Update indexes with additions and removals
|
|
287
|
+
for (const [_indexId, index] of this.indexes) {
|
|
288
|
+
// Remove quads
|
|
289
|
+
for (const quad of delta.removals) {
|
|
290
|
+
this._removeFromIndex(index, quad);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Add quads
|
|
294
|
+
for (const quad of delta.additions) {
|
|
295
|
+
this._addToIndex(index, quad);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
index.lastUpdated = Date.now();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get optimizer statistics
|
|
304
|
+
* @returns {Object} Statistics
|
|
305
|
+
*/
|
|
306
|
+
getStats() {
|
|
307
|
+
const cacheHitRate =
|
|
308
|
+
this.stats.totalQueries > 0 ? this.stats.cacheHits / this.stats.totalQueries : 0;
|
|
309
|
+
|
|
310
|
+
const indexHitRate =
|
|
311
|
+
this.stats.totalQueries > 0 ? this.stats.indexHits / this.stats.totalQueries : 0;
|
|
312
|
+
|
|
313
|
+
// Get LRU cache statistics
|
|
314
|
+
const lruStats = {
|
|
315
|
+
size: this.cache.size,
|
|
316
|
+
maxSize: this.config.maxCacheSize,
|
|
317
|
+
itemCount: this.cache.size,
|
|
318
|
+
// LRU cache provides automatic eviction tracking
|
|
319
|
+
calculatedSize: this.cache.calculatedSize || 0,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
config: this.config,
|
|
324
|
+
cache: {
|
|
325
|
+
...lruStats,
|
|
326
|
+
hitRate: cacheHitRate,
|
|
327
|
+
hits: this.stats.cacheHits,
|
|
328
|
+
misses: this.stats.cacheMisses,
|
|
329
|
+
efficiency: cacheHitRate * 100, // Percentage
|
|
330
|
+
},
|
|
331
|
+
indexes: {
|
|
332
|
+
count: this.indexes.size,
|
|
333
|
+
hitRate: indexHitRate,
|
|
334
|
+
hits: this.stats.indexHits,
|
|
335
|
+
misses: this.stats.indexMisses,
|
|
336
|
+
},
|
|
337
|
+
optimization: {
|
|
338
|
+
deltaOptimizations: this.stats.deltaOptimizations,
|
|
339
|
+
totalQueries: this.stats.totalQueries,
|
|
340
|
+
averageCacheHitRate: cacheHitRate,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Clear all caches and indexes
|
|
347
|
+
*/
|
|
348
|
+
clear() {
|
|
349
|
+
// Clear query plans with deep cleanup
|
|
350
|
+
for (const plan of this.queryPlans.values()) {
|
|
351
|
+
if (plan.plan && plan.plan.operations) {
|
|
352
|
+
plan.plan.operations.length = 0;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
this.queryPlans.clear();
|
|
356
|
+
|
|
357
|
+
// Clear indexes with deep cleanup
|
|
358
|
+
for (const index of this.indexes.values()) {
|
|
359
|
+
if (index.data && typeof index.data.clear === 'function') {
|
|
360
|
+
index.data.clear();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
this.indexes.clear();
|
|
364
|
+
|
|
365
|
+
// Clear cache
|
|
366
|
+
this.cache.clear();
|
|
367
|
+
|
|
368
|
+
// Reset stats
|
|
369
|
+
this.stats = {
|
|
370
|
+
cacheHits: 0,
|
|
371
|
+
cacheMisses: 0,
|
|
372
|
+
indexHits: 0,
|
|
373
|
+
indexMisses: 0,
|
|
374
|
+
deltaOptimizations: 0,
|
|
375
|
+
totalQueries: 0,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Cleanup query optimizer resources
|
|
381
|
+
*/
|
|
382
|
+
async cleanup() {
|
|
383
|
+
this.clear();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Hash a query
|
|
388
|
+
* @param {string} query - Query string
|
|
389
|
+
* @param {string} type - Query type
|
|
390
|
+
* @returns {string} Query hash
|
|
391
|
+
* @private
|
|
392
|
+
*/
|
|
393
|
+
_hashQuery(query, type) {
|
|
394
|
+
const content = `${type}:${query}`;
|
|
395
|
+
return bytesToHex(sha3_256(utf8ToBytes(content)));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get cached query plan
|
|
400
|
+
* @param {string} queryHash - Query hash
|
|
401
|
+
* @returns {Object|null} Cached plan or null
|
|
402
|
+
* @private
|
|
403
|
+
* @deprecated Use LRU cache directly via this.cache.get()
|
|
404
|
+
*/
|
|
405
|
+
_getCachedPlan(queryHash) {
|
|
406
|
+
return this.cache.get(queryHash) || null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Cache a query plan
|
|
411
|
+
* @param {Object} plan - Query plan
|
|
412
|
+
* @private
|
|
413
|
+
* @deprecated Use LRU cache directly via this.cache.set()
|
|
414
|
+
*/
|
|
415
|
+
_cachePlan(plan) {
|
|
416
|
+
// LRU cache handles eviction automatically
|
|
417
|
+
this.cache.set(plan.hash, plan);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Create a query plan
|
|
422
|
+
* @param {string} query - Query string
|
|
423
|
+
* @param {string} type - Query type
|
|
424
|
+
* @param {string} queryHash - Query hash
|
|
425
|
+
* @param {Store} graph - RDF graph
|
|
426
|
+
* @param {Object} [delta] - Delta for optimization
|
|
427
|
+
* @returns {Promise<Object>} Query plan
|
|
428
|
+
* @private
|
|
429
|
+
*/
|
|
430
|
+
async _createQueryPlan(query, type, queryHash, graph, delta) {
|
|
431
|
+
const plan = {
|
|
432
|
+
id: randomUUID(),
|
|
433
|
+
query,
|
|
434
|
+
type,
|
|
435
|
+
hash: queryHash,
|
|
436
|
+
plan: {
|
|
437
|
+
operations: [],
|
|
438
|
+
estimatedCost: 0,
|
|
439
|
+
estimatedRows: 0,
|
|
440
|
+
indexes: [],
|
|
441
|
+
},
|
|
442
|
+
createdAt: Date.now(),
|
|
443
|
+
lastUsed: Date.now(),
|
|
444
|
+
hitCount: 0,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// Analyze query and create execution plan
|
|
448
|
+
switch (type) {
|
|
449
|
+
case 'sparql-ask':
|
|
450
|
+
plan.plan = await this._analyzeSparqlAsk(query, graph, delta);
|
|
451
|
+
break;
|
|
452
|
+
case 'sparql-select':
|
|
453
|
+
plan.plan = await this._analyzeSparqlSelect(query, graph, delta);
|
|
454
|
+
break;
|
|
455
|
+
case 'shacl':
|
|
456
|
+
plan.plan = await this._analyzeShacl(query, graph, delta);
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return plan;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Analyze SPARQL ASK query
|
|
465
|
+
* @param {string} query - Query string
|
|
466
|
+
* @param {Store} graph - RDF graph
|
|
467
|
+
* @param {Object} [delta] - Delta for optimization
|
|
468
|
+
* @returns {Promise<Object>} Execution plan
|
|
469
|
+
* @private
|
|
470
|
+
*/
|
|
471
|
+
async _analyzeSparqlAsk(query, graph, delta) {
|
|
472
|
+
const plan = {
|
|
473
|
+
operations: [],
|
|
474
|
+
estimatedCost: 0,
|
|
475
|
+
estimatedRows: 0,
|
|
476
|
+
indexes: [],
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// Simple analysis - in production this would be more sophisticated
|
|
480
|
+
const operations = this._parseSparqlOperations(query);
|
|
481
|
+
|
|
482
|
+
for (const op of operations) {
|
|
483
|
+
const cost = this._estimateOperationCost(op, graph);
|
|
484
|
+
const selectivity = this._estimateSelectivity(op, graph);
|
|
485
|
+
|
|
486
|
+
plan.operations.push({
|
|
487
|
+
type: op.type,
|
|
488
|
+
cost,
|
|
489
|
+
selectivity,
|
|
490
|
+
dependencies: op.dependencies,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
plan.estimatedCost += cost;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Add delta-aware optimizations
|
|
497
|
+
if (delta) {
|
|
498
|
+
plan.deltaAware = this._createDeltaAwarePlan(operations, delta);
|
|
499
|
+
this.stats.deltaOptimizations++;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return plan;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Analyze SPARQL SELECT query
|
|
507
|
+
* @param {string} query - Query string
|
|
508
|
+
* @param {Store} graph - RDF graph
|
|
509
|
+
* @param {Object} [delta] - Delta for optimization
|
|
510
|
+
* @returns {Promise<Object>} Execution plan
|
|
511
|
+
* @private
|
|
512
|
+
*/
|
|
513
|
+
async _analyzeSparqlSelect(query, graph, delta) {
|
|
514
|
+
// Similar to ASK but with result estimation
|
|
515
|
+
const plan = await this._analyzeSparqlAsk(query, graph, delta);
|
|
516
|
+
plan.estimatedRows = this._estimateResultRows(query, graph);
|
|
517
|
+
return plan;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Analyze SHACL validation
|
|
522
|
+
* @param {string} query - SHACL shapes
|
|
523
|
+
* @param {Store} graph - RDF graph
|
|
524
|
+
* @param {Object} [delta] - Delta for optimization
|
|
525
|
+
* @returns {Promise<Object>} Execution plan
|
|
526
|
+
* @private
|
|
527
|
+
*/
|
|
528
|
+
async _analyzeShacl(query, graph, _delta) {
|
|
529
|
+
const plan = {
|
|
530
|
+
operations: [],
|
|
531
|
+
estimatedCost: 0,
|
|
532
|
+
estimatedRows: 0,
|
|
533
|
+
indexes: [],
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// SHACL-specific analysis
|
|
537
|
+
const shapes = this._parseShaclShapes(query);
|
|
538
|
+
|
|
539
|
+
for (const shape of shapes) {
|
|
540
|
+
const cost = this._estimateShaclCost(shape, graph);
|
|
541
|
+
plan.operations.push({
|
|
542
|
+
type: 'shacl-validation',
|
|
543
|
+
cost,
|
|
544
|
+
selectivity: 0.1, // SHACL typically has low selectivity
|
|
545
|
+
dependencies: [],
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
plan.estimatedCost += cost;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return plan;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Execute delta-aware query
|
|
556
|
+
* @param {Object} plan - Query plan
|
|
557
|
+
* @param {Store} graph - RDF graph
|
|
558
|
+
* @param {Object} delta - Delta
|
|
559
|
+
* @returns {Promise<any>} Query result
|
|
560
|
+
* @private
|
|
561
|
+
*/
|
|
562
|
+
async _executeDeltaAware(plan, graph, delta) {
|
|
563
|
+
// Use delta information to optimize execution
|
|
564
|
+
const affectedEntities = this._extractAffectedEntities(delta);
|
|
565
|
+
|
|
566
|
+
// Check if query can be optimized based on delta
|
|
567
|
+
if (plan.plan.deltaAware) {
|
|
568
|
+
return this._executeOptimizedDeltaAware(plan, graph, delta, affectedEntities);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Fall back to standard execution
|
|
572
|
+
return this._executeStandard(plan, graph);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Execute standard query
|
|
577
|
+
* @param {Object} plan - Query plan
|
|
578
|
+
* @param {Store} graph - RDF graph
|
|
579
|
+
* @returns {Promise<any>} Query result
|
|
580
|
+
* @private
|
|
581
|
+
*/
|
|
582
|
+
async _executeStandard(_plan, _graph) {
|
|
583
|
+
// Standard query execution
|
|
584
|
+
// This would integrate with the actual query engine
|
|
585
|
+
return { result: 'standard execution' };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Create predicate index
|
|
590
|
+
* @param {Array} quads - RDF quads
|
|
591
|
+
* @returns {Object} Index
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
_createPredicateIndex(quads) {
|
|
595
|
+
const index = new Map();
|
|
596
|
+
|
|
597
|
+
for (const quad of quads) {
|
|
598
|
+
const predicate = quad.predicate.value;
|
|
599
|
+
if (!index.has(predicate)) {
|
|
600
|
+
index.set(predicate, []);
|
|
601
|
+
}
|
|
602
|
+
index.get(predicate).push(quad);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
id: randomUUID(),
|
|
607
|
+
name: 'predicate_index',
|
|
608
|
+
type: 'predicate',
|
|
609
|
+
fields: ['predicate'],
|
|
610
|
+
selectivity: this._calculateSelectivity(index),
|
|
611
|
+
size: index.size,
|
|
612
|
+
createdAt: Date.now(),
|
|
613
|
+
lastUpdated: Date.now(),
|
|
614
|
+
data: index,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Create subject index
|
|
620
|
+
* @param {Array} quads - RDF quads
|
|
621
|
+
* @returns {Object} Index
|
|
622
|
+
* @private
|
|
623
|
+
*/
|
|
624
|
+
_createSubjectIndex(quads) {
|
|
625
|
+
const index = new Map();
|
|
626
|
+
|
|
627
|
+
for (const quad of quads) {
|
|
628
|
+
const subject = quad.subject.value;
|
|
629
|
+
if (!index.has(subject)) {
|
|
630
|
+
index.set(subject, []);
|
|
631
|
+
}
|
|
632
|
+
index.get(subject).push(quad);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
id: randomUUID(),
|
|
637
|
+
name: 'subject_index',
|
|
638
|
+
type: 'subject',
|
|
639
|
+
fields: ['subject'],
|
|
640
|
+
selectivity: this._calculateSelectivity(index),
|
|
641
|
+
size: index.size,
|
|
642
|
+
createdAt: Date.now(),
|
|
643
|
+
lastUpdated: Date.now(),
|
|
644
|
+
data: index,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Create object index
|
|
650
|
+
* @param {Array} quads - RDF quads
|
|
651
|
+
* @returns {Object} Index
|
|
652
|
+
* @private
|
|
653
|
+
*/
|
|
654
|
+
_createObjectIndex(quads) {
|
|
655
|
+
const index = new Map();
|
|
656
|
+
|
|
657
|
+
for (const quad of quads) {
|
|
658
|
+
const object = quad.object.value;
|
|
659
|
+
if (!index.has(object)) {
|
|
660
|
+
index.set(object, []);
|
|
661
|
+
}
|
|
662
|
+
index.get(object).push(quad);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
id: randomUUID(),
|
|
667
|
+
name: 'object_index',
|
|
668
|
+
type: 'object',
|
|
669
|
+
fields: ['object'],
|
|
670
|
+
selectivity: this._calculateSelectivity(index),
|
|
671
|
+
size: index.size,
|
|
672
|
+
createdAt: Date.now(),
|
|
673
|
+
lastUpdated: Date.now(),
|
|
674
|
+
data: index,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Create composite indexes
|
|
680
|
+
* @param {Array} quads - RDF quads
|
|
681
|
+
* @returns {Array} Indexes
|
|
682
|
+
* @private
|
|
683
|
+
*/
|
|
684
|
+
_createCompositeIndexes(quads) {
|
|
685
|
+
const indexes = [];
|
|
686
|
+
|
|
687
|
+
// SPO index (most common pattern)
|
|
688
|
+
const spoIndex = new Map();
|
|
689
|
+
for (const quad of quads) {
|
|
690
|
+
const key = `${quad.subject.value}:${quad.predicate.value}:${quad.object.value}`;
|
|
691
|
+
if (!spoIndex.has(key)) {
|
|
692
|
+
spoIndex.set(key, []);
|
|
693
|
+
}
|
|
694
|
+
spoIndex.get(key).push(quad);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
indexes.push({
|
|
698
|
+
id: randomUUID(),
|
|
699
|
+
name: 'spo_index',
|
|
700
|
+
type: 'composite',
|
|
701
|
+
fields: ['subject', 'predicate', 'object'],
|
|
702
|
+
selectivity: this._calculateSelectivity(spoIndex),
|
|
703
|
+
size: spoIndex.size,
|
|
704
|
+
createdAt: Date.now(),
|
|
705
|
+
lastUpdated: Date.now(),
|
|
706
|
+
data: spoIndex,
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
return indexes;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Calculate index selectivity
|
|
714
|
+
* @param {Map} index - Index data
|
|
715
|
+
* @returns {number} Selectivity
|
|
716
|
+
* @private
|
|
717
|
+
*/
|
|
718
|
+
_calculateSelectivity(index) {
|
|
719
|
+
const totalEntries = Array.from(index.values()).reduce(
|
|
720
|
+
(sum, entries) => sum + entries.length,
|
|
721
|
+
0
|
|
722
|
+
);
|
|
723
|
+
const uniqueKeys = index.size;
|
|
724
|
+
return uniqueKeys / totalEntries;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Parse SPARQL operations
|
|
729
|
+
* @param {string} query - SPARQL query
|
|
730
|
+
* @returns {Array} Operations
|
|
731
|
+
* @private
|
|
732
|
+
*/
|
|
733
|
+
_parseSparqlOperations(query) {
|
|
734
|
+
// Simple parsing - in production this would use a proper SPARQL parser
|
|
735
|
+
const operations = [];
|
|
736
|
+
|
|
737
|
+
if (query.includes('WHERE')) {
|
|
738
|
+
operations.push({
|
|
739
|
+
type: 'where-clause',
|
|
740
|
+
cost: 100,
|
|
741
|
+
dependencies: [],
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (query.includes('FILTER')) {
|
|
746
|
+
operations.push({
|
|
747
|
+
type: 'filter',
|
|
748
|
+
cost: 50,
|
|
749
|
+
dependencies: ['where-clause'],
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return operations;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Estimate operation cost
|
|
758
|
+
* @param {Object} operation - Operation
|
|
759
|
+
* @param {Store} graph - RDF graph
|
|
760
|
+
* @returns {number} Estimated cost
|
|
761
|
+
* @private
|
|
762
|
+
*/
|
|
763
|
+
_estimateOperationCost(operation, graph) {
|
|
764
|
+
const baseCost = operation.cost || 100;
|
|
765
|
+
const graphSize = graph.size;
|
|
766
|
+
return baseCost * Math.log(graphSize + 1);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Estimate operation selectivity
|
|
771
|
+
* @param {Object} operation - Operation
|
|
772
|
+
* @param {Store} graph - RDF graph
|
|
773
|
+
* @returns {number} Estimated selectivity
|
|
774
|
+
* @private
|
|
775
|
+
*/
|
|
776
|
+
_estimateSelectivity(operation, _graph) {
|
|
777
|
+
// Simple estimation - in production this would be more sophisticated
|
|
778
|
+
switch (operation.type) {
|
|
779
|
+
case 'where-clause':
|
|
780
|
+
return 0.1;
|
|
781
|
+
case 'filter':
|
|
782
|
+
return 0.5;
|
|
783
|
+
default:
|
|
784
|
+
return 0.3;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Create delta-aware plan
|
|
790
|
+
* @param {Array} operations - Operations
|
|
791
|
+
* @param {Object} delta - Delta
|
|
792
|
+
* @returns {Object} Delta-aware plan
|
|
793
|
+
* @private
|
|
794
|
+
*/
|
|
795
|
+
_createDeltaAwarePlan(operations, delta) {
|
|
796
|
+
return {
|
|
797
|
+
affectedEntities: this._extractAffectedEntities(delta),
|
|
798
|
+
optimizedOperations: operations.filter(op => this._isOperationAffected(op, delta)),
|
|
799
|
+
skipFullScan: delta.additions.length + delta.removals.length < 100,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Extract affected entities from delta
|
|
805
|
+
* @param {Object} delta - Delta
|
|
806
|
+
* @returns {Object} Affected entities
|
|
807
|
+
* @private
|
|
808
|
+
*/
|
|
809
|
+
_extractAffectedEntities(delta) {
|
|
810
|
+
const affected = {
|
|
811
|
+
subjects: new Set(),
|
|
812
|
+
predicates: new Set(),
|
|
813
|
+
objects: new Set(),
|
|
814
|
+
graphs: new Set(),
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
for (const quad of [...delta.additions, ...delta.removals]) {
|
|
818
|
+
affected.subjects.add(quad.subject.value);
|
|
819
|
+
affected.predicates.add(quad.predicate.value);
|
|
820
|
+
affected.objects.add(quad.object.value);
|
|
821
|
+
if (quad.graph) {
|
|
822
|
+
affected.graphs.add(quad.graph.value);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return affected;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Check if operation is affected by delta
|
|
831
|
+
* @param {Object} operation - Operation
|
|
832
|
+
* @param {Object} delta - Delta
|
|
833
|
+
* @returns {boolean} Is affected
|
|
834
|
+
* @private
|
|
835
|
+
*/
|
|
836
|
+
_isOperationAffected(_operation, _delta) {
|
|
837
|
+
// Simple check - in production this would analyze the operation
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Estimate result rows
|
|
843
|
+
* @param {string} query - Query
|
|
844
|
+
* @param {Store} graph - RDF graph
|
|
845
|
+
* @returns {number} Estimated rows
|
|
846
|
+
* @private
|
|
847
|
+
*/
|
|
848
|
+
_estimateResultRows(query, graph) {
|
|
849
|
+
// Simple estimation
|
|
850
|
+
return Math.min(graph.size * 0.1, 1000);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Estimate SHACL cost
|
|
855
|
+
* @param {Object} shape - SHACL shape
|
|
856
|
+
* @param {Store} graph - RDF graph
|
|
857
|
+
* @returns {number} Estimated cost
|
|
858
|
+
* @private
|
|
859
|
+
*/
|
|
860
|
+
_estimateShaclCost(shape, graph) {
|
|
861
|
+
return graph.size * 0.5; // SHACL validation is expensive
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Parse SHACL shapes
|
|
866
|
+
* @param {string} query - SHACL shapes
|
|
867
|
+
* @returns {Array} Shapes
|
|
868
|
+
* @private
|
|
869
|
+
*/
|
|
870
|
+
_parseShaclShapes(_query) {
|
|
871
|
+
// Simple parsing - in production this would use a proper SHACL parser
|
|
872
|
+
return [{ type: 'shacl-shape', cost: 100 }];
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Add quad to index
|
|
877
|
+
* @param {Object} index - Index
|
|
878
|
+
* @param {Object} quad - RDF quad
|
|
879
|
+
* @private
|
|
880
|
+
*/
|
|
881
|
+
_addToIndex(index, quad) {
|
|
882
|
+
if (!index.data || !(index.data instanceof Map)) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const key = this._getIndexKey(index, quad);
|
|
887
|
+
if (!key) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (!index.data.has(key)) {
|
|
892
|
+
index.data.set(key, []);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const quads = index.data.get(key);
|
|
896
|
+
// Check if quad already exists to avoid duplicates
|
|
897
|
+
const exists = quads.some(
|
|
898
|
+
q =>
|
|
899
|
+
q.subject.value === quad.subject.value &&
|
|
900
|
+
q.predicate.value === quad.predicate.value &&
|
|
901
|
+
q.object.value === quad.object.value &&
|
|
902
|
+
(q.graph?.value || null) === (quad.graph?.value || null)
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
if (!exists) {
|
|
906
|
+
quads.push(quad);
|
|
907
|
+
index.size = index.data.size;
|
|
908
|
+
index.lastUpdated = Date.now();
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Remove quad from index
|
|
914
|
+
* @param {Object} index - Index
|
|
915
|
+
* @param {Object} quad - RDF quad
|
|
916
|
+
* @private
|
|
917
|
+
*/
|
|
918
|
+
_removeFromIndex(index, quad) {
|
|
919
|
+
if (!index.data || !(index.data instanceof Map)) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const key = this._getIndexKey(index, quad);
|
|
924
|
+
if (!key || !index.data.has(key)) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const quads = index.data.get(key);
|
|
929
|
+
const indexToRemove = quads.findIndex(
|
|
930
|
+
q =>
|
|
931
|
+
q.subject.value === quad.subject.value &&
|
|
932
|
+
q.predicate.value === quad.predicate.value &&
|
|
933
|
+
q.object.value === quad.object.value &&
|
|
934
|
+
(q.graph?.value || null) === (quad.graph?.value || null)
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
if (indexToRemove !== -1) {
|
|
938
|
+
quads.splice(indexToRemove, 1);
|
|
939
|
+
if (quads.length === 0) {
|
|
940
|
+
index.data.delete(key);
|
|
941
|
+
}
|
|
942
|
+
index.size = index.data.size;
|
|
943
|
+
index.lastUpdated = Date.now();
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Get index key for a quad based on index type
|
|
949
|
+
* @param {Object} index - Index
|
|
950
|
+
* @param {Object} quad - RDF quad
|
|
951
|
+
* @returns {string|null} Index key
|
|
952
|
+
* @private
|
|
953
|
+
*/
|
|
954
|
+
_getIndexKey(index, quad) {
|
|
955
|
+
switch (index.type) {
|
|
956
|
+
case 'predicate':
|
|
957
|
+
return quad.predicate.value;
|
|
958
|
+
case 'subject':
|
|
959
|
+
return quad.subject.value;
|
|
960
|
+
case 'object':
|
|
961
|
+
return quad.object.value;
|
|
962
|
+
case 'graph':
|
|
963
|
+
return quad.graph?.value || null;
|
|
964
|
+
case 'composite':
|
|
965
|
+
// For composite indexes, use the first field
|
|
966
|
+
if (index.fields && index.fields.length > 0) {
|
|
967
|
+
const field = index.fields[0];
|
|
968
|
+
if (field === 'subject') return quad.subject.value;
|
|
969
|
+
if (field === 'predicate') return quad.predicate.value;
|
|
970
|
+
if (field === 'object') return quad.object.value;
|
|
971
|
+
if (field === 'graph') return quad.graph?.value || null;
|
|
972
|
+
}
|
|
973
|
+
return null;
|
|
974
|
+
default:
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Execute optimized delta-aware query
|
|
981
|
+
* @param {Object} plan - Query plan
|
|
982
|
+
* @param {Store} graph - RDF graph
|
|
983
|
+
* @param {Object} delta - Delta
|
|
984
|
+
* @param {Object} affectedEntities - Affected entities
|
|
985
|
+
* @returns {Promise<any>} Query result
|
|
986
|
+
* @private
|
|
987
|
+
*/
|
|
988
|
+
async _executeOptimizedDeltaAware(plan, graph, delta, affectedEntities) {
|
|
989
|
+
// Use delta information to optimize execution
|
|
990
|
+
// Only re-execute if affected entities match query patterns
|
|
991
|
+
// Use provided affectedEntities if available, otherwise compute from delta
|
|
992
|
+
const affectedSubjects = affectedEntities?.subjects
|
|
993
|
+
? new Set(affectedEntities.subjects)
|
|
994
|
+
: new Set(
|
|
995
|
+
delta.additions
|
|
996
|
+
.concat(delta.removals)
|
|
997
|
+
.map(q => q.subject?.value)
|
|
998
|
+
.filter(Boolean)
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
// Check if query would be affected by delta
|
|
1002
|
+
const queryAffected = plan.plan.operations.some(op => {
|
|
1003
|
+
// Simple heuristic: if operation references affected subjects
|
|
1004
|
+
if (op.patterns) {
|
|
1005
|
+
return op.patterns.some(pattern => {
|
|
1006
|
+
if (pattern.subject && affectedSubjects.has(pattern.subject)) {
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
return false;
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
return false;
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
if (!queryAffected && delta.additions.length === 0 && delta.removals.length === 0) {
|
|
1016
|
+
// No changes, return cached result if available
|
|
1017
|
+
this.stats.deltaOptimizations++;
|
|
1018
|
+
return { result: 'unchanged', optimized: true };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Execute query with delta-aware optimization
|
|
1022
|
+
// For removals, filter them out; for additions, include them
|
|
1023
|
+
const result = await this._executeStandard(plan, graph);
|
|
1024
|
+
|
|
1025
|
+
// Mark as delta-optimized
|
|
1026
|
+
this.stats.deltaOptimizations++;
|
|
1027
|
+
|
|
1028
|
+
return {
|
|
1029
|
+
...result,
|
|
1030
|
+
optimized: true,
|
|
1031
|
+
deltaApplied: {
|
|
1032
|
+
additions: delta.additions.length,
|
|
1033
|
+
removals: delta.removals.length,
|
|
1034
|
+
},
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Create a query optimizer instance
|
|
1041
|
+
* @param {Object} [config] - Configuration
|
|
1042
|
+
* @returns {QueryOptimizer} Query optimizer
|
|
1043
|
+
*/
|
|
1044
|
+
export function createQueryOptimizer(config = {}) {
|
|
1045
|
+
return new QueryOptimizer(config);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Default query optimizer instance
|
|
1050
|
+
*/
|
|
1051
|
+
export const defaultQueryOptimizer = createQueryOptimizer();
|