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,2248 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
|
+
// src/graph/graph.ts
|
|
20
|
+
class Graph {
|
|
21
|
+
nodes = new Map;
|
|
22
|
+
edges = new Map;
|
|
23
|
+
outIdx = new Map;
|
|
24
|
+
addNode(n) {
|
|
25
|
+
if (this.nodes.has(n.id))
|
|
26
|
+
throw new Error(`node exists: ${n.id}`);
|
|
27
|
+
this.nodes.set(n.id, n);
|
|
28
|
+
if (!this.outIdx.has(n.id))
|
|
29
|
+
this.outIdx.set(n.id, []);
|
|
30
|
+
}
|
|
31
|
+
addEdge(e) {
|
|
32
|
+
if (!this.nodes.has(e.from) || !this.nodes.has(e.to)) {
|
|
33
|
+
throw new Error(`dangling edge ${e.id}: ${e.from} -> ${e.to}`);
|
|
34
|
+
}
|
|
35
|
+
if (this.edges.has(e.id))
|
|
36
|
+
throw new Error(`edge exists: ${e.id}`);
|
|
37
|
+
this.edges.set(e.id, e);
|
|
38
|
+
this.outIdx.get(e.from).push(e.id);
|
|
39
|
+
}
|
|
40
|
+
getNode(id) {
|
|
41
|
+
return this.nodes.get(id);
|
|
42
|
+
}
|
|
43
|
+
out(from) {
|
|
44
|
+
const ids = this.outIdx.get(from) || [];
|
|
45
|
+
return ids.map((id) => this.edges.get(id)).filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
allNodes() {
|
|
48
|
+
return this.nodes.values();
|
|
49
|
+
}
|
|
50
|
+
allEdges() {
|
|
51
|
+
return this.edges.values();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// src/graph/validators.ts
|
|
55
|
+
function validateGraph(g) {
|
|
56
|
+
for (const n of g.allNodes()) {
|
|
57
|
+
const byLabel = new Map;
|
|
58
|
+
for (const e of g.out(n.id)) {
|
|
59
|
+
if (!e.label)
|
|
60
|
+
continue;
|
|
61
|
+
const list = byLabel.get(e.label) || [];
|
|
62
|
+
list.push(e.id);
|
|
63
|
+
byLabel.set(e.label, list);
|
|
64
|
+
}
|
|
65
|
+
for (const [label, ids] of byLabel) {
|
|
66
|
+
if (ids.length > 1) {
|
|
67
|
+
throw new Error(`node ${n.id} has duplicate label "${label}" on edges ${ids.join(",")}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// src/graph/engine.ts
|
|
73
|
+
import crypto from "node:crypto";
|
|
74
|
+
|
|
75
|
+
// src/graph/util.ts
|
|
76
|
+
var interpolate = (tpl, vars) => tpl.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_m, k) => String(pluck(vars, k) ?? ""));
|
|
77
|
+
function pluck(obj, path) {
|
|
78
|
+
return path.split(".").reduce((o, k) => o && o[k] !== undefined ? o[k] : undefined, obj);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/graph/executors.ts
|
|
82
|
+
function makeDefaultExecutors(opts) {
|
|
83
|
+
const { llm, stream, tools } = opts;
|
|
84
|
+
const Agent = async (node, state) => {
|
|
85
|
+
const { system = "", prompt = "", model = "gpt-4o", vars = {}, stream: wantStream = false } = node.data || {};
|
|
86
|
+
const rendered = interpolate(String(prompt || ""), { ...vars, input: state.input, state });
|
|
87
|
+
state.log?.("info", `Agent executing: ${node.id}`, { model, system: system.slice(0, 100) });
|
|
88
|
+
if (wantStream && typeof stream === "function") {
|
|
89
|
+
const iter = await stream({ model, system, prompt: rendered });
|
|
90
|
+
return { output: { stream: iter }, next: "success" };
|
|
91
|
+
} else {
|
|
92
|
+
const { text } = await llm({ model, system, prompt: rendered });
|
|
93
|
+
return { output: { text }, next: "success" };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const Tool = async (node, state) => {
|
|
97
|
+
const { name, args = {} } = node.data || {};
|
|
98
|
+
console.log(`\uD83D\uDD27 [TOOL] Executing tool: ${name}`);
|
|
99
|
+
console.log(`\uD83D\uDCCB [TOOL] Args:`, args);
|
|
100
|
+
console.log(`\uD83D\uDCE5 [TOOL] Input:`, state.output?.text ? `"${String(state.output.text).slice(0, 100)}..."` : "none");
|
|
101
|
+
console.log(`\uD83E\uDDE0 [TOOL] State memory keys:`, Object.keys(state.memory || {}));
|
|
102
|
+
const fn = tools[name];
|
|
103
|
+
if (!fn) {
|
|
104
|
+
console.log(`❌ [TOOL] Tool '${name}' not found in registry`);
|
|
105
|
+
console.log(`\uD83D\uDD0D [TOOL] Available tools:`, Object.keys(tools));
|
|
106
|
+
throw new Error(`tool missing: ${name}`);
|
|
107
|
+
}
|
|
108
|
+
state.log?.("info", `Tool executing: ${name}`, { args });
|
|
109
|
+
try {
|
|
110
|
+
const toolArgs = { ...args, input: state.output?.text, state };
|
|
111
|
+
console.log(`⚡ [TOOL] Calling tool with:`, {
|
|
112
|
+
argKeys: Object.keys(toolArgs),
|
|
113
|
+
hasState: !!toolArgs.state,
|
|
114
|
+
hasInput: !!toolArgs.input
|
|
115
|
+
});
|
|
116
|
+
const res = await fn(toolArgs);
|
|
117
|
+
console.log(`✅ [TOOL] Tool '${name}' completed successfully`);
|
|
118
|
+
console.log(`\uD83D\uDCE4 [TOOL] Result type:`, typeof res);
|
|
119
|
+
console.log(`\uD83D\uDCE4 [TOOL] Result preview:`, res && typeof res === "object" ? Object.keys(res) : String(res).slice(0, 100));
|
|
120
|
+
return { output: { tool: name, result: res }, next: "success" };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.log(`❌ [TOOL] Tool '${name}' failed:`, error.message);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const Router = async (node, state) => {
|
|
127
|
+
const { routes = [] } = node.data || {};
|
|
128
|
+
const hit = routes.find((r) => r?.when?.(state));
|
|
129
|
+
const label = hit?.label || "default";
|
|
130
|
+
state.log?.("debug", `Router routing to: ${label}`, { nodeId: node.id });
|
|
131
|
+
return { output: state.output, next: label };
|
|
132
|
+
};
|
|
133
|
+
const Guard = async (node, state) => {
|
|
134
|
+
const { allow } = node.data || {};
|
|
135
|
+
const ok = typeof allow === "function" ? allow(state) : !!allow;
|
|
136
|
+
state.log?.("debug", `Guard ${ok ? "passed" : "failed"}`, { nodeId: node.id });
|
|
137
|
+
return { output: state.output, next: ok ? "pass" : "fail" };
|
|
138
|
+
};
|
|
139
|
+
const MemoryRead = async (node, state) => {
|
|
140
|
+
const { key } = node.data || {};
|
|
141
|
+
const v = state.memory?.[key];
|
|
142
|
+
state.log?.("debug", `Memory read: ${key}`, { value: v });
|
|
143
|
+
return { output: { ...state.output, memory: { [key]: v } }, next: "success" };
|
|
144
|
+
};
|
|
145
|
+
const MemoryWrite = async (node, state) => {
|
|
146
|
+
const { key, from = "output.text" } = node.data || {};
|
|
147
|
+
const val = pluck(state, from);
|
|
148
|
+
state.memory ||= {};
|
|
149
|
+
state.memory[key] = val;
|
|
150
|
+
state.log?.("debug", `Memory write: ${key}`, { value: val });
|
|
151
|
+
return { output: state.output, next: "success" };
|
|
152
|
+
};
|
|
153
|
+
const End = async (_node, state) => {
|
|
154
|
+
state.log?.("info", "Execution completed");
|
|
155
|
+
return { output: state.output, next: null };
|
|
156
|
+
};
|
|
157
|
+
return { Agent, Tool, Router, Guard, MemoryRead, MemoryWrite, End };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/graph/engine.ts
|
|
161
|
+
class Engine {
|
|
162
|
+
g;
|
|
163
|
+
exec;
|
|
164
|
+
llm;
|
|
165
|
+
tools;
|
|
166
|
+
maxSteps;
|
|
167
|
+
perNodeMs;
|
|
168
|
+
emit;
|
|
169
|
+
orchestrator;
|
|
170
|
+
constructor(graph, {
|
|
171
|
+
executors = {},
|
|
172
|
+
llm,
|
|
173
|
+
tools = {},
|
|
174
|
+
maxSteps = 200,
|
|
175
|
+
perNodeMs = 15000,
|
|
176
|
+
orchestrator,
|
|
177
|
+
onEvent
|
|
178
|
+
} = {}) {
|
|
179
|
+
this.g = graph;
|
|
180
|
+
this.llm = llm ?? (async () => ({ text: "(mock LLM)" }));
|
|
181
|
+
this.tools = tools;
|
|
182
|
+
this.maxSteps = maxSteps;
|
|
183
|
+
this.perNodeMs = perNodeMs;
|
|
184
|
+
this.orchestrator = orchestrator;
|
|
185
|
+
this.emit = onEvent;
|
|
186
|
+
this.exec = {
|
|
187
|
+
...makeDefaultExecutors({ llm: this.llm, stream: this.llm?.stream, tools: this.tools }),
|
|
188
|
+
...executors
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async* run(startId, input, seedState = {}) {
|
|
192
|
+
let current = this.g.getNode(startId);
|
|
193
|
+
if (!current)
|
|
194
|
+
throw new Error("no start node");
|
|
195
|
+
let steps = 0;
|
|
196
|
+
const runId = crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
197
|
+
const traces = [];
|
|
198
|
+
const journal = [];
|
|
199
|
+
let state = {
|
|
200
|
+
runId,
|
|
201
|
+
step: 0,
|
|
202
|
+
input,
|
|
203
|
+
output: null,
|
|
204
|
+
memory: {},
|
|
205
|
+
meta: {},
|
|
206
|
+
traces,
|
|
207
|
+
journal,
|
|
208
|
+
log: (level, msg, data) => this.log(state, level, msg, data),
|
|
209
|
+
...seedState
|
|
210
|
+
};
|
|
211
|
+
console.log(`\uD83D\uDE80 [ENGINE] Starting run ${runId} at node '${startId}'`);
|
|
212
|
+
console.log(`\uD83D\uDCCA [ENGINE] Initial state:`, {
|
|
213
|
+
step: state.step,
|
|
214
|
+
inputType: typeof input,
|
|
215
|
+
memoryKeys: Object.keys(state.memory || {}),
|
|
216
|
+
startNode: current.id,
|
|
217
|
+
nodeKind: current.kind
|
|
218
|
+
});
|
|
219
|
+
this.emit?.({ type: "run.start", ts: Date.now(), runId, data: { input, startId } });
|
|
220
|
+
await this.orchestrator?.onRunStart?.(state);
|
|
221
|
+
while (current) {
|
|
222
|
+
if (++steps > this.maxSteps)
|
|
223
|
+
throw new Error(`step budget exceeded (${this.maxSteps})`);
|
|
224
|
+
state.step = steps;
|
|
225
|
+
console.log(`
|
|
226
|
+
\uD83D\uDD04 [ENGINE] Step ${steps}: Executing node '${current.id}' (${current.kind})`);
|
|
227
|
+
console.log(`\uD83D\uDCCB [ENGINE] Node data:`, current.data);
|
|
228
|
+
console.log(`\uD83E\uDDE0 [ENGINE] Current state memory keys:`, Object.keys(state.memory || {}));
|
|
229
|
+
const outEdges = this.g.out(current.id);
|
|
230
|
+
console.log(`\uD83D\uDD17 [ENGINE] Outgoing edges from '${current.id}': [${outEdges.map((e) => `${e.to}(${e.label || "default"})`).join(", ")}]`);
|
|
231
|
+
this.emit?.({ type: "node.start", ts: Date.now(), runId, nodeId: current.id });
|
|
232
|
+
await this.orchestrator?.beforeNode?.(current, state);
|
|
233
|
+
const t0 = Date.now();
|
|
234
|
+
let result = { output: state.output, next: null };
|
|
235
|
+
let err;
|
|
236
|
+
try {
|
|
237
|
+
const exec = this.exec[current.kind];
|
|
238
|
+
if (!exec)
|
|
239
|
+
throw new Error(`no executor for kind: ${current.kind}`);
|
|
240
|
+
console.log(`⚡ [ENGINE] Executing ${current.kind} executor...`);
|
|
241
|
+
result = await withTimeout(exec(current, state), this.perNodeMs);
|
|
242
|
+
console.log(`✅ [ENGINE] Executor completed:`, {
|
|
243
|
+
outputType: typeof result.output,
|
|
244
|
+
outputKeys: result.output && typeof result.output === "object" ? Object.keys(result.output) : "n/a",
|
|
245
|
+
nextNode: result.next,
|
|
246
|
+
hasOutput: !!result.output
|
|
247
|
+
});
|
|
248
|
+
await this.orchestrator?.afterNode?.(current, state, result);
|
|
249
|
+
} catch (e) {
|
|
250
|
+
err = e?.message ?? String(e);
|
|
251
|
+
console.log(`❌ [ENGINE] Executor failed:`, { error: err, node: current.id, kind: current.kind });
|
|
252
|
+
const errorAction = await this.orchestrator?.onError?.(current, state, e);
|
|
253
|
+
if (errorAction === "retry") {
|
|
254
|
+
continue;
|
|
255
|
+
} else if (errorAction === "skip") {
|
|
256
|
+
result = { output: state.output, next: "success" };
|
|
257
|
+
} else {
|
|
258
|
+
result = { output: state.output, next: "fail" };
|
|
259
|
+
}
|
|
260
|
+
this.emit?.({ type: "node.error", ts: Date.now(), runId, nodeId: current.id, data: { error: err } });
|
|
261
|
+
}
|
|
262
|
+
state.output = result.output;
|
|
263
|
+
const trace = {
|
|
264
|
+
nodeId: current.id,
|
|
265
|
+
kind: current.kind,
|
|
266
|
+
tStart: t0,
|
|
267
|
+
tEnd: Date.now(),
|
|
268
|
+
next: result.next,
|
|
269
|
+
error: err
|
|
270
|
+
};
|
|
271
|
+
traces.push(trace);
|
|
272
|
+
console.log(`\uD83C\uDFC1 [ENGINE] Node '${current.id}' completed in ${trace.tEnd - trace.tStart}ms`);
|
|
273
|
+
console.log(`\uD83D\uDCE4 [ENGINE] Output type: ${typeof state.output}`);
|
|
274
|
+
console.log(`\uD83D\uDD00 [ENGINE] Next routing label: '${result.next || "null"}'`);
|
|
275
|
+
this.emit?.({ type: "node.end", ts: trace.tEnd, runId, nodeId: current.id, data: trace });
|
|
276
|
+
yield { state, trace };
|
|
277
|
+
if (!result.next) {
|
|
278
|
+
console.log(`\uD83D\uDED1 [ENGINE] No next routing label - ending execution`);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
const edgeOverride = await this.orchestrator?.beforeEdgeSelect?.(current.id, result.next, state);
|
|
282
|
+
const finalLabel = edgeOverride?.overrideLabel || result.next;
|
|
283
|
+
console.log(`\uD83D\uDD0D [ENGINE] Selecting edge from '${current.id}' with label '${finalLabel}'`);
|
|
284
|
+
const edge = this.selectEdge(current.id, finalLabel, state);
|
|
285
|
+
if (!edge) {
|
|
286
|
+
console.log(`❌ [ENGINE] No matching edge found for label '${finalLabel}' from '${current.id}'`);
|
|
287
|
+
const availableEdges = this.g.out(current.id);
|
|
288
|
+
console.log(`\uD83D\uDD17 [ENGINE] Available edges:`, availableEdges.map((e) => ({
|
|
289
|
+
to: e.to,
|
|
290
|
+
label: e.label || "default",
|
|
291
|
+
hasCond: !!e.cond
|
|
292
|
+
})));
|
|
293
|
+
if (err)
|
|
294
|
+
throw new Error(`unhandled error at ${current.id}: ${err}`);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
console.log(`✅ [ENGINE] Selected edge: ${current.id} -> ${edge.to} (label: '${edge.label || "default"}')`);
|
|
298
|
+
this.emit?.({ type: "edge.select", ts: Date.now(), runId, edgeId: edge.id, data: { from: current.id, to: edge.to, label: finalLabel } });
|
|
299
|
+
const nextNode = this.g.getNode(edge.to);
|
|
300
|
+
if (!nextNode) {
|
|
301
|
+
console.log(`❌ [ENGINE] Target node '${edge.to}' not found in graph`);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
console.log(`➡️ [ENGINE] Transitioning to node '${edge.to}' (${nextNode.kind})`);
|
|
305
|
+
current = nextNode;
|
|
306
|
+
}
|
|
307
|
+
console.log(`
|
|
308
|
+
\uD83C\uDFC1 [ENGINE] Run ${runId} completed after ${steps} steps`);
|
|
309
|
+
console.log(`\uD83D\uDCCA [ENGINE] Final state:`, {
|
|
310
|
+
outputType: typeof state.output,
|
|
311
|
+
memoryKeys: Object.keys(state.memory || {}),
|
|
312
|
+
traceCount: traces.length,
|
|
313
|
+
lastNode: traces[traces.length - 1]?.nodeId
|
|
314
|
+
});
|
|
315
|
+
this.emit?.({ type: "run.end", ts: Date.now(), runId, data: { output: state.output, steps } });
|
|
316
|
+
await this.orchestrator?.onRunEnd?.(state);
|
|
317
|
+
return state;
|
|
318
|
+
}
|
|
319
|
+
async runToEnd(...args) {
|
|
320
|
+
const gen = this.run(...args);
|
|
321
|
+
for await (const _ of gen) {}
|
|
322
|
+
const result = await gen.next();
|
|
323
|
+
return result.value;
|
|
324
|
+
}
|
|
325
|
+
selectEdge(fromId, label, state) {
|
|
326
|
+
const outs = this.g.out(fromId);
|
|
327
|
+
console.log(`\uD83D\uDD0D [ENGINE] selectEdge: Checking ${outs.length} outgoing edges from '${fromId}' for label '${label}'`);
|
|
328
|
+
const labeled = outs.filter((e) => e.label === label);
|
|
329
|
+
console.log(`\uD83C\uDFF7️ [ENGINE] Found ${labeled.length} edges with exact label match`);
|
|
330
|
+
const pool = labeled.length ? labeled : outs.filter((e) => e.label === "default");
|
|
331
|
+
console.log(`\uD83C\uDFAF [ENGINE] Edge pool size: ${pool.length} (using ${labeled.length ? "labeled" : "default"} edges)`);
|
|
332
|
+
for (const edge of pool) {
|
|
333
|
+
const condResult = typeof edge.cond === "function" ? edge.cond(state) : true;
|
|
334
|
+
console.log(`\uD83D\uDD04 [ENGINE] Testing edge ${fromId} -> ${edge.to}: condition=${condResult}`);
|
|
335
|
+
if (condResult) {
|
|
336
|
+
console.log(`✅ [ENGINE] Selected edge: ${edge.id} (${fromId} -> ${edge.to})`);
|
|
337
|
+
return edge;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
console.log(`❌ [ENGINE] No suitable edge found`);
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
log(state, level, msg, data) {
|
|
344
|
+
const entry = {
|
|
345
|
+
ts: Date.now(),
|
|
346
|
+
level,
|
|
347
|
+
nodeId: state.traces.at(-1)?.nodeId,
|
|
348
|
+
msg,
|
|
349
|
+
data
|
|
350
|
+
};
|
|
351
|
+
state.journal.push(entry);
|
|
352
|
+
this.emit?.({ type: "run.log", ts: entry.ts, runId: state.runId, data: entry });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function withTimeout(p, ms) {
|
|
356
|
+
let to;
|
|
357
|
+
const timeout = new Promise((_, rej) => {
|
|
358
|
+
to = setTimeout(() => rej(new Error(`node timeout ${ms}ms`)), ms);
|
|
359
|
+
});
|
|
360
|
+
return Promise.race([p.finally(() => clearTimeout(to)), timeout]);
|
|
361
|
+
}
|
|
362
|
+
// src/store/eav-store.ts
|
|
363
|
+
function* flatten(obj, base = "") {
|
|
364
|
+
if (Array.isArray(obj)) {
|
|
365
|
+
for (const v of obj) {
|
|
366
|
+
yield* flatten(v, base);
|
|
367
|
+
}
|
|
368
|
+
} else if (obj && typeof obj === "object") {
|
|
369
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
370
|
+
yield* flatten(v, base ? `${base}.${k}` : k);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
yield [base, obj];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function jsonEntityFacts(entityId, root, type) {
|
|
377
|
+
const facts = [{ e: entityId, a: "type", v: type }];
|
|
378
|
+
for (const [a, v] of flatten(root)) {
|
|
379
|
+
if (v === undefined || v === null)
|
|
380
|
+
continue;
|
|
381
|
+
if (Array.isArray(v)) {
|
|
382
|
+
for (const el of v) {
|
|
383
|
+
facts.push({ e: entityId, a, v: el });
|
|
384
|
+
}
|
|
385
|
+
} else if (typeof v === "object") {} else {
|
|
386
|
+
facts.push({ e: entityId, a, v });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return facts;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
class EAVStore {
|
|
393
|
+
facts = [];
|
|
394
|
+
links = [];
|
|
395
|
+
catalog = new Map;
|
|
396
|
+
eavIndex = new Map;
|
|
397
|
+
aevIndex = new Map;
|
|
398
|
+
aveIndex = new Map;
|
|
399
|
+
linkIndex = new Map;
|
|
400
|
+
linkReverseIndex = new Map;
|
|
401
|
+
linkAttrIndex = new Map;
|
|
402
|
+
distinct = new Map;
|
|
403
|
+
addFacts(facts) {
|
|
404
|
+
for (let i = 0;i < facts.length; i++) {
|
|
405
|
+
const fact = facts[i];
|
|
406
|
+
if (fact) {
|
|
407
|
+
this.facts.push(fact);
|
|
408
|
+
this.updateIndexes(fact, this.facts.length - 1);
|
|
409
|
+
this.updateCatalog(fact);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
addLinks(links) {
|
|
414
|
+
for (const link of links) {
|
|
415
|
+
this.links.push(link);
|
|
416
|
+
this.updateLinkIndexes(link);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
deleteFacts(factsToDelete) {
|
|
420
|
+
for (const fact of factsToDelete) {
|
|
421
|
+
const valueKey = this.valueKey(fact.v);
|
|
422
|
+
const indices = this.aveIndex.get(fact.a)?.get(valueKey);
|
|
423
|
+
if (!indices)
|
|
424
|
+
continue;
|
|
425
|
+
let foundIdx = -1;
|
|
426
|
+
for (const idx of indices) {
|
|
427
|
+
const storedFact = this.facts[idx];
|
|
428
|
+
if (storedFact && storedFact.e === fact.e && storedFact.a === fact.a) {
|
|
429
|
+
foundIdx = idx;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (foundIdx !== -1) {
|
|
434
|
+
this.facts[foundIdx] = undefined;
|
|
435
|
+
this.eavIndex.get(fact.e)?.get(fact.a)?.delete(foundIdx);
|
|
436
|
+
this.aevIndex.get(fact.a)?.get(fact.e)?.delete(foundIdx);
|
|
437
|
+
this.aveIndex.get(fact.a)?.get(valueKey)?.delete(foundIdx);
|
|
438
|
+
const entry = this.catalog.get(fact.a);
|
|
439
|
+
if (entry) {}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
deleteLinks(linksToDelete) {
|
|
444
|
+
for (const link of linksToDelete) {
|
|
445
|
+
const initialLen = this.links.length;
|
|
446
|
+
this.links = this.links.filter((l) => !(l.e1 === link.e1 && l.a === link.a && l.e2 === link.e2));
|
|
447
|
+
if (this.links.length < initialLen) {
|
|
448
|
+
this.linkIndex.get(link.e1)?.get(link.a)?.delete(link.e2);
|
|
449
|
+
this.linkReverseIndex.get(link.e2)?.get(link.a)?.delete(link.e1);
|
|
450
|
+
const attrPairs = this.linkAttrIndex.get(link.a);
|
|
451
|
+
if (attrPairs) {
|
|
452
|
+
for (const pair of attrPairs) {
|
|
453
|
+
if (pair[0] === link.e1 && pair[1] === link.e2) {
|
|
454
|
+
attrPairs.delete(pair);
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
updateIndexes(fact, index) {
|
|
463
|
+
if (!this.eavIndex.has(fact.e)) {
|
|
464
|
+
this.eavIndex.set(fact.e, new Map);
|
|
465
|
+
}
|
|
466
|
+
if (!this.eavIndex.get(fact.e).has(fact.a)) {
|
|
467
|
+
this.eavIndex.get(fact.e).set(fact.a, new Set);
|
|
468
|
+
}
|
|
469
|
+
this.eavIndex.get(fact.e).get(fact.a).add(index);
|
|
470
|
+
if (!this.aevIndex.has(fact.a)) {
|
|
471
|
+
this.aevIndex.set(fact.a, new Map);
|
|
472
|
+
}
|
|
473
|
+
if (!this.aevIndex.get(fact.a).has(fact.e)) {
|
|
474
|
+
this.aevIndex.get(fact.a).set(fact.e, new Set);
|
|
475
|
+
}
|
|
476
|
+
this.aevIndex.get(fact.a).get(fact.e).add(index);
|
|
477
|
+
if (!this.aveIndex.has(fact.a)) {
|
|
478
|
+
this.aveIndex.set(fact.a, new Map);
|
|
479
|
+
}
|
|
480
|
+
const valueKey = this.valueKey(fact.v);
|
|
481
|
+
if (!this.aveIndex.get(fact.a).has(valueKey)) {
|
|
482
|
+
this.aveIndex.get(fact.a).set(valueKey, new Set);
|
|
483
|
+
}
|
|
484
|
+
this.aveIndex.get(fact.a).get(valueKey).add(index);
|
|
485
|
+
}
|
|
486
|
+
updateLinkIndexes(link) {
|
|
487
|
+
if (!this.linkIndex.has(link.e1)) {
|
|
488
|
+
this.linkIndex.set(link.e1, new Map);
|
|
489
|
+
}
|
|
490
|
+
const e1Attrs = this.linkIndex.get(link.e1);
|
|
491
|
+
if (!e1Attrs.has(link.a)) {
|
|
492
|
+
e1Attrs.set(link.a, new Set);
|
|
493
|
+
}
|
|
494
|
+
e1Attrs.get(link.a).add(link.e2);
|
|
495
|
+
if (!this.linkReverseIndex.has(link.e2)) {
|
|
496
|
+
this.linkReverseIndex.set(link.e2, new Map);
|
|
497
|
+
}
|
|
498
|
+
const e2Attrs = this.linkReverseIndex.get(link.e2);
|
|
499
|
+
if (!e2Attrs.has(link.a)) {
|
|
500
|
+
e2Attrs.set(link.a, new Set);
|
|
501
|
+
}
|
|
502
|
+
e2Attrs.get(link.a).add(link.e1);
|
|
503
|
+
if (!this.linkAttrIndex.has(link.a)) {
|
|
504
|
+
this.linkAttrIndex.set(link.a, new Set);
|
|
505
|
+
}
|
|
506
|
+
this.linkAttrIndex.get(link.a).add([link.e1, link.e2]);
|
|
507
|
+
}
|
|
508
|
+
valueKey(v) {
|
|
509
|
+
if (v instanceof Date)
|
|
510
|
+
return `date:${v.toISOString()}`;
|
|
511
|
+
return `${typeof v}:${v}`;
|
|
512
|
+
}
|
|
513
|
+
updateCatalog(fact) {
|
|
514
|
+
const entry = this.catalog.get(fact.a) || {
|
|
515
|
+
attribute: fact.a,
|
|
516
|
+
type: this.inferType(fact.v),
|
|
517
|
+
cardinality: "one",
|
|
518
|
+
distinctCount: 0,
|
|
519
|
+
examples: []
|
|
520
|
+
};
|
|
521
|
+
const factType = this.inferType(fact.v);
|
|
522
|
+
if (entry.type !== factType && entry.type !== "mixed") {
|
|
523
|
+
entry.type = "mixed";
|
|
524
|
+
}
|
|
525
|
+
const entityAttrs = this.eavIndex.get(fact.e)?.get(fact.a);
|
|
526
|
+
if (entityAttrs && entityAttrs.size > 1) {
|
|
527
|
+
entry.cardinality = "many";
|
|
528
|
+
}
|
|
529
|
+
const k = this.valueKey(fact.v);
|
|
530
|
+
const s = this.distinct.get(fact.a) || (this.distinct.set(fact.a, new Set), this.distinct.get(fact.a));
|
|
531
|
+
s.add(k);
|
|
532
|
+
entry.distinctCount = s.size;
|
|
533
|
+
if (entry.examples.length < 5 && !entry.examples.includes(fact.v)) {
|
|
534
|
+
entry.examples.push(fact.v);
|
|
535
|
+
}
|
|
536
|
+
if (typeof fact.v === "number") {
|
|
537
|
+
entry.min = Math.min(entry.min ?? fact.v, fact.v);
|
|
538
|
+
entry.max = Math.max(entry.max ?? fact.v, fact.v);
|
|
539
|
+
}
|
|
540
|
+
this.catalog.set(fact.a, entry);
|
|
541
|
+
}
|
|
542
|
+
inferType(v) {
|
|
543
|
+
if (typeof v === "string")
|
|
544
|
+
return "string";
|
|
545
|
+
if (typeof v === "number")
|
|
546
|
+
return "number";
|
|
547
|
+
if (typeof v === "boolean")
|
|
548
|
+
return "boolean";
|
|
549
|
+
if (v instanceof Date)
|
|
550
|
+
return "date";
|
|
551
|
+
return "mixed";
|
|
552
|
+
}
|
|
553
|
+
getFactsByEntity(entity) {
|
|
554
|
+
const indices = this.eavIndex.get(entity);
|
|
555
|
+
if (!indices)
|
|
556
|
+
return [];
|
|
557
|
+
const result = [];
|
|
558
|
+
for (const attrIndices of indices.values()) {
|
|
559
|
+
for (const idx of attrIndices) {
|
|
560
|
+
const fact = this.facts[idx];
|
|
561
|
+
if (fact) {
|
|
562
|
+
result.push(fact);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return result;
|
|
567
|
+
}
|
|
568
|
+
getFactsByAttribute(attribute) {
|
|
569
|
+
const indices = this.aevIndex.get(attribute);
|
|
570
|
+
if (!indices)
|
|
571
|
+
return [];
|
|
572
|
+
const result = [];
|
|
573
|
+
for (const entityIndices of indices.values()) {
|
|
574
|
+
for (const idx of entityIndices) {
|
|
575
|
+
const fact = this.facts[idx];
|
|
576
|
+
if (fact) {
|
|
577
|
+
result.push(fact);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
getFactsByValue(attribute, value) {
|
|
584
|
+
const indices = this.aveIndex.get(attribute)?.get(this.valueKey(value));
|
|
585
|
+
if (!indices)
|
|
586
|
+
return [];
|
|
587
|
+
return Array.from(indices).map((idx) => this.facts[idx]).filter((fact) => fact !== undefined);
|
|
588
|
+
}
|
|
589
|
+
getCatalog() {
|
|
590
|
+
return Array.from(this.catalog.values());
|
|
591
|
+
}
|
|
592
|
+
getCatalogEntry(attribute) {
|
|
593
|
+
return this.catalog.get(attribute);
|
|
594
|
+
}
|
|
595
|
+
getAllFacts() {
|
|
596
|
+
return this.facts.filter((f) => f !== undefined);
|
|
597
|
+
}
|
|
598
|
+
getAllLinks() {
|
|
599
|
+
return [...this.links];
|
|
600
|
+
}
|
|
601
|
+
getLinksByEntity(entity) {
|
|
602
|
+
const results = [];
|
|
603
|
+
const forwardLinks = this.linkIndex.get(entity);
|
|
604
|
+
if (forwardLinks) {
|
|
605
|
+
for (const [attr, targets] of forwardLinks) {
|
|
606
|
+
for (const target of targets) {
|
|
607
|
+
results.push({ e1: entity, a: attr, e2: target });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const reverseLinks = this.linkReverseIndex.get(entity);
|
|
612
|
+
if (reverseLinks) {
|
|
613
|
+
for (const [attr, sources] of reverseLinks) {
|
|
614
|
+
for (const source of sources) {
|
|
615
|
+
results.push({ e1: source, a: attr, e2: entity });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return results;
|
|
620
|
+
}
|
|
621
|
+
getLinksByAttribute(attribute) {
|
|
622
|
+
const results = [];
|
|
623
|
+
const links = this.linkAttrIndex.get(attribute);
|
|
624
|
+
if (links) {
|
|
625
|
+
for (const [e1, e2] of links) {
|
|
626
|
+
results.push({ e1, a: attribute, e2 });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return results;
|
|
630
|
+
}
|
|
631
|
+
getLinksByEntityAndAttribute(entity, attribute) {
|
|
632
|
+
const results = [];
|
|
633
|
+
const attrs = this.linkIndex.get(entity);
|
|
634
|
+
if (attrs) {
|
|
635
|
+
const targets = attrs.get(attribute);
|
|
636
|
+
if (targets) {
|
|
637
|
+
for (const target of targets) {
|
|
638
|
+
results.push({ e1: entity, a: attribute, e2: target });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return results;
|
|
643
|
+
}
|
|
644
|
+
getStats() {
|
|
645
|
+
return {
|
|
646
|
+
totalFacts: this.facts.length,
|
|
647
|
+
totalLinks: this.links.length,
|
|
648
|
+
uniqueEntities: this.eavIndex.size,
|
|
649
|
+
uniqueAttributes: this.aevIndex.size,
|
|
650
|
+
catalogEntries: this.catalog.size
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
snapshot() {
|
|
654
|
+
return {
|
|
655
|
+
facts: this.facts.filter((f) => f !== undefined),
|
|
656
|
+
links: [...this.links],
|
|
657
|
+
catalog: this.getCatalog()
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
restore(snapshot) {
|
|
661
|
+
this.facts = [];
|
|
662
|
+
this.links = [];
|
|
663
|
+
this.catalog.clear();
|
|
664
|
+
this.eavIndex.clear();
|
|
665
|
+
this.aevIndex.clear();
|
|
666
|
+
this.aveIndex.clear();
|
|
667
|
+
this.linkIndex.clear();
|
|
668
|
+
this.linkReverseIndex.clear();
|
|
669
|
+
this.linkAttrIndex.clear();
|
|
670
|
+
this.distinct.clear();
|
|
671
|
+
this.addFacts(snapshot.facts);
|
|
672
|
+
this.addLinks(snapshot.links);
|
|
673
|
+
if (snapshot.catalog) {
|
|
674
|
+
for (const entry of snapshot.catalog) {
|
|
675
|
+
this.catalog.set(entry.attribute, entry);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/query/attribute-resolver.ts
|
|
682
|
+
class AttributeResolver {
|
|
683
|
+
schema = {};
|
|
684
|
+
buildSchema(catalog) {
|
|
685
|
+
this.schema = {};
|
|
686
|
+
for (const entry of catalog) {
|
|
687
|
+
const entityType = "default";
|
|
688
|
+
const attributeName = entry.attribute;
|
|
689
|
+
if (!this.schema[entityType]) {
|
|
690
|
+
this.schema[entityType] = {};
|
|
691
|
+
}
|
|
692
|
+
this.schema[entityType][attributeName] = {
|
|
693
|
+
type: entry.type,
|
|
694
|
+
distinctCount: entry.distinctCount,
|
|
695
|
+
examples: entry.examples
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
resolveAttribute(entityType, queryAttribute) {
|
|
700
|
+
const entitySchema = this.schema[entityType];
|
|
701
|
+
if (!entitySchema) {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
const queryLower = queryAttribute.toLowerCase();
|
|
705
|
+
if (entitySchema[queryAttribute]) {
|
|
706
|
+
return queryAttribute;
|
|
707
|
+
}
|
|
708
|
+
for (const [actualAttribute] of Object.entries(entitySchema)) {
|
|
709
|
+
if (actualAttribute.toLowerCase() === queryLower) {
|
|
710
|
+
return actualAttribute;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
validateQuery(entityType, attributes) {
|
|
716
|
+
const errors = [];
|
|
717
|
+
const resolved = new Map;
|
|
718
|
+
for (const attr of attributes) {
|
|
719
|
+
const resolvedAttr = this.resolveAttribute(entityType, attr);
|
|
720
|
+
if (resolvedAttr) {
|
|
721
|
+
resolved.set(attr, resolvedAttr);
|
|
722
|
+
} else {
|
|
723
|
+
errors.push(`Unknown attribute '${attr}' for entity type '${entityType}'. Available attributes: ${Object.keys(this.schema[entityType] || {}).join(", ")}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
valid: errors.length === 0,
|
|
728
|
+
errors,
|
|
729
|
+
resolved
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
getAvailableAttributes(entityType) {
|
|
733
|
+
return Object.keys(this.schema[entityType] || {});
|
|
734
|
+
}
|
|
735
|
+
getSchema() {
|
|
736
|
+
return this.schema;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/query/query-optimizer.ts
|
|
741
|
+
class QueryOptimizer {
|
|
742
|
+
catalog;
|
|
743
|
+
constructor(catalog = []) {
|
|
744
|
+
this.catalog = catalog;
|
|
745
|
+
}
|
|
746
|
+
optimize(query) {
|
|
747
|
+
if (query.goals.length <= 1)
|
|
748
|
+
return query;
|
|
749
|
+
const optimizedGoals = [];
|
|
750
|
+
const remainingGoals = [...query.goals];
|
|
751
|
+
const boundVars = new Set;
|
|
752
|
+
const typeGoalIdx = remainingGoals.findIndex((g) => g.predicate === "attr" && g.terms[1] === "type");
|
|
753
|
+
if (typeGoalIdx !== -1) {
|
|
754
|
+
const typeGoal = remainingGoals.splice(typeGoalIdx, 1)[0];
|
|
755
|
+
optimizedGoals.push(typeGoal);
|
|
756
|
+
this.collectVars(typeGoal, boundVars);
|
|
757
|
+
}
|
|
758
|
+
while (remainingGoals.length > 0) {
|
|
759
|
+
const bestIdx = this.findBestNextGoal(remainingGoals, boundVars);
|
|
760
|
+
if (bestIdx === -1) {
|
|
761
|
+
const goal = remainingGoals.splice(0, 1)[0];
|
|
762
|
+
optimizedGoals.push(goal);
|
|
763
|
+
this.collectVars(goal, boundVars);
|
|
764
|
+
} else {
|
|
765
|
+
const goal = remainingGoals.splice(bestIdx, 1)[0];
|
|
766
|
+
optimizedGoals.push(goal);
|
|
767
|
+
this.collectVars(goal, boundVars);
|
|
768
|
+
}
|
|
769
|
+
let pushdownPossible = true;
|
|
770
|
+
while (pushdownPossible) {
|
|
771
|
+
const filterIdx = remainingGoals.findIndex((g) => this.isFilter(g) && this.isSatisfied(g, boundVars));
|
|
772
|
+
if (filterIdx !== -1) {
|
|
773
|
+
const filter = remainingGoals.splice(filterIdx, 1)[0];
|
|
774
|
+
optimizedGoals.push(filter);
|
|
775
|
+
} else {
|
|
776
|
+
pushdownPossible = false;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
...query,
|
|
782
|
+
goals: optimizedGoals
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
findBestNextGoal(goals, boundVars) {
|
|
786
|
+
let bestIdx = -1;
|
|
787
|
+
let bestScore = -1;
|
|
788
|
+
const filterVars = new Set;
|
|
789
|
+
for (const goal of goals) {
|
|
790
|
+
if (this.isFilter(goal)) {
|
|
791
|
+
for (const term of goal.terms) {
|
|
792
|
+
if (typeof term === "string" && term.startsWith("?")) {
|
|
793
|
+
filterVars.add(term);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
for (let i = 0;i < goals.length; i++) {
|
|
799
|
+
const goal = goals[i];
|
|
800
|
+
if (this.isFilter(goal))
|
|
801
|
+
continue;
|
|
802
|
+
let score = this.calculateRestrictiveness(goal, boundVars);
|
|
803
|
+
for (const term of goal.terms) {
|
|
804
|
+
if (typeof term === "string" && term.startsWith("?") && !boundVars.has(term) && filterVars.has(term)) {
|
|
805
|
+
score += 25;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (score > bestScore) {
|
|
809
|
+
bestScore = score;
|
|
810
|
+
bestIdx = i;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return bestIdx;
|
|
814
|
+
}
|
|
815
|
+
calculateRestrictiveness(goal, boundVars) {
|
|
816
|
+
let score = 0;
|
|
817
|
+
const terms = goal.terms;
|
|
818
|
+
for (const term of terms) {
|
|
819
|
+
if (typeof term !== "string" || !term.startsWith("?")) {
|
|
820
|
+
score += 100;
|
|
821
|
+
} else if (boundVars.has(term)) {
|
|
822
|
+
score += 50;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (goal.predicate === "attr" && typeof terms[1] === "string") {
|
|
826
|
+
const entry = this.catalog.find((e) => e.attribute === terms[1]);
|
|
827
|
+
if (entry) {
|
|
828
|
+
if (entry.cardinality === "one") {
|
|
829
|
+
score += 20;
|
|
830
|
+
}
|
|
831
|
+
score -= Math.min(10, entry.distinctCount / 100);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return score;
|
|
835
|
+
}
|
|
836
|
+
isFilter(goal) {
|
|
837
|
+
const filters = new Set([
|
|
838
|
+
"gt",
|
|
839
|
+
"lt",
|
|
840
|
+
"between",
|
|
841
|
+
"regex",
|
|
842
|
+
"contains",
|
|
843
|
+
">",
|
|
844
|
+
"<",
|
|
845
|
+
">=",
|
|
846
|
+
"<=",
|
|
847
|
+
"=",
|
|
848
|
+
"!=",
|
|
849
|
+
"after",
|
|
850
|
+
"betweenDate"
|
|
851
|
+
]);
|
|
852
|
+
return filters.has(goal.predicate) || goal.predicate.startsWith("ext_");
|
|
853
|
+
}
|
|
854
|
+
isSatisfied(goal, boundVars) {
|
|
855
|
+
return goal.terms.every((term) => {
|
|
856
|
+
if (typeof term === "string" && term.startsWith("?")) {
|
|
857
|
+
return boundVars.has(term);
|
|
858
|
+
}
|
|
859
|
+
return true;
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
collectVars(goal, boundVars) {
|
|
863
|
+
for (const term of goal.terms) {
|
|
864
|
+
if (typeof term === "string" && term.startsWith("?")) {
|
|
865
|
+
boundVars.add(term);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/query/eqls-parser.ts
|
|
872
|
+
class EQLSParser {
|
|
873
|
+
tokens = [];
|
|
874
|
+
current = 0;
|
|
875
|
+
errors = [];
|
|
876
|
+
static KEYWORDS = new Set([
|
|
877
|
+
"FIND",
|
|
878
|
+
"AS",
|
|
879
|
+
"WHERE",
|
|
880
|
+
"AND",
|
|
881
|
+
"OR",
|
|
882
|
+
"RETURN",
|
|
883
|
+
"ORDER",
|
|
884
|
+
"BY",
|
|
885
|
+
"LIMIT",
|
|
886
|
+
"ASC",
|
|
887
|
+
"DESC",
|
|
888
|
+
"BETWEEN",
|
|
889
|
+
"CONTAINS",
|
|
890
|
+
"MATCHES",
|
|
891
|
+
"IN"
|
|
892
|
+
]);
|
|
893
|
+
static SINGLE_CHAR_OPERATORS = new Set(["=", ">", "<"]);
|
|
894
|
+
static MULTI_CHAR_OPERATORS = new Set([
|
|
895
|
+
"CONTAINS",
|
|
896
|
+
"MATCHES",
|
|
897
|
+
"BETWEEN",
|
|
898
|
+
"IN"
|
|
899
|
+
]);
|
|
900
|
+
parse(query) {
|
|
901
|
+
this.tokens = this.tokenize(query);
|
|
902
|
+
this.current = 0;
|
|
903
|
+
this.errors = [];
|
|
904
|
+
try {
|
|
905
|
+
const parsed = this.parseQuery();
|
|
906
|
+
if (this.errors.length > 0) {
|
|
907
|
+
return { errors: this.errors };
|
|
908
|
+
}
|
|
909
|
+
return { query: parsed, errors: [] };
|
|
910
|
+
} catch (error) {
|
|
911
|
+
this.errors.push({
|
|
912
|
+
line: 1,
|
|
913
|
+
column: 1,
|
|
914
|
+
message: `Parse error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
915
|
+
});
|
|
916
|
+
return { errors: this.errors };
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
tokenize(input) {
|
|
920
|
+
const tokens = [];
|
|
921
|
+
const lines = input.split(`
|
|
922
|
+
`);
|
|
923
|
+
for (let lineNum = 0;lineNum < lines.length; lineNum++) {
|
|
924
|
+
const line = lines[lineNum];
|
|
925
|
+
const trimmed = line.trim();
|
|
926
|
+
if (!trimmed || trimmed.startsWith("--"))
|
|
927
|
+
continue;
|
|
928
|
+
let pos = 0;
|
|
929
|
+
while (pos < line.length) {
|
|
930
|
+
const char = line[pos];
|
|
931
|
+
if (char === " ") {
|
|
932
|
+
pos++;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
if (char === '"') {
|
|
936
|
+
const start = pos;
|
|
937
|
+
pos++;
|
|
938
|
+
while (pos < line.length && line[pos] !== '"') {
|
|
939
|
+
if (line[pos] === "\\" && pos + 1 < line.length) {
|
|
940
|
+
pos += 2;
|
|
941
|
+
} else {
|
|
942
|
+
pos++;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (pos < line.length) {
|
|
946
|
+
pos++;
|
|
947
|
+
const value = line.slice(start + 1, pos - 1);
|
|
948
|
+
tokens.push({
|
|
949
|
+
type: "STRING",
|
|
950
|
+
value,
|
|
951
|
+
line: lineNum + 1,
|
|
952
|
+
column: start + 1
|
|
953
|
+
});
|
|
954
|
+
} else {
|
|
955
|
+
this.errors.push({
|
|
956
|
+
line: lineNum + 1,
|
|
957
|
+
column: start + 1,
|
|
958
|
+
message: "Unterminated string literal"
|
|
959
|
+
});
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
} else if (char === "/" && pos + 1 < line.length) {
|
|
963
|
+
const start = pos;
|
|
964
|
+
pos++;
|
|
965
|
+
while (pos < line.length && line[pos] !== "/") {
|
|
966
|
+
if (line[pos] === "\\" && pos + 1 < line.length) {
|
|
967
|
+
pos += 2;
|
|
968
|
+
} else {
|
|
969
|
+
pos++;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (pos < line.length) {
|
|
973
|
+
pos++;
|
|
974
|
+
const pattern = line.slice(start, pos);
|
|
975
|
+
tokens.push({
|
|
976
|
+
type: "REGEX",
|
|
977
|
+
value: pattern,
|
|
978
|
+
line: lineNum + 1,
|
|
979
|
+
column: start + 1
|
|
980
|
+
});
|
|
981
|
+
} else {
|
|
982
|
+
this.errors.push({
|
|
983
|
+
line: lineNum + 1,
|
|
984
|
+
column: start + 1,
|
|
985
|
+
message: "Unterminated regex literal"
|
|
986
|
+
});
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
989
|
+
} else if (char.match(/[A-Za-z_@]/)) {
|
|
990
|
+
const start = pos;
|
|
991
|
+
while (pos < line.length && line[pos].match(/[A-Za-z0-9_:@-]/)) {
|
|
992
|
+
pos++;
|
|
993
|
+
}
|
|
994
|
+
const value = line.slice(start, pos);
|
|
995
|
+
const upperValue = value.toUpperCase();
|
|
996
|
+
let type = "IDENTIFIER";
|
|
997
|
+
let tokenValue = value;
|
|
998
|
+
if (EQLSParser.KEYWORDS.has(upperValue)) {
|
|
999
|
+
type = upperValue;
|
|
1000
|
+
tokenValue = upperValue;
|
|
1001
|
+
} else if (EQLSParser.MULTI_CHAR_OPERATORS.has(upperValue)) {
|
|
1002
|
+
type = "OPERATOR";
|
|
1003
|
+
tokenValue = upperValue;
|
|
1004
|
+
}
|
|
1005
|
+
tokens.push({
|
|
1006
|
+
type,
|
|
1007
|
+
value: tokenValue,
|
|
1008
|
+
line: lineNum + 1,
|
|
1009
|
+
column: start + 1
|
|
1010
|
+
});
|
|
1011
|
+
} else if (char.match(/[0-9]/)) {
|
|
1012
|
+
const start = pos;
|
|
1013
|
+
let hasDecimal = false;
|
|
1014
|
+
while (pos < line.length) {
|
|
1015
|
+
const nextChar = line[pos];
|
|
1016
|
+
if (nextChar.match(/[0-9]/)) {
|
|
1017
|
+
pos++;
|
|
1018
|
+
} else if (nextChar === "." && !hasDecimal && pos + 1 < line.length && line[pos + 1].match(/[0-9]/)) {
|
|
1019
|
+
hasDecimal = true;
|
|
1020
|
+
pos++;
|
|
1021
|
+
} else {
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const value = line.slice(start, pos);
|
|
1026
|
+
const numValue = value.includes(".") ? parseFloat(value) : parseInt(value, 10);
|
|
1027
|
+
tokens.push({
|
|
1028
|
+
type: "NUMBER",
|
|
1029
|
+
value: numValue,
|
|
1030
|
+
line: lineNum + 1,
|
|
1031
|
+
column: start + 1
|
|
1032
|
+
});
|
|
1033
|
+
} else if (char === ".") {
|
|
1034
|
+
tokens.push({
|
|
1035
|
+
type: "DOT",
|
|
1036
|
+
value: ".",
|
|
1037
|
+
line: lineNum + 1,
|
|
1038
|
+
column: pos + 1
|
|
1039
|
+
});
|
|
1040
|
+
pos++;
|
|
1041
|
+
} else if (char === "?") {
|
|
1042
|
+
const start = pos;
|
|
1043
|
+
pos++;
|
|
1044
|
+
while (pos < line.length && line[pos].match(/[A-Za-z0-9_]/)) {
|
|
1045
|
+
pos++;
|
|
1046
|
+
}
|
|
1047
|
+
const value = line.slice(start, pos);
|
|
1048
|
+
tokens.push({
|
|
1049
|
+
type: "VARIABLE",
|
|
1050
|
+
value,
|
|
1051
|
+
line: lineNum + 1,
|
|
1052
|
+
column: start + 1
|
|
1053
|
+
});
|
|
1054
|
+
} 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] === "=") {
|
|
1055
|
+
const start = pos;
|
|
1056
|
+
if (char === "!" || char === ">" || char === "<" || char === "=") {
|
|
1057
|
+
pos += 2;
|
|
1058
|
+
} else {
|
|
1059
|
+
pos++;
|
|
1060
|
+
}
|
|
1061
|
+
const value = line.slice(start, pos);
|
|
1062
|
+
tokens.push({
|
|
1063
|
+
type: "OPERATOR",
|
|
1064
|
+
value,
|
|
1065
|
+
line: lineNum + 1,
|
|
1066
|
+
column: start + 1
|
|
1067
|
+
});
|
|
1068
|
+
} else if (char === ",") {
|
|
1069
|
+
tokens.push({
|
|
1070
|
+
type: "COMMA",
|
|
1071
|
+
value: ",",
|
|
1072
|
+
line: lineNum + 1,
|
|
1073
|
+
column: pos + 1
|
|
1074
|
+
});
|
|
1075
|
+
pos++;
|
|
1076
|
+
} else if (char === "(") {
|
|
1077
|
+
tokens.push({
|
|
1078
|
+
type: "LPAREN",
|
|
1079
|
+
value: "(",
|
|
1080
|
+
line: lineNum + 1,
|
|
1081
|
+
column: pos + 1
|
|
1082
|
+
});
|
|
1083
|
+
pos++;
|
|
1084
|
+
} else if (char === ")") {
|
|
1085
|
+
tokens.push({
|
|
1086
|
+
type: "RPAREN",
|
|
1087
|
+
value: ")",
|
|
1088
|
+
line: lineNum + 1,
|
|
1089
|
+
column: pos + 1
|
|
1090
|
+
});
|
|
1091
|
+
pos++;
|
|
1092
|
+
} else {
|
|
1093
|
+
this.errors.push({
|
|
1094
|
+
line: lineNum + 1,
|
|
1095
|
+
column: pos + 1,
|
|
1096
|
+
message: `Unexpected character '${char}'`,
|
|
1097
|
+
expected: ["identifier", "string", "number", "operator"]
|
|
1098
|
+
});
|
|
1099
|
+
pos++;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return tokens;
|
|
1104
|
+
}
|
|
1105
|
+
parseQuery() {
|
|
1106
|
+
this.expect("FIND");
|
|
1107
|
+
const find = this.expect("IDENTIFIER").value;
|
|
1108
|
+
this.expect("AS");
|
|
1109
|
+
const as = this.expect("VARIABLE").value;
|
|
1110
|
+
let where;
|
|
1111
|
+
if (this.match("WHERE")) {
|
|
1112
|
+
where = this.parseExpression();
|
|
1113
|
+
}
|
|
1114
|
+
let returnFields;
|
|
1115
|
+
if (this.match("RETURN")) {
|
|
1116
|
+
returnFields = this.parseReturnFields();
|
|
1117
|
+
}
|
|
1118
|
+
let orderBy;
|
|
1119
|
+
if (this.match("ORDER")) {
|
|
1120
|
+
this.expect("BY");
|
|
1121
|
+
const field = this.parseAttributeReference();
|
|
1122
|
+
const direction = this.match("DESC") ? "DESC" : this.match("ASC") ? "ASC" : "ASC";
|
|
1123
|
+
orderBy = { field, direction };
|
|
1124
|
+
}
|
|
1125
|
+
let limit;
|
|
1126
|
+
if (this.match("LIMIT")) {
|
|
1127
|
+
limit = this.expect("NUMBER").value;
|
|
1128
|
+
}
|
|
1129
|
+
return { find, as, where, return: returnFields, orderBy, limit };
|
|
1130
|
+
}
|
|
1131
|
+
parseExpression() {
|
|
1132
|
+
let left = this.parseTerm();
|
|
1133
|
+
while (this.match("AND") || this.match("OR")) {
|
|
1134
|
+
const op = this.previous().value;
|
|
1135
|
+
const right = this.parseTerm();
|
|
1136
|
+
left = { op, left, right };
|
|
1137
|
+
}
|
|
1138
|
+
return left;
|
|
1139
|
+
}
|
|
1140
|
+
parseTerm() {
|
|
1141
|
+
if (this.match("LPAREN")) {
|
|
1142
|
+
const expr = this.parseExpression();
|
|
1143
|
+
this.expect("RPAREN");
|
|
1144
|
+
return expr;
|
|
1145
|
+
}
|
|
1146
|
+
if ((this.check("STRING") || this.check("NUMBER") || this.check("IDENTIFIER")) && this.tokens[this.current + 1]?.type === "IN") {
|
|
1147
|
+
const value = this.parseValue();
|
|
1148
|
+
this.expect("IN");
|
|
1149
|
+
const field = this.parseAttributeReference();
|
|
1150
|
+
return { type: "MEMBERSHIP", value, field };
|
|
1151
|
+
}
|
|
1152
|
+
return this.parsePredicate();
|
|
1153
|
+
}
|
|
1154
|
+
parsePredicate() {
|
|
1155
|
+
const field = this.parseAttributeReference();
|
|
1156
|
+
if (this.match("BETWEEN")) {
|
|
1157
|
+
const min = this.expect("NUMBER").value;
|
|
1158
|
+
this.expect("AND");
|
|
1159
|
+
const max = this.expect("NUMBER").value;
|
|
1160
|
+
return { type: "BETWEEN", field, min, max };
|
|
1161
|
+
}
|
|
1162
|
+
if (this.match("CONTAINS")) {
|
|
1163
|
+
const pattern = this.expect("STRING").value;
|
|
1164
|
+
return { type: "CONTAINS", field, pattern };
|
|
1165
|
+
}
|
|
1166
|
+
if (this.match("MATCHES")) {
|
|
1167
|
+
const regex = this.expect("REGEX").value;
|
|
1168
|
+
return { type: "MATCHES", field, regex };
|
|
1169
|
+
}
|
|
1170
|
+
if (this.match("IN")) {
|
|
1171
|
+
const value = this.parseValue();
|
|
1172
|
+
return { type: "MEMBERSHIP", value, field };
|
|
1173
|
+
}
|
|
1174
|
+
const op = this.expect("OPERATOR").value.trim();
|
|
1175
|
+
const right = this.parseValue();
|
|
1176
|
+
if (op === "=" || op === "==") {
|
|
1177
|
+
return { type: "EQUALS", field, value: right };
|
|
1178
|
+
} else {
|
|
1179
|
+
return { type: "COMP", left: field, op, right };
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
parseAttributeReference() {
|
|
1183
|
+
const variable = this.expect("VARIABLE").value;
|
|
1184
|
+
const attributeParts = [];
|
|
1185
|
+
while (this.check("DOT")) {
|
|
1186
|
+
this.advance();
|
|
1187
|
+
const attributePart = this.expect("IDENTIFIER").value;
|
|
1188
|
+
attributeParts.push(this.toCamelCase(attributePart));
|
|
1189
|
+
}
|
|
1190
|
+
if (attributeParts.length > 0) {
|
|
1191
|
+
return `${variable}.${attributeParts.join(".")}`;
|
|
1192
|
+
}
|
|
1193
|
+
return variable;
|
|
1194
|
+
}
|
|
1195
|
+
toCamelCase(str) {
|
|
1196
|
+
return str;
|
|
1197
|
+
}
|
|
1198
|
+
parseValue() {
|
|
1199
|
+
if (this.match("STRING"))
|
|
1200
|
+
return this.previous().value;
|
|
1201
|
+
if (this.match("NUMBER"))
|
|
1202
|
+
return this.previous().value;
|
|
1203
|
+
if (this.match("IDENTIFIER")) {
|
|
1204
|
+
const value = this.previous().value;
|
|
1205
|
+
if (value === "true")
|
|
1206
|
+
return true;
|
|
1207
|
+
if (value === "false")
|
|
1208
|
+
return false;
|
|
1209
|
+
return value;
|
|
1210
|
+
}
|
|
1211
|
+
if (this.match("VARIABLE"))
|
|
1212
|
+
return this.previous().value;
|
|
1213
|
+
throw new Error(`Expected value, got ${this.peek().type}`);
|
|
1214
|
+
}
|
|
1215
|
+
parseReturnFields() {
|
|
1216
|
+
const fields = [];
|
|
1217
|
+
do {
|
|
1218
|
+
const field = this.parseAttributeReference();
|
|
1219
|
+
fields.push(field);
|
|
1220
|
+
} while (this.match("COMMA"));
|
|
1221
|
+
return fields;
|
|
1222
|
+
}
|
|
1223
|
+
extractContainsFields(expr) {
|
|
1224
|
+
const fields = [];
|
|
1225
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1226
|
+
fields.push(...this.extractContainsFields(expr.left));
|
|
1227
|
+
fields.push(...this.extractContainsFields(expr.right));
|
|
1228
|
+
} else if ("type" in expr && expr.type === "CONTAINS" && "field" in expr) {
|
|
1229
|
+
fields.push(expr.field);
|
|
1230
|
+
}
|
|
1231
|
+
return fields;
|
|
1232
|
+
}
|
|
1233
|
+
match(type) {
|
|
1234
|
+
if (this.check(type)) {
|
|
1235
|
+
this.advance();
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
check(type) {
|
|
1241
|
+
if (this.isAtEnd())
|
|
1242
|
+
return false;
|
|
1243
|
+
return this.peek().type === type;
|
|
1244
|
+
}
|
|
1245
|
+
advance() {
|
|
1246
|
+
if (!this.isAtEnd())
|
|
1247
|
+
this.current++;
|
|
1248
|
+
return this.previous();
|
|
1249
|
+
}
|
|
1250
|
+
isAtEnd() {
|
|
1251
|
+
return this.peek().type === "EOF";
|
|
1252
|
+
}
|
|
1253
|
+
peek() {
|
|
1254
|
+
return this.tokens[this.current] || {
|
|
1255
|
+
type: "EOF",
|
|
1256
|
+
value: "",
|
|
1257
|
+
line: 0,
|
|
1258
|
+
column: 0
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
previous() {
|
|
1262
|
+
return this.tokens[this.current - 1] || {
|
|
1263
|
+
type: "EOF",
|
|
1264
|
+
value: "",
|
|
1265
|
+
line: 0,
|
|
1266
|
+
column: 0
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
expect(type) {
|
|
1270
|
+
if (this.check(type)) {
|
|
1271
|
+
return this.advance();
|
|
1272
|
+
}
|
|
1273
|
+
const token = this.peek();
|
|
1274
|
+
this.errors.push({
|
|
1275
|
+
line: token.line,
|
|
1276
|
+
column: token.column,
|
|
1277
|
+
message: `Expected ${type}, got ${token.type}`,
|
|
1278
|
+
expected: [type]
|
|
1279
|
+
});
|
|
1280
|
+
throw new Error(`Expected ${type}, got ${token.type}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
class EQLSCompiler {
|
|
1285
|
+
projectionMap = new Map;
|
|
1286
|
+
tempCounter = 0;
|
|
1287
|
+
compileAll(eqlsQuery) {
|
|
1288
|
+
const baseGoals = [];
|
|
1289
|
+
const baseVariables = new Set;
|
|
1290
|
+
this.projectionMap.clear();
|
|
1291
|
+
this.tempCounter = 0;
|
|
1292
|
+
baseGoals.push({
|
|
1293
|
+
predicate: "attr",
|
|
1294
|
+
terms: [eqlsQuery.as, "type", eqlsQuery.find]
|
|
1295
|
+
});
|
|
1296
|
+
baseVariables.add(eqlsQuery.as.substring(1));
|
|
1297
|
+
const returnGoals = [];
|
|
1298
|
+
const returnVars = new Set;
|
|
1299
|
+
if (eqlsQuery.return) {
|
|
1300
|
+
for (const field of eqlsQuery.return) {
|
|
1301
|
+
if (this.isAttributeReference(field)) {
|
|
1302
|
+
const [entityVar, attributePath] = this.splitAttributeReference(field);
|
|
1303
|
+
const outputVar = this.generateTempVar();
|
|
1304
|
+
returnVars.add(outputVar);
|
|
1305
|
+
returnGoals.push({
|
|
1306
|
+
predicate: "attr",
|
|
1307
|
+
terms: [entityVar, attributePath, `?${outputVar}`]
|
|
1308
|
+
});
|
|
1309
|
+
this.projectionMap.set(field, `?${outputVar}`);
|
|
1310
|
+
} else {
|
|
1311
|
+
returnVars.add(field.substring(1));
|
|
1312
|
+
this.projectionMap.set(field, field);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
const clauses = eqlsQuery.where ? this.toDNF(eqlsQuery.where) : [[]];
|
|
1317
|
+
const compiledQueries = [];
|
|
1318
|
+
for (const clause of clauses) {
|
|
1319
|
+
const goals = [...baseGoals];
|
|
1320
|
+
const variables = new Set(baseVariables);
|
|
1321
|
+
for (const pred of clause) {
|
|
1322
|
+
this.compilePredicate(pred, goals, variables);
|
|
1323
|
+
}
|
|
1324
|
+
for (const g of returnGoals)
|
|
1325
|
+
goals.push(g);
|
|
1326
|
+
for (const v of returnVars)
|
|
1327
|
+
variables.add(v);
|
|
1328
|
+
compiledQueries.push({ goals, variables });
|
|
1329
|
+
}
|
|
1330
|
+
return compiledQueries;
|
|
1331
|
+
}
|
|
1332
|
+
compile(eqlsQuery) {
|
|
1333
|
+
const all = this.compileAll(eqlsQuery);
|
|
1334
|
+
return all[0] || { goals: [], variables: new Set };
|
|
1335
|
+
}
|
|
1336
|
+
getProjectionMap() {
|
|
1337
|
+
return this.projectionMap;
|
|
1338
|
+
}
|
|
1339
|
+
isAttributeReference(field) {
|
|
1340
|
+
return field.includes(".") && field.startsWith("?");
|
|
1341
|
+
}
|
|
1342
|
+
splitAttributeReference(field) {
|
|
1343
|
+
const parts = field.split(".");
|
|
1344
|
+
if (parts.length < 2) {
|
|
1345
|
+
throw new Error(`Invalid attribute reference: ${field}`);
|
|
1346
|
+
}
|
|
1347
|
+
const entityVar = parts[0];
|
|
1348
|
+
const attributePath = parts.slice(1).join(".");
|
|
1349
|
+
return [entityVar, attributePath];
|
|
1350
|
+
}
|
|
1351
|
+
compileExpression(expr, goals, variables) {
|
|
1352
|
+
if (!expr || typeof expr !== "object") {
|
|
1353
|
+
throw new Error(`Invalid expression: ${expr}`);
|
|
1354
|
+
}
|
|
1355
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1356
|
+
this.compileExpression(expr.left, goals, variables);
|
|
1357
|
+
this.compileExpression(expr.right, goals, variables);
|
|
1358
|
+
} else {
|
|
1359
|
+
this.compilePredicate(expr, goals, variables);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
compilePredicate(pred, goals, variables) {
|
|
1363
|
+
switch (pred.type) {
|
|
1364
|
+
case "EQUALS":
|
|
1365
|
+
goals.push({
|
|
1366
|
+
predicate: "attr",
|
|
1367
|
+
terms: [
|
|
1368
|
+
this.extractEntityVar(pred.field),
|
|
1369
|
+
this.extractAttributePath(pred.field),
|
|
1370
|
+
pred.value
|
|
1371
|
+
]
|
|
1372
|
+
});
|
|
1373
|
+
break;
|
|
1374
|
+
case "MEMBERSHIP":
|
|
1375
|
+
goals.push({
|
|
1376
|
+
predicate: "attr",
|
|
1377
|
+
terms: [
|
|
1378
|
+
this.extractEntityVar(pred.field),
|
|
1379
|
+
this.extractAttributePath(pred.field),
|
|
1380
|
+
pred.value
|
|
1381
|
+
]
|
|
1382
|
+
});
|
|
1383
|
+
break;
|
|
1384
|
+
case "COMP":
|
|
1385
|
+
const tempVar = this.generateTempVar();
|
|
1386
|
+
variables.add(tempVar);
|
|
1387
|
+
goals.push({
|
|
1388
|
+
predicate: "attr",
|
|
1389
|
+
terms: [
|
|
1390
|
+
this.extractEntityVar(pred.left),
|
|
1391
|
+
this.extractAttributePath(pred.left),
|
|
1392
|
+
`?${tempVar}`
|
|
1393
|
+
]
|
|
1394
|
+
});
|
|
1395
|
+
goals.push({
|
|
1396
|
+
predicate: pred.op.toLowerCase(),
|
|
1397
|
+
terms: [`?${tempVar}`, pred.right]
|
|
1398
|
+
});
|
|
1399
|
+
break;
|
|
1400
|
+
case "BETWEEN":
|
|
1401
|
+
const tempVar2 = this.generateTempVar();
|
|
1402
|
+
variables.add(tempVar2);
|
|
1403
|
+
goals.push({
|
|
1404
|
+
predicate: "attr",
|
|
1405
|
+
terms: [
|
|
1406
|
+
this.extractEntityVar(pred.field),
|
|
1407
|
+
this.extractAttributePath(pred.field),
|
|
1408
|
+
`?${tempVar2}`
|
|
1409
|
+
]
|
|
1410
|
+
});
|
|
1411
|
+
goals.push({
|
|
1412
|
+
predicate: "between",
|
|
1413
|
+
terms: [`?${tempVar2}`, pred.min, pred.max]
|
|
1414
|
+
});
|
|
1415
|
+
break;
|
|
1416
|
+
case "CONTAINS":
|
|
1417
|
+
const tempVar3 = this.generateTempVar();
|
|
1418
|
+
variables.add(tempVar3);
|
|
1419
|
+
goals.push({
|
|
1420
|
+
predicate: "attr",
|
|
1421
|
+
terms: [
|
|
1422
|
+
this.extractEntityVar(pred.field),
|
|
1423
|
+
this.extractAttributePath(pred.field),
|
|
1424
|
+
`?${tempVar3}`
|
|
1425
|
+
]
|
|
1426
|
+
});
|
|
1427
|
+
goals.push({
|
|
1428
|
+
predicate: "contains",
|
|
1429
|
+
terms: [`?${tempVar3}`, pred.pattern]
|
|
1430
|
+
});
|
|
1431
|
+
break;
|
|
1432
|
+
case "MATCHES":
|
|
1433
|
+
const tempVar4 = this.generateTempVar();
|
|
1434
|
+
variables.add(tempVar4);
|
|
1435
|
+
const attributePath = this.extractAttributePath(pred.field);
|
|
1436
|
+
const entityVar = this.extractEntityVar(pred.field);
|
|
1437
|
+
goals.push({
|
|
1438
|
+
predicate: "attr",
|
|
1439
|
+
terms: [entityVar, attributePath, `?${tempVar4}`]
|
|
1440
|
+
});
|
|
1441
|
+
goals.push({
|
|
1442
|
+
predicate: "regex",
|
|
1443
|
+
terms: [`?${tempVar4}`, pred.regex]
|
|
1444
|
+
});
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
extractEntityVar(field) {
|
|
1449
|
+
const parts = field.split(".");
|
|
1450
|
+
return parts[0];
|
|
1451
|
+
}
|
|
1452
|
+
extractAttributePath(field) {
|
|
1453
|
+
const parts = field.split(".");
|
|
1454
|
+
if (parts.length > 1) {
|
|
1455
|
+
return parts.slice(1).join(".");
|
|
1456
|
+
}
|
|
1457
|
+
return field.substring(1);
|
|
1458
|
+
}
|
|
1459
|
+
generateTempVar() {
|
|
1460
|
+
this.tempCounter += 1;
|
|
1461
|
+
return `temp${this.tempCounter}`;
|
|
1462
|
+
}
|
|
1463
|
+
toDNF(expr) {
|
|
1464
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1465
|
+
const left = this.toDNF(expr.left);
|
|
1466
|
+
const right = this.toDNF(expr.right);
|
|
1467
|
+
if (expr.op === "OR") {
|
|
1468
|
+
return [...left, ...right];
|
|
1469
|
+
}
|
|
1470
|
+
const combined = [];
|
|
1471
|
+
for (const l of left) {
|
|
1472
|
+
for (const r of right) {
|
|
1473
|
+
combined.push([...l, ...r]);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return combined;
|
|
1477
|
+
}
|
|
1478
|
+
return [[expr]];
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
class EQLSProcessor {
|
|
1483
|
+
parser = new EQLSParser;
|
|
1484
|
+
compiler = new EQLSCompiler;
|
|
1485
|
+
attributeResolver = new AttributeResolver;
|
|
1486
|
+
catalog = [];
|
|
1487
|
+
setSchema(catalog) {
|
|
1488
|
+
this.catalog = catalog;
|
|
1489
|
+
this.attributeResolver.buildSchema(catalog);
|
|
1490
|
+
}
|
|
1491
|
+
process(query) {
|
|
1492
|
+
const parseResult = this.parser.parse(query);
|
|
1493
|
+
if (parseResult.errors.length > 0) {
|
|
1494
|
+
return { errors: parseResult.errors };
|
|
1495
|
+
}
|
|
1496
|
+
this.ensureFieldsInProjection(parseResult.query);
|
|
1497
|
+
if (Object.keys(this.attributeResolver.getSchema()).length > 0) {
|
|
1498
|
+
const entityType = "default";
|
|
1499
|
+
const attributes = this.extractAttributes(parseResult.query);
|
|
1500
|
+
const validation = this.attributeResolver.validateQuery(entityType, attributes);
|
|
1501
|
+
if (!validation.valid) {
|
|
1502
|
+
return {
|
|
1503
|
+
errors: validation.errors.map((msg) => ({
|
|
1504
|
+
message: msg,
|
|
1505
|
+
line: 1,
|
|
1506
|
+
column: 1
|
|
1507
|
+
}))
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
this.resolveAttributesInQuery(parseResult.query, validation.resolved);
|
|
1511
|
+
}
|
|
1512
|
+
const compiledQueries = this.compiler.compileAll(parseResult.query);
|
|
1513
|
+
const optimizer = new QueryOptimizer(this.catalog);
|
|
1514
|
+
const optimizedQueries = compiledQueries.map((q) => optimizer.optimize(q));
|
|
1515
|
+
const projectionMap = this.compiler.getProjectionMap();
|
|
1516
|
+
return {
|
|
1517
|
+
query: optimizedQueries[0],
|
|
1518
|
+
queries: optimizedQueries,
|
|
1519
|
+
errors: [],
|
|
1520
|
+
projectionMap,
|
|
1521
|
+
meta: {
|
|
1522
|
+
orderBy: parseResult.query.orderBy,
|
|
1523
|
+
limit: parseResult.query.limit
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
ensureFieldsInProjection(eqlsQuery) {
|
|
1528
|
+
if (!eqlsQuery.return) {
|
|
1529
|
+
eqlsQuery.return = [];
|
|
1530
|
+
}
|
|
1531
|
+
if (eqlsQuery.where) {
|
|
1532
|
+
const matchesFields = this.extractMatchesFields(eqlsQuery.where);
|
|
1533
|
+
for (const field of matchesFields) {
|
|
1534
|
+
if (!eqlsQuery.return.includes(field)) {
|
|
1535
|
+
eqlsQuery.return.push(field);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const containsFields = this.extractContainsFields(eqlsQuery.where);
|
|
1539
|
+
for (const field of containsFields) {
|
|
1540
|
+
if (!eqlsQuery.return.includes(field)) {
|
|
1541
|
+
eqlsQuery.return.push(field);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (eqlsQuery.orderBy?.field) {
|
|
1546
|
+
const field = eqlsQuery.orderBy.field;
|
|
1547
|
+
if (!eqlsQuery.return.includes(field)) {
|
|
1548
|
+
eqlsQuery.return.push(field);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
extractMatchesFields(expr) {
|
|
1553
|
+
const fields = [];
|
|
1554
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1555
|
+
fields.push(...this.extractMatchesFields(expr.left));
|
|
1556
|
+
fields.push(...this.extractMatchesFields(expr.right));
|
|
1557
|
+
} else if ("type" in expr && expr.type === "MATCHES" && "field" in expr) {
|
|
1558
|
+
fields.push(expr.field);
|
|
1559
|
+
}
|
|
1560
|
+
return fields;
|
|
1561
|
+
}
|
|
1562
|
+
extractContainsFields(expr) {
|
|
1563
|
+
const fields = [];
|
|
1564
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1565
|
+
fields.push(...this.extractContainsFields(expr.left));
|
|
1566
|
+
fields.push(...this.extractContainsFields(expr.right));
|
|
1567
|
+
} else if ("type" in expr && expr.type === "CONTAINS" && "field" in expr) {
|
|
1568
|
+
fields.push(expr.field);
|
|
1569
|
+
}
|
|
1570
|
+
return fields;
|
|
1571
|
+
}
|
|
1572
|
+
extractAttributes(eqlsQuery) {
|
|
1573
|
+
const attributes = new Set;
|
|
1574
|
+
if (eqlsQuery.where) {
|
|
1575
|
+
this.extractAttributesFromExpression(eqlsQuery.where, attributes);
|
|
1576
|
+
}
|
|
1577
|
+
if (eqlsQuery.return) {
|
|
1578
|
+
for (const field of eqlsQuery.return) {
|
|
1579
|
+
if (this.isAttributeReference(field)) {
|
|
1580
|
+
const [, attribute] = this.splitAttributeReference(field);
|
|
1581
|
+
attributes.add(attribute);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return Array.from(attributes);
|
|
1586
|
+
}
|
|
1587
|
+
extractAttributesFromExpression(expr, attributes) {
|
|
1588
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1589
|
+
this.extractAttributesFromExpression(expr.left, attributes);
|
|
1590
|
+
this.extractAttributesFromExpression(expr.right, attributes);
|
|
1591
|
+
} else if ("field" in expr) {
|
|
1592
|
+
if (this.isAttributeReference(expr.field)) {
|
|
1593
|
+
const [, attribute] = this.splitAttributeReference(expr.field);
|
|
1594
|
+
attributes.add(attribute);
|
|
1595
|
+
}
|
|
1596
|
+
} else if ("left" in expr && "right" in expr) {
|
|
1597
|
+
if (typeof expr.left === "string" && this.isAttributeReference(expr.left)) {
|
|
1598
|
+
const [, attribute] = this.splitAttributeReference(expr.left);
|
|
1599
|
+
attributes.add(attribute);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
resolveAttributesInQuery(eqlsQuery, resolved) {
|
|
1604
|
+
if (eqlsQuery.where) {
|
|
1605
|
+
this.resolveAttributesInExpression(eqlsQuery.where, resolved);
|
|
1606
|
+
}
|
|
1607
|
+
if (eqlsQuery.return) {
|
|
1608
|
+
for (let i = 0;i < eqlsQuery.return.length; i++) {
|
|
1609
|
+
const field = eqlsQuery.return[i];
|
|
1610
|
+
if (this.isAttributeReference(field)) {
|
|
1611
|
+
const [entityVar, attribute] = this.splitAttributeReference(field);
|
|
1612
|
+
const resolvedAttr = resolved.get(attribute);
|
|
1613
|
+
if (resolvedAttr) {
|
|
1614
|
+
eqlsQuery.return[i] = `${entityVar}.${resolvedAttr}`;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
resolveAttributesInExpression(expr, resolved) {
|
|
1621
|
+
if ("op" in expr && (expr.op === "AND" || expr.op === "OR")) {
|
|
1622
|
+
this.resolveAttributesInExpression(expr.left, resolved);
|
|
1623
|
+
this.resolveAttributesInExpression(expr.right, resolved);
|
|
1624
|
+
} else if ("field" in expr) {
|
|
1625
|
+
if (this.isAttributeReference(expr.field)) {
|
|
1626
|
+
const [entityVar, attribute] = this.splitAttributeReference(expr.field);
|
|
1627
|
+
const resolvedAttr = resolved.get(attribute);
|
|
1628
|
+
if (resolvedAttr) {
|
|
1629
|
+
expr.field = `${entityVar}.${resolvedAttr}`;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
} else if ("left" in expr && "right" in expr) {
|
|
1633
|
+
if (typeof expr.left === "string" && this.isAttributeReference(expr.left)) {
|
|
1634
|
+
const [entityVar, attribute] = this.splitAttributeReference(expr.left);
|
|
1635
|
+
const resolvedAttr = resolved.get(attribute);
|
|
1636
|
+
if (resolvedAttr) {
|
|
1637
|
+
expr.left = `${entityVar}.${resolvedAttr}`;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
isAttributeReference(field) {
|
|
1643
|
+
return field.includes(".");
|
|
1644
|
+
}
|
|
1645
|
+
splitAttributeReference(field) {
|
|
1646
|
+
const parts = field.split(".");
|
|
1647
|
+
return [parts[0], parts.slice(1).join(".")];
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// src/query/datalog-evaluator.ts
|
|
1652
|
+
class ExternalPredicates {
|
|
1653
|
+
static regex(str, pattern) {
|
|
1654
|
+
if (typeof pattern === "string") {
|
|
1655
|
+
try {
|
|
1656
|
+
const regexMatch = pattern.match(/^\/(.*)\/([gimuy]*)$/);
|
|
1657
|
+
if (regexMatch) {
|
|
1658
|
+
const [, regexPattern, flags] = regexMatch;
|
|
1659
|
+
const regex = new RegExp(regexPattern, flags || "i");
|
|
1660
|
+
return regex.test(str);
|
|
1661
|
+
}
|
|
1662
|
+
return new RegExp(pattern, "i").test(str);
|
|
1663
|
+
} catch (e) {
|
|
1664
|
+
console.warn(`Invalid regex pattern: ${pattern}`, e);
|
|
1665
|
+
return str.toLowerCase().includes(pattern.toLowerCase());
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return pattern.test(str);
|
|
1669
|
+
}
|
|
1670
|
+
static gt(a, b) {
|
|
1671
|
+
return a > b;
|
|
1672
|
+
}
|
|
1673
|
+
static lt(a, b) {
|
|
1674
|
+
return a < b;
|
|
1675
|
+
}
|
|
1676
|
+
static between(val, min, max) {
|
|
1677
|
+
return val >= min && val <= max;
|
|
1678
|
+
}
|
|
1679
|
+
static contains(str, substr) {
|
|
1680
|
+
return str.toLowerCase().includes(substr.toLowerCase());
|
|
1681
|
+
}
|
|
1682
|
+
static after(a, b) {
|
|
1683
|
+
return a > b;
|
|
1684
|
+
}
|
|
1685
|
+
static betweenDate(d, start, end) {
|
|
1686
|
+
return d >= start && d <= end;
|
|
1687
|
+
}
|
|
1688
|
+
static sum(values) {
|
|
1689
|
+
return values.reduce((a, b) => a + b, 0);
|
|
1690
|
+
}
|
|
1691
|
+
static count(values) {
|
|
1692
|
+
return values.length;
|
|
1693
|
+
}
|
|
1694
|
+
static avg(values) {
|
|
1695
|
+
return values.length > 0 ? this.sum(values) / values.length : 0;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
class DatalogEvaluator {
|
|
1700
|
+
store;
|
|
1701
|
+
rules = [];
|
|
1702
|
+
ws = new Map;
|
|
1703
|
+
constructor(store) {
|
|
1704
|
+
this.store = store;
|
|
1705
|
+
}
|
|
1706
|
+
addRule(rule) {
|
|
1707
|
+
this.rules.push(rule);
|
|
1708
|
+
}
|
|
1709
|
+
seedBaseFacts() {
|
|
1710
|
+
const attrRows = [];
|
|
1711
|
+
for (const f of this.store.getAllFacts()) {
|
|
1712
|
+
if (f) {
|
|
1713
|
+
attrRows.push([f.e, f.a, f.v]);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
this.ws.set("attr", attrRows);
|
|
1717
|
+
const linkRows = [];
|
|
1718
|
+
for (const link of this.store.getAllLinks()) {
|
|
1719
|
+
linkRows.push([link.e1, link.a, link.e2]);
|
|
1720
|
+
}
|
|
1721
|
+
this.ws.set("link", linkRows);
|
|
1722
|
+
}
|
|
1723
|
+
pushDerived(predicate, tuple) {
|
|
1724
|
+
const bucket = this.ws.get(predicate) || [];
|
|
1725
|
+
if (!this.ws.has(predicate)) {
|
|
1726
|
+
this.ws.set(predicate, bucket);
|
|
1727
|
+
}
|
|
1728
|
+
const key = JSON.stringify(tuple);
|
|
1729
|
+
if (!bucket._keys) {
|
|
1730
|
+
bucket._keys = new Set;
|
|
1731
|
+
}
|
|
1732
|
+
const keys = bucket._keys;
|
|
1733
|
+
if (keys.has(key)) {
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
bucket.push(tuple);
|
|
1737
|
+
keys.add(key);
|
|
1738
|
+
return true;
|
|
1739
|
+
}
|
|
1740
|
+
evaluate(query, limit) {
|
|
1741
|
+
const startTime = performance.now();
|
|
1742
|
+
const trace = [];
|
|
1743
|
+
this.seedBaseFacts();
|
|
1744
|
+
let added = true;
|
|
1745
|
+
let iterations = 0;
|
|
1746
|
+
const maxIterations = 100;
|
|
1747
|
+
while (added && iterations < maxIterations) {
|
|
1748
|
+
added = false;
|
|
1749
|
+
for (const rule of this.rules) {
|
|
1750
|
+
const bindings2 = this.findBindingsOverWS(rule.body);
|
|
1751
|
+
for (const binding of bindings2) {
|
|
1752
|
+
const head = this.substitute(rule.head, binding);
|
|
1753
|
+
const tuple = head.terms.map((term) => this.resolveTerm(term, binding));
|
|
1754
|
+
if (this.pushDerived(head.predicate, tuple)) {
|
|
1755
|
+
added = true;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
iterations++;
|
|
1760
|
+
}
|
|
1761
|
+
const bindings = this.findBindingsOverWS(query.goals, trace, limit);
|
|
1762
|
+
return {
|
|
1763
|
+
bindings,
|
|
1764
|
+
executionTime: performance.now() - startTime,
|
|
1765
|
+
plan: `Semi-naive evaluation: ${iterations} iterations, ${this.getTotalFacts()} facts`,
|
|
1766
|
+
trace
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
getTotalFacts() {
|
|
1770
|
+
let total = 0;
|
|
1771
|
+
for (const tuples of this.ws.values()) {
|
|
1772
|
+
total += tuples.length;
|
|
1773
|
+
}
|
|
1774
|
+
return total;
|
|
1775
|
+
}
|
|
1776
|
+
findBindingsOverWS(goals, trace, limit) {
|
|
1777
|
+
if (goals.length === 0) {
|
|
1778
|
+
return [{}];
|
|
1779
|
+
}
|
|
1780
|
+
let bindings = [{}];
|
|
1781
|
+
for (const goal of goals) {
|
|
1782
|
+
const goalStartTime = performance.now();
|
|
1783
|
+
const newBindings = [];
|
|
1784
|
+
outer:
|
|
1785
|
+
for (const binding of bindings) {
|
|
1786
|
+
const goalBindings = this.evaluateGoal(goal, binding);
|
|
1787
|
+
for (const goalBinding of goalBindings) {
|
|
1788
|
+
const merged = { ...binding, ...goalBinding };
|
|
1789
|
+
let hasConflict = false;
|
|
1790
|
+
for (const key in merged) {
|
|
1791
|
+
if (binding[key] !== undefined && goalBinding[key] !== undefined && binding[key] !== goalBinding[key]) {
|
|
1792
|
+
hasConflict = true;
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (!hasConflict) {
|
|
1797
|
+
newBindings.push(merged);
|
|
1798
|
+
if (limit !== undefined && newBindings.length >= limit)
|
|
1799
|
+
break outer;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
bindings = newBindings;
|
|
1804
|
+
if (trace) {
|
|
1805
|
+
trace.push({
|
|
1806
|
+
goal: `${goal.predicate}(${goal.terms.join(", ")})`,
|
|
1807
|
+
bindingsCount: bindings.length,
|
|
1808
|
+
durationMs: performance.now() - goalStartTime
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
const uniqueBindings = new Map;
|
|
1813
|
+
for (const binding of bindings) {
|
|
1814
|
+
const key = JSON.stringify(binding);
|
|
1815
|
+
uniqueBindings.set(key, binding);
|
|
1816
|
+
}
|
|
1817
|
+
return Array.from(uniqueBindings.values());
|
|
1818
|
+
}
|
|
1819
|
+
evaluateGoal(goal, binding) {
|
|
1820
|
+
const { predicate, terms } = goal;
|
|
1821
|
+
if (predicate === "not") {
|
|
1822
|
+
const inner = goal.terms[0];
|
|
1823
|
+
const res = this.evaluateGoal(inner, binding);
|
|
1824
|
+
return res.length === 0 ? [binding] : [];
|
|
1825
|
+
}
|
|
1826
|
+
if (predicate === "attr") {
|
|
1827
|
+
return this.evaluateAttrPredicate(terms, binding);
|
|
1828
|
+
}
|
|
1829
|
+
if (predicate === "link") {
|
|
1830
|
+
return this.evaluateLinkPredicate(terms, binding);
|
|
1831
|
+
}
|
|
1832
|
+
if (predicate === "gt" || predicate === "lt" || predicate === "between" || predicate === ">" || predicate === "<" || predicate === ">=" || predicate === "<=" || predicate === "=" || predicate === "!=") {
|
|
1833
|
+
return this.evaluateComparisonPredicate(goal, binding);
|
|
1834
|
+
}
|
|
1835
|
+
if (predicate === "regex" || predicate === "contains") {
|
|
1836
|
+
return this.evaluateStringPredicate(goal, binding);
|
|
1837
|
+
}
|
|
1838
|
+
if (predicate === "after" || predicate === "betweenDate") {
|
|
1839
|
+
return this.evaluateDatePredicate(goal, binding);
|
|
1840
|
+
}
|
|
1841
|
+
if (predicate.startsWith("ext_")) {
|
|
1842
|
+
return this.evaluateExternalPredicate(goal, binding);
|
|
1843
|
+
}
|
|
1844
|
+
return this.evalPredicateFromWS(predicate, terms, binding);
|
|
1845
|
+
}
|
|
1846
|
+
evalPredicateFromWS(predicate, terms, binding) {
|
|
1847
|
+
const rows = this.ws.get(predicate) || [];
|
|
1848
|
+
const results = [];
|
|
1849
|
+
rowloop:
|
|
1850
|
+
for (const row of rows) {
|
|
1851
|
+
const newBinding = { ...binding };
|
|
1852
|
+
for (let i = 0;i < terms.length; i++) {
|
|
1853
|
+
const term = terms[i];
|
|
1854
|
+
const val = row[i];
|
|
1855
|
+
if (typeof term === "string" && term.startsWith("?")) {
|
|
1856
|
+
const bound = newBinding[term];
|
|
1857
|
+
if (bound !== undefined && bound !== val) {
|
|
1858
|
+
continue rowloop;
|
|
1859
|
+
}
|
|
1860
|
+
newBinding[term] = val;
|
|
1861
|
+
} else {
|
|
1862
|
+
if (term !== val) {
|
|
1863
|
+
continue rowloop;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
results.push(newBinding);
|
|
1868
|
+
}
|
|
1869
|
+
return results;
|
|
1870
|
+
}
|
|
1871
|
+
evaluateAttrPredicate(terms, binding) {
|
|
1872
|
+
if (terms.length !== 3)
|
|
1873
|
+
return [];
|
|
1874
|
+
const [entity, attribute, value] = terms.map((term) => this.resolveTerm(term, binding));
|
|
1875
|
+
const results = [];
|
|
1876
|
+
if (typeof entity === "string" && !entity.startsWith("?") && typeof attribute === "string" && !attribute.startsWith("?") && (typeof value !== "string" || !value.startsWith("?"))) {
|
|
1877
|
+
const facts = this.store.getFactsByValue(attribute, value);
|
|
1878
|
+
for (const fact of facts) {
|
|
1879
|
+
if (fact.e === entity) {
|
|
1880
|
+
results.push({});
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return results;
|
|
1884
|
+
}
|
|
1885
|
+
if (typeof entity === "string" && !entity.startsWith("?") && typeof attribute === "string" && !attribute.startsWith("?")) {
|
|
1886
|
+
const facts = this.store.getFactsByEntity(entity);
|
|
1887
|
+
for (const fact of facts) {
|
|
1888
|
+
if (fact.a === attribute) {
|
|
1889
|
+
const newBinding = { ...binding };
|
|
1890
|
+
if (typeof value === "string" && value.startsWith("?")) {
|
|
1891
|
+
newBinding[value] = fact.v;
|
|
1892
|
+
results.push(newBinding);
|
|
1893
|
+
} else if (fact.v === value) {
|
|
1894
|
+
results.push(newBinding);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return results;
|
|
1899
|
+
}
|
|
1900
|
+
if (typeof attribute === "string" && !attribute.startsWith("?")) {
|
|
1901
|
+
const facts = this.store.getFactsByAttribute(attribute);
|
|
1902
|
+
for (const fact of facts) {
|
|
1903
|
+
const newBinding = { ...binding };
|
|
1904
|
+
if (typeof entity === "string" && !entity.startsWith("?") && fact.e !== entity) {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
if ((typeof value !== "string" || !value.startsWith("?")) && fact.v !== value) {
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
if (typeof entity === "string" && entity.startsWith("?")) {
|
|
1911
|
+
newBinding[entity] = fact.e;
|
|
1912
|
+
}
|
|
1913
|
+
if (typeof value === "string" && value.startsWith("?")) {
|
|
1914
|
+
newBinding[value] = fact.v;
|
|
1915
|
+
}
|
|
1916
|
+
results.push(newBinding);
|
|
1917
|
+
}
|
|
1918
|
+
return results;
|
|
1919
|
+
}
|
|
1920
|
+
return [];
|
|
1921
|
+
}
|
|
1922
|
+
evaluateLinkPredicate(terms, binding) {
|
|
1923
|
+
if (terms.length !== 3)
|
|
1924
|
+
return [];
|
|
1925
|
+
const [e1, a, e2] = terms;
|
|
1926
|
+
const results = [];
|
|
1927
|
+
const links = this.store.getAllLinks();
|
|
1928
|
+
for (const link of links) {
|
|
1929
|
+
const newBinding = { ...binding };
|
|
1930
|
+
let matches = true;
|
|
1931
|
+
if (typeof e1 === "string" && !e1.startsWith("?")) {
|
|
1932
|
+
if (link.e1 !== e1)
|
|
1933
|
+
continue;
|
|
1934
|
+
} else if (typeof e1 === "string" && e1.startsWith("?")) {
|
|
1935
|
+
newBinding[e1] = link.e1;
|
|
1936
|
+
}
|
|
1937
|
+
if (typeof a === "string" && !a.startsWith("?")) {
|
|
1938
|
+
if (link.a !== a)
|
|
1939
|
+
continue;
|
|
1940
|
+
} else if (typeof a === "string" && a.startsWith("?")) {
|
|
1941
|
+
newBinding[a] = link.a;
|
|
1942
|
+
}
|
|
1943
|
+
if (typeof e2 === "string" && !e2.startsWith("?")) {
|
|
1944
|
+
if (link.e2 !== e2)
|
|
1945
|
+
continue;
|
|
1946
|
+
} else if (typeof e2 === "string" && e2.startsWith("?")) {
|
|
1947
|
+
newBinding[e2] = link.e2;
|
|
1948
|
+
}
|
|
1949
|
+
if (matches) {
|
|
1950
|
+
results.push(newBinding);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return results;
|
|
1954
|
+
}
|
|
1955
|
+
evaluateComparisonPredicate(goal, binding) {
|
|
1956
|
+
const { predicate, terms } = goal;
|
|
1957
|
+
if (terms.length < 2)
|
|
1958
|
+
return [];
|
|
1959
|
+
const left = this.resolveTerm(terms[0], binding);
|
|
1960
|
+
const right = this.resolveTerm(terms[1], binding);
|
|
1961
|
+
let leftNum = left;
|
|
1962
|
+
let rightNum = right;
|
|
1963
|
+
if (typeof left === "string" && !isNaN(Number(left))) {
|
|
1964
|
+
leftNum = Number(left);
|
|
1965
|
+
}
|
|
1966
|
+
if (typeof right === "string" && !isNaN(Number(right))) {
|
|
1967
|
+
rightNum = Number(right);
|
|
1968
|
+
}
|
|
1969
|
+
if (typeof leftNum !== "number" || typeof rightNum !== "number")
|
|
1970
|
+
return [];
|
|
1971
|
+
let result = false;
|
|
1972
|
+
switch (predicate) {
|
|
1973
|
+
case "gt":
|
|
1974
|
+
case ">":
|
|
1975
|
+
result = ExternalPredicates.gt(leftNum, rightNum);
|
|
1976
|
+
break;
|
|
1977
|
+
case "lt":
|
|
1978
|
+
case "<":
|
|
1979
|
+
result = ExternalPredicates.lt(leftNum, rightNum);
|
|
1980
|
+
break;
|
|
1981
|
+
case ">=":
|
|
1982
|
+
result = leftNum >= rightNum;
|
|
1983
|
+
break;
|
|
1984
|
+
case "<=":
|
|
1985
|
+
result = leftNum <= rightNum;
|
|
1986
|
+
break;
|
|
1987
|
+
case "=":
|
|
1988
|
+
result = leftNum === rightNum;
|
|
1989
|
+
break;
|
|
1990
|
+
case "!=":
|
|
1991
|
+
result = leftNum !== rightNum;
|
|
1992
|
+
break;
|
|
1993
|
+
case "between":
|
|
1994
|
+
if (terms.length >= 3) {
|
|
1995
|
+
const max = this.resolveTerm(terms[2], binding);
|
|
1996
|
+
let maxNum = max;
|
|
1997
|
+
if (typeof max === "string" && !isNaN(Number(max))) {
|
|
1998
|
+
maxNum = Number(max);
|
|
1999
|
+
}
|
|
2000
|
+
if (typeof maxNum === "number") {
|
|
2001
|
+
result = ExternalPredicates.between(leftNum, rightNum, maxNum);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
break;
|
|
2005
|
+
}
|
|
2006
|
+
return result ? [{}] : [];
|
|
2007
|
+
}
|
|
2008
|
+
evaluateStringPredicate(goal, binding) {
|
|
2009
|
+
const { predicate, terms } = goal;
|
|
2010
|
+
if (terms.length < 2)
|
|
2011
|
+
return [];
|
|
2012
|
+
const str = this.resolveTerm(terms[0], binding);
|
|
2013
|
+
const pattern = this.resolveTerm(terms[1], binding);
|
|
2014
|
+
if (typeof str !== "string" || typeof pattern !== "string")
|
|
2015
|
+
return [];
|
|
2016
|
+
let result = false;
|
|
2017
|
+
switch (predicate) {
|
|
2018
|
+
case "regex":
|
|
2019
|
+
result = ExternalPredicates.regex(str, pattern);
|
|
2020
|
+
break;
|
|
2021
|
+
case "contains":
|
|
2022
|
+
result = ExternalPredicates.contains(str, pattern);
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
return result ? [{}] : [];
|
|
2026
|
+
}
|
|
2027
|
+
evaluateDatePredicate(goal, binding) {
|
|
2028
|
+
const { predicate, terms } = goal;
|
|
2029
|
+
if (terms.length < 2)
|
|
2030
|
+
return [];
|
|
2031
|
+
const left = this.resolveTerm(terms[0], binding);
|
|
2032
|
+
const right = this.resolveTerm(terms[1], binding);
|
|
2033
|
+
if (!(left instanceof Date) || !(right instanceof Date))
|
|
2034
|
+
return [];
|
|
2035
|
+
let result = false;
|
|
2036
|
+
switch (predicate) {
|
|
2037
|
+
case "after":
|
|
2038
|
+
result = ExternalPredicates.after(left, right);
|
|
2039
|
+
break;
|
|
2040
|
+
case "betweenDate":
|
|
2041
|
+
if (terms.length >= 3) {
|
|
2042
|
+
const end = this.resolveTerm(terms[2], binding);
|
|
2043
|
+
if (end instanceof Date) {
|
|
2044
|
+
result = ExternalPredicates.betweenDate(left, right, end);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
2049
|
+
return result ? [{}] : [];
|
|
2050
|
+
}
|
|
2051
|
+
evaluateExternalPredicate(goal, binding) {
|
|
2052
|
+
const { predicate, terms } = goal;
|
|
2053
|
+
const resolvedTerms = terms.map((term) => this.resolveTerm(term, binding));
|
|
2054
|
+
let result = false;
|
|
2055
|
+
switch (predicate) {
|
|
2056
|
+
case "ext_regex":
|
|
2057
|
+
if (resolvedTerms.length >= 2 && typeof resolvedTerms[0] === "string") {
|
|
2058
|
+
result = ExternalPredicates.regex(resolvedTerms[0], resolvedTerms[1]);
|
|
2059
|
+
}
|
|
2060
|
+
break;
|
|
2061
|
+
case "ext_gt":
|
|
2062
|
+
if (resolvedTerms.length >= 2 && typeof resolvedTerms[0] === "number" && typeof resolvedTerms[1] === "number") {
|
|
2063
|
+
result = ExternalPredicates.gt(resolvedTerms[0], resolvedTerms[1]);
|
|
2064
|
+
}
|
|
2065
|
+
break;
|
|
2066
|
+
case "ext_between":
|
|
2067
|
+
if (resolvedTerms.length >= 3 && typeof resolvedTerms[0] === "number" && typeof resolvedTerms[1] === "number" && typeof resolvedTerms[2] === "number") {
|
|
2068
|
+
result = ExternalPredicates.between(resolvedTerms[0], resolvedTerms[1], resolvedTerms[2]);
|
|
2069
|
+
}
|
|
2070
|
+
break;
|
|
2071
|
+
case "ext_contains":
|
|
2072
|
+
if (resolvedTerms.length >= 2 && typeof resolvedTerms[0] === "string") {
|
|
2073
|
+
result = ExternalPredicates.contains(resolvedTerms[0], resolvedTerms[1]);
|
|
2074
|
+
}
|
|
2075
|
+
break;
|
|
2076
|
+
}
|
|
2077
|
+
return result ? [{}] : [];
|
|
2078
|
+
}
|
|
2079
|
+
resolveTerm(term, binding) {
|
|
2080
|
+
if (typeof term === "string" && term.startsWith("?")) {
|
|
2081
|
+
return binding[term] || term;
|
|
2082
|
+
}
|
|
2083
|
+
return term;
|
|
2084
|
+
}
|
|
2085
|
+
substitute(atom, binding) {
|
|
2086
|
+
return {
|
|
2087
|
+
predicate: atom.predicate,
|
|
2088
|
+
terms: atom.terms.map((term) => this.resolveTerm(term, binding))
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// src/graph/tools.ts
|
|
2094
|
+
var vm = null;
|
|
2095
|
+
try {
|
|
2096
|
+
vm = await import("node:vm");
|
|
2097
|
+
} catch {}
|
|
2098
|
+
var builtinTools = {
|
|
2099
|
+
async run_js({ input }) {
|
|
2100
|
+
const m = String(input ?? "").match(/```(?:js|javascript)?\s*([\s\S]*?)```/i);
|
|
2101
|
+
if (!m)
|
|
2102
|
+
return { error: "no code block" };
|
|
2103
|
+
const code = m[1];
|
|
2104
|
+
if (vm?.default || vm?.Script) {
|
|
2105
|
+
try {
|
|
2106
|
+
const Script = vm.default?.Script ?? vm.Script;
|
|
2107
|
+
const script = new Script(code, { filename: "user.js" });
|
|
2108
|
+
const ctx = (vm.default?.createContext ?? vm.createContext)({});
|
|
2109
|
+
const res = await script.runInContext(ctx, {
|
|
2110
|
+
timeout: 500,
|
|
2111
|
+
microtaskMode: "afterEvaluate"
|
|
2112
|
+
});
|
|
2113
|
+
return { ok: true, result: res };
|
|
2114
|
+
} catch (e) {
|
|
2115
|
+
return { ok: false, error: String(e) };
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
try {
|
|
2119
|
+
const fn = new Function(code);
|
|
2120
|
+
const res = await (async () => fn())();
|
|
2121
|
+
return { ok: true, result: res };
|
|
2122
|
+
} catch (e) {
|
|
2123
|
+
return { ok: false, error: String(e) };
|
|
2124
|
+
}
|
|
2125
|
+
},
|
|
2126
|
+
async tql_query({
|
|
2127
|
+
query,
|
|
2128
|
+
data,
|
|
2129
|
+
dataUrl,
|
|
2130
|
+
entityType = "item",
|
|
2131
|
+
idKey = "id",
|
|
2132
|
+
limit = 100,
|
|
2133
|
+
state
|
|
2134
|
+
}) {
|
|
2135
|
+
try {
|
|
2136
|
+
const eqlsQuery = query || (typeof state?.input === "string" ? state.input : "");
|
|
2137
|
+
if (!eqlsQuery) {
|
|
2138
|
+
return { error: "No EQL-S query provided" };
|
|
2139
|
+
}
|
|
2140
|
+
const store = new EAVStore;
|
|
2141
|
+
let sourceData = data;
|
|
2142
|
+
if (!sourceData && dataUrl) {
|
|
2143
|
+
const response = await fetch(dataUrl);
|
|
2144
|
+
if (!response.ok) {
|
|
2145
|
+
return { error: `Failed to fetch data: ${response.statusText}` };
|
|
2146
|
+
}
|
|
2147
|
+
sourceData = await response.json();
|
|
2148
|
+
}
|
|
2149
|
+
if (!sourceData) {
|
|
2150
|
+
const memoryKey = entityType === "post" ? "posts" : entityType === "user" ? "users" : entityType;
|
|
2151
|
+
sourceData = state?.memory?.[memoryKey] || state?.memory?.data || state?.data;
|
|
2152
|
+
}
|
|
2153
|
+
if (!sourceData) {
|
|
2154
|
+
return {
|
|
2155
|
+
error: "No data source provided (data, dataUrl, or state.memory.data)"
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
const dataArray = Array.isArray(sourceData) ? sourceData : [sourceData];
|
|
2159
|
+
for (let i = 0;i < dataArray.length; i++) {
|
|
2160
|
+
const item = dataArray[i];
|
|
2161
|
+
const entityId = item[idKey] ? `${entityType}:${item[idKey]}` : `${entityType}:${i}`;
|
|
2162
|
+
const facts = jsonEntityFacts(entityId, item, entityType);
|
|
2163
|
+
store.addFacts(facts);
|
|
2164
|
+
}
|
|
2165
|
+
const processor = new EQLSProcessor;
|
|
2166
|
+
const catalog = store.getCatalog();
|
|
2167
|
+
processor.setSchema(catalog);
|
|
2168
|
+
const evaluator = new DatalogEvaluator(store);
|
|
2169
|
+
const parseResult = processor.process(eqlsQuery);
|
|
2170
|
+
if (parseResult.errors.length > 0) {
|
|
2171
|
+
return {
|
|
2172
|
+
ok: false,
|
|
2173
|
+
error: "Query parsing failed",
|
|
2174
|
+
parseErrors: parseResult.errors.map((e) => ({
|
|
2175
|
+
line: e.line,
|
|
2176
|
+
column: e.column,
|
|
2177
|
+
message: e.message,
|
|
2178
|
+
expected: e.expected
|
|
2179
|
+
}))
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
const result = evaluator.evaluate(parseResult.query);
|
|
2183
|
+
const limitedResults = limit > 0 ? result.bindings.slice(0, limit) : result.bindings;
|
|
2184
|
+
return {
|
|
2185
|
+
ok: true,
|
|
2186
|
+
results: limitedResults,
|
|
2187
|
+
count: limitedResults.length,
|
|
2188
|
+
totalCount: result.bindings.length,
|
|
2189
|
+
executionTime: result.executionTime,
|
|
2190
|
+
store: {
|
|
2191
|
+
totalFacts: store.getStats().totalFacts,
|
|
2192
|
+
uniqueEntities: store.getStats().uniqueEntities,
|
|
2193
|
+
uniqueAttributes: store.getStats().uniqueAttributes
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
} catch (e) {
|
|
2197
|
+
return {
|
|
2198
|
+
ok: false,
|
|
2199
|
+
error: e?.message || String(e),
|
|
2200
|
+
stack: e?.stack
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
},
|
|
2204
|
+
async tql_load_data({
|
|
2205
|
+
data,
|
|
2206
|
+
dataUrl,
|
|
2207
|
+
key = "data",
|
|
2208
|
+
state
|
|
2209
|
+
}) {
|
|
2210
|
+
try {
|
|
2211
|
+
let sourceData = data;
|
|
2212
|
+
if (!sourceData && dataUrl) {
|
|
2213
|
+
const response = await fetch(dataUrl);
|
|
2214
|
+
if (!response.ok) {
|
|
2215
|
+
return { error: `Failed to fetch data: ${response.statusText}` };
|
|
2216
|
+
}
|
|
2217
|
+
sourceData = await response.json();
|
|
2218
|
+
}
|
|
2219
|
+
if (!sourceData) {
|
|
2220
|
+
return { error: "No data source provided (data or dataUrl)" };
|
|
2221
|
+
}
|
|
2222
|
+
if (state?.memory) {
|
|
2223
|
+
state.memory[key] = sourceData;
|
|
2224
|
+
}
|
|
2225
|
+
const count = Array.isArray(sourceData) ? sourceData.length : 1;
|
|
2226
|
+
return {
|
|
2227
|
+
ok: true,
|
|
2228
|
+
message: `Loaded ${count} items into state.memory.${key}`,
|
|
2229
|
+
count,
|
|
2230
|
+
dataType: Array.isArray(sourceData) ? "array" : typeof sourceData
|
|
2231
|
+
};
|
|
2232
|
+
} catch (e) {
|
|
2233
|
+
return {
|
|
2234
|
+
ok: false,
|
|
2235
|
+
error: e?.message || String(e)
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
export {
|
|
2241
|
+
validateGraph,
|
|
2242
|
+
pluck,
|
|
2243
|
+
makeDefaultExecutors,
|
|
2244
|
+
interpolate,
|
|
2245
|
+
builtinTools,
|
|
2246
|
+
Graph,
|
|
2247
|
+
Engine
|
|
2248
|
+
};
|