trellis 1.0.4 → 1.0.7
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/dist/ai/index.js +688 -0
- package/dist/cli/server.js +141 -27258
- package/dist/cli/tql.js +2959 -45695
- package/dist/graph/index.js +2248 -0
- package/dist/index.js +88 -12417
- package/dist/kernel/logic-middleware.js +179 -0
- package/dist/kernel/middleware.js +0 -0
- package/dist/kernel/operations.js +32 -0
- package/dist/kernel/schema-middleware.js +34 -0
- package/dist/kernel/security-middleware.js +53 -0
- package/dist/kernel/trellis-kernel.js +2239 -0
- package/dist/kernel/workspace.js +91 -0
- package/dist/persist/backend.js +0 -0
- package/dist/persist/sqlite-backend.js +123 -0
- package/dist/query/index.js +1643 -0
- package/dist/server/index.js +3309 -0
- package/dist/store/eav-store.js +323 -0
- package/dist/workflows/index.js +3160 -0
- package/package.json +9 -3
- package/.//out//windows-style.json +0 -602
- package/bun.lock +0 -350
- package/dist/cli/iroh.linux-x64-gnu-2y4tmrmh.node +0 -0
- package/dist/cli/iroh.linux-x64-musl-50ncx5bz.node +0 -0
- package/index.ts +0 -29
- package/run-server.sh +0 -5
|
@@ -0,0 +1,1643 @@
|
|
|
1
|
+
// src/query/datalog-evaluator.ts
|
|
2
|
+
class ExternalPredicates {
|
|
3
|
+
static regex(str, pattern) {
|
|
4
|
+
if (typeof pattern === "string") {
|
|
5
|
+
try {
|
|
6
|
+
const regexMatch = pattern.match(/^\/(.*)\/([gimuy]*)$/);
|
|
7
|
+
if (regexMatch) {
|
|
8
|
+
const [, regexPattern, flags] = regexMatch;
|
|
9
|
+
const regex = new RegExp(regexPattern, flags || "i");
|
|
10
|
+
return regex.test(str);
|
|
11
|
+
}
|
|
12
|
+
return new RegExp(pattern, "i").test(str);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
console.warn(`Invalid regex pattern: ${pattern}`, e);
|
|
15
|
+
return str.toLowerCase().includes(pattern.toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return pattern.test(str);
|
|
19
|
+
}
|
|
20
|
+
static gt(a, b) {
|
|
21
|
+
return a > b;
|
|
22
|
+
}
|
|
23
|
+
static lt(a, b) {
|
|
24
|
+
return a < b;
|
|
25
|
+
}
|
|
26
|
+
static between(val, min, max) {
|
|
27
|
+
return val >= min && val <= max;
|
|
28
|
+
}
|
|
29
|
+
static contains(str, substr) {
|
|
30
|
+
return str.toLowerCase().includes(substr.toLowerCase());
|
|
31
|
+
}
|
|
32
|
+
static after(a, b) {
|
|
33
|
+
return a > b;
|
|
34
|
+
}
|
|
35
|
+
static betweenDate(d, start, end) {
|
|
36
|
+
return d >= start && d <= end;
|
|
37
|
+
}
|
|
38
|
+
static sum(values) {
|
|
39
|
+
return values.reduce((a, b) => a + b, 0);
|
|
40
|
+
}
|
|
41
|
+
static count(values) {
|
|
42
|
+
return values.length;
|
|
43
|
+
}
|
|
44
|
+
static avg(values) {
|
|
45
|
+
return values.length > 0 ? this.sum(values) / values.length : 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class DatalogEvaluator {
|
|
50
|
+
store;
|
|
51
|
+
rules = [];
|
|
52
|
+
ws = new Map;
|
|
53
|
+
constructor(store) {
|
|
54
|
+
this.store = store;
|
|
55
|
+
}
|
|
56
|
+
addRule(rule) {
|
|
57
|
+
this.rules.push(rule);
|
|
58
|
+
}
|
|
59
|
+
seedBaseFacts() {
|
|
60
|
+
const attrRows = [];
|
|
61
|
+
for (const f of this.store.getAllFacts()) {
|
|
62
|
+
if (f) {
|
|
63
|
+
attrRows.push([f.e, f.a, f.v]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.ws.set("attr", attrRows);
|
|
67
|
+
const linkRows = [];
|
|
68
|
+
for (const link of this.store.getAllLinks()) {
|
|
69
|
+
linkRows.push([link.e1, link.a, link.e2]);
|
|
70
|
+
}
|
|
71
|
+
this.ws.set("link", linkRows);
|
|
72
|
+
}
|
|
73
|
+
pushDerived(predicate, tuple) {
|
|
74
|
+
const bucket = this.ws.get(predicate) || [];
|
|
75
|
+
if (!this.ws.has(predicate)) {
|
|
76
|
+
this.ws.set(predicate, bucket);
|
|
77
|
+
}
|
|
78
|
+
const key = JSON.stringify(tuple);
|
|
79
|
+
if (!bucket._keys) {
|
|
80
|
+
bucket._keys = new Set;
|
|
81
|
+
}
|
|
82
|
+
const keys = bucket._keys;
|
|
83
|
+
if (keys.has(key)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
bucket.push(tuple);
|
|
87
|
+
keys.add(key);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
evaluate(query, limit) {
|
|
91
|
+
const startTime = performance.now();
|
|
92
|
+
const trace = [];
|
|
93
|
+
this.seedBaseFacts();
|
|
94
|
+
let added = true;
|
|
95
|
+
let iterations = 0;
|
|
96
|
+
const maxIterations = 100;
|
|
97
|
+
while (added && iterations < maxIterations) {
|
|
98
|
+
added = false;
|
|
99
|
+
for (const rule of this.rules) {
|
|
100
|
+
const bindings2 = this.findBindingsOverWS(rule.body);
|
|
101
|
+
for (const binding of bindings2) {
|
|
102
|
+
const head = this.substitute(rule.head, binding);
|
|
103
|
+
const tuple = head.terms.map((term) => this.resolveTerm(term, binding));
|
|
104
|
+
if (this.pushDerived(head.predicate, tuple)) {
|
|
105
|
+
added = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
iterations++;
|
|
110
|
+
}
|
|
111
|
+
const bindings = this.findBindingsOverWS(query.goals, trace, limit);
|
|
112
|
+
return {
|
|
113
|
+
bindings,
|
|
114
|
+
executionTime: performance.now() - startTime,
|
|
115
|
+
plan: `Semi-naive evaluation: ${iterations} iterations, ${this.getTotalFacts()} facts`,
|
|
116
|
+
trace
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
getTotalFacts() {
|
|
120
|
+
let total = 0;
|
|
121
|
+
for (const tuples of this.ws.values()) {
|
|
122
|
+
total += tuples.length;
|
|
123
|
+
}
|
|
124
|
+
return total;
|
|
125
|
+
}
|
|
126
|
+
findBindingsOverWS(goals, trace, limit) {
|
|
127
|
+
if (goals.length === 0) {
|
|
128
|
+
return [{}];
|
|
129
|
+
}
|
|
130
|
+
let bindings = [{}];
|
|
131
|
+
for (const goal of goals) {
|
|
132
|
+
const goalStartTime = performance.now();
|
|
133
|
+
const newBindings = [];
|
|
134
|
+
outer:
|
|
135
|
+
for (const binding of bindings) {
|
|
136
|
+
const goalBindings = this.evaluateGoal(goal, binding);
|
|
137
|
+
for (const goalBinding of goalBindings) {
|
|
138
|
+
const merged = { ...binding, ...goalBinding };
|
|
139
|
+
let hasConflict = false;
|
|
140
|
+
for (const key in merged) {
|
|
141
|
+
if (binding[key] !== undefined && goalBinding[key] !== undefined && binding[key] !== goalBinding[key]) {
|
|
142
|
+
hasConflict = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!hasConflict) {
|
|
147
|
+
newBindings.push(merged);
|
|
148
|
+
if (limit !== undefined && newBindings.length >= limit)
|
|
149
|
+
break outer;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
bindings = newBindings;
|
|
154
|
+
if (trace) {
|
|
155
|
+
trace.push({
|
|
156
|
+
goal: `${goal.predicate}(${goal.terms.join(", ")})`,
|
|
157
|
+
bindingsCount: bindings.length,
|
|
158
|
+
durationMs: performance.now() - goalStartTime
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const uniqueBindings = new Map;
|
|
163
|
+
for (const binding of bindings) {
|
|
164
|
+
const key = JSON.stringify(binding);
|
|
165
|
+
uniqueBindings.set(key, binding);
|
|
166
|
+
}
|
|
167
|
+
return Array.from(uniqueBindings.values());
|
|
168
|
+
}
|
|
169
|
+
evaluateGoal(goal, binding) {
|
|
170
|
+
const { predicate, terms } = goal;
|
|
171
|
+
if (predicate === "not") {
|
|
172
|
+
const inner = goal.terms[0];
|
|
173
|
+
const res = this.evaluateGoal(inner, binding);
|
|
174
|
+
return res.length === 0 ? [binding] : [];
|
|
175
|
+
}
|
|
176
|
+
if (predicate === "attr") {
|
|
177
|
+
return this.evaluateAttrPredicate(terms, binding);
|
|
178
|
+
}
|
|
179
|
+
if (predicate === "link") {
|
|
180
|
+
return this.evaluateLinkPredicate(terms, binding);
|
|
181
|
+
}
|
|
182
|
+
if (predicate === "gt" || predicate === "lt" || predicate === "between" || predicate === ">" || predicate === "<" || predicate === ">=" || predicate === "<=" || predicate === "=" || predicate === "!=") {
|
|
183
|
+
return this.evaluateComparisonPredicate(goal, binding);
|
|
184
|
+
}
|
|
185
|
+
if (predicate === "regex" || predicate === "contains") {
|
|
186
|
+
return this.evaluateStringPredicate(goal, binding);
|
|
187
|
+
}
|
|
188
|
+
if (predicate === "after" || predicate === "betweenDate") {
|
|
189
|
+
return this.evaluateDatePredicate(goal, binding);
|
|
190
|
+
}
|
|
191
|
+
if (predicate.startsWith("ext_")) {
|
|
192
|
+
return this.evaluateExternalPredicate(goal, binding);
|
|
193
|
+
}
|
|
194
|
+
return this.evalPredicateFromWS(predicate, terms, binding);
|
|
195
|
+
}
|
|
196
|
+
evalPredicateFromWS(predicate, terms, binding) {
|
|
197
|
+
const rows = this.ws.get(predicate) || [];
|
|
198
|
+
const results = [];
|
|
199
|
+
rowloop:
|
|
200
|
+
for (const row of rows) {
|
|
201
|
+
const newBinding = { ...binding };
|
|
202
|
+
for (let i = 0;i < terms.length; i++) {
|
|
203
|
+
const term = terms[i];
|
|
204
|
+
const val = row[i];
|
|
205
|
+
if (typeof term === "string" && term.startsWith("?")) {
|
|
206
|
+
const bound = newBinding[term];
|
|
207
|
+
if (bound !== undefined && bound !== val) {
|
|
208
|
+
continue rowloop;
|
|
209
|
+
}
|
|
210
|
+
newBinding[term] = val;
|
|
211
|
+
} else {
|
|
212
|
+
if (term !== val) {
|
|
213
|
+
continue rowloop;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
results.push(newBinding);
|
|
218
|
+
}
|
|
219
|
+
return results;
|
|
220
|
+
}
|
|
221
|
+
evaluateAttrPredicate(terms, binding) {
|
|
222
|
+
if (terms.length !== 3)
|
|
223
|
+
return [];
|
|
224
|
+
const [entity, attribute, value] = terms.map((term) => this.resolveTerm(term, binding));
|
|
225
|
+
const results = [];
|
|
226
|
+
if (typeof entity === "string" && !entity.startsWith("?") && typeof attribute === "string" && !attribute.startsWith("?") && (typeof value !== "string" || !value.startsWith("?"))) {
|
|
227
|
+
const facts = this.store.getFactsByValue(attribute, value);
|
|
228
|
+
for (const fact of facts) {
|
|
229
|
+
if (fact.e === entity) {
|
|
230
|
+
results.push({});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return results;
|
|
234
|
+
}
|
|
235
|
+
if (typeof entity === "string" && !entity.startsWith("?") && typeof attribute === "string" && !attribute.startsWith("?")) {
|
|
236
|
+
const facts = this.store.getFactsByEntity(entity);
|
|
237
|
+
for (const fact of facts) {
|
|
238
|
+
if (fact.a === attribute) {
|
|
239
|
+
const newBinding = { ...binding };
|
|
240
|
+
if (typeof value === "string" && value.startsWith("?")) {
|
|
241
|
+
newBinding[value] = fact.v;
|
|
242
|
+
results.push(newBinding);
|
|
243
|
+
} else if (fact.v === value) {
|
|
244
|
+
results.push(newBinding);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
if (typeof attribute === "string" && !attribute.startsWith("?")) {
|
|
251
|
+
const facts = this.store.getFactsByAttribute(attribute);
|
|
252
|
+
for (const fact of facts) {
|
|
253
|
+
const newBinding = { ...binding };
|
|
254
|
+
if (typeof entity === "string" && !entity.startsWith("?") && fact.e !== entity) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if ((typeof value !== "string" || !value.startsWith("?")) && fact.v !== value) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (typeof entity === "string" && entity.startsWith("?")) {
|
|
261
|
+
newBinding[entity] = fact.e;
|
|
262
|
+
}
|
|
263
|
+
if (typeof value === "string" && value.startsWith("?")) {
|
|
264
|
+
newBinding[value] = fact.v;
|
|
265
|
+
}
|
|
266
|
+
results.push(newBinding);
|
|
267
|
+
}
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
evaluateLinkPredicate(terms, binding) {
|
|
273
|
+
if (terms.length !== 3)
|
|
274
|
+
return [];
|
|
275
|
+
const [e1, a, e2] = terms;
|
|
276
|
+
const results = [];
|
|
277
|
+
const links = this.store.getAllLinks();
|
|
278
|
+
for (const link of links) {
|
|
279
|
+
const newBinding = { ...binding };
|
|
280
|
+
let matches = true;
|
|
281
|
+
if (typeof e1 === "string" && !e1.startsWith("?")) {
|
|
282
|
+
if (link.e1 !== e1)
|
|
283
|
+
continue;
|
|
284
|
+
} else if (typeof e1 === "string" && e1.startsWith("?")) {
|
|
285
|
+
newBinding[e1] = link.e1;
|
|
286
|
+
}
|
|
287
|
+
if (typeof a === "string" && !a.startsWith("?")) {
|
|
288
|
+
if (link.a !== a)
|
|
289
|
+
continue;
|
|
290
|
+
} else if (typeof a === "string" && a.startsWith("?")) {
|
|
291
|
+
newBinding[a] = link.a;
|
|
292
|
+
}
|
|
293
|
+
if (typeof e2 === "string" && !e2.startsWith("?")) {
|
|
294
|
+
if (link.e2 !== e2)
|
|
295
|
+
continue;
|
|
296
|
+
} else if (typeof e2 === "string" && e2.startsWith("?")) {
|
|
297
|
+
newBinding[e2] = link.e2;
|
|
298
|
+
}
|
|
299
|
+
if (matches) {
|
|
300
|
+
results.push(newBinding);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return results;
|
|
304
|
+
}
|
|
305
|
+
evaluateComparisonPredicate(goal, binding) {
|
|
306
|
+
const { predicate, terms } = goal;
|
|
307
|
+
if (terms.length < 2)
|
|
308
|
+
return [];
|
|
309
|
+
const left = this.resolveTerm(terms[0], binding);
|
|
310
|
+
const right = this.resolveTerm(terms[1], binding);
|
|
311
|
+
let leftNum = left;
|
|
312
|
+
let rightNum = right;
|
|
313
|
+
if (typeof left === "string" && !isNaN(Number(left))) {
|
|
314
|
+
leftNum = Number(left);
|
|
315
|
+
}
|
|
316
|
+
if (typeof right === "string" && !isNaN(Number(right))) {
|
|
317
|
+
rightNum = Number(right);
|
|
318
|
+
}
|
|
319
|
+
if (typeof leftNum !== "number" || typeof rightNum !== "number")
|
|
320
|
+
return [];
|
|
321
|
+
let result = false;
|
|
322
|
+
switch (predicate) {
|
|
323
|
+
case "gt":
|
|
324
|
+
case ">":
|
|
325
|
+
result = ExternalPredicates.gt(leftNum, rightNum);
|
|
326
|
+
break;
|
|
327
|
+
case "lt":
|
|
328
|
+
case "<":
|
|
329
|
+
result = ExternalPredicates.lt(leftNum, rightNum);
|
|
330
|
+
break;
|
|
331
|
+
case ">=":
|
|
332
|
+
result = leftNum >= rightNum;
|
|
333
|
+
break;
|
|
334
|
+
case "<=":
|
|
335
|
+
result = leftNum <= rightNum;
|
|
336
|
+
break;
|
|
337
|
+
case "=":
|
|
338
|
+
result = leftNum === rightNum;
|
|
339
|
+
break;
|
|
340
|
+
case "!=":
|
|
341
|
+
result = leftNum !== rightNum;
|
|
342
|
+
break;
|
|
343
|
+
case "between":
|
|
344
|
+
if (terms.length >= 3) {
|
|
345
|
+
const max = this.resolveTerm(terms[2], binding);
|
|
346
|
+
let maxNum = max;
|
|
347
|
+
if (typeof max === "string" && !isNaN(Number(max))) {
|
|
348
|
+
maxNum = Number(max);
|
|
349
|
+
}
|
|
350
|
+
if (typeof maxNum === "number") {
|
|
351
|
+
result = ExternalPredicates.between(leftNum, rightNum, maxNum);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
return result ? [{}] : [];
|
|
357
|
+
}
|
|
358
|
+
evaluateStringPredicate(goal, binding) {
|
|
359
|
+
const { predicate, terms } = goal;
|
|
360
|
+
if (terms.length < 2)
|
|
361
|
+
return [];
|
|
362
|
+
const str = this.resolveTerm(terms[0], binding);
|
|
363
|
+
const pattern = this.resolveTerm(terms[1], binding);
|
|
364
|
+
if (typeof str !== "string" || typeof pattern !== "string")
|
|
365
|
+
return [];
|
|
366
|
+
let result = false;
|
|
367
|
+
switch (predicate) {
|
|
368
|
+
case "regex":
|
|
369
|
+
result = ExternalPredicates.regex(str, pattern);
|
|
370
|
+
break;
|
|
371
|
+
case "contains":
|
|
372
|
+
result = ExternalPredicates.contains(str, pattern);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
return result ? [{}] : [];
|
|
376
|
+
}
|
|
377
|
+
evaluateDatePredicate(goal, binding) {
|
|
378
|
+
const { predicate, terms } = goal;
|
|
379
|
+
if (terms.length < 2)
|
|
380
|
+
return [];
|
|
381
|
+
const left = this.resolveTerm(terms[0], binding);
|
|
382
|
+
const right = this.resolveTerm(terms[1], binding);
|
|
383
|
+
if (!(left instanceof Date) || !(right instanceof Date))
|
|
384
|
+
return [];
|
|
385
|
+
let result = false;
|
|
386
|
+
switch (predicate) {
|
|
387
|
+
case "after":
|
|
388
|
+
result = ExternalPredicates.after(left, right);
|
|
389
|
+
break;
|
|
390
|
+
case "betweenDate":
|
|
391
|
+
if (terms.length >= 3) {
|
|
392
|
+
const end = this.resolveTerm(terms[2], binding);
|
|
393
|
+
if (end instanceof Date) {
|
|
394
|
+
result = ExternalPredicates.betweenDate(left, right, end);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
return result ? [{}] : [];
|
|
400
|
+
}
|
|
401
|
+
evaluateExternalPredicate(goal, binding) {
|
|
402
|
+
const { predicate, terms } = goal;
|
|
403
|
+
const resolvedTerms = terms.map((term) => this.resolveTerm(term, binding));
|
|
404
|
+
let result = false;
|
|
405
|
+
switch (predicate) {
|
|
406
|
+
case "ext_regex":
|
|
407
|
+
if (resolvedTerms.length >= 2 && typeof resolvedTerms[0] === "string") {
|
|
408
|
+
result = ExternalPredicates.regex(resolvedTerms[0], resolvedTerms[1]);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
case "ext_gt":
|
|
412
|
+
if (resolvedTerms.length >= 2 && typeof resolvedTerms[0] === "number" && typeof resolvedTerms[1] === "number") {
|
|
413
|
+
result = ExternalPredicates.gt(resolvedTerms[0], resolvedTerms[1]);
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
case "ext_between":
|
|
417
|
+
if (resolvedTerms.length >= 3 && typeof resolvedTerms[0] === "number" && typeof resolvedTerms[1] === "number" && typeof resolvedTerms[2] === "number") {
|
|
418
|
+
result = ExternalPredicates.between(resolvedTerms[0], resolvedTerms[1], resolvedTerms[2]);
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
case "ext_contains":
|
|
422
|
+
if (resolvedTerms.length >= 2 && typeof resolvedTerms[0] === "string") {
|
|
423
|
+
result = ExternalPredicates.contains(resolvedTerms[0], resolvedTerms[1]);
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
return result ? [{}] : [];
|
|
428
|
+
}
|
|
429
|
+
resolveTerm(term, binding) {
|
|
430
|
+
if (typeof term === "string" && term.startsWith("?")) {
|
|
431
|
+
return binding[term] || term;
|
|
432
|
+
}
|
|
433
|
+
return term;
|
|
434
|
+
}
|
|
435
|
+
substitute(atom, binding) {
|
|
436
|
+
return {
|
|
437
|
+
predicate: atom.predicate,
|
|
438
|
+
terms: atom.terms.map((term) => this.resolveTerm(term, binding))
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// src/query/attribute-resolver.ts
|
|
443
|
+
class AttributeResolver {
|
|
444
|
+
schema = {};
|
|
445
|
+
buildSchema(catalog) {
|
|
446
|
+
this.schema = {};
|
|
447
|
+
for (const entry of catalog) {
|
|
448
|
+
const entityType = "default";
|
|
449
|
+
const attributeName = entry.attribute;
|
|
450
|
+
if (!this.schema[entityType]) {
|
|
451
|
+
this.schema[entityType] = {};
|
|
452
|
+
}
|
|
453
|
+
this.schema[entityType][attributeName] = {
|
|
454
|
+
type: entry.type,
|
|
455
|
+
distinctCount: entry.distinctCount,
|
|
456
|
+
examples: entry.examples
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
resolveAttribute(entityType, queryAttribute) {
|
|
461
|
+
const entitySchema = this.schema[entityType];
|
|
462
|
+
if (!entitySchema) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const queryLower = queryAttribute.toLowerCase();
|
|
466
|
+
if (entitySchema[queryAttribute]) {
|
|
467
|
+
return queryAttribute;
|
|
468
|
+
}
|
|
469
|
+
for (const [actualAttribute] of Object.entries(entitySchema)) {
|
|
470
|
+
if (actualAttribute.toLowerCase() === queryLower) {
|
|
471
|
+
return actualAttribute;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
validateQuery(entityType, attributes) {
|
|
477
|
+
const errors = [];
|
|
478
|
+
const resolved = new Map;
|
|
479
|
+
for (const attr of attributes) {
|
|
480
|
+
const resolvedAttr = this.resolveAttribute(entityType, attr);
|
|
481
|
+
if (resolvedAttr) {
|
|
482
|
+
resolved.set(attr, resolvedAttr);
|
|
483
|
+
} else {
|
|
484
|
+
errors.push(`Unknown attribute '${attr}' for entity type '${entityType}'. Available attributes: ${Object.keys(this.schema[entityType] || {}).join(", ")}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
valid: errors.length === 0,
|
|
489
|
+
errors,
|
|
490
|
+
resolved
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
getAvailableAttributes(entityType) {
|
|
494
|
+
return Object.keys(this.schema[entityType] || {});
|
|
495
|
+
}
|
|
496
|
+
getSchema() {
|
|
497
|
+
return this.schema;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/query/query-optimizer.ts
|
|
502
|
+
class QueryOptimizer {
|
|
503
|
+
catalog;
|
|
504
|
+
constructor(catalog = []) {
|
|
505
|
+
this.catalog = catalog;
|
|
506
|
+
}
|
|
507
|
+
optimize(query) {
|
|
508
|
+
if (query.goals.length <= 1)
|
|
509
|
+
return query;
|
|
510
|
+
const optimizedGoals = [];
|
|
511
|
+
const remainingGoals = [...query.goals];
|
|
512
|
+
const boundVars = new Set;
|
|
513
|
+
const typeGoalIdx = remainingGoals.findIndex((g) => g.predicate === "attr" && g.terms[1] === "type");
|
|
514
|
+
if (typeGoalIdx !== -1) {
|
|
515
|
+
const typeGoal = remainingGoals.splice(typeGoalIdx, 1)[0];
|
|
516
|
+
optimizedGoals.push(typeGoal);
|
|
517
|
+
this.collectVars(typeGoal, boundVars);
|
|
518
|
+
}
|
|
519
|
+
while (remainingGoals.length > 0) {
|
|
520
|
+
const bestIdx = this.findBestNextGoal(remainingGoals, boundVars);
|
|
521
|
+
if (bestIdx === -1) {
|
|
522
|
+
const goal = remainingGoals.splice(0, 1)[0];
|
|
523
|
+
optimizedGoals.push(goal);
|
|
524
|
+
this.collectVars(goal, boundVars);
|
|
525
|
+
} else {
|
|
526
|
+
const goal = remainingGoals.splice(bestIdx, 1)[0];
|
|
527
|
+
optimizedGoals.push(goal);
|
|
528
|
+
this.collectVars(goal, boundVars);
|
|
529
|
+
}
|
|
530
|
+
let pushdownPossible = true;
|
|
531
|
+
while (pushdownPossible) {
|
|
532
|
+
const filterIdx = remainingGoals.findIndex((g) => this.isFilter(g) && this.isSatisfied(g, boundVars));
|
|
533
|
+
if (filterIdx !== -1) {
|
|
534
|
+
const filter = remainingGoals.splice(filterIdx, 1)[0];
|
|
535
|
+
optimizedGoals.push(filter);
|
|
536
|
+
} else {
|
|
537
|
+
pushdownPossible = false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
...query,
|
|
543
|
+
goals: optimizedGoals
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
findBestNextGoal(goals, boundVars) {
|
|
547
|
+
let bestIdx = -1;
|
|
548
|
+
let bestScore = -1;
|
|
549
|
+
const filterVars = new Set;
|
|
550
|
+
for (const goal of goals) {
|
|
551
|
+
if (this.isFilter(goal)) {
|
|
552
|
+
for (const term of goal.terms) {
|
|
553
|
+
if (typeof term === "string" && term.startsWith("?")) {
|
|
554
|
+
filterVars.add(term);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
for (let i = 0;i < goals.length; i++) {
|
|
560
|
+
const goal = goals[i];
|
|
561
|
+
if (this.isFilter(goal))
|
|
562
|
+
continue;
|
|
563
|
+
let score = this.calculateRestrictiveness(goal, boundVars);
|
|
564
|
+
for (const term of goal.terms) {
|
|
565
|
+
if (typeof term === "string" && term.startsWith("?") && !boundVars.has(term) && filterVars.has(term)) {
|
|
566
|
+
score += 25;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (score > bestScore) {
|
|
570
|
+
bestScore = score;
|
|
571
|
+
bestIdx = i;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return bestIdx;
|
|
575
|
+
}
|
|
576
|
+
calculateRestrictiveness(goal, boundVars) {
|
|
577
|
+
let score = 0;
|
|
578
|
+
const terms = goal.terms;
|
|
579
|
+
for (const term of terms) {
|
|
580
|
+
if (typeof term !== "string" || !term.startsWith("?")) {
|
|
581
|
+
score += 100;
|
|
582
|
+
} else if (boundVars.has(term)) {
|
|
583
|
+
score += 50;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (goal.predicate === "attr" && typeof terms[1] === "string") {
|
|
587
|
+
const entry = this.catalog.find((e) => e.attribute === terms[1]);
|
|
588
|
+
if (entry) {
|
|
589
|
+
if (entry.cardinality === "one") {
|
|
590
|
+
score += 20;
|
|
591
|
+
}
|
|
592
|
+
score -= Math.min(10, entry.distinctCount / 100);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return score;
|
|
596
|
+
}
|
|
597
|
+
isFilter(goal) {
|
|
598
|
+
const filters = new Set([
|
|
599
|
+
"gt",
|
|
600
|
+
"lt",
|
|
601
|
+
"between",
|
|
602
|
+
"regex",
|
|
603
|
+
"contains",
|
|
604
|
+
">",
|
|
605
|
+
"<",
|
|
606
|
+
">=",
|
|
607
|
+
"<=",
|
|
608
|
+
"=",
|
|
609
|
+
"!=",
|
|
610
|
+
"after",
|
|
611
|
+
"betweenDate"
|
|
612
|
+
]);
|
|
613
|
+
return filters.has(goal.predicate) || goal.predicate.startsWith("ext_");
|
|
614
|
+
}
|
|
615
|
+
isSatisfied(goal, boundVars) {
|
|
616
|
+
return goal.terms.every((term) => {
|
|
617
|
+
if (typeof term === "string" && term.startsWith("?")) {
|
|
618
|
+
return boundVars.has(term);
|
|
619
|
+
}
|
|
620
|
+
return true;
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
collectVars(goal, boundVars) {
|
|
624
|
+
for (const term of goal.terms) {
|
|
625
|
+
if (typeof term === "string" && term.startsWith("?")) {
|
|
626
|
+
boundVars.add(term);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/query/eqls-parser.ts
|
|
633
|
+
class EQLSParser {
|
|
634
|
+
tokens = [];
|
|
635
|
+
current = 0;
|
|
636
|
+
errors = [];
|
|
637
|
+
static KEYWORDS = new Set([
|
|
638
|
+
"FIND",
|
|
639
|
+
"AS",
|
|
640
|
+
"WHERE",
|
|
641
|
+
"AND",
|
|
642
|
+
"OR",
|
|
643
|
+
"RETURN",
|
|
644
|
+
"ORDER",
|
|
645
|
+
"BY",
|
|
646
|
+
"LIMIT",
|
|
647
|
+
"ASC",
|
|
648
|
+
"DESC",
|
|
649
|
+
"BETWEEN",
|
|
650
|
+
"CONTAINS",
|
|
651
|
+
"MATCHES",
|
|
652
|
+
"IN"
|
|
653
|
+
]);
|
|
654
|
+
static SINGLE_CHAR_OPERATORS = new Set(["=", ">", "<"]);
|
|
655
|
+
static MULTI_CHAR_OPERATORS = new Set([
|
|
656
|
+
"CONTAINS",
|
|
657
|
+
"MATCHES",
|
|
658
|
+
"BETWEEN",
|
|
659
|
+
"IN"
|
|
660
|
+
]);
|
|
661
|
+
parse(query) {
|
|
662
|
+
this.tokens = this.tokenize(query);
|
|
663
|
+
this.current = 0;
|
|
664
|
+
this.errors = [];
|
|
665
|
+
try {
|
|
666
|
+
const parsed = this.parseQuery();
|
|
667
|
+
if (this.errors.length > 0) {
|
|
668
|
+
return { errors: this.errors };
|
|
669
|
+
}
|
|
670
|
+
return { query: parsed, errors: [] };
|
|
671
|
+
} catch (error) {
|
|
672
|
+
this.errors.push({
|
|
673
|
+
line: 1,
|
|
674
|
+
column: 1,
|
|
675
|
+
message: `Parse error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
676
|
+
});
|
|
677
|
+
return { errors: this.errors };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
tokenize(input) {
|
|
681
|
+
const tokens = [];
|
|
682
|
+
const lines = input.split(`
|
|
683
|
+
`);
|
|
684
|
+
for (let lineNum = 0;lineNum < lines.length; lineNum++) {
|
|
685
|
+
const line = lines[lineNum];
|
|
686
|
+
const trimmed = line.trim();
|
|
687
|
+
if (!trimmed || trimmed.startsWith("--"))
|
|
688
|
+
continue;
|
|
689
|
+
let pos = 0;
|
|
690
|
+
while (pos < line.length) {
|
|
691
|
+
const char = line[pos];
|
|
692
|
+
if (char === " ") {
|
|
693
|
+
pos++;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (char === '"') {
|
|
697
|
+
const start = pos;
|
|
698
|
+
pos++;
|
|
699
|
+
while (pos < line.length && line[pos] !== '"') {
|
|
700
|
+
if (line[pos] === "\\" && pos + 1 < line.length) {
|
|
701
|
+
pos += 2;
|
|
702
|
+
} else {
|
|
703
|
+
pos++;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (pos < line.length) {
|
|
707
|
+
pos++;
|
|
708
|
+
const value = line.slice(start + 1, pos - 1);
|
|
709
|
+
tokens.push({
|
|
710
|
+
type: "STRING",
|
|
711
|
+
value,
|
|
712
|
+
line: lineNum + 1,
|
|
713
|
+
column: start + 1
|
|
714
|
+
});
|
|
715
|
+
} else {
|
|
716
|
+
this.errors.push({
|
|
717
|
+
line: lineNum + 1,
|
|
718
|
+
column: start + 1,
|
|
719
|
+
message: "Unterminated string literal"
|
|
720
|
+
});
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
} else if (char === "/" && pos + 1 < line.length) {
|
|
724
|
+
const start = pos;
|
|
725
|
+
pos++;
|
|
726
|
+
while (pos < line.length && line[pos] !== "/") {
|
|
727
|
+
if (line[pos] === "\\" && pos + 1 < line.length) {
|
|
728
|
+
pos += 2;
|
|
729
|
+
} else {
|
|
730
|
+
pos++;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (pos < line.length) {
|
|
734
|
+
pos++;
|
|
735
|
+
const pattern = line.slice(start, pos);
|
|
736
|
+
tokens.push({
|
|
737
|
+
type: "REGEX",
|
|
738
|
+
value: pattern,
|
|
739
|
+
line: lineNum + 1,
|
|
740
|
+
column: start + 1
|
|
741
|
+
});
|
|
742
|
+
} else {
|
|
743
|
+
this.errors.push({
|
|
744
|
+
line: lineNum + 1,
|
|
745
|
+
column: start + 1,
|
|
746
|
+
message: "Unterminated regex literal"
|
|
747
|
+
});
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
} else if (char.match(/[A-Za-z_@]/)) {
|
|
751
|
+
const start = pos;
|
|
752
|
+
while (pos < line.length && line[pos].match(/[A-Za-z0-9_:@-]/)) {
|
|
753
|
+
pos++;
|
|
754
|
+
}
|
|
755
|
+
const value = line.slice(start, pos);
|
|
756
|
+
const upperValue = value.toUpperCase();
|
|
757
|
+
let type = "IDENTIFIER";
|
|
758
|
+
let tokenValue = value;
|
|
759
|
+
if (EQLSParser.KEYWORDS.has(upperValue)) {
|
|
760
|
+
type = upperValue;
|
|
761
|
+
tokenValue = upperValue;
|
|
762
|
+
} else if (EQLSParser.MULTI_CHAR_OPERATORS.has(upperValue)) {
|
|
763
|
+
type = "OPERATOR";
|
|
764
|
+
tokenValue = upperValue;
|
|
765
|
+
}
|
|
766
|
+
tokens.push({
|
|
767
|
+
type,
|
|
768
|
+
value: tokenValue,
|
|
769
|
+
line: lineNum + 1,
|
|
770
|
+
column: start + 1
|
|
771
|
+
});
|
|
772
|
+
} else if (char.match(/[0-9]/)) {
|
|
773
|
+
const start = pos;
|
|
774
|
+
let hasDecimal = false;
|
|
775
|
+
while (pos < line.length) {
|
|
776
|
+
const nextChar = line[pos];
|
|
777
|
+
if (nextChar.match(/[0-9]/)) {
|
|
778
|
+
pos++;
|
|
779
|
+
} else if (nextChar === "." && !hasDecimal && pos + 1 < line.length && line[pos + 1].match(/[0-9]/)) {
|
|
780
|
+
hasDecimal = true;
|
|
781
|
+
pos++;
|
|
782
|
+
} else {
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const value = line.slice(start, pos);
|
|
787
|
+
const numValue = value.includes(".") ? parseFloat(value) : parseInt(value, 10);
|
|
788
|
+
tokens.push({
|
|
789
|
+
type: "NUMBER",
|
|
790
|
+
value: numValue,
|
|
791
|
+
line: lineNum + 1,
|
|
792
|
+
column: start + 1
|
|
793
|
+
});
|
|
794
|
+
} else if (char === ".") {
|
|
795
|
+
tokens.push({
|
|
796
|
+
type: "DOT",
|
|
797
|
+
value: ".",
|
|
798
|
+
line: lineNum + 1,
|
|
799
|
+
column: pos + 1
|
|
800
|
+
});
|
|
801
|
+
pos++;
|
|
802
|
+
} else if (char === "?") {
|
|
803
|
+
const start = pos;
|
|
804
|
+
pos++;
|
|
805
|
+
while (pos < line.length && line[pos].match(/[A-Za-z0-9_]/)) {
|
|
806
|
+
pos++;
|
|
807
|
+
}
|
|
808
|
+
const value = line.slice(start, pos);
|
|
809
|
+
tokens.push({
|
|
810
|
+
type: "VARIABLE",
|
|
811
|
+
value,
|
|
812
|
+
line: lineNum + 1,
|
|
813
|
+
column: start + 1
|
|
814
|
+
});
|
|
815
|
+
} else if (EQLSParser.SINGLE_CHAR_OPERATORS.has(char) || char === "!" && pos + 1 < line.length && line[pos + 1] === "=" || char === ">" && pos + 1 < line.length && line[pos + 1] === "=" || char === "<" && pos + 1 < line.length && line[pos + 1] === "=" || char === "=" && pos + 1 < line.length && line[pos + 1] === "=") {
|
|
816
|
+
const start = pos;
|
|
817
|
+
if (char === "!" || char === ">" || char === "<" || char === "=") {
|
|
818
|
+
pos += 2;
|
|
819
|
+
} else {
|
|
820
|
+
pos++;
|
|
821
|
+
}
|
|
822
|
+
const value = line.slice(start, pos);
|
|
823
|
+
tokens.push({
|
|
824
|
+
type: "OPERATOR",
|
|
825
|
+
value,
|
|
826
|
+
line: lineNum + 1,
|
|
827
|
+
column: start + 1
|
|
828
|
+
});
|
|
829
|
+
} else if (char === ",") {
|
|
830
|
+
tokens.push({
|
|
831
|
+
type: "COMMA",
|
|
832
|
+
value: ",",
|
|
833
|
+
line: lineNum + 1,
|
|
834
|
+
column: pos + 1
|
|
835
|
+
});
|
|
836
|
+
pos++;
|
|
837
|
+
} else if (char === "(") {
|
|
838
|
+
tokens.push({
|
|
839
|
+
type: "LPAREN",
|
|
840
|
+
value: "(",
|
|
841
|
+
line: lineNum + 1,
|
|
842
|
+
column: pos + 1
|
|
843
|
+
});
|
|
844
|
+
pos++;
|
|
845
|
+
} else if (char === ")") {
|
|
846
|
+
tokens.push({
|
|
847
|
+
type: "RPAREN",
|
|
848
|
+
value: ")",
|
|
849
|
+
line: lineNum + 1,
|
|
850
|
+
column: pos + 1
|
|
851
|
+
});
|
|
852
|
+
pos++;
|
|
853
|
+
} else {
|
|
854
|
+
this.errors.push({
|
|
855
|
+
line: lineNum + 1,
|
|
856
|
+
column: pos + 1,
|
|
857
|
+
message: `Unexpected character '${char}'`,
|
|
858
|
+
expected: ["identifier", "string", "number", "operator"]
|
|
859
|
+
});
|
|
860
|
+
pos++;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return tokens;
|
|
865
|
+
}
|
|
866
|
+
parseQuery() {
|
|
867
|
+
this.expect("FIND");
|
|
868
|
+
const find = this.expect("IDENTIFIER").value;
|
|
869
|
+
this.expect("AS");
|
|
870
|
+
const as = this.expect("VARIABLE").value;
|
|
871
|
+
let where;
|
|
872
|
+
if (this.match("WHERE")) {
|
|
873
|
+
where = this.parseExpression();
|
|
874
|
+
}
|
|
875
|
+
let returnFields;
|
|
876
|
+
if (this.match("RETURN")) {
|
|
877
|
+
returnFields = this.parseReturnFields();
|
|
878
|
+
}
|
|
879
|
+
let orderBy;
|
|
880
|
+
if (this.match("ORDER")) {
|
|
881
|
+
this.expect("BY");
|
|
882
|
+
const field = this.parseAttributeReference();
|
|
883
|
+
const direction = this.match("DESC") ? "DESC" : this.match("ASC") ? "ASC" : "ASC";
|
|
884
|
+
orderBy = { field, direction };
|
|
885
|
+
}
|
|
886
|
+
let limit;
|
|
887
|
+
if (this.match("LIMIT")) {
|
|
888
|
+
limit = this.expect("NUMBER").value;
|
|
889
|
+
}
|
|
890
|
+
return { find, as, where, return: returnFields, orderBy, limit };
|
|
891
|
+
}
|
|
892
|
+
parseExpression() {
|
|
893
|
+
let left = this.parseTerm();
|
|
894
|
+
while (this.match("AND") || this.match("OR")) {
|
|
895
|
+
const op = this.previous().value;
|
|
896
|
+
const right = this.parseTerm();
|
|
897
|
+
left = { op, left, right };
|
|
898
|
+
}
|
|
899
|
+
return left;
|
|
900
|
+
}
|
|
901
|
+
parseTerm() {
|
|
902
|
+
if (this.match("LPAREN")) {
|
|
903
|
+
const expr = this.parseExpression();
|
|
904
|
+
this.expect("RPAREN");
|
|
905
|
+
return expr;
|
|
906
|
+
}
|
|
907
|
+
if ((this.check("STRING") || this.check("NUMBER") || this.check("IDENTIFIER")) && this.tokens[this.current + 1]?.type === "IN") {
|
|
908
|
+
const value = this.parseValue();
|
|
909
|
+
this.expect("IN");
|
|
910
|
+
const field = this.parseAttributeReference();
|
|
911
|
+
return { type: "MEMBERSHIP", value, field };
|
|
912
|
+
}
|
|
913
|
+
return this.parsePredicate();
|
|
914
|
+
}
|
|
915
|
+
parsePredicate() {
|
|
916
|
+
const field = this.parseAttributeReference();
|
|
917
|
+
if (this.match("BETWEEN")) {
|
|
918
|
+
const min = this.expect("NUMBER").value;
|
|
919
|
+
this.expect("AND");
|
|
920
|
+
const max = this.expect("NUMBER").value;
|
|
921
|
+
return { type: "BETWEEN", field, min, max };
|
|
922
|
+
}
|
|
923
|
+
if (this.match("CONTAINS")) {
|
|
924
|
+
const pattern = this.expect("STRING").value;
|
|
925
|
+
return { type: "CONTAINS", field, pattern };
|
|
926
|
+
}
|
|
927
|
+
if (this.match("MATCHES")) {
|
|
928
|
+
const regex = this.expect("REGEX").value;
|
|
929
|
+
return { type: "MATCHES", field, regex };
|
|
930
|
+
}
|
|
931
|
+
if (this.match("IN")) {
|
|
932
|
+
const value = this.parseValue();
|
|
933
|
+
return { type: "MEMBERSHIP", value, field };
|
|
934
|
+
}
|
|
935
|
+
const op = this.expect("OPERATOR").value.trim();
|
|
936
|
+
const right = this.parseValue();
|
|
937
|
+
if (op === "=" || op === "==") {
|
|
938
|
+
return { type: "EQUALS", field, value: right };
|
|
939
|
+
} else {
|
|
940
|
+
return { type: "COMP", left: field, op, right };
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
parseAttributeReference() {
|
|
944
|
+
const variable = this.expect("VARIABLE").value;
|
|
945
|
+
const attributeParts = [];
|
|
946
|
+
while (this.check("DOT")) {
|
|
947
|
+
this.advance();
|
|
948
|
+
const attributePart = this.expect("IDENTIFIER").value;
|
|
949
|
+
attributeParts.push(this.toCamelCase(attributePart));
|
|
950
|
+
}
|
|
951
|
+
if (attributeParts.length > 0) {
|
|
952
|
+
return `${variable}.${attributeParts.join(".")}`;
|
|
953
|
+
}
|
|
954
|
+
return variable;
|
|
955
|
+
}
|
|
956
|
+
toCamelCase(str) {
|
|
957
|
+
return str;
|
|
958
|
+
}
|
|
959
|
+
parseValue() {
|
|
960
|
+
if (this.match("STRING"))
|
|
961
|
+
return this.previous().value;
|
|
962
|
+
if (this.match("NUMBER"))
|
|
963
|
+
return this.previous().value;
|
|
964
|
+
if (this.match("IDENTIFIER")) {
|
|
965
|
+
const value = this.previous().value;
|
|
966
|
+
if (value === "true")
|
|
967
|
+
return true;
|
|
968
|
+
if (value === "false")
|
|
969
|
+
return false;
|
|
970
|
+
return value;
|
|
971
|
+
}
|
|
972
|
+
if (this.match("VARIABLE"))
|
|
973
|
+
return this.previous().value;
|
|
974
|
+
throw new Error(`Expected value, got ${this.peek().type}`);
|
|
975
|
+
}
|
|
976
|
+
parseReturnFields() {
|
|
977
|
+
const fields = [];
|
|
978
|
+
do {
|
|
979
|
+
const field = this.parseAttributeReference();
|
|
980
|
+
fields.push(field);
|
|
981
|
+
} while (this.match("COMMA"));
|
|
982
|
+
return fields;
|
|
983
|
+
}
|
|
984
|
+
extractContainsFields(expr) {
|
|
985
|
+
const fields = [];
|
|
986
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
987
|
+
fields.push(...this.extractContainsFields(expr.left));
|
|
988
|
+
fields.push(...this.extractContainsFields(expr.right));
|
|
989
|
+
} else if ("type" in expr && expr.type === "CONTAINS" && "field" in expr) {
|
|
990
|
+
fields.push(expr.field);
|
|
991
|
+
}
|
|
992
|
+
return fields;
|
|
993
|
+
}
|
|
994
|
+
match(type) {
|
|
995
|
+
if (this.check(type)) {
|
|
996
|
+
this.advance();
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
check(type) {
|
|
1002
|
+
if (this.isAtEnd())
|
|
1003
|
+
return false;
|
|
1004
|
+
return this.peek().type === type;
|
|
1005
|
+
}
|
|
1006
|
+
advance() {
|
|
1007
|
+
if (!this.isAtEnd())
|
|
1008
|
+
this.current++;
|
|
1009
|
+
return this.previous();
|
|
1010
|
+
}
|
|
1011
|
+
isAtEnd() {
|
|
1012
|
+
return this.peek().type === "EOF";
|
|
1013
|
+
}
|
|
1014
|
+
peek() {
|
|
1015
|
+
return this.tokens[this.current] || {
|
|
1016
|
+
type: "EOF",
|
|
1017
|
+
value: "",
|
|
1018
|
+
line: 0,
|
|
1019
|
+
column: 0
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
previous() {
|
|
1023
|
+
return this.tokens[this.current - 1] || {
|
|
1024
|
+
type: "EOF",
|
|
1025
|
+
value: "",
|
|
1026
|
+
line: 0,
|
|
1027
|
+
column: 0
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
expect(type) {
|
|
1031
|
+
if (this.check(type)) {
|
|
1032
|
+
return this.advance();
|
|
1033
|
+
}
|
|
1034
|
+
const token = this.peek();
|
|
1035
|
+
this.errors.push({
|
|
1036
|
+
line: token.line,
|
|
1037
|
+
column: token.column,
|
|
1038
|
+
message: `Expected ${type}, got ${token.type}`,
|
|
1039
|
+
expected: [type]
|
|
1040
|
+
});
|
|
1041
|
+
throw new Error(`Expected ${type}, got ${token.type}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
class EQLSCompiler {
|
|
1046
|
+
projectionMap = new Map;
|
|
1047
|
+
tempCounter = 0;
|
|
1048
|
+
compileAll(eqlsQuery) {
|
|
1049
|
+
const baseGoals = [];
|
|
1050
|
+
const baseVariables = new Set;
|
|
1051
|
+
this.projectionMap.clear();
|
|
1052
|
+
this.tempCounter = 0;
|
|
1053
|
+
baseGoals.push({
|
|
1054
|
+
predicate: "attr",
|
|
1055
|
+
terms: [eqlsQuery.as, "type", eqlsQuery.find]
|
|
1056
|
+
});
|
|
1057
|
+
baseVariables.add(eqlsQuery.as.substring(1));
|
|
1058
|
+
const returnGoals = [];
|
|
1059
|
+
const returnVars = new Set;
|
|
1060
|
+
if (eqlsQuery.return) {
|
|
1061
|
+
for (const field of eqlsQuery.return) {
|
|
1062
|
+
if (this.isAttributeReference(field)) {
|
|
1063
|
+
const [entityVar, attributePath] = this.splitAttributeReference(field);
|
|
1064
|
+
const outputVar = this.generateTempVar();
|
|
1065
|
+
returnVars.add(outputVar);
|
|
1066
|
+
returnGoals.push({
|
|
1067
|
+
predicate: "attr",
|
|
1068
|
+
terms: [entityVar, attributePath, `?${outputVar}`]
|
|
1069
|
+
});
|
|
1070
|
+
this.projectionMap.set(field, `?${outputVar}`);
|
|
1071
|
+
} else {
|
|
1072
|
+
returnVars.add(field.substring(1));
|
|
1073
|
+
this.projectionMap.set(field, field);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const clauses = eqlsQuery.where ? this.toDNF(eqlsQuery.where) : [[]];
|
|
1078
|
+
const compiledQueries = [];
|
|
1079
|
+
for (const clause of clauses) {
|
|
1080
|
+
const goals = [...baseGoals];
|
|
1081
|
+
const variables = new Set(baseVariables);
|
|
1082
|
+
for (const pred of clause) {
|
|
1083
|
+
this.compilePredicate(pred, goals, variables);
|
|
1084
|
+
}
|
|
1085
|
+
for (const g of returnGoals)
|
|
1086
|
+
goals.push(g);
|
|
1087
|
+
for (const v of returnVars)
|
|
1088
|
+
variables.add(v);
|
|
1089
|
+
compiledQueries.push({ goals, variables });
|
|
1090
|
+
}
|
|
1091
|
+
return compiledQueries;
|
|
1092
|
+
}
|
|
1093
|
+
compile(eqlsQuery) {
|
|
1094
|
+
const all = this.compileAll(eqlsQuery);
|
|
1095
|
+
return all[0] || { goals: [], variables: new Set };
|
|
1096
|
+
}
|
|
1097
|
+
getProjectionMap() {
|
|
1098
|
+
return this.projectionMap;
|
|
1099
|
+
}
|
|
1100
|
+
isAttributeReference(field) {
|
|
1101
|
+
return field.includes(".") && field.startsWith("?");
|
|
1102
|
+
}
|
|
1103
|
+
splitAttributeReference(field) {
|
|
1104
|
+
const parts = field.split(".");
|
|
1105
|
+
if (parts.length < 2) {
|
|
1106
|
+
throw new Error(`Invalid attribute reference: ${field}`);
|
|
1107
|
+
}
|
|
1108
|
+
const entityVar = parts[0];
|
|
1109
|
+
const attributePath = parts.slice(1).join(".");
|
|
1110
|
+
return [entityVar, attributePath];
|
|
1111
|
+
}
|
|
1112
|
+
compileExpression(expr, goals, variables) {
|
|
1113
|
+
if (!expr || typeof expr !== "object") {
|
|
1114
|
+
throw new Error(`Invalid expression: ${expr}`);
|
|
1115
|
+
}
|
|
1116
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1117
|
+
this.compileExpression(expr.left, goals, variables);
|
|
1118
|
+
this.compileExpression(expr.right, goals, variables);
|
|
1119
|
+
} else {
|
|
1120
|
+
this.compilePredicate(expr, goals, variables);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
compilePredicate(pred, goals, variables) {
|
|
1124
|
+
switch (pred.type) {
|
|
1125
|
+
case "EQUALS":
|
|
1126
|
+
goals.push({
|
|
1127
|
+
predicate: "attr",
|
|
1128
|
+
terms: [
|
|
1129
|
+
this.extractEntityVar(pred.field),
|
|
1130
|
+
this.extractAttributePath(pred.field),
|
|
1131
|
+
pred.value
|
|
1132
|
+
]
|
|
1133
|
+
});
|
|
1134
|
+
break;
|
|
1135
|
+
case "MEMBERSHIP":
|
|
1136
|
+
goals.push({
|
|
1137
|
+
predicate: "attr",
|
|
1138
|
+
terms: [
|
|
1139
|
+
this.extractEntityVar(pred.field),
|
|
1140
|
+
this.extractAttributePath(pred.field),
|
|
1141
|
+
pred.value
|
|
1142
|
+
]
|
|
1143
|
+
});
|
|
1144
|
+
break;
|
|
1145
|
+
case "COMP":
|
|
1146
|
+
const tempVar = this.generateTempVar();
|
|
1147
|
+
variables.add(tempVar);
|
|
1148
|
+
goals.push({
|
|
1149
|
+
predicate: "attr",
|
|
1150
|
+
terms: [
|
|
1151
|
+
this.extractEntityVar(pred.left),
|
|
1152
|
+
this.extractAttributePath(pred.left),
|
|
1153
|
+
`?${tempVar}`
|
|
1154
|
+
]
|
|
1155
|
+
});
|
|
1156
|
+
goals.push({
|
|
1157
|
+
predicate: pred.op.toLowerCase(),
|
|
1158
|
+
terms: [`?${tempVar}`, pred.right]
|
|
1159
|
+
});
|
|
1160
|
+
break;
|
|
1161
|
+
case "BETWEEN":
|
|
1162
|
+
const tempVar2 = this.generateTempVar();
|
|
1163
|
+
variables.add(tempVar2);
|
|
1164
|
+
goals.push({
|
|
1165
|
+
predicate: "attr",
|
|
1166
|
+
terms: [
|
|
1167
|
+
this.extractEntityVar(pred.field),
|
|
1168
|
+
this.extractAttributePath(pred.field),
|
|
1169
|
+
`?${tempVar2}`
|
|
1170
|
+
]
|
|
1171
|
+
});
|
|
1172
|
+
goals.push({
|
|
1173
|
+
predicate: "between",
|
|
1174
|
+
terms: [`?${tempVar2}`, pred.min, pred.max]
|
|
1175
|
+
});
|
|
1176
|
+
break;
|
|
1177
|
+
case "CONTAINS":
|
|
1178
|
+
const tempVar3 = this.generateTempVar();
|
|
1179
|
+
variables.add(tempVar3);
|
|
1180
|
+
goals.push({
|
|
1181
|
+
predicate: "attr",
|
|
1182
|
+
terms: [
|
|
1183
|
+
this.extractEntityVar(pred.field),
|
|
1184
|
+
this.extractAttributePath(pred.field),
|
|
1185
|
+
`?${tempVar3}`
|
|
1186
|
+
]
|
|
1187
|
+
});
|
|
1188
|
+
goals.push({
|
|
1189
|
+
predicate: "contains",
|
|
1190
|
+
terms: [`?${tempVar3}`, pred.pattern]
|
|
1191
|
+
});
|
|
1192
|
+
break;
|
|
1193
|
+
case "MATCHES":
|
|
1194
|
+
const tempVar4 = this.generateTempVar();
|
|
1195
|
+
variables.add(tempVar4);
|
|
1196
|
+
const attributePath = this.extractAttributePath(pred.field);
|
|
1197
|
+
const entityVar = this.extractEntityVar(pred.field);
|
|
1198
|
+
goals.push({
|
|
1199
|
+
predicate: "attr",
|
|
1200
|
+
terms: [entityVar, attributePath, `?${tempVar4}`]
|
|
1201
|
+
});
|
|
1202
|
+
goals.push({
|
|
1203
|
+
predicate: "regex",
|
|
1204
|
+
terms: [`?${tempVar4}`, pred.regex]
|
|
1205
|
+
});
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
extractEntityVar(field) {
|
|
1210
|
+
const parts = field.split(".");
|
|
1211
|
+
return parts[0];
|
|
1212
|
+
}
|
|
1213
|
+
extractAttributePath(field) {
|
|
1214
|
+
const parts = field.split(".");
|
|
1215
|
+
if (parts.length > 1) {
|
|
1216
|
+
return parts.slice(1).join(".");
|
|
1217
|
+
}
|
|
1218
|
+
return field.substring(1);
|
|
1219
|
+
}
|
|
1220
|
+
generateTempVar() {
|
|
1221
|
+
this.tempCounter += 1;
|
|
1222
|
+
return `temp${this.tempCounter}`;
|
|
1223
|
+
}
|
|
1224
|
+
toDNF(expr) {
|
|
1225
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1226
|
+
const left = this.toDNF(expr.left);
|
|
1227
|
+
const right = this.toDNF(expr.right);
|
|
1228
|
+
if (expr.op === "OR") {
|
|
1229
|
+
return [...left, ...right];
|
|
1230
|
+
}
|
|
1231
|
+
const combined = [];
|
|
1232
|
+
for (const l of left) {
|
|
1233
|
+
for (const r of right) {
|
|
1234
|
+
combined.push([...l, ...r]);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return combined;
|
|
1238
|
+
}
|
|
1239
|
+
return [[expr]];
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
class EQLSProcessor {
|
|
1244
|
+
parser = new EQLSParser;
|
|
1245
|
+
compiler = new EQLSCompiler;
|
|
1246
|
+
attributeResolver = new AttributeResolver;
|
|
1247
|
+
catalog = [];
|
|
1248
|
+
setSchema(catalog) {
|
|
1249
|
+
this.catalog = catalog;
|
|
1250
|
+
this.attributeResolver.buildSchema(catalog);
|
|
1251
|
+
}
|
|
1252
|
+
process(query) {
|
|
1253
|
+
const parseResult = this.parser.parse(query);
|
|
1254
|
+
if (parseResult.errors.length > 0) {
|
|
1255
|
+
return { errors: parseResult.errors };
|
|
1256
|
+
}
|
|
1257
|
+
this.ensureFieldsInProjection(parseResult.query);
|
|
1258
|
+
if (Object.keys(this.attributeResolver.getSchema()).length > 0) {
|
|
1259
|
+
const entityType = "default";
|
|
1260
|
+
const attributes = this.extractAttributes(parseResult.query);
|
|
1261
|
+
const validation = this.attributeResolver.validateQuery(entityType, attributes);
|
|
1262
|
+
if (!validation.valid) {
|
|
1263
|
+
return {
|
|
1264
|
+
errors: validation.errors.map((msg) => ({
|
|
1265
|
+
message: msg,
|
|
1266
|
+
line: 1,
|
|
1267
|
+
column: 1
|
|
1268
|
+
}))
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
this.resolveAttributesInQuery(parseResult.query, validation.resolved);
|
|
1272
|
+
}
|
|
1273
|
+
const compiledQueries = this.compiler.compileAll(parseResult.query);
|
|
1274
|
+
const optimizer = new QueryOptimizer(this.catalog);
|
|
1275
|
+
const optimizedQueries = compiledQueries.map((q) => optimizer.optimize(q));
|
|
1276
|
+
const projectionMap = this.compiler.getProjectionMap();
|
|
1277
|
+
return {
|
|
1278
|
+
query: optimizedQueries[0],
|
|
1279
|
+
queries: optimizedQueries,
|
|
1280
|
+
errors: [],
|
|
1281
|
+
projectionMap,
|
|
1282
|
+
meta: {
|
|
1283
|
+
orderBy: parseResult.query.orderBy,
|
|
1284
|
+
limit: parseResult.query.limit
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
ensureFieldsInProjection(eqlsQuery) {
|
|
1289
|
+
if (!eqlsQuery.return) {
|
|
1290
|
+
eqlsQuery.return = [];
|
|
1291
|
+
}
|
|
1292
|
+
if (eqlsQuery.where) {
|
|
1293
|
+
const matchesFields = this.extractMatchesFields(eqlsQuery.where);
|
|
1294
|
+
for (const field of matchesFields) {
|
|
1295
|
+
if (!eqlsQuery.return.includes(field)) {
|
|
1296
|
+
eqlsQuery.return.push(field);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const containsFields = this.extractContainsFields(eqlsQuery.where);
|
|
1300
|
+
for (const field of containsFields) {
|
|
1301
|
+
if (!eqlsQuery.return.includes(field)) {
|
|
1302
|
+
eqlsQuery.return.push(field);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (eqlsQuery.orderBy?.field) {
|
|
1307
|
+
const field = eqlsQuery.orderBy.field;
|
|
1308
|
+
if (!eqlsQuery.return.includes(field)) {
|
|
1309
|
+
eqlsQuery.return.push(field);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
extractMatchesFields(expr) {
|
|
1314
|
+
const fields = [];
|
|
1315
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1316
|
+
fields.push(...this.extractMatchesFields(expr.left));
|
|
1317
|
+
fields.push(...this.extractMatchesFields(expr.right));
|
|
1318
|
+
} else if ("type" in expr && expr.type === "MATCHES" && "field" in expr) {
|
|
1319
|
+
fields.push(expr.field);
|
|
1320
|
+
}
|
|
1321
|
+
return fields;
|
|
1322
|
+
}
|
|
1323
|
+
extractContainsFields(expr) {
|
|
1324
|
+
const fields = [];
|
|
1325
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1326
|
+
fields.push(...this.extractContainsFields(expr.left));
|
|
1327
|
+
fields.push(...this.extractContainsFields(expr.right));
|
|
1328
|
+
} else if ("type" in expr && expr.type === "CONTAINS" && "field" in expr) {
|
|
1329
|
+
fields.push(expr.field);
|
|
1330
|
+
}
|
|
1331
|
+
return fields;
|
|
1332
|
+
}
|
|
1333
|
+
extractAttributes(eqlsQuery) {
|
|
1334
|
+
const attributes = new Set;
|
|
1335
|
+
if (eqlsQuery.where) {
|
|
1336
|
+
this.extractAttributesFromExpression(eqlsQuery.where, attributes);
|
|
1337
|
+
}
|
|
1338
|
+
if (eqlsQuery.return) {
|
|
1339
|
+
for (const field of eqlsQuery.return) {
|
|
1340
|
+
if (this.isAttributeReference(field)) {
|
|
1341
|
+
const [, attribute] = this.splitAttributeReference(field);
|
|
1342
|
+
attributes.add(attribute);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return Array.from(attributes);
|
|
1347
|
+
}
|
|
1348
|
+
extractAttributesFromExpression(expr, attributes) {
|
|
1349
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1350
|
+
this.extractAttributesFromExpression(expr.left, attributes);
|
|
1351
|
+
this.extractAttributesFromExpression(expr.right, attributes);
|
|
1352
|
+
} else if ("field" in expr) {
|
|
1353
|
+
if (this.isAttributeReference(expr.field)) {
|
|
1354
|
+
const [, attribute] = this.splitAttributeReference(expr.field);
|
|
1355
|
+
attributes.add(attribute);
|
|
1356
|
+
}
|
|
1357
|
+
} else if ("left" in expr && "right" in expr) {
|
|
1358
|
+
if (typeof expr.left === "string" && this.isAttributeReference(expr.left)) {
|
|
1359
|
+
const [, attribute] = this.splitAttributeReference(expr.left);
|
|
1360
|
+
attributes.add(attribute);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
resolveAttributesInQuery(eqlsQuery, resolved) {
|
|
1365
|
+
if (eqlsQuery.where) {
|
|
1366
|
+
this.resolveAttributesInExpression(eqlsQuery.where, resolved);
|
|
1367
|
+
}
|
|
1368
|
+
if (eqlsQuery.return) {
|
|
1369
|
+
for (let i = 0;i < eqlsQuery.return.length; i++) {
|
|
1370
|
+
const field = eqlsQuery.return[i];
|
|
1371
|
+
if (this.isAttributeReference(field)) {
|
|
1372
|
+
const [entityVar, attribute] = this.splitAttributeReference(field);
|
|
1373
|
+
const resolvedAttr = resolved.get(attribute);
|
|
1374
|
+
if (resolvedAttr) {
|
|
1375
|
+
eqlsQuery.return[i] = `${entityVar}.${resolvedAttr}`;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
resolveAttributesInExpression(expr, resolved) {
|
|
1382
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1383
|
+
this.resolveAttributesInExpression(expr.left, resolved);
|
|
1384
|
+
this.resolveAttributesInExpression(expr.right, resolved);
|
|
1385
|
+
} else if ("field" in expr) {
|
|
1386
|
+
if (this.isAttributeReference(expr.field)) {
|
|
1387
|
+
const [entityVar, attribute] = this.splitAttributeReference(expr.field);
|
|
1388
|
+
const resolvedAttr = resolved.get(attribute);
|
|
1389
|
+
if (resolvedAttr) {
|
|
1390
|
+
expr.field = `${entityVar}.${resolvedAttr}`;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
} else if ("left" in expr && "right" in expr) {
|
|
1394
|
+
if (typeof expr.left === "string" && this.isAttributeReference(expr.left)) {
|
|
1395
|
+
const [entityVar, attribute] = this.splitAttributeReference(expr.left);
|
|
1396
|
+
const resolvedAttr = resolved.get(attribute);
|
|
1397
|
+
if (resolvedAttr) {
|
|
1398
|
+
expr.left = `${entityVar}.${resolvedAttr}`;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
isAttributeReference(field) {
|
|
1404
|
+
return field.includes(".");
|
|
1405
|
+
}
|
|
1406
|
+
splitAttributeReference(field) {
|
|
1407
|
+
const parts = field.split(".");
|
|
1408
|
+
return [parts[0], parts.slice(1).join(".")];
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
// src/query/query-examples.ts
|
|
1412
|
+
class QueryBuilder {
|
|
1413
|
+
evaluator;
|
|
1414
|
+
constructor(evaluator) {
|
|
1415
|
+
this.evaluator = evaluator;
|
|
1416
|
+
}
|
|
1417
|
+
popularCrimePosts() {
|
|
1418
|
+
return {
|
|
1419
|
+
goals: [
|
|
1420
|
+
{ predicate: "attr", terms: ["?P", "type", "post"] },
|
|
1421
|
+
{ predicate: "attr", terms: ["?P", "tags", "crime"] },
|
|
1422
|
+
{ predicate: "attr", terms: ["?P", "reactions.likes", "?Likes"] },
|
|
1423
|
+
{ predicate: "gt", terms: ["?Likes", 1000] }
|
|
1424
|
+
],
|
|
1425
|
+
variables: new Set(["P", "Likes"])
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
stormyMidViews() {
|
|
1429
|
+
return {
|
|
1430
|
+
goals: [
|
|
1431
|
+
{ predicate: "attr", terms: ["?P", "type", "post"] },
|
|
1432
|
+
{ predicate: "attr", terms: ["?P", "body", "?Body"] },
|
|
1433
|
+
{ predicate: "regex", terms: ["?Body", "(storm|forest)"] },
|
|
1434
|
+
{ predicate: "attr", terms: ["?P", "views", "?Views"] },
|
|
1435
|
+
{ predicate: "between", terms: ["?Views", 1000, 5000] }
|
|
1436
|
+
],
|
|
1437
|
+
variables: new Set(["P", "Body", "Views"])
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
topTagsByLikes() {
|
|
1441
|
+
return {
|
|
1442
|
+
goals: [
|
|
1443
|
+
{ predicate: "attr", terms: ["?P", "type", "post"] },
|
|
1444
|
+
{ predicate: "attr", terms: ["?P", "tags", "?Tag"] },
|
|
1445
|
+
{ predicate: "attr", terms: ["?P", "reactions.likes", "?Likes"] }
|
|
1446
|
+
],
|
|
1447
|
+
variables: new Set(["Tag", "Likes"])
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
highEngagementPosts() {
|
|
1451
|
+
return {
|
|
1452
|
+
goals: [
|
|
1453
|
+
{ predicate: "attr", terms: ["?P", "type", "post"] },
|
|
1454
|
+
{ predicate: "attr", terms: ["?P", "reactions.likes", "?Likes"] },
|
|
1455
|
+
{ predicate: "attr", terms: ["?P", "views", "?Views"] },
|
|
1456
|
+
{ predicate: "gt", terms: ["?Likes", "?HalfViews"] }
|
|
1457
|
+
],
|
|
1458
|
+
variables: new Set(["P", "Likes", "Views"])
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
postsByUser(userId) {
|
|
1462
|
+
return {
|
|
1463
|
+
goals: [
|
|
1464
|
+
{ predicate: "attr", terms: ["?P", "type", "post"] },
|
|
1465
|
+
{ predicate: "attr", terms: ["?P", "userId", userId] }
|
|
1466
|
+
],
|
|
1467
|
+
variables: new Set(["P"])
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
postsWithTag(tag) {
|
|
1471
|
+
return {
|
|
1472
|
+
goals: [
|
|
1473
|
+
{ predicate: "attr", terms: ["?P", "type", "post"] },
|
|
1474
|
+
{ predicate: "attr", terms: ["?P", "tags", tag] }
|
|
1475
|
+
],
|
|
1476
|
+
variables: new Set(["P"])
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
postsWithTitleKeyword(keyword) {
|
|
1480
|
+
return {
|
|
1481
|
+
goals: [
|
|
1482
|
+
{ predicate: "attr", terms: ["?P", "type", "post"] },
|
|
1483
|
+
{ predicate: "attr", terms: ["?P", "title", "?Title"] },
|
|
1484
|
+
{ predicate: "contains", terms: ["?Title", keyword] }
|
|
1485
|
+
],
|
|
1486
|
+
variables: new Set(["P", "Title"])
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
class EmailQueryBuilder {
|
|
1492
|
+
evaluator;
|
|
1493
|
+
constructor(evaluator) {
|
|
1494
|
+
this.evaluator = evaluator;
|
|
1495
|
+
}
|
|
1496
|
+
awaitingReply() {
|
|
1497
|
+
return {
|
|
1498
|
+
goals: [
|
|
1499
|
+
{ predicate: "attr", terms: ["?M", "type", "email"] },
|
|
1500
|
+
{ predicate: "attr", terms: ["?M", "is_last_in_thread", true] },
|
|
1501
|
+
{ predicate: "attr", terms: ["?M", "to", "trent@..."] }
|
|
1502
|
+
],
|
|
1503
|
+
variables: new Set(["M"])
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
unpaidInvoices() {
|
|
1507
|
+
return {
|
|
1508
|
+
goals: [
|
|
1509
|
+
{ predicate: "attr", terms: ["?M", "type", "email"] },
|
|
1510
|
+
{ predicate: "attr", terms: ["?M", "subject", "?Subject"] },
|
|
1511
|
+
{ predicate: "ext_regex", terms: ["?Subject", "invoice|receipt|bill"] }
|
|
1512
|
+
],
|
|
1513
|
+
variables: new Set(["M", "Subject"])
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
class QueryRunner {
|
|
1519
|
+
store;
|
|
1520
|
+
evaluator;
|
|
1521
|
+
queryBuilder;
|
|
1522
|
+
constructor(store) {
|
|
1523
|
+
this.store = store;
|
|
1524
|
+
this.evaluator = new DatalogEvaluator(store);
|
|
1525
|
+
this.queryBuilder = new QueryBuilder(this.evaluator);
|
|
1526
|
+
this.addDerivedPredicates();
|
|
1527
|
+
}
|
|
1528
|
+
addDerivedPredicates() {
|
|
1529
|
+
const popularRule = {
|
|
1530
|
+
head: { predicate: "popular", terms: ["?P", "?Likes"] },
|
|
1531
|
+
body: [
|
|
1532
|
+
{ predicate: "attr", terms: ["?P", "type", "post"] },
|
|
1533
|
+
{ predicate: "attr", terms: ["?P", "reactions.likes", "?Likes"] },
|
|
1534
|
+
{ predicate: "gt", terms: ["?Likes", 500] }
|
|
1535
|
+
]
|
|
1536
|
+
};
|
|
1537
|
+
const veryPopularRule = {
|
|
1538
|
+
head: { predicate: "very_popular", terms: ["?P"] },
|
|
1539
|
+
body: [
|
|
1540
|
+
{ predicate: "popular", terms: ["?P", "?Likes"] },
|
|
1541
|
+
{ predicate: "gt", terms: ["?Likes", 1000] }
|
|
1542
|
+
]
|
|
1543
|
+
};
|
|
1544
|
+
const unfeaturedRule = {
|
|
1545
|
+
head: { predicate: "unfeatured", terms: ["?P"] },
|
|
1546
|
+
body: [
|
|
1547
|
+
{ predicate: "attr", terms: ["?P", "type", "post"] }
|
|
1548
|
+
]
|
|
1549
|
+
};
|
|
1550
|
+
this.evaluator.addRule(popularRule);
|
|
1551
|
+
this.evaluator.addRule(veryPopularRule);
|
|
1552
|
+
this.evaluator.addRule(unfeaturedRule);
|
|
1553
|
+
}
|
|
1554
|
+
async runQuery(query) {
|
|
1555
|
+
const result = this.evaluator.evaluate(query);
|
|
1556
|
+
return {
|
|
1557
|
+
results: result.bindings,
|
|
1558
|
+
count: result.bindings.length,
|
|
1559
|
+
executionTime: result.executionTime,
|
|
1560
|
+
plan: result.plan
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
async getPopularCrimePosts() {
|
|
1564
|
+
return this.runQuery(this.queryBuilder.popularCrimePosts());
|
|
1565
|
+
}
|
|
1566
|
+
async getStormyMidViews() {
|
|
1567
|
+
return this.runQuery(this.queryBuilder.stormyMidViews());
|
|
1568
|
+
}
|
|
1569
|
+
async getTopTagsByLikes() {
|
|
1570
|
+
const result = await this.runQuery(this.queryBuilder.topTagsByLikes());
|
|
1571
|
+
const tagLikes = new Map;
|
|
1572
|
+
for (const binding of result.results) {
|
|
1573
|
+
const tag = binding.Tag;
|
|
1574
|
+
const likes = binding.Likes;
|
|
1575
|
+
tagLikes.set(tag, (tagLikes.get(tag) || 0) + likes);
|
|
1576
|
+
}
|
|
1577
|
+
const sortedTags = Array.from(tagLikes.entries()).sort(([, a], [, b]) => b - a).map(([tag, totalLikes]) => ({ tag, totalLikes }));
|
|
1578
|
+
return {
|
|
1579
|
+
...result,
|
|
1580
|
+
aggregatedResults: sortedTags
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
async getHighEngagementPosts() {
|
|
1584
|
+
return this.runQuery(this.queryBuilder.highEngagementPosts());
|
|
1585
|
+
}
|
|
1586
|
+
async getPostsByUser(userId) {
|
|
1587
|
+
return this.runQuery(this.queryBuilder.postsByUser(userId));
|
|
1588
|
+
}
|
|
1589
|
+
async getPostsWithTag(tag) {
|
|
1590
|
+
return this.runQuery(this.queryBuilder.postsWithTag(tag));
|
|
1591
|
+
}
|
|
1592
|
+
async getPostsWithTitleKeyword(keyword) {
|
|
1593
|
+
return this.runQuery(this.queryBuilder.postsWithTitleKeyword(keyword));
|
|
1594
|
+
}
|
|
1595
|
+
async getPopularPosts() {
|
|
1596
|
+
const query = {
|
|
1597
|
+
goals: [{ predicate: "popular", terms: ["?P", "?Likes"] }],
|
|
1598
|
+
variables: new Set(["P", "Likes"])
|
|
1599
|
+
};
|
|
1600
|
+
return this.runQuery(query);
|
|
1601
|
+
}
|
|
1602
|
+
async getVeryPopularPosts() {
|
|
1603
|
+
const query = {
|
|
1604
|
+
goals: [{ predicate: "very_popular", terms: ["?P"] }],
|
|
1605
|
+
variables: new Set(["P"])
|
|
1606
|
+
};
|
|
1607
|
+
return this.runQuery(query);
|
|
1608
|
+
}
|
|
1609
|
+
async getUnfeaturedPosts() {
|
|
1610
|
+
const query = {
|
|
1611
|
+
goals: [{ predicate: "unfeatured", terms: ["?P"] }],
|
|
1612
|
+
variables: new Set(["P"])
|
|
1613
|
+
};
|
|
1614
|
+
return this.runQuery(query);
|
|
1615
|
+
}
|
|
1616
|
+
async getPopularUnfeaturedPosts() {
|
|
1617
|
+
const query = {
|
|
1618
|
+
goals: [
|
|
1619
|
+
{ predicate: "popular", terms: ["?P", "?Likes"] },
|
|
1620
|
+
{ predicate: "unfeatured", terms: ["?P"] }
|
|
1621
|
+
],
|
|
1622
|
+
variables: new Set(["P", "Likes"])
|
|
1623
|
+
};
|
|
1624
|
+
return this.runQuery(query);
|
|
1625
|
+
}
|
|
1626
|
+
getCatalog() {
|
|
1627
|
+
return {
|
|
1628
|
+
entries: this.store.getCatalog(),
|
|
1629
|
+
stats: this.store.getStats()
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
export {
|
|
1634
|
+
QueryRunner,
|
|
1635
|
+
QueryBuilder,
|
|
1636
|
+
ExternalPredicates,
|
|
1637
|
+
EmailQueryBuilder,
|
|
1638
|
+
EQLSProcessor,
|
|
1639
|
+
EQLSParser,
|
|
1640
|
+
EQLSCompiler,
|
|
1641
|
+
DatalogEvaluator,
|
|
1642
|
+
AttributeResolver
|
|
1643
|
+
};
|