eyeleng 1.0.13 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -5
- package/dist/browser/eyeleng.browser.js +637 -14
- package/eyeleng.js +685 -24
- package/package.json +1 -1
- package/reports/w3c-shacl12-rules-earl.ttl +89 -89
- package/src/api.js +4 -1
- package/src/backward.js +490 -0
- package/src/cli.js +48 -10
- package/src/engine.js +68 -5
- package/src/query.js +69 -7
- package/src/store.js +2 -0
- package/test/api.test.js +127 -1
- package/test/cli.test.js +11 -2
package/src/engine.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { TripleStore, bindingKey } = require('./store.js');
|
|
3
|
+
const { TripleStore, bindingKey, instantiateTerm } = require('./store.js');
|
|
4
4
|
const { tripleKey, termKey, termEquals, blankNode, tripleTerm } = require('./term.js');
|
|
5
5
|
const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
|
|
6
6
|
const { analyze } = require('./analyze.js');
|
|
7
|
+
const { BackwardProver, preferredBackwardPredicates, ruleIsBackwardOriented } = require('./backward.js');
|
|
7
8
|
|
|
8
9
|
function evaluate(program, options = {}) {
|
|
9
10
|
const maxIterations = options.maxIterations ?? 10000;
|
|
@@ -19,6 +20,7 @@ function evaluate(program, options = {}) {
|
|
|
19
20
|
applications: 0,
|
|
20
21
|
added: 0,
|
|
21
22
|
runOnce: !!rule.runOnce,
|
|
23
|
+
backward: false,
|
|
22
24
|
}));
|
|
23
25
|
|
|
24
26
|
const analysis = options.analysis || analyze(program, options);
|
|
@@ -35,6 +37,17 @@ function evaluate(program, options = {}) {
|
|
|
35
37
|
const relaxedRecursiveRunOnce = options.relaxedRecursion === false
|
|
36
38
|
? new Set()
|
|
37
39
|
: recursiveTermGenerationRuleIndexes(analysis);
|
|
40
|
+
const hybridBackwardPredicates = options.hybrid || options.backwardBodyCalls
|
|
41
|
+
? preferredBackwardPredicates(program, options)
|
|
42
|
+
: new Set();
|
|
43
|
+
const hybridBackwardRules = new Set();
|
|
44
|
+
if (hybridBackwardPredicates.size > 0) {
|
|
45
|
+
for (let ruleIndex = 0; ruleIndex < program.rules.length; ruleIndex += 1) {
|
|
46
|
+
if (ruleIsBackwardOriented(program.rules[ruleIndex], hybridBackwardPredicates)) hybridBackwardRules.add(ruleIndex);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const hybridStats = hybridBackwardPredicates.size > 0 ? emptyBackwardStats() : null;
|
|
50
|
+
for (const ruleIndex of hybridBackwardRules) perRule[ruleIndex].backward = true;
|
|
38
51
|
const baseContext = {
|
|
39
52
|
...evalOptions,
|
|
40
53
|
maxIterations,
|
|
@@ -46,12 +59,16 @@ function evaluate(program, options = {}) {
|
|
|
46
59
|
iteration: 0,
|
|
47
60
|
startingIterations: 0,
|
|
48
61
|
recursiveLayer: false,
|
|
62
|
+
hybridBackwardPredicates,
|
|
63
|
+
hybridBackwardRules,
|
|
64
|
+
hybridStats,
|
|
49
65
|
};
|
|
50
66
|
|
|
51
67
|
for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {
|
|
52
68
|
const layer = layerIndexes[layerIndex];
|
|
53
|
-
const
|
|
54
|
-
const
|
|
69
|
+
const forwardLayer = hybridBackwardRules.size > 0 ? layer.filter((ruleIndex) => !hybridBackwardRules.has(ruleIndex)) : layer;
|
|
70
|
+
const ordinary = forwardLayer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
|
|
71
|
+
const runOnce = forwardLayer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
|
|
55
72
|
|
|
56
73
|
if (runOnce.length > 0) {
|
|
57
74
|
iterations += 1;
|
|
@@ -84,6 +101,7 @@ function evaluate(program, options = {}) {
|
|
|
84
101
|
ruleApplications,
|
|
85
102
|
perRule,
|
|
86
103
|
trace,
|
|
104
|
+
hybridStats,
|
|
87
105
|
};
|
|
88
106
|
}
|
|
89
107
|
|
|
@@ -153,9 +171,10 @@ function applyRuleOnce(program, store, ruleIndex, context) {
|
|
|
153
171
|
const seenBindings = dedupeBindings ? new Set() : null;
|
|
154
172
|
const headBlankLabels = collectHeadBlankLabels(rule.head);
|
|
155
173
|
|
|
156
|
-
const
|
|
174
|
+
const bodyContext = prepareBodyContext(program, store, context);
|
|
175
|
+
const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple' && !shouldUseBackwardForTriple(rule.body[0].triple, {}, bodyContext)
|
|
157
176
|
? store.match(rule.body[0].triple, {})
|
|
158
|
-
: evaluateBodyStream(rule.body, store, {},
|
|
177
|
+
: evaluateBodyStream(rule.body, store, {}, bodyContext);
|
|
159
178
|
|
|
160
179
|
for (const binding of bodyBindings) {
|
|
161
180
|
if (seenBindings) {
|
|
@@ -188,9 +207,36 @@ function applyRuleOnce(program, store, ruleIndex, context) {
|
|
|
188
207
|
}
|
|
189
208
|
}
|
|
190
209
|
|
|
210
|
+
if (bodyContext.backwardProver && context.hybridStats) mergeBackwardStats(context.hybridStats, bodyContext.backwardProver.stats);
|
|
191
211
|
return { applications, added };
|
|
192
212
|
}
|
|
193
213
|
|
|
214
|
+
function prepareBodyContext(program, store, context) {
|
|
215
|
+
if (!context.hybridBackwardPredicates || context.hybridBackwardPredicates.size === 0) return context;
|
|
216
|
+
return {
|
|
217
|
+
...context,
|
|
218
|
+
backwardProver: new BackwardProver(program, {
|
|
219
|
+
...context,
|
|
220
|
+
store,
|
|
221
|
+
allowedPredicates: context.hybridBackwardPredicates,
|
|
222
|
+
}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function emptyBackwardStats() {
|
|
227
|
+
return { mode: 'hybrid', goals: 0, facts: 0, rules: 0, memoHits: 0, memoStores: 0, maxDepth: 0 };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function mergeBackwardStats(total, item) {
|
|
231
|
+
if (!total || !item) return;
|
|
232
|
+
total.goals += item.goals || 0;
|
|
233
|
+
total.facts += item.facts || 0;
|
|
234
|
+
total.rules += item.rules || 0;
|
|
235
|
+
total.memoHits += item.memoHits || 0;
|
|
236
|
+
total.memoStores += item.memoStores || 0;
|
|
237
|
+
total.maxDepth = Math.max(total.maxDepth || 0, item.maxDepth || 0);
|
|
238
|
+
}
|
|
239
|
+
|
|
194
240
|
function recursiveTermGenerationRuleIndexes(analysis) {
|
|
195
241
|
const out = new Set();
|
|
196
242
|
if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;
|
|
@@ -299,9 +345,20 @@ function* evaluateBodyStream(clauses, store, initialBinding = {}, options = {},
|
|
|
299
345
|
|
|
300
346
|
const clause = clauses[index];
|
|
301
347
|
if (clause.type === 'triple') {
|
|
348
|
+
const seen = new Set();
|
|
302
349
|
for (const matched of store.match(clause.triple, initialBinding)) {
|
|
350
|
+
const key = bindingKey(matched);
|
|
351
|
+
seen.add(key);
|
|
303
352
|
yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
|
|
304
353
|
}
|
|
354
|
+
if (shouldUseBackwardForTriple(clause.triple, initialBinding, options)) {
|
|
355
|
+
for (const matched of options.backwardProver.solveTriple(clause.triple, initialBinding)) {
|
|
356
|
+
const key = bindingKey(matched);
|
|
357
|
+
if (seen.has(key)) continue;
|
|
358
|
+
seen.add(key);
|
|
359
|
+
yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
305
362
|
return;
|
|
306
363
|
}
|
|
307
364
|
|
|
@@ -347,6 +404,12 @@ function* evaluateBodyStream(clauses, store, initialBinding = {}, options = {},
|
|
|
347
404
|
throw new Error(`Unsupported body clause ${clause.type}`);
|
|
348
405
|
}
|
|
349
406
|
|
|
407
|
+
function shouldUseBackwardForTriple(pattern, binding, options = {}) {
|
|
408
|
+
if (!options.backwardProver || !options.hybridBackwardPredicates || options.hybridBackwardPredicates.size === 0) return false;
|
|
409
|
+
const predicate = instantiateTerm(pattern.p, binding);
|
|
410
|
+
return !!(predicate && predicate.type === 'iri' && options.hybridBackwardPredicates.has(predicate.value));
|
|
411
|
+
}
|
|
412
|
+
|
|
350
413
|
function bodyHasAny(clauses, store, initialBinding, options) {
|
|
351
414
|
for (const _ of evaluateBodyStream(clauses, store, initialBinding, options)) return true;
|
|
352
415
|
return false;
|
package/src/query.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { parseQuery } = require('./parser.js');
|
|
4
4
|
const { TripleStore, bindingKey } = require('./store.js');
|
|
5
5
|
const { evaluateBody } = require('./engine.js');
|
|
6
|
+
const { backwardQuery, planBackwardQuery, preferredBackwardPredicates } = require('./backward.js');
|
|
6
7
|
|
|
7
8
|
function queryResult(result, querySpec, options = {}) {
|
|
8
9
|
const store = new TripleStore(result.closure || []);
|
|
@@ -13,28 +14,89 @@ function queryResult(result, querySpec, options = {}) {
|
|
|
13
14
|
prefixes: result.prefixes,
|
|
14
15
|
select,
|
|
15
16
|
bindings: projectBindings(bindings, select),
|
|
17
|
+
mode: options.queryMode === 'hybrid' || options.hybrid ? 'hybrid' : 'forward',
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
function queryProgram(program, querySpec, options = {}) {
|
|
22
|
+
const mode = options.queryMode || 'auto';
|
|
23
|
+
if (mode !== 'forward' && mode !== 'hybrid') {
|
|
24
|
+
const planned = planBackwardQuery(program, querySpec, options);
|
|
25
|
+
if (planned.ok) {
|
|
26
|
+
const result = backwardQuery(program, querySpec, options);
|
|
27
|
+
if (result.ok) {
|
|
28
|
+
const select = normalizeSelect(querySpec.select, result.bindings);
|
|
29
|
+
return {
|
|
30
|
+
baseIRI: program.baseIRI,
|
|
31
|
+
prefixes: program.prefixes,
|
|
32
|
+
select,
|
|
33
|
+
bindings: projectBindings(result.bindings, select),
|
|
34
|
+
mode: 'backward',
|
|
35
|
+
stats: result.stats,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (mode === 'backward') throw new Error(result.reason || 'Backward query failed');
|
|
39
|
+
} else if (mode === 'backward') {
|
|
40
|
+
throw new Error(`Backward query is not supported for this ruleset: ${planned.reason}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
function runQuery(source, querySource = null, options = {}) {
|
|
20
47
|
const { run, compile } = require('./api.js');
|
|
21
|
-
const { program, diagnostics } = compile(source, options);
|
|
22
|
-
const result = run(program, options);
|
|
23
|
-
result.diagnostics = diagnostics;
|
|
48
|
+
const { program, diagnostics, analysis } = compile(source, options);
|
|
24
49
|
|
|
25
50
|
let querySpec;
|
|
26
51
|
if (querySource) querySpec = parseQuery(querySource, { ...options, prefixes: program.prefixes, baseIRI: program.baseIRI });
|
|
27
52
|
else throw new Error('No query supplied. Use --query or --query-file with a raw body pattern.');
|
|
28
53
|
|
|
29
|
-
const
|
|
30
|
-
|
|
54
|
+
const direct = queryProgram(program, querySpec, options);
|
|
55
|
+
if (direct) {
|
|
56
|
+
return {
|
|
57
|
+
baseIRI: program.baseIRI,
|
|
58
|
+
version: program.version || null,
|
|
59
|
+
imports: program.imports || [],
|
|
60
|
+
prefixes: program.prefixes,
|
|
61
|
+
input: program.data.slice(),
|
|
62
|
+
inferred: [],
|
|
63
|
+
closure: program.data.slice(),
|
|
64
|
+
iterations: 0,
|
|
65
|
+
layers: [],
|
|
66
|
+
ruleApplications: 0,
|
|
67
|
+
perRule: [],
|
|
68
|
+
trace: [],
|
|
69
|
+
diagnostics,
|
|
70
|
+
analysis,
|
|
71
|
+
query: direct,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const runOptions = queryRunOptions(program, querySpec, options);
|
|
76
|
+
const result = run(program, runOptions);
|
|
77
|
+
result.diagnostics = diagnostics;
|
|
78
|
+
result.query = queryResult(result, querySpec, runOptions);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function queryRunOptions(program, querySpec, options = {}) {
|
|
83
|
+
if (shouldUseHybridForQuery(program, querySpec, options)) return { ...options, hybrid: true };
|
|
84
|
+
return options;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function shouldUseHybridForQuery(program, querySpec, options = {}) {
|
|
88
|
+
const mode = options.queryMode || 'auto';
|
|
89
|
+
if (options.hybrid || mode === 'hybrid') return true;
|
|
90
|
+
if (mode !== 'auto') return false;
|
|
91
|
+
if (!querySpec) return false;
|
|
92
|
+
return preferredBackwardPredicates(program, options).size > 0;
|
|
31
93
|
}
|
|
32
94
|
|
|
33
95
|
function normalizeSelect(select, bindings) {
|
|
34
96
|
if (select && select.length > 0) return select.slice();
|
|
35
97
|
const vars = new Set();
|
|
36
98
|
for (const binding of bindings) for (const key of Object.keys(binding)) vars.add(key);
|
|
37
|
-
return Array.from(vars).sort();
|
|
99
|
+
return Array.from(vars).sort().filter((name) => !name.includes('__b'));
|
|
38
100
|
}
|
|
39
101
|
|
|
40
102
|
function projectBindings(bindings, select) {
|
|
@@ -52,4 +114,4 @@ function projectBindings(bindings, select) {
|
|
|
52
114
|
return out;
|
|
53
115
|
}
|
|
54
116
|
|
|
55
|
-
module.exports = { runQuery, queryResult, parseQuery, normalizeSelect, projectBindings };
|
|
117
|
+
module.exports = { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery, parseQuery, normalizeSelect, projectBindings };
|
package/src/store.js
CHANGED
|
@@ -8,6 +8,7 @@ class TripleStore {
|
|
|
8
8
|
this.byPredicate = new Map();
|
|
9
9
|
this.byPredicateSubject = new Map();
|
|
10
10
|
this.byPredicateObject = new Map();
|
|
11
|
+
this.version = 0;
|
|
11
12
|
for (const triple of triples) this.add(triple);
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -22,6 +23,7 @@ class TripleStore {
|
|
|
22
23
|
addIndex(this.byPredicate, predicate, key, normalized);
|
|
23
24
|
addNestedIndex(this.byPredicateSubject, predicate, subject, key, normalized);
|
|
24
25
|
addNestedIndex(this.byPredicateObject, predicate, object, key, normalized);
|
|
26
|
+
this.version += 1;
|
|
25
27
|
return true;
|
|
26
28
|
}
|
|
27
29
|
|
package/test/api.test.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { test, main } = require('./harness.js').createHarness('API');
|
|
4
4
|
const assert = require('node:assert/strict');
|
|
5
|
-
const { parse, compile, run, runToString } = require('../src/index.js');
|
|
5
|
+
const { parse, compile, run, runToString, runQuery } = require('../src/index.js');
|
|
6
6
|
const { tripleKey } = require('../src/term.js');
|
|
7
7
|
|
|
8
8
|
test('parse reads prefixes, data, and rules', () => {
|
|
@@ -159,6 +159,114 @@ RULE { ?x :ancestorOf ?z } WHERE { ?x :parentOf ?y . ?y :ancestorOf ?z }
|
|
|
159
159
|
assert.match(output, /\?d = :carol/);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
test('backward query mode proves recursive derived predicates with tabling', () => {
|
|
165
|
+
const { runQuery, formatBindings } = require('../src/index.js');
|
|
166
|
+
const result = runQuery(`
|
|
167
|
+
PREFIX : <http://example/>
|
|
168
|
+
DATA { :alice :parentOf :bob . :bob :parentOf :carol . }
|
|
169
|
+
RULE { ?x :ancestorOf ?y } WHERE { ?x :parentOf ?y }
|
|
170
|
+
RULE { ?x :ancestorOf ?z } WHERE { ?x :parentOf ?y . ?y :ancestorOf ?z }
|
|
171
|
+
`, ':alice :ancestorOf ?d', { queryMode: 'backward' });
|
|
172
|
+
assert.equal(result.iterations, 0);
|
|
173
|
+
assert.equal(result.query.mode, 'backward');
|
|
174
|
+
assert.ok(result.query.stats.memoStores > 0);
|
|
175
|
+
const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
|
|
176
|
+
assert.match(output, /\?d = :bob/);
|
|
177
|
+
assert.match(output, /\?d = :carol/);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('backward query mode computes function-like rules just in time', () => {
|
|
181
|
+
const { runQuery, formatBindings } = require('../src/index.js');
|
|
182
|
+
const result = runQuery(`
|
|
183
|
+
PREFIX : <http://example/>
|
|
184
|
+
DATA { :alice :score 41 . :bob :score 2 . }
|
|
185
|
+
RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
|
|
186
|
+
`, '?who :nextScore ?score', { queryMode: 'backward' });
|
|
187
|
+
assert.equal(result.iterations, 0);
|
|
188
|
+
const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
|
|
189
|
+
assert.match(output, /\?score = 42; \?who = :alice/);
|
|
190
|
+
assert.match(output, /\?score = 3; \?who = :bob/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('auto query mode uses backward planning for supported rules', () => {
|
|
194
|
+
const { runQuery } = require('../src/index.js');
|
|
195
|
+
const result = runQuery(`
|
|
196
|
+
PREFIX : <http://example/>
|
|
197
|
+
DATA { :a :p :b . }
|
|
198
|
+
RULE { ?x :q ?y } WHERE { ?x :p ?y }
|
|
199
|
+
`, ':a :q ?y');
|
|
200
|
+
assert.equal(result.query.mode, 'backward');
|
|
201
|
+
assert.equal(result.iterations, 0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
test('hybrid execution proves function-like body predicates backward without materializing them', () => {
|
|
207
|
+
const result = run(`
|
|
208
|
+
PREFIX : <http://example/>
|
|
209
|
+
DATA { :alice :score 41 . :bob :score 2 . }
|
|
210
|
+
RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
|
|
211
|
+
RULE { ?x :ready true } WHERE { ?x :nextScore 42 }
|
|
212
|
+
`, { hybrid: true });
|
|
213
|
+
const keys = result.closure.map(tripleKey).join('\n');
|
|
214
|
+
assert.match(keys, /ready/);
|
|
215
|
+
assert.doesNotMatch(keys, /nextScore/);
|
|
216
|
+
assert.equal(result.perRule[0].applications, 0);
|
|
217
|
+
assert.equal(result.perRule[0].backward, true);
|
|
218
|
+
assert.ok(result.hybridStats.goals > 0);
|
|
219
|
+
assert.ok(result.hybridStats.rules > 0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('hybrid query mode runs forward rules with backward body calls', () => {
|
|
223
|
+
const { runQuery, formatBindings } = require('../src/index.js');
|
|
224
|
+
const result = runQuery(`
|
|
225
|
+
PREFIX : <http://example/>
|
|
226
|
+
DATA { :alice :score 41 . :bob :score 2 . }
|
|
227
|
+
RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
|
|
228
|
+
RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
|
|
229
|
+
`, '?who :ready ?score', { queryMode: 'hybrid' });
|
|
230
|
+
assert.equal(result.query.mode, 'hybrid');
|
|
231
|
+
assert.ok(result.hybridStats.goals > 0);
|
|
232
|
+
const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
|
|
233
|
+
assert.match(output, /\?score = 42; \?who = :alice/);
|
|
234
|
+
assert.doesNotMatch(output, /bob/);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('auto query mode uses pure backward planning when unsupported rules are irrelevant', () => {
|
|
238
|
+
const { runQuery, formatBindings } = require('../src/index.js');
|
|
239
|
+
const result = runQuery(`
|
|
240
|
+
PREFIX : <http://example/>
|
|
241
|
+
DATA { :alice :score 41 . :bob :score 2 . :seed :seen :x . }
|
|
242
|
+
RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
|
|
243
|
+
RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
|
|
244
|
+
RULE { [] :unrelated :x } WHERE { :seed :seen :x }
|
|
245
|
+
`, '?who :ready ?score');
|
|
246
|
+
assert.equal(result.query.mode, 'backward');
|
|
247
|
+
assert.ok(result.query.stats.goals > 0);
|
|
248
|
+
const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
|
|
249
|
+
assert.match(output, /\?score = 42; \?who = :alice/);
|
|
250
|
+
assert.doesNotMatch(output, /bob/);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('auto query mode falls back to hybrid when a demanded predicate has unsupported rules', () => {
|
|
254
|
+
const { runQuery, formatBindings } = require('../src/index.js');
|
|
255
|
+
const result = runQuery(`
|
|
256
|
+
PREFIX : <http://example/>
|
|
257
|
+
DATA { :alice :score 41 . :bob :score 2 . :seed :seen :x . }
|
|
258
|
+
RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
|
|
259
|
+
RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
|
|
260
|
+
RULE { [] :ready 999 } WHERE { :seed :seen :x }
|
|
261
|
+
`, '?who :ready ?score');
|
|
262
|
+
assert.equal(result.query.mode, 'hybrid');
|
|
263
|
+
assert.ok(result.hybridStats.goals > 0);
|
|
264
|
+
assert.equal(result.perRule[0].backward, true);
|
|
265
|
+
assert.doesNotMatch(result.closure.map(tripleKey).join('\n'), /nextScore/);
|
|
266
|
+
const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
|
|
267
|
+
assert.match(output, /\?score = 42; \?who = :alice/);
|
|
268
|
+
});
|
|
269
|
+
|
|
162
270
|
test('parseQuery accepts raw body text and rejects non-SRL QUERY/SELECT syntax', () => {
|
|
163
271
|
const { parseQuery } = require('../src/index.js');
|
|
164
272
|
const raw = parseQuery('?x :p ?y', { prefixes: { '': 'http://example/' } });
|
|
@@ -599,4 +707,22 @@ RULE { ?x :p ?v1 } WHERE { ?x :p ?v . SET(?v1 := ?v + 1) }
|
|
|
599
707
|
`, { shacl12Conformance: true, strict: true }), /creates terms in a recursive dependency cycle/);
|
|
600
708
|
});
|
|
601
709
|
|
|
710
|
+
|
|
711
|
+
test('auto backward query ignores unsupported rules outside the demanded slice', () => {
|
|
712
|
+
const source = `
|
|
713
|
+
PREFIX : <http://example/>
|
|
714
|
+
DATA { :alice :parent :bob . :bob :parent :carol . :a :q :b . :b :q :c . }
|
|
715
|
+
RULE { ?x :ancestor ?y } WHERE { ?x :parent ?y }
|
|
716
|
+
RULE { ?x :ancestor ?z } WHERE { ?x :parent ?y . ?y :ancestor ?z }
|
|
717
|
+
RULE { ?x :twoStep ?y } WHERE { ?x :q/:q ?y }
|
|
718
|
+
`;
|
|
719
|
+
const result = runQuery(source, ':alice :ancestor ?who');
|
|
720
|
+
assert.equal(result.query.mode, 'backward');
|
|
721
|
+
assert.ok(result.query.stats.goals > 0);
|
|
722
|
+
assert.deepEqual(
|
|
723
|
+
result.query.bindings.map((binding) => binding.who.value).sort(),
|
|
724
|
+
['http://example/bob', 'http://example/carol'],
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
|
|
602
728
|
main();
|
package/test/cli.test.js
CHANGED
|
@@ -22,7 +22,7 @@ function readmeCliOptions() {
|
|
|
22
22
|
test('CLI help documents RDF Message Log flags', () => {
|
|
23
23
|
const text = help();
|
|
24
24
|
assert.match(text, /--rdf-messages\s+Parse input as an RDF Message Log/);
|
|
25
|
-
assert.
|
|
25
|
+
assert.doesNotMatch(text, /--stream-messages/);
|
|
26
26
|
assert.match(text, /--include-message-facts\s+Include payload facts while parsing RDF Message Logs/);
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -34,8 +34,17 @@ test('README CLI options stay in sync with --help', () => {
|
|
|
34
34
|
|
|
35
35
|
test('RDF Message Log flags are accepted by parseArgs', () => {
|
|
36
36
|
assert.equal(parseArgs(['--rdf-messages']).options.rdfMessages, true);
|
|
37
|
-
assert.equal(parseArgs(['--stream-messages']).options.rdfMessages, true);
|
|
38
37
|
assert.equal(parseArgs(['--include-message-facts']).options.includeMessageFacts, true);
|
|
39
38
|
});
|
|
40
39
|
|
|
40
|
+
test('query mode flag is accepted by parseArgs', () => {
|
|
41
|
+
assert.equal(parseArgs(['--query-mode', 'auto']).options.queryMode, 'auto');
|
|
42
|
+
assert.equal(parseArgs(['--query-mode', 'forward']).options.queryMode, 'forward');
|
|
43
|
+
assert.equal(parseArgs(['--query-mode', 'backward']).options.queryMode, 'backward');
|
|
44
|
+
assert.equal(parseArgs(['--hybrid']).options.hybrid, true);
|
|
45
|
+
assert.throws(() => parseArgs(['--query-mode', 'hybrid']), /--query-mode requires auto, forward, or backward/);
|
|
46
|
+
assert.throws(() => parseArgs(['--query-mode', 'sideways']), /--query-mode requires auto, forward, or backward/);
|
|
47
|
+
assert.throws(() => parseArgs(['--stream-messages']), /Unknown option --stream-messages/);
|
|
48
|
+
});
|
|
49
|
+
|
|
41
50
|
main();
|