eyeleng 1.0.13 → 1.1.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/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,18 @@ function evaluate(program, options = {}) {
35
37
  const relaxedRecursiveRunOnce = options.relaxedRecursion === false
36
38
  ? new Set()
37
39
  : recursiveTermGenerationRuleIndexes(analysis);
40
+ const useHybrid = options.hybrid !== false && !options.shacl12Conformance;
41
+ const hybridBackwardPredicates = useHybrid || options.backwardBodyCalls
42
+ ? preferredBackwardPredicates(program, options)
43
+ : new Set();
44
+ const hybridBackwardRules = new Set();
45
+ if (hybridBackwardPredicates.size > 0) {
46
+ for (let ruleIndex = 0; ruleIndex < program.rules.length; ruleIndex += 1) {
47
+ if (ruleIsBackwardOriented(program.rules[ruleIndex], hybridBackwardPredicates)) hybridBackwardRules.add(ruleIndex);
48
+ }
49
+ }
50
+ const hybridStats = hybridBackwardPredicates.size > 0 ? emptyBackwardStats() : null;
51
+ for (const ruleIndex of hybridBackwardRules) perRule[ruleIndex].backward = true;
38
52
  const baseContext = {
39
53
  ...evalOptions,
40
54
  maxIterations,
@@ -46,12 +60,16 @@ function evaluate(program, options = {}) {
46
60
  iteration: 0,
47
61
  startingIterations: 0,
48
62
  recursiveLayer: false,
63
+ hybridBackwardPredicates,
64
+ hybridBackwardRules,
65
+ hybridStats,
49
66
  };
50
67
 
51
68
  for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {
52
69
  const layer = layerIndexes[layerIndex];
53
- const ordinary = layer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
54
- const runOnce = layer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
70
+ const forwardLayer = hybridBackwardRules.size > 0 ? layer.filter((ruleIndex) => !hybridBackwardRules.has(ruleIndex)) : layer;
71
+ const ordinary = forwardLayer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
72
+ const runOnce = forwardLayer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
55
73
 
56
74
  if (runOnce.length > 0) {
57
75
  iterations += 1;
@@ -84,6 +102,7 @@ function evaluate(program, options = {}) {
84
102
  ruleApplications,
85
103
  perRule,
86
104
  trace,
105
+ hybridStats,
87
106
  };
88
107
  }
89
108
 
@@ -153,9 +172,10 @@ function applyRuleOnce(program, store, ruleIndex, context) {
153
172
  const seenBindings = dedupeBindings ? new Set() : null;
154
173
  const headBlankLabels = collectHeadBlankLabels(rule.head);
155
174
 
156
- const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple'
175
+ const bodyContext = prepareBodyContext(program, store, context);
176
+ const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple' && !shouldUseBackwardForTriple(rule.body[0].triple, {}, bodyContext)
157
177
  ? store.match(rule.body[0].triple, {})
158
- : evaluateBodyStream(rule.body, store, {}, context);
178
+ : evaluateBodyStream(rule.body, store, {}, bodyContext);
159
179
 
160
180
  for (const binding of bodyBindings) {
161
181
  if (seenBindings) {
@@ -188,9 +208,36 @@ function applyRuleOnce(program, store, ruleIndex, context) {
188
208
  }
189
209
  }
190
210
 
211
+ if (bodyContext.backwardProver && context.hybridStats) mergeBackwardStats(context.hybridStats, bodyContext.backwardProver.stats);
191
212
  return { applications, added };
192
213
  }
193
214
 
215
+ function prepareBodyContext(program, store, context) {
216
+ if (!context.hybridBackwardPredicates || context.hybridBackwardPredicates.size === 0) return context;
217
+ return {
218
+ ...context,
219
+ backwardProver: new BackwardProver(program, {
220
+ ...context,
221
+ store,
222
+ allowedPredicates: context.hybridBackwardPredicates,
223
+ }),
224
+ };
225
+ }
226
+
227
+ function emptyBackwardStats() {
228
+ return { mode: 'hybrid', goals: 0, facts: 0, rules: 0, memoHits: 0, memoStores: 0, maxDepth: 0 };
229
+ }
230
+
231
+ function mergeBackwardStats(total, item) {
232
+ if (!total || !item) return;
233
+ total.goals += item.goals || 0;
234
+ total.facts += item.facts || 0;
235
+ total.rules += item.rules || 0;
236
+ total.memoHits += item.memoHits || 0;
237
+ total.memoStores += item.memoStores || 0;
238
+ total.maxDepth = Math.max(total.maxDepth || 0, item.maxDepth || 0);
239
+ }
240
+
194
241
  function recursiveTermGenerationRuleIndexes(analysis) {
195
242
  const out = new Set();
196
243
  if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;
@@ -299,9 +346,20 @@ function* evaluateBodyStream(clauses, store, initialBinding = {}, options = {},
299
346
 
300
347
  const clause = clauses[index];
301
348
  if (clause.type === 'triple') {
349
+ const seen = new Set();
302
350
  for (const matched of store.match(clause.triple, initialBinding)) {
351
+ const key = bindingKey(matched);
352
+ seen.add(key);
303
353
  yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
304
354
  }
355
+ if (shouldUseBackwardForTriple(clause.triple, initialBinding, options)) {
356
+ for (const matched of options.backwardProver.solveTriple(clause.triple, initialBinding)) {
357
+ const key = bindingKey(matched);
358
+ if (seen.has(key)) continue;
359
+ seen.add(key);
360
+ yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
361
+ }
362
+ }
305
363
  return;
306
364
  }
307
365
 
@@ -347,6 +405,12 @@ function* evaluateBodyStream(clauses, store, initialBinding = {}, options = {},
347
405
  throw new Error(`Unsupported body clause ${clause.type}`);
348
406
  }
349
407
 
408
+ function shouldUseBackwardForTriple(pattern, binding, options = {}) {
409
+ if (!options.backwardProver || !options.hybridBackwardPredicates || options.hybridBackwardPredicates.size === 0) return false;
410
+ const predicate = instantiateTerm(pattern.p, binding);
411
+ return !!(predicate && predicate.type === 'iri' && options.hybridBackwardPredicates.has(predicate.value));
412
+ }
413
+
350
414
  function bodyHasAny(clauses, store, initialBinding, options) {
351
415
  for (const _ of evaluateBodyStream(clauses, store, initialBinding, options)) return true;
352
416
  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,92 @@ function queryResult(result, querySpec, options = {}) {
13
14
  prefixes: result.prefixes,
14
15
  select,
15
16
  bindings: projectBindings(bindings, select),
17
+ mode: result.hybridStats ? '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 query = queryResult(result, querySpec, options);
30
- return { ...result, query };
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
+ const mode = options.queryMode || 'auto';
84
+ if (mode === 'forward') return { ...options, hybrid: false };
85
+ if (shouldUseHybridForQuery(program, querySpec, options)) return { ...options, hybrid: options.hybrid ?? 'auto' };
86
+ return options;
87
+ }
88
+
89
+ function shouldUseHybridForQuery(program, querySpec, options = {}) {
90
+ const mode = options.queryMode || 'auto';
91
+ if (options.hybrid === false) return false;
92
+ if (options.hybrid === true) return true;
93
+ if (mode !== 'auto') return false;
94
+ if (!querySpec) return false;
95
+ return preferredBackwardPredicates(program, options).size > 0;
31
96
  }
32
97
 
33
98
  function normalizeSelect(select, bindings) {
34
99
  if (select && select.length > 0) return select.slice();
35
100
  const vars = new Set();
36
101
  for (const binding of bindings) for (const key of Object.keys(binding)) vars.add(key);
37
- return Array.from(vars).sort();
102
+ return Array.from(vars).sort().filter((name) => !name.includes('__b'));
38
103
  }
39
104
 
40
105
  function projectBindings(bindings, select) {
@@ -52,4 +117,4 @@ function projectBindings(bindings, select) {
52
117
  return out;
53
118
  }
54
119
 
55
- module.exports = { runQuery, queryResult, parseQuery, normalizeSelect, projectBindings };
120
+ 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,168 @@ 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
+
223
+
224
+ test('hybrid backward tabling completes sibling recursive goals before replaying answers', () => {
225
+ const result = run(`
226
+ PREFIX : <http://example/fib/>
227
+ DATA {
228
+ :n0 :index 0 ; :fibPairF 0 ; :fibPairG 1 .
229
+ :n1 :index 1 .
230
+ :n2 :index 2 .
231
+ :n5 :index 5 .
232
+ :n10 a :FibCase ; :index 10 .
233
+ }
234
+ RULE { ?node :fibPairF ?c ; :fibPairG ?d }
235
+ WHERE {
236
+ ?node :index ?n .
237
+ FILTER(?n > 0)
238
+ SET(?half := FLOOR(?n / 2))
239
+ SET(?halfNode := IRI(CONCAT("http://example/fib/n", STR(?half))))
240
+ ?halfNode :fibPairF ?a ; :fibPairG ?b .
241
+ SET(?twob := ?b * 2)
242
+ SET(?twobminusa := ?twob - ?a)
243
+ SET(?c := ?a * ?twobminusa)
244
+ SET(?aa := ?a * ?a)
245
+ SET(?bb := ?b * ?b)
246
+ SET(?d := ?aa + ?bb)
247
+ SET(?parity := ?n - (?half * 2))
248
+ FILTER(?parity = 0)
249
+ }
250
+ RULE { ?node :fibPairF ?d ; :fibPairG ?next }
251
+ WHERE {
252
+ ?node :index ?n .
253
+ FILTER(?n > 0)
254
+ SET(?half := FLOOR(?n / 2))
255
+ SET(?halfNode := IRI(CONCAT("http://example/fib/n", STR(?half))))
256
+ ?halfNode :fibPairF ?a ; :fibPairG ?b .
257
+ SET(?twob := ?b * 2)
258
+ SET(?twobminusa := ?twob - ?a)
259
+ SET(?c := ?a * ?twobminusa)
260
+ SET(?aa := ?a * ?a)
261
+ SET(?bb := ?b * ?b)
262
+ SET(?d := ?aa + ?bb)
263
+ SET(?parity := ?n - (?half * 2))
264
+ FILTER(?parity = 1)
265
+ SET(?next := ?c + ?d)
266
+ }
267
+ RULE { ?node :fib ?value } WHERE { ?node a :FibCase ; :fibPairF ?value }
268
+ `, { hybrid: true });
269
+ const keys = result.closure.map(tripleKey).join('\n');
270
+ assert.match(keys, /I:http:\/\/example\/fib\/n10 I:http:\/\/example\/fib\/fib L:55\^\^http:\/\/www\.w3\.org\/2001\/XMLSchema#integer@--/);
271
+ assert.equal(result.perRule[0].backward, true);
272
+ assert.equal(result.perRule[1].backward, true);
273
+ assert.ok(result.hybridStats.memoHits > 0);
274
+ });
275
+
276
+ test('hybrid query mode runs forward rules with backward body calls', () => {
277
+ const { runQuery, formatBindings } = require('../src/index.js');
278
+ const result = runQuery(`
279
+ PREFIX : <http://example/>
280
+ DATA { :alice :score 41 . :bob :score 2 . }
281
+ RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
282
+ RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
283
+ `, '?who :ready ?score', { queryMode: 'hybrid' });
284
+ assert.equal(result.query.mode, 'hybrid');
285
+ assert.ok(result.hybridStats.goals > 0);
286
+ const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
287
+ assert.match(output, /\?score = 42; \?who = :alice/);
288
+ assert.doesNotMatch(output, /bob/);
289
+ });
290
+
291
+ test('auto query mode uses pure backward planning when unsupported rules are irrelevant', () => {
292
+ const { runQuery, formatBindings } = require('../src/index.js');
293
+ const result = runQuery(`
294
+ PREFIX : <http://example/>
295
+ DATA { :alice :score 41 . :bob :score 2 . :seed :seen :x . }
296
+ RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
297
+ RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
298
+ RULE { [] :unrelated :x } WHERE { :seed :seen :x }
299
+ `, '?who :ready ?score');
300
+ assert.equal(result.query.mode, 'backward');
301
+ assert.ok(result.query.stats.goals > 0);
302
+ const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
303
+ assert.match(output, /\?score = 42; \?who = :alice/);
304
+ assert.doesNotMatch(output, /bob/);
305
+ });
306
+
307
+ test('auto query mode falls back to hybrid when a demanded predicate has unsupported rules', () => {
308
+ const { runQuery, formatBindings } = require('../src/index.js');
309
+ const result = runQuery(`
310
+ PREFIX : <http://example/>
311
+ DATA { :alice :score 41 . :bob :score 2 . :seed :seen :x . }
312
+ RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
313
+ RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
314
+ RULE { [] :ready 999 } WHERE { :seed :seen :x }
315
+ `, '?who :ready ?score');
316
+ assert.equal(result.query.mode, 'hybrid');
317
+ assert.ok(result.hybridStats.goals > 0);
318
+ assert.equal(result.perRule[0].backward, true);
319
+ assert.doesNotMatch(result.closure.map(tripleKey).join('\n'), /nextScore/);
320
+ const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
321
+ assert.match(output, /\?score = 42; \?who = :alice/);
322
+ });
323
+
162
324
  test('parseQuery accepts raw body text and rejects non-SRL QUERY/SELECT syntax', () => {
163
325
  const { parseQuery } = require('../src/index.js');
164
326
  const raw = parseQuery('?x :p ?y', { prefixes: { '': 'http://example/' } });
@@ -599,4 +761,22 @@ RULE { ?x :p ?v1 } WHERE { ?x :p ?v . SET(?v1 := ?v + 1) }
599
761
  `, { shacl12Conformance: true, strict: true }), /creates terms in a recursive dependency cycle/);
600
762
  });
601
763
 
764
+
765
+ test('auto backward query ignores unsupported rules outside the demanded slice', () => {
766
+ const source = `
767
+ PREFIX : <http://example/>
768
+ DATA { :alice :parent :bob . :bob :parent :carol . :a :q :b . :b :q :c . }
769
+ RULE { ?x :ancestor ?y } WHERE { ?x :parent ?y }
770
+ RULE { ?x :ancestor ?z } WHERE { ?x :parent ?y . ?y :ancestor ?z }
771
+ RULE { ?x :twoStep ?y } WHERE { ?x :q/:q ?y }
772
+ `;
773
+ const result = runQuery(source, ':alice :ancestor ?who');
774
+ assert.equal(result.query.mode, 'backward');
775
+ assert.ok(result.query.stats.goals > 0);
776
+ assert.deepEqual(
777
+ result.query.bindings.map((binding) => binding.who.value).sort(),
778
+ ['http://example/bob', 'http://example/carol'],
779
+ );
780
+ });
781
+
602
782
  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.match(text, /--stream-messages\s+Replay RDF Message Log envelopes/);
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,19 @@ 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([]).options.hybrid, 'auto');
45
+ assert.equal(parseArgs(['--hybrid']).options.hybrid, true);
46
+ assert.equal(parseArgs(['--no-hybrid']).options.hybrid, false);
47
+ assert.throws(() => parseArgs(['--query-mode', 'hybrid']), /--query-mode requires auto, forward, or backward/);
48
+ assert.throws(() => parseArgs(['--query-mode', 'sideways']), /--query-mode requires auto, forward, or backward/);
49
+ assert.throws(() => parseArgs(['--stream-messages']), /Unknown option --stream-messages/);
50
+ });
51
+
41
52
  main();
@@ -11,28 +11,28 @@
11
11
  "name": "deep-taxonomy-100000.srl",
12
12
  "file": "examples/deep-taxonomy-100000.srl",
13
13
  "repeat": 1,
14
- "baselineMs": 5427.8,
14
+ "baselineMs": 12316.7,
15
15
  "outputBytes": 5068161
16
16
  },
17
17
  {
18
18
  "name": "fibonacci.srl",
19
19
  "file": "examples/fibonacci.srl",
20
20
  "repeat": 1,
21
- "baselineMs": 1409.4,
22
- "outputBytes": 10810713
21
+ "baselineMs": 124.0,
22
+ "outputBytes": 2405
23
23
  },
24
24
  {
25
25
  "name": "fft32-numeric.srl",
26
26
  "file": "examples/fft32-numeric.srl",
27
- "repeat": 3,
28
- "baselineMs": 182.2,
27
+ "repeat": 1,
28
+ "baselineMs": 607.8,
29
29
  "outputBytes": 466351
30
30
  },
31
31
  {
32
32
  "name": "path-discovery.srl",
33
33
  "file": "examples/path-discovery.srl",
34
- "repeat": 5,
35
- "baselineMs": 1317.7,
34
+ "repeat": 1,
35
+ "baselineMs": 3054.6,
36
36
  "outputBytes": 736870
37
37
  }
38
38
  ]