ai-mind-map 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +554 -0
- package/dist/change-tracker/change-log.d.ts +160 -0
- package/dist/change-tracker/change-log.d.ts.map +1 -0
- package/dist/change-tracker/change-log.js +507 -0
- package/dist/change-tracker/change-log.js.map +1 -0
- package/dist/change-tracker/diff-engine.d.ts +149 -0
- package/dist/change-tracker/diff-engine.d.ts.map +1 -0
- package/dist/change-tracker/diff-engine.js +530 -0
- package/dist/change-tracker/diff-engine.js.map +1 -0
- package/dist/change-tracker/watcher.d.ts +137 -0
- package/dist/change-tracker/watcher.d.ts.map +1 -0
- package/dist/change-tracker/watcher.js +300 -0
- package/dist/change-tracker/watcher.js.map +1 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +937 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +222 -0
- package/dist/config.js.map +1 -0
- package/dist/context/compressor.d.ts +49 -0
- package/dist/context/compressor.d.ts.map +1 -0
- package/dist/context/compressor.js +769 -0
- package/dist/context/compressor.js.map +1 -0
- package/dist/context/progressive-disclosure.d.ts +71 -0
- package/dist/context/progressive-disclosure.d.ts.map +1 -0
- package/dist/context/progressive-disclosure.js +470 -0
- package/dist/context/progressive-disclosure.js.map +1 -0
- package/dist/context/token-budget.d.ts +121 -0
- package/dist/context/token-budget.d.ts.map +1 -0
- package/dist/context/token-budget.js +282 -0
- package/dist/context/token-budget.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +944 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +66 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +946 -0
- package/dist/install.js.map +1 -0
- package/dist/knowledge-graph/architecture.d.ts +213 -0
- package/dist/knowledge-graph/architecture.d.ts.map +1 -0
- package/dist/knowledge-graph/architecture.js +585 -0
- package/dist/knowledge-graph/architecture.js.map +1 -0
- package/dist/knowledge-graph/cypher.d.ts +113 -0
- package/dist/knowledge-graph/cypher.d.ts.map +1 -0
- package/dist/knowledge-graph/cypher.js +1051 -0
- package/dist/knowledge-graph/cypher.js.map +1 -0
- package/dist/knowledge-graph/dead-code.d.ts +121 -0
- package/dist/knowledge-graph/dead-code.d.ts.map +1 -0
- package/dist/knowledge-graph/dead-code.js +331 -0
- package/dist/knowledge-graph/dead-code.js.map +1 -0
- package/dist/knowledge-graph/flow-analyzer.d.ts +167 -0
- package/dist/knowledge-graph/flow-analyzer.d.ts.map +1 -0
- package/dist/knowledge-graph/flow-analyzer.js +739 -0
- package/dist/knowledge-graph/flow-analyzer.js.map +1 -0
- package/dist/knowledge-graph/graph.d.ts +291 -0
- package/dist/knowledge-graph/graph.d.ts.map +1 -0
- package/dist/knowledge-graph/graph.js +978 -0
- package/dist/knowledge-graph/graph.js.map +1 -0
- package/dist/knowledge-graph/index.d.ts +17 -0
- package/dist/knowledge-graph/index.d.ts.map +1 -0
- package/dist/knowledge-graph/index.js +14 -0
- package/dist/knowledge-graph/index.js.map +1 -0
- package/dist/knowledge-graph/indexer.d.ts +112 -0
- package/dist/knowledge-graph/indexer.d.ts.map +1 -0
- package/dist/knowledge-graph/indexer.js +506 -0
- package/dist/knowledge-graph/indexer.js.map +1 -0
- package/dist/knowledge-graph/pagerank.d.ts +141 -0
- package/dist/knowledge-graph/pagerank.d.ts.map +1 -0
- package/dist/knowledge-graph/pagerank.js +493 -0
- package/dist/knowledge-graph/pagerank.js.map +1 -0
- package/dist/knowledge-graph/parser.d.ts +55 -0
- package/dist/knowledge-graph/parser.d.ts.map +1 -0
- package/dist/knowledge-graph/parser.js +1090 -0
- package/dist/knowledge-graph/parser.js.map +1 -0
- package/dist/knowledge-graph/snapshot.d.ts +107 -0
- package/dist/knowledge-graph/snapshot.d.ts.map +1 -0
- package/dist/knowledge-graph/snapshot.js +435 -0
- package/dist/knowledge-graph/snapshot.js.map +1 -0
- package/dist/memory/decision-log.d.ts +151 -0
- package/dist/memory/decision-log.d.ts.map +1 -0
- package/dist/memory/decision-log.js +482 -0
- package/dist/memory/decision-log.js.map +1 -0
- package/dist/memory/persistent-memory.d.ts +182 -0
- package/dist/memory/persistent-memory.d.ts.map +1 -0
- package/dist/memory/persistent-memory.js +579 -0
- package/dist/memory/persistent-memory.js.map +1 -0
- package/dist/memory/session-memory.d.ts +165 -0
- package/dist/memory/session-memory.d.ts.map +1 -0
- package/dist/memory/session-memory.js +382 -0
- package/dist/memory/session-memory.js.map +1 -0
- package/dist/stress-test.d.ts +10 -0
- package/dist/stress-test.d.ts.map +1 -0
- package/dist/stress-test.js +258 -0
- package/dist/stress-test.js.map +1 -0
- package/dist/tools/advanced-tools.d.ts +32 -0
- package/dist/tools/advanced-tools.d.ts.map +1 -0
- package/dist/tools/advanced-tools.js +480 -0
- package/dist/tools/advanced-tools.js.map +1 -0
- package/dist/tools/change-tools.d.ts +76 -0
- package/dist/tools/change-tools.d.ts.map +1 -0
- package/dist/tools/change-tools.js +93 -0
- package/dist/tools/change-tools.js.map +1 -0
- package/dist/tools/context-tools.d.ts +68 -0
- package/dist/tools/context-tools.d.ts.map +1 -0
- package/dist/tools/context-tools.js +141 -0
- package/dist/tools/context-tools.js.map +1 -0
- package/dist/tools/debug-tools.d.ts +25 -0
- package/dist/tools/debug-tools.d.ts.map +1 -0
- package/dist/tools/debug-tools.js +286 -0
- package/dist/tools/debug-tools.js.map +1 -0
- package/dist/tools/evolving-tools.d.ts +23 -0
- package/dist/tools/evolving-tools.d.ts.map +1 -0
- package/dist/tools/evolving-tools.js +207 -0
- package/dist/tools/evolving-tools.js.map +1 -0
- package/dist/tools/flow-tools.d.ts +24 -0
- package/dist/tools/flow-tools.d.ts.map +1 -0
- package/dist/tools/flow-tools.js +265 -0
- package/dist/tools/flow-tools.js.map +1 -0
- package/dist/tools/graph-tools.d.ts +71 -0
- package/dist/tools/graph-tools.d.ts.map +1 -0
- package/dist/tools/graph-tools.js +165 -0
- package/dist/tools/graph-tools.js.map +1 -0
- package/dist/tools/memory-tools.d.ts +62 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +195 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/smart-tools.d.ts +23 -0
- package/dist/tools/smart-tools.d.ts.map +1 -0
- package/dist/tools/smart-tools.js +482 -0
- package/dist/tools/smart-tools.js.map +1 -0
- package/dist/tools/snapshot-tools.d.ts +19 -0
- package/dist/tools/snapshot-tools.d.ts.map +1 -0
- package/dist/tools/snapshot-tools.js +149 -0
- package/dist/tools/snapshot-tools.js.map +1 -0
- package/dist/types.d.ts +181 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +45 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/logger.d.ts +59 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +142 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/token-counter.d.ts +51 -0
- package/dist/utils/token-counter.d.ts.map +1 -0
- package/dist/utils/token-counter.js +181 -0
- package/dist/utils/token-counter.js.map +1 -0
- package/install.ps1 +321 -0
- package/install.sh +345 -0
- package/package.json +94 -0
- package/setup.bat +62 -0
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Mind Map — Mini Cypher Query Parser & Executor
|
|
3
|
+
*
|
|
4
|
+
* A read-only openCypher subset that compiles Cypher queries into SQL
|
|
5
|
+
* targeting the knowledge graph's `nodes` and `edges` tables in SQLite.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by codebase-memory-mcp's Cypher-like query interface.
|
|
8
|
+
*
|
|
9
|
+
* Supported Cypher syntax:
|
|
10
|
+
* MATCH (f:Function) RETURN f.name LIMIT 10
|
|
11
|
+
* MATCH (f:Function)-[:CALLS]->(g:Function) WHERE f.name = 'auth' RETURN f.name, g.name
|
|
12
|
+
* MATCH (f:Function) WHERE f.filePath CONTAINS 'auth' RETURN f.name, f.signature
|
|
13
|
+
* MATCH (f:Function) WHERE NOT EXISTS { (f)<-[:CALLS]-() } RETURN f.name
|
|
14
|
+
*/
|
|
15
|
+
// ============================================================
|
|
16
|
+
// Token Types
|
|
17
|
+
// ============================================================
|
|
18
|
+
var TokenType;
|
|
19
|
+
(function (TokenType) {
|
|
20
|
+
// Keywords
|
|
21
|
+
TokenType["MATCH"] = "MATCH";
|
|
22
|
+
TokenType["WHERE"] = "WHERE";
|
|
23
|
+
TokenType["RETURN"] = "RETURN";
|
|
24
|
+
TokenType["ORDER"] = "ORDER";
|
|
25
|
+
TokenType["BY"] = "BY";
|
|
26
|
+
TokenType["LIMIT"] = "LIMIT";
|
|
27
|
+
TokenType["DISTINCT"] = "DISTINCT";
|
|
28
|
+
TokenType["AND"] = "AND";
|
|
29
|
+
TokenType["OR"] = "OR";
|
|
30
|
+
TokenType["NOT"] = "NOT";
|
|
31
|
+
TokenType["IN"] = "IN";
|
|
32
|
+
TokenType["CONTAINS"] = "CONTAINS";
|
|
33
|
+
TokenType["STARTS"] = "STARTS";
|
|
34
|
+
TokenType["ENDS"] = "ENDS";
|
|
35
|
+
TokenType["WITH"] = "WITH";
|
|
36
|
+
TokenType["EXISTS"] = "EXISTS";
|
|
37
|
+
TokenType["ASC"] = "ASC";
|
|
38
|
+
TokenType["DESC"] = "DESC";
|
|
39
|
+
TokenType["AS"] = "AS";
|
|
40
|
+
TokenType["TRUE"] = "TRUE";
|
|
41
|
+
TokenType["FALSE"] = "FALSE";
|
|
42
|
+
TokenType["NULL"] = "NULL";
|
|
43
|
+
// Literals & identifiers
|
|
44
|
+
TokenType["IDENTIFIER"] = "IDENTIFIER";
|
|
45
|
+
TokenType["STRING"] = "STRING";
|
|
46
|
+
TokenType["NUMBER"] = "NUMBER";
|
|
47
|
+
// Symbols
|
|
48
|
+
TokenType["LPAREN"] = "LPAREN";
|
|
49
|
+
TokenType["RPAREN"] = "RPAREN";
|
|
50
|
+
TokenType["LBRACKET"] = "LBRACKET";
|
|
51
|
+
TokenType["RBRACKET"] = "RBRACKET";
|
|
52
|
+
TokenType["LBRACE"] = "LBRACE";
|
|
53
|
+
TokenType["RBRACE"] = "RBRACE";
|
|
54
|
+
TokenType["COLON"] = "COLON";
|
|
55
|
+
TokenType["DOT"] = "DOT";
|
|
56
|
+
TokenType["COMMA"] = "COMMA";
|
|
57
|
+
TokenType["DASH"] = "DASH";
|
|
58
|
+
TokenType["GT"] = "GT";
|
|
59
|
+
TokenType["LT"] = "LT";
|
|
60
|
+
TokenType["EQ"] = "EQ";
|
|
61
|
+
TokenType["NEQ"] = "NEQ";
|
|
62
|
+
TokenType["GTE"] = "GTE";
|
|
63
|
+
TokenType["LTE"] = "LTE";
|
|
64
|
+
TokenType["ARROW_RIGHT"] = "ARROW_RIGHT";
|
|
65
|
+
TokenType["ARROW_LEFT"] = "ARROW_LEFT";
|
|
66
|
+
TokenType["STAR"] = "STAR";
|
|
67
|
+
// End
|
|
68
|
+
TokenType["EOF"] = "EOF";
|
|
69
|
+
})(TokenType || (TokenType = {}));
|
|
70
|
+
// ============================================================
|
|
71
|
+
// Property name → SQL column name mapping
|
|
72
|
+
// ============================================================
|
|
73
|
+
/** Map Cypher property names to actual SQLite column names */
|
|
74
|
+
const PROPERTY_TO_COLUMN = {
|
|
75
|
+
name: 'name',
|
|
76
|
+
type: 'type',
|
|
77
|
+
id: 'id',
|
|
78
|
+
qualifiedName: 'qualifiedName',
|
|
79
|
+
filePath: 'filePath',
|
|
80
|
+
startLine: 'startLine',
|
|
81
|
+
endLine: 'endLine',
|
|
82
|
+
signature: 'signature',
|
|
83
|
+
docComment: 'docComment',
|
|
84
|
+
hash: 'hash',
|
|
85
|
+
language: 'language',
|
|
86
|
+
visibility: 'visibility',
|
|
87
|
+
isAsync: 'isAsync',
|
|
88
|
+
isStatic: 'isStatic',
|
|
89
|
+
isExported: 'isExported',
|
|
90
|
+
parameters: 'parameters',
|
|
91
|
+
returnType: 'returnType',
|
|
92
|
+
updatedAt: 'updatedAt',
|
|
93
|
+
};
|
|
94
|
+
/** Map Cypher labels (case-insensitive lookup) to node type values in the database */
|
|
95
|
+
const LABEL_TO_TYPE = {
|
|
96
|
+
file: 'file',
|
|
97
|
+
function: 'function',
|
|
98
|
+
class: 'class',
|
|
99
|
+
method: 'method',
|
|
100
|
+
interface: 'interface',
|
|
101
|
+
type_alias: 'type_alias',
|
|
102
|
+
typealias: 'type_alias',
|
|
103
|
+
enum: 'enum',
|
|
104
|
+
variable: 'variable',
|
|
105
|
+
constant: 'constant',
|
|
106
|
+
module: 'module',
|
|
107
|
+
namespace: 'namespace',
|
|
108
|
+
property: 'property',
|
|
109
|
+
constructor: 'constructor',
|
|
110
|
+
decorator: 'decorator',
|
|
111
|
+
route: 'route',
|
|
112
|
+
component: 'component',
|
|
113
|
+
hook: 'hook',
|
|
114
|
+
test: 'test',
|
|
115
|
+
config: 'config',
|
|
116
|
+
};
|
|
117
|
+
/** Map Cypher edge type labels (case-insensitive) to edge type values */
|
|
118
|
+
const EDGE_LABEL_TO_TYPE = {
|
|
119
|
+
calls: 'calls',
|
|
120
|
+
imports: 'imports',
|
|
121
|
+
exports: 'exports',
|
|
122
|
+
inherits: 'inherits',
|
|
123
|
+
implements: 'implements',
|
|
124
|
+
uses: 'uses',
|
|
125
|
+
decorates: 'decorates',
|
|
126
|
+
overrides: 'overrides',
|
|
127
|
+
contains: 'contains',
|
|
128
|
+
tests: 'tests',
|
|
129
|
+
depends_on: 'depends_on',
|
|
130
|
+
dependson: 'depends_on',
|
|
131
|
+
routes_to: 'routes_to',
|
|
132
|
+
routesto: 'routes_to',
|
|
133
|
+
};
|
|
134
|
+
// ============================================================
|
|
135
|
+
// Lexer
|
|
136
|
+
// ============================================================
|
|
137
|
+
const KEYWORDS = new Set([
|
|
138
|
+
'MATCH', 'WHERE', 'RETURN', 'ORDER', 'BY', 'LIMIT', 'DISTINCT',
|
|
139
|
+
'AND', 'OR', 'NOT', 'IN', 'CONTAINS', 'STARTS', 'ENDS', 'WITH',
|
|
140
|
+
'EXISTS', 'ASC', 'DESC', 'AS', 'TRUE', 'FALSE', 'NULL',
|
|
141
|
+
'CREATE', 'MERGE', 'SET', 'DELETE', 'REMOVE', 'DETACH',
|
|
142
|
+
]);
|
|
143
|
+
/** Forbidden write keywords — we only allow read-only queries */
|
|
144
|
+
const WRITE_KEYWORDS = new Set([
|
|
145
|
+
'CREATE', 'MERGE', 'SET', 'DELETE', 'REMOVE', 'DETACH',
|
|
146
|
+
]);
|
|
147
|
+
/**
|
|
148
|
+
* Tokenize a Cypher query string into a list of tokens.
|
|
149
|
+
*/
|
|
150
|
+
function tokenize(input) {
|
|
151
|
+
const tokens = [];
|
|
152
|
+
let pos = 0;
|
|
153
|
+
while (pos < input.length) {
|
|
154
|
+
// Skip whitespace
|
|
155
|
+
if (/\s/.test(input[pos])) {
|
|
156
|
+
pos++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Skip line comments //
|
|
160
|
+
if (input[pos] === '/' && input[pos + 1] === '/') {
|
|
161
|
+
while (pos < input.length && input[pos] !== '\n')
|
|
162
|
+
pos++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const startPos = pos;
|
|
166
|
+
// String literals (single or double quoted)
|
|
167
|
+
if (input[pos] === '\'' || input[pos] === '"') {
|
|
168
|
+
const quote = input[pos];
|
|
169
|
+
pos++;
|
|
170
|
+
let value = '';
|
|
171
|
+
while (pos < input.length && input[pos] !== quote) {
|
|
172
|
+
if (input[pos] === '\\' && pos + 1 < input.length) {
|
|
173
|
+
pos++;
|
|
174
|
+
if (input[pos] === 'n')
|
|
175
|
+
value += '\n';
|
|
176
|
+
else if (input[pos] === 't')
|
|
177
|
+
value += '\t';
|
|
178
|
+
else
|
|
179
|
+
value += input[pos];
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
value += input[pos];
|
|
183
|
+
}
|
|
184
|
+
pos++;
|
|
185
|
+
}
|
|
186
|
+
if (pos >= input.length) {
|
|
187
|
+
throw new CypherSyntaxError(`Unterminated string literal`, startPos);
|
|
188
|
+
}
|
|
189
|
+
pos++; // consume closing quote
|
|
190
|
+
tokens.push({ type: TokenType.STRING, value, position: startPos });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Numbers (integers and decimals)
|
|
194
|
+
if (/\d/.test(input[pos]) || (input[pos] === '-' && pos + 1 < input.length && /\d/.test(input[pos + 1]) && (tokens.length === 0 || [TokenType.EQ, TokenType.NEQ, TokenType.GT, TokenType.LT, TokenType.GTE, TokenType.LTE, TokenType.COMMA, TokenType.LPAREN, TokenType.AND, TokenType.OR].includes(tokens[tokens.length - 1].type)))) {
|
|
195
|
+
let numStr = '';
|
|
196
|
+
if (input[pos] === '-') {
|
|
197
|
+
numStr = '-';
|
|
198
|
+
pos++;
|
|
199
|
+
}
|
|
200
|
+
while (pos < input.length && /[\d.]/.test(input[pos])) {
|
|
201
|
+
numStr += input[pos];
|
|
202
|
+
pos++;
|
|
203
|
+
}
|
|
204
|
+
tokens.push({ type: TokenType.NUMBER, value: numStr, position: startPos });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
// Identifiers and keywords
|
|
208
|
+
if (/[a-zA-Z_]/.test(input[pos])) {
|
|
209
|
+
let ident = '';
|
|
210
|
+
while (pos < input.length && /[a-zA-Z0-9_]/.test(input[pos])) {
|
|
211
|
+
ident += input[pos];
|
|
212
|
+
pos++;
|
|
213
|
+
}
|
|
214
|
+
const upper = ident.toUpperCase();
|
|
215
|
+
if (WRITE_KEYWORDS.has(upper)) {
|
|
216
|
+
throw new CypherSyntaxError(`Write operations are not supported. This is a read-only query engine. Found: ${ident}`, startPos);
|
|
217
|
+
}
|
|
218
|
+
if (KEYWORDS.has(upper)) {
|
|
219
|
+
tokens.push({ type: upper, value: ident, position: startPos });
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
tokens.push({ type: TokenType.IDENTIFIER, value: ident, position: startPos });
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
// Multi-character operators
|
|
227
|
+
if (input[pos] === '-' && input[pos + 1] === '>') {
|
|
228
|
+
tokens.push({ type: TokenType.ARROW_RIGHT, value: '->', position: startPos });
|
|
229
|
+
pos += 2;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (input[pos] === '<' && input[pos + 1] === '-') {
|
|
233
|
+
tokens.push({ type: TokenType.ARROW_LEFT, value: '<-', position: startPos });
|
|
234
|
+
pos += 2;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (input[pos] === '<' && input[pos + 1] === '>') {
|
|
238
|
+
tokens.push({ type: TokenType.NEQ, value: '<>', position: startPos });
|
|
239
|
+
pos += 2;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (input[pos] === '>' && input[pos + 1] === '=') {
|
|
243
|
+
tokens.push({ type: TokenType.GTE, value: '>=', position: startPos });
|
|
244
|
+
pos += 2;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (input[pos] === '<' && input[pos + 1] === '=') {
|
|
248
|
+
tokens.push({ type: TokenType.LTE, value: '<=', position: startPos });
|
|
249
|
+
pos += 2;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// Single-character tokens
|
|
253
|
+
const singleCharMap = {
|
|
254
|
+
'(': TokenType.LPAREN,
|
|
255
|
+
')': TokenType.RPAREN,
|
|
256
|
+
'[': TokenType.LBRACKET,
|
|
257
|
+
']': TokenType.RBRACKET,
|
|
258
|
+
'{': TokenType.LBRACE,
|
|
259
|
+
'}': TokenType.RBRACE,
|
|
260
|
+
':': TokenType.COLON,
|
|
261
|
+
'.': TokenType.DOT,
|
|
262
|
+
',': TokenType.COMMA,
|
|
263
|
+
'-': TokenType.DASH,
|
|
264
|
+
'>': TokenType.GT,
|
|
265
|
+
'<': TokenType.LT,
|
|
266
|
+
'=': TokenType.EQ,
|
|
267
|
+
'*': TokenType.STAR,
|
|
268
|
+
};
|
|
269
|
+
if (singleCharMap[input[pos]]) {
|
|
270
|
+
tokens.push({ type: singleCharMap[input[pos]], value: input[pos], position: startPos });
|
|
271
|
+
pos++;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
throw new CypherSyntaxError(`Unexpected character: '${input[pos]}'`, pos);
|
|
275
|
+
}
|
|
276
|
+
tokens.push({ type: TokenType.EOF, value: '', position: pos });
|
|
277
|
+
return tokens;
|
|
278
|
+
}
|
|
279
|
+
// ============================================================
|
|
280
|
+
// Parser
|
|
281
|
+
// ============================================================
|
|
282
|
+
/**
|
|
283
|
+
* Recursive descent parser that produces a CypherAST from a token stream.
|
|
284
|
+
*/
|
|
285
|
+
class CypherParser {
|
|
286
|
+
tokens;
|
|
287
|
+
pos;
|
|
288
|
+
constructor(tokens) {
|
|
289
|
+
this.tokens = tokens;
|
|
290
|
+
this.pos = 0;
|
|
291
|
+
}
|
|
292
|
+
/** Peek at the current token without consuming it */
|
|
293
|
+
peek() {
|
|
294
|
+
return this.tokens[this.pos];
|
|
295
|
+
}
|
|
296
|
+
/** Consume and return the current token */
|
|
297
|
+
advance() {
|
|
298
|
+
const token = this.tokens[this.pos];
|
|
299
|
+
this.pos++;
|
|
300
|
+
return token;
|
|
301
|
+
}
|
|
302
|
+
/** Expect a specific token type; throw if not matched */
|
|
303
|
+
expect(type) {
|
|
304
|
+
const token = this.peek();
|
|
305
|
+
if (token.type !== type) {
|
|
306
|
+
throw new CypherSyntaxError(`Expected ${type} but found ${token.type} ('${token.value}')`, token.position);
|
|
307
|
+
}
|
|
308
|
+
return this.advance();
|
|
309
|
+
}
|
|
310
|
+
/** Check if current token is of a given type (case-insensitive for keywords) */
|
|
311
|
+
check(type) {
|
|
312
|
+
return this.peek().type === type;
|
|
313
|
+
}
|
|
314
|
+
/** If current token matches, consume it and return true */
|
|
315
|
+
match(type) {
|
|
316
|
+
if (this.check(type)) {
|
|
317
|
+
this.advance();
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
/** Main parse entry point */
|
|
323
|
+
parse() {
|
|
324
|
+
this.expect(TokenType.MATCH);
|
|
325
|
+
const matchPattern = this.parseMatchPattern();
|
|
326
|
+
let where = null;
|
|
327
|
+
if (this.check(TokenType.WHERE)) {
|
|
328
|
+
this.advance();
|
|
329
|
+
where = this.parseWhereExpr();
|
|
330
|
+
}
|
|
331
|
+
this.expect(TokenType.RETURN);
|
|
332
|
+
let distinct = false;
|
|
333
|
+
if (this.check(TokenType.DISTINCT)) {
|
|
334
|
+
this.advance();
|
|
335
|
+
distinct = true;
|
|
336
|
+
}
|
|
337
|
+
const returnItems = this.parseReturnItems();
|
|
338
|
+
const orderBy = [];
|
|
339
|
+
if (this.check(TokenType.ORDER)) {
|
|
340
|
+
this.advance();
|
|
341
|
+
this.expect(TokenType.BY);
|
|
342
|
+
orderBy.push(...this.parseOrderByItems());
|
|
343
|
+
}
|
|
344
|
+
let limit = null;
|
|
345
|
+
if (this.check(TokenType.LIMIT)) {
|
|
346
|
+
this.advance();
|
|
347
|
+
const numToken = this.expect(TokenType.NUMBER);
|
|
348
|
+
limit = parseInt(numToken.value, 10);
|
|
349
|
+
if (isNaN(limit) || limit < 0) {
|
|
350
|
+
throw new CypherSyntaxError(`Invalid LIMIT value: ${numToken.value}`, numToken.position);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (!this.check(TokenType.EOF)) {
|
|
354
|
+
const token = this.peek();
|
|
355
|
+
throw new CypherSyntaxError(`Unexpected token after query end: ${token.type} ('${token.value}')`, token.position);
|
|
356
|
+
}
|
|
357
|
+
return { matchPattern, where, returnItems, distinct, orderBy, limit };
|
|
358
|
+
}
|
|
359
|
+
// ---- MATCH pattern parsing ----
|
|
360
|
+
parseMatchPattern() {
|
|
361
|
+
const startNode = this.parseNodePattern();
|
|
362
|
+
const chain = [];
|
|
363
|
+
while (this.isEdgeStart()) {
|
|
364
|
+
const edge = this.parseEdgePattern();
|
|
365
|
+
const node = this.parseNodePattern();
|
|
366
|
+
chain.push({ edge, node });
|
|
367
|
+
}
|
|
368
|
+
return { startNode, chain };
|
|
369
|
+
}
|
|
370
|
+
isEdgeStart() {
|
|
371
|
+
return this.check(TokenType.DASH) || this.check(TokenType.ARROW_LEFT);
|
|
372
|
+
}
|
|
373
|
+
parseNodePattern() {
|
|
374
|
+
this.expect(TokenType.LPAREN);
|
|
375
|
+
let variable = null;
|
|
376
|
+
let label = null;
|
|
377
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
378
|
+
variable = this.advance().value;
|
|
379
|
+
}
|
|
380
|
+
if (this.check(TokenType.COLON)) {
|
|
381
|
+
this.advance();
|
|
382
|
+
const labelToken = this.expect(TokenType.IDENTIFIER);
|
|
383
|
+
label = labelToken.value;
|
|
384
|
+
}
|
|
385
|
+
this.expect(TokenType.RPAREN);
|
|
386
|
+
return { variable, label };
|
|
387
|
+
}
|
|
388
|
+
parseEdgePattern() {
|
|
389
|
+
let direction = 'both';
|
|
390
|
+
let variable = null;
|
|
391
|
+
let edgeType = null;
|
|
392
|
+
if (this.match(TokenType.ARROW_LEFT)) {
|
|
393
|
+
// <-[...]- or <-
|
|
394
|
+
direction = 'left';
|
|
395
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
396
|
+
this.advance();
|
|
397
|
+
({ variable, edgeType } = this.parseEdgeBody());
|
|
398
|
+
this.expect(TokenType.RBRACKET);
|
|
399
|
+
}
|
|
400
|
+
this.expect(TokenType.DASH);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
// - or -[...]-> or -[...]-
|
|
404
|
+
this.expect(TokenType.DASH);
|
|
405
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
406
|
+
this.advance();
|
|
407
|
+
({ variable, edgeType } = this.parseEdgeBody());
|
|
408
|
+
this.expect(TokenType.RBRACKET);
|
|
409
|
+
}
|
|
410
|
+
if (this.match(TokenType.ARROW_RIGHT)) {
|
|
411
|
+
direction = 'right';
|
|
412
|
+
}
|
|
413
|
+
else if (this.check(TokenType.DASH)) {
|
|
414
|
+
this.advance();
|
|
415
|
+
direction = 'both';
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return { variable, edgeType, direction };
|
|
419
|
+
}
|
|
420
|
+
parseEdgeBody() {
|
|
421
|
+
let variable = null;
|
|
422
|
+
let edgeType = null;
|
|
423
|
+
// Optional variable
|
|
424
|
+
if (this.check(TokenType.IDENTIFIER) && !this.isColonAhead()) {
|
|
425
|
+
variable = this.advance().value;
|
|
426
|
+
}
|
|
427
|
+
// Optional :TYPE
|
|
428
|
+
if (this.check(TokenType.COLON)) {
|
|
429
|
+
this.advance();
|
|
430
|
+
const typeToken = this.expect(TokenType.IDENTIFIER);
|
|
431
|
+
edgeType = typeToken.value;
|
|
432
|
+
}
|
|
433
|
+
return { variable, edgeType };
|
|
434
|
+
}
|
|
435
|
+
/** Look ahead to see if a colon follows the current token (for edge body parsing) */
|
|
436
|
+
isColonAhead() {
|
|
437
|
+
return this.pos + 1 < this.tokens.length && this.tokens[this.pos + 1].type === TokenType.COLON;
|
|
438
|
+
}
|
|
439
|
+
// ---- WHERE clause parsing ----
|
|
440
|
+
parseWhereExpr() {
|
|
441
|
+
return this.parseOrExpr();
|
|
442
|
+
}
|
|
443
|
+
parseOrExpr() {
|
|
444
|
+
let left = this.parseAndExpr();
|
|
445
|
+
while (this.check(TokenType.OR)) {
|
|
446
|
+
this.advance();
|
|
447
|
+
const right = this.parseAndExpr();
|
|
448
|
+
left = { kind: 'or', left, right };
|
|
449
|
+
}
|
|
450
|
+
return left;
|
|
451
|
+
}
|
|
452
|
+
parseAndExpr() {
|
|
453
|
+
let left = this.parsePrimaryExpr();
|
|
454
|
+
while (this.check(TokenType.AND)) {
|
|
455
|
+
this.advance();
|
|
456
|
+
const right = this.parsePrimaryExpr();
|
|
457
|
+
left = { kind: 'and', left, right };
|
|
458
|
+
}
|
|
459
|
+
return left;
|
|
460
|
+
}
|
|
461
|
+
parsePrimaryExpr() {
|
|
462
|
+
// NOT
|
|
463
|
+
if (this.check(TokenType.NOT)) {
|
|
464
|
+
this.advance();
|
|
465
|
+
// NOT EXISTS { (f)<-[:CALLS]-() }
|
|
466
|
+
if (this.check(TokenType.EXISTS)) {
|
|
467
|
+
return this.parseNotExistsExpr();
|
|
468
|
+
}
|
|
469
|
+
const operand = this.parsePrimaryExpr();
|
|
470
|
+
return { kind: 'not', operand };
|
|
471
|
+
}
|
|
472
|
+
// Parenthesized expression
|
|
473
|
+
if (this.check(TokenType.LPAREN) && this.isSubExprParen()) {
|
|
474
|
+
this.advance();
|
|
475
|
+
const expr = this.parseWhereExpr();
|
|
476
|
+
this.expect(TokenType.RPAREN);
|
|
477
|
+
return expr;
|
|
478
|
+
}
|
|
479
|
+
// Property comparison: f.name = 'value'
|
|
480
|
+
return this.parseComparison();
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Distinguish between a parenthesized sub-expression and a node pattern.
|
|
484
|
+
* If the token after `(` is an identifier followed by `.`, it's a comparison.
|
|
485
|
+
* Otherwise it's likely a sub-expression if followed by NOT or another `(`.
|
|
486
|
+
*/
|
|
487
|
+
isSubExprParen() {
|
|
488
|
+
if (this.pos + 1 >= this.tokens.length)
|
|
489
|
+
return false;
|
|
490
|
+
const next = this.tokens[this.pos + 1];
|
|
491
|
+
// If we see NOT or another (, it's a sub-expression
|
|
492
|
+
if (next.type === TokenType.NOT || next.type === TokenType.LPAREN)
|
|
493
|
+
return true;
|
|
494
|
+
// If we see identifier.identifier, it's a comparison in parens
|
|
495
|
+
if (next.type === TokenType.IDENTIFIER && this.pos + 2 < this.tokens.length &&
|
|
496
|
+
this.tokens[this.pos + 2].type === TokenType.DOT)
|
|
497
|
+
return true;
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
parseNotExistsExpr() {
|
|
501
|
+
this.expect(TokenType.EXISTS);
|
|
502
|
+
this.expect(TokenType.LBRACE);
|
|
503
|
+
// Parse the pattern: (variable)<-[:TYPE]-() or (variable)-[:TYPE]->()
|
|
504
|
+
this.expect(TokenType.LPAREN);
|
|
505
|
+
const varToken = this.expect(TokenType.IDENTIFIER);
|
|
506
|
+
const variable = varToken.value;
|
|
507
|
+
this.expect(TokenType.RPAREN);
|
|
508
|
+
let direction = 'incoming';
|
|
509
|
+
let edgeType = null;
|
|
510
|
+
if (this.check(TokenType.ARROW_LEFT)) {
|
|
511
|
+
// <-[:TYPE]-()
|
|
512
|
+
this.advance();
|
|
513
|
+
direction = 'incoming';
|
|
514
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
515
|
+
this.advance();
|
|
516
|
+
if (this.check(TokenType.COLON)) {
|
|
517
|
+
this.advance();
|
|
518
|
+
edgeType = this.expect(TokenType.IDENTIFIER).value;
|
|
519
|
+
}
|
|
520
|
+
this.expect(TokenType.RBRACKET);
|
|
521
|
+
}
|
|
522
|
+
this.expect(TokenType.DASH);
|
|
523
|
+
}
|
|
524
|
+
else if (this.check(TokenType.DASH)) {
|
|
525
|
+
// -[:TYPE]->()
|
|
526
|
+
this.advance();
|
|
527
|
+
direction = 'outgoing';
|
|
528
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
529
|
+
this.advance();
|
|
530
|
+
if (this.check(TokenType.COLON)) {
|
|
531
|
+
this.advance();
|
|
532
|
+
edgeType = this.expect(TokenType.IDENTIFIER).value;
|
|
533
|
+
}
|
|
534
|
+
this.expect(TokenType.RBRACKET);
|
|
535
|
+
}
|
|
536
|
+
this.expect(TokenType.ARROW_RIGHT);
|
|
537
|
+
}
|
|
538
|
+
this.expect(TokenType.LPAREN);
|
|
539
|
+
this.expect(TokenType.RPAREN);
|
|
540
|
+
this.expect(TokenType.RBRACE);
|
|
541
|
+
return { kind: 'not_exists', variable, edgeType, direction };
|
|
542
|
+
}
|
|
543
|
+
parseComparison() {
|
|
544
|
+
const left = this.parsePropertyRef();
|
|
545
|
+
const opToken = this.peek();
|
|
546
|
+
let op;
|
|
547
|
+
switch (opToken.type) {
|
|
548
|
+
case TokenType.EQ:
|
|
549
|
+
this.advance();
|
|
550
|
+
op = '=';
|
|
551
|
+
break;
|
|
552
|
+
case TokenType.NEQ:
|
|
553
|
+
this.advance();
|
|
554
|
+
op = '<>';
|
|
555
|
+
break;
|
|
556
|
+
case TokenType.GT:
|
|
557
|
+
this.advance();
|
|
558
|
+
op = '>';
|
|
559
|
+
break;
|
|
560
|
+
case TokenType.LT:
|
|
561
|
+
this.advance();
|
|
562
|
+
op = '<';
|
|
563
|
+
break;
|
|
564
|
+
case TokenType.GTE:
|
|
565
|
+
this.advance();
|
|
566
|
+
op = '>=';
|
|
567
|
+
break;
|
|
568
|
+
case TokenType.LTE:
|
|
569
|
+
this.advance();
|
|
570
|
+
op = '<=';
|
|
571
|
+
break;
|
|
572
|
+
case TokenType.CONTAINS:
|
|
573
|
+
this.advance();
|
|
574
|
+
op = 'CONTAINS';
|
|
575
|
+
break;
|
|
576
|
+
case TokenType.STARTS:
|
|
577
|
+
this.advance();
|
|
578
|
+
this.expect(TokenType.WITH);
|
|
579
|
+
op = 'STARTS WITH';
|
|
580
|
+
break;
|
|
581
|
+
case TokenType.ENDS:
|
|
582
|
+
this.advance();
|
|
583
|
+
this.expect(TokenType.WITH);
|
|
584
|
+
op = 'ENDS WITH';
|
|
585
|
+
break;
|
|
586
|
+
case TokenType.IN:
|
|
587
|
+
this.advance();
|
|
588
|
+
op = 'IN';
|
|
589
|
+
break;
|
|
590
|
+
default:
|
|
591
|
+
throw new CypherSyntaxError(`Expected comparison operator but found ${opToken.type} ('${opToken.value}')`, opToken.position);
|
|
592
|
+
}
|
|
593
|
+
if (op === 'IN') {
|
|
594
|
+
const right = this.parseLiteralList();
|
|
595
|
+
return { kind: 'comparison', left, op, right };
|
|
596
|
+
}
|
|
597
|
+
const right = this.parseLiteral();
|
|
598
|
+
return { kind: 'comparison', left, op, right };
|
|
599
|
+
}
|
|
600
|
+
parsePropertyRef() {
|
|
601
|
+
const variableToken = this.expect(TokenType.IDENTIFIER);
|
|
602
|
+
this.expect(TokenType.DOT);
|
|
603
|
+
const propertyToken = this.expect(TokenType.IDENTIFIER);
|
|
604
|
+
return { variable: variableToken.value, property: propertyToken.value };
|
|
605
|
+
}
|
|
606
|
+
parseLiteral() {
|
|
607
|
+
const token = this.peek();
|
|
608
|
+
switch (token.type) {
|
|
609
|
+
case TokenType.STRING:
|
|
610
|
+
this.advance();
|
|
611
|
+
return token.value;
|
|
612
|
+
case TokenType.NUMBER:
|
|
613
|
+
this.advance();
|
|
614
|
+
return token.value.includes('.') ? parseFloat(token.value) : parseInt(token.value, 10);
|
|
615
|
+
case TokenType.TRUE:
|
|
616
|
+
this.advance();
|
|
617
|
+
return true;
|
|
618
|
+
case TokenType.FALSE:
|
|
619
|
+
this.advance();
|
|
620
|
+
return false;
|
|
621
|
+
case TokenType.NULL:
|
|
622
|
+
this.advance();
|
|
623
|
+
return null;
|
|
624
|
+
default:
|
|
625
|
+
throw new CypherSyntaxError(`Expected literal value but found ${token.type} ('${token.value}')`, token.position);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
parseLiteralList() {
|
|
629
|
+
this.expect(TokenType.LBRACKET);
|
|
630
|
+
const values = [];
|
|
631
|
+
if (!this.check(TokenType.RBRACKET)) {
|
|
632
|
+
values.push(this.parseLiteral());
|
|
633
|
+
while (this.match(TokenType.COMMA)) {
|
|
634
|
+
values.push(this.parseLiteral());
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
this.expect(TokenType.RBRACKET);
|
|
638
|
+
return values;
|
|
639
|
+
}
|
|
640
|
+
// ---- RETURN clause parsing ----
|
|
641
|
+
parseReturnItems() {
|
|
642
|
+
const items = [];
|
|
643
|
+
items.push(this.parseReturnItem());
|
|
644
|
+
while (this.match(TokenType.COMMA)) {
|
|
645
|
+
items.push(this.parseReturnItem());
|
|
646
|
+
}
|
|
647
|
+
return items;
|
|
648
|
+
}
|
|
649
|
+
parseReturnItem() {
|
|
650
|
+
// Check for variable.* (return all properties)
|
|
651
|
+
if (this.check(TokenType.IDENTIFIER) && this.pos + 1 < this.tokens.length &&
|
|
652
|
+
this.tokens[this.pos + 1].type === TokenType.DOT &&
|
|
653
|
+
this.pos + 2 < this.tokens.length &&
|
|
654
|
+
this.tokens[this.pos + 2].type === TokenType.STAR) {
|
|
655
|
+
const variable = this.advance().value;
|
|
656
|
+
this.advance(); // .
|
|
657
|
+
this.advance(); // *
|
|
658
|
+
let alias = null;
|
|
659
|
+
if (this.check(TokenType.AS)) {
|
|
660
|
+
this.advance();
|
|
661
|
+
alias = this.expect(TokenType.IDENTIFIER).value;
|
|
662
|
+
}
|
|
663
|
+
return { expr: { kind: 'star', variable }, alias };
|
|
664
|
+
}
|
|
665
|
+
// variable.property
|
|
666
|
+
const ref = this.parsePropertyRef();
|
|
667
|
+
let alias = null;
|
|
668
|
+
if (this.check(TokenType.AS)) {
|
|
669
|
+
this.advance();
|
|
670
|
+
alias = this.expect(TokenType.IDENTIFIER).value;
|
|
671
|
+
}
|
|
672
|
+
return { expr: ref, alias };
|
|
673
|
+
}
|
|
674
|
+
// ---- ORDER BY clause parsing ----
|
|
675
|
+
parseOrderByItems() {
|
|
676
|
+
const items = [];
|
|
677
|
+
items.push(this.parseOrderByItem());
|
|
678
|
+
while (this.match(TokenType.COMMA)) {
|
|
679
|
+
items.push(this.parseOrderByItem());
|
|
680
|
+
}
|
|
681
|
+
return items;
|
|
682
|
+
}
|
|
683
|
+
parseOrderByItem() {
|
|
684
|
+
const expr = this.parsePropertyRef();
|
|
685
|
+
let direction = 'ASC';
|
|
686
|
+
if (this.check(TokenType.ASC)) {
|
|
687
|
+
this.advance();
|
|
688
|
+
direction = 'ASC';
|
|
689
|
+
}
|
|
690
|
+
else if (this.check(TokenType.DESC)) {
|
|
691
|
+
this.advance();
|
|
692
|
+
direction = 'DESC';
|
|
693
|
+
}
|
|
694
|
+
return { expr, direction };
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// ============================================================
|
|
698
|
+
// SQL Code Generator / Executor
|
|
699
|
+
// ============================================================
|
|
700
|
+
/**
|
|
701
|
+
* Mini Cypher query engine that compiles Cypher queries into SQL
|
|
702
|
+
* and executes them against the knowledge graph's SQLite database.
|
|
703
|
+
*
|
|
704
|
+
* This is a read-only engine — no CREATE, MERGE, SET, or DELETE.
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* ```ts
|
|
708
|
+
* const engine = new CypherEngine(db);
|
|
709
|
+
* const result = engine.execute("MATCH (f:Function) RETURN f.name LIMIT 10");
|
|
710
|
+
* console.log(result.rows);
|
|
711
|
+
* ```
|
|
712
|
+
*/
|
|
713
|
+
export class CypherEngine {
|
|
714
|
+
db;
|
|
715
|
+
/**
|
|
716
|
+
* Create a new CypherEngine.
|
|
717
|
+
*
|
|
718
|
+
* @param graph - A KnowledgeGraph instance (the DB is extracted via graph.getDb())
|
|
719
|
+
*/
|
|
720
|
+
constructor(graph) {
|
|
721
|
+
this.db = graph.getDb();
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Execute a Cypher query and return the results.
|
|
725
|
+
*
|
|
726
|
+
* @param query - A Cypher query string (read-only subset)
|
|
727
|
+
* @param projectFilter - Optional project path to scope results by filePath
|
|
728
|
+
* @returns The query result with columns, rows, and metadata
|
|
729
|
+
* @throws {CypherSyntaxError} If the query has a syntax error
|
|
730
|
+
* @throws {CypherExecutionError} If the query fails during execution
|
|
731
|
+
*/
|
|
732
|
+
execute(query, projectFilter) {
|
|
733
|
+
const startTime = performance.now();
|
|
734
|
+
try {
|
|
735
|
+
// 1. Tokenize
|
|
736
|
+
const tokens = tokenize(query.trim());
|
|
737
|
+
// 2. Parse into AST
|
|
738
|
+
const parser = new CypherParser(tokens);
|
|
739
|
+
const ast = parser.parse();
|
|
740
|
+
// 2b. If a project filter is given, inject a filePath CONTAINS condition
|
|
741
|
+
if (projectFilter) {
|
|
742
|
+
this.injectProjectFilter(ast, projectFilter);
|
|
743
|
+
}
|
|
744
|
+
// 3. Compile AST to SQL
|
|
745
|
+
const { sql, params, columns } = this.compileToSql(ast);
|
|
746
|
+
// 4. Execute SQL
|
|
747
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
748
|
+
const executionTimeMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
749
|
+
return {
|
|
750
|
+
columns,
|
|
751
|
+
rows,
|
|
752
|
+
count: rows.length,
|
|
753
|
+
generatedSql: sql,
|
|
754
|
+
executionTimeMs,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
if (error instanceof CypherSyntaxError || error instanceof CypherExecutionError) {
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
762
|
+
throw new CypherExecutionError(`Query execution failed: ${message}`, query);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Inject a filePath filter into the AST's WHERE clause to scope results to a project path.
|
|
767
|
+
*/
|
|
768
|
+
injectProjectFilter(ast, projectPath) {
|
|
769
|
+
// Find the first node variable in the MATCH pattern
|
|
770
|
+
const variable = ast.matchPattern.startNode.variable;
|
|
771
|
+
if (!variable)
|
|
772
|
+
return;
|
|
773
|
+
const filterExpr = {
|
|
774
|
+
kind: 'comparison',
|
|
775
|
+
left: { variable, property: 'filePath' },
|
|
776
|
+
op: 'STARTS WITH',
|
|
777
|
+
right: projectPath,
|
|
778
|
+
};
|
|
779
|
+
if (ast.where) {
|
|
780
|
+
ast.where = { kind: 'and', left: ast.where, right: filterExpr };
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
ast.where = filterExpr;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Compile a CypherAST into a SQL query string, bound parameters, and column list.
|
|
788
|
+
*/
|
|
789
|
+
compileToSql(ast) {
|
|
790
|
+
const params = [];
|
|
791
|
+
const { matchPattern, where, returnItems, distinct, orderBy, limit } = ast;
|
|
792
|
+
// Build a map from variable names to their SQL table aliases and node labels
|
|
793
|
+
const variableMap = new Map();
|
|
794
|
+
let tableIndex = 0;
|
|
795
|
+
let edgeIndex = 0;
|
|
796
|
+
const fromClauses = [];
|
|
797
|
+
const joinConditions = [];
|
|
798
|
+
// Process start node
|
|
799
|
+
const startAlias = `n${tableIndex++}`;
|
|
800
|
+
if (matchPattern.startNode.variable) {
|
|
801
|
+
variableMap.set(matchPattern.startNode.variable, {
|
|
802
|
+
alias: startAlias,
|
|
803
|
+
label: matchPattern.startNode.label,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
fromClauses.push(`nodes AS ${startAlias}`);
|
|
807
|
+
if (matchPattern.startNode.label) {
|
|
808
|
+
const nodeType = this.resolveNodeLabel(matchPattern.startNode.label);
|
|
809
|
+
joinConditions.push(`${startAlias}.type = ?`);
|
|
810
|
+
params.push(nodeType);
|
|
811
|
+
}
|
|
812
|
+
// Process chain (edge→node pairs)
|
|
813
|
+
for (const { edge, node } of matchPattern.chain) {
|
|
814
|
+
const edgeAlias = `e${edgeIndex++}`;
|
|
815
|
+
const nodeAlias = `n${tableIndex++}`;
|
|
816
|
+
fromClauses.push(`edges AS ${edgeAlias}`);
|
|
817
|
+
fromClauses.push(`nodes AS ${nodeAlias}`);
|
|
818
|
+
if (node.variable) {
|
|
819
|
+
variableMap.set(node.variable, { alias: nodeAlias, label: node.label });
|
|
820
|
+
}
|
|
821
|
+
if (edge.variable) {
|
|
822
|
+
variableMap.set(edge.variable, { alias: edgeAlias, label: null });
|
|
823
|
+
}
|
|
824
|
+
// Wire up the edge based on direction
|
|
825
|
+
const prevAlias = `n${tableIndex - 2}`;
|
|
826
|
+
switch (edge.direction) {
|
|
827
|
+
case 'right':
|
|
828
|
+
// (prev)-[edge]->(node): prev.id = edge.sourceId, edge.targetId = node.id
|
|
829
|
+
joinConditions.push(`${edgeAlias}.sourceId = ${prevAlias}.id`);
|
|
830
|
+
joinConditions.push(`${edgeAlias}.targetId = ${nodeAlias}.id`);
|
|
831
|
+
break;
|
|
832
|
+
case 'left':
|
|
833
|
+
// (prev)<-[edge]-(node): prev.id = edge.targetId, edge.sourceId = node.id
|
|
834
|
+
joinConditions.push(`${edgeAlias}.targetId = ${prevAlias}.id`);
|
|
835
|
+
joinConditions.push(`${edgeAlias}.sourceId = ${nodeAlias}.id`);
|
|
836
|
+
break;
|
|
837
|
+
case 'both':
|
|
838
|
+
// Undirected: either direction
|
|
839
|
+
joinConditions.push(`((${edgeAlias}.sourceId = ${prevAlias}.id AND ${edgeAlias}.targetId = ${nodeAlias}.id) OR ` +
|
|
840
|
+
`(${edgeAlias}.targetId = ${prevAlias}.id AND ${edgeAlias}.sourceId = ${nodeAlias}.id))`);
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
// Filter by edge type
|
|
844
|
+
if (edge.edgeType) {
|
|
845
|
+
const resolvedType = this.resolveEdgeType(edge.edgeType);
|
|
846
|
+
joinConditions.push(`${edgeAlias}.type = ?`);
|
|
847
|
+
params.push(resolvedType);
|
|
848
|
+
}
|
|
849
|
+
// Filter by node label
|
|
850
|
+
if (node.label) {
|
|
851
|
+
const nodeType = this.resolveNodeLabel(node.label);
|
|
852
|
+
joinConditions.push(`${nodeAlias}.type = ?`);
|
|
853
|
+
params.push(nodeType);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
// Compile WHERE clause
|
|
857
|
+
if (where) {
|
|
858
|
+
const whereSql = this.compileWhereExpr(where, variableMap, params);
|
|
859
|
+
joinConditions.push(whereSql);
|
|
860
|
+
}
|
|
861
|
+
// Compile SELECT (RETURN) clause
|
|
862
|
+
const selectItems = [];
|
|
863
|
+
const columns = [];
|
|
864
|
+
for (const item of returnItems) {
|
|
865
|
+
if ('kind' in item.expr && item.expr.kind === 'star') {
|
|
866
|
+
// variable.* — return all columns with prefixed names
|
|
867
|
+
const info = variableMap.get(item.expr.variable);
|
|
868
|
+
if (!info) {
|
|
869
|
+
throw new CypherExecutionError(`Unknown variable '${item.expr.variable}' in RETURN clause`, '');
|
|
870
|
+
}
|
|
871
|
+
for (const [propName, colName] of Object.entries(PROPERTY_TO_COLUMN)) {
|
|
872
|
+
const alias = `${item.expr.variable}_${propName}`;
|
|
873
|
+
selectItems.push(`${info.alias}.${colName} AS ${alias}`);
|
|
874
|
+
columns.push(alias);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
const ref = item.expr;
|
|
879
|
+
const colExpr = this.resolvePropertyRef(ref, variableMap);
|
|
880
|
+
const alias = item.alias ?? `${ref.variable}_${ref.property}`;
|
|
881
|
+
selectItems.push(`${colExpr} AS ${alias}`);
|
|
882
|
+
columns.push(alias);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// Build the final query
|
|
886
|
+
let sql = `SELECT ${distinct ? 'DISTINCT ' : ''}${selectItems.join(', ')}\nFROM ${fromClauses.join(', ')}`;
|
|
887
|
+
if (joinConditions.length > 0) {
|
|
888
|
+
sql += `\nWHERE ${joinConditions.join('\n AND ')}`;
|
|
889
|
+
}
|
|
890
|
+
// ORDER BY
|
|
891
|
+
if (orderBy.length > 0) {
|
|
892
|
+
const orderClauses = orderBy.map(item => {
|
|
893
|
+
const colExpr = this.resolvePropertyRef(item.expr, variableMap);
|
|
894
|
+
return `${colExpr} ${item.direction}`;
|
|
895
|
+
});
|
|
896
|
+
sql += `\nORDER BY ${orderClauses.join(', ')}`;
|
|
897
|
+
}
|
|
898
|
+
// LIMIT
|
|
899
|
+
if (limit !== null) {
|
|
900
|
+
sql += `\nLIMIT ?`;
|
|
901
|
+
params.push(limit);
|
|
902
|
+
}
|
|
903
|
+
return { sql, params, columns };
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Compile a WHERE expression into a SQL fragment, appending bound params.
|
|
907
|
+
*/
|
|
908
|
+
compileWhereExpr(expr, variableMap, params) {
|
|
909
|
+
switch (expr.kind) {
|
|
910
|
+
case 'comparison':
|
|
911
|
+
return this.compileComparison(expr, variableMap, params);
|
|
912
|
+
case 'not_exists':
|
|
913
|
+
return this.compileNotExists(expr, variableMap, params);
|
|
914
|
+
case 'and':
|
|
915
|
+
return `(${this.compileWhereExpr(expr.left, variableMap, params)} AND ${this.compileWhereExpr(expr.right, variableMap, params)})`;
|
|
916
|
+
case 'or':
|
|
917
|
+
return `(${this.compileWhereExpr(expr.left, variableMap, params)} OR ${this.compileWhereExpr(expr.right, variableMap, params)})`;
|
|
918
|
+
case 'not':
|
|
919
|
+
return `NOT (${this.compileWhereExpr(expr.operand, variableMap, params)})`;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Compile a comparison expression into SQL.
|
|
924
|
+
*/
|
|
925
|
+
compileComparison(expr, variableMap, params) {
|
|
926
|
+
const leftCol = this.resolvePropertyRef(expr.left, variableMap);
|
|
927
|
+
switch (expr.op) {
|
|
928
|
+
case '=':
|
|
929
|
+
case '<>':
|
|
930
|
+
case '>':
|
|
931
|
+
case '<':
|
|
932
|
+
case '>=':
|
|
933
|
+
case '<=':
|
|
934
|
+
params.push(this.toLiteralSqlValue(expr.right));
|
|
935
|
+
return `${leftCol} ${expr.op} ?`;
|
|
936
|
+
case 'CONTAINS':
|
|
937
|
+
params.push(`%${expr.right}%`);
|
|
938
|
+
return `${leftCol} LIKE ?`;
|
|
939
|
+
case 'STARTS WITH':
|
|
940
|
+
params.push(`${expr.right}%`);
|
|
941
|
+
return `${leftCol} LIKE ?`;
|
|
942
|
+
case 'ENDS WITH':
|
|
943
|
+
params.push(`%${expr.right}`);
|
|
944
|
+
return `${leftCol} LIKE ?`;
|
|
945
|
+
case 'IN': {
|
|
946
|
+
const list = expr.right;
|
|
947
|
+
const placeholders = list.map(() => '?').join(', ');
|
|
948
|
+
for (const v of list) {
|
|
949
|
+
params.push(this.toLiteralSqlValue(v));
|
|
950
|
+
}
|
|
951
|
+
return `${leftCol} IN (${placeholders})`;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Compile a NOT EXISTS pattern into a SQL NOT EXISTS subquery.
|
|
957
|
+
*/
|
|
958
|
+
compileNotExists(expr, variableMap, _params) {
|
|
959
|
+
const info = variableMap.get(expr.variable);
|
|
960
|
+
if (!info) {
|
|
961
|
+
throw new CypherExecutionError(`Unknown variable '${expr.variable}' in NOT EXISTS pattern`, '');
|
|
962
|
+
}
|
|
963
|
+
let subquery;
|
|
964
|
+
const edgeTypeFilter = expr.edgeType
|
|
965
|
+
? (() => {
|
|
966
|
+
const resolved = this.resolveEdgeType(expr.edgeType);
|
|
967
|
+
_params.push(resolved);
|
|
968
|
+
return ` AND _e.type = ?`;
|
|
969
|
+
})()
|
|
970
|
+
: '';
|
|
971
|
+
if (expr.direction === 'incoming') {
|
|
972
|
+
// No incoming edges to this node: NOT EXISTS (SELECT 1 FROM edges WHERE targetId = node.id)
|
|
973
|
+
subquery = `NOT EXISTS (SELECT 1 FROM edges _e WHERE _e.targetId = ${info.alias}.id${edgeTypeFilter})`;
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
// No outgoing edges from this node
|
|
977
|
+
subquery = `NOT EXISTS (SELECT 1 FROM edges _e WHERE _e.sourceId = ${info.alias}.id${edgeTypeFilter})`;
|
|
978
|
+
}
|
|
979
|
+
return subquery;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Resolve a property reference to a SQL column expression.
|
|
983
|
+
*/
|
|
984
|
+
resolvePropertyRef(ref, variableMap) {
|
|
985
|
+
const info = variableMap.get(ref.variable);
|
|
986
|
+
if (!info) {
|
|
987
|
+
throw new CypherExecutionError(`Unknown variable '${ref.variable}'. Available variables: ${[...variableMap.keys()].join(', ')}`, '');
|
|
988
|
+
}
|
|
989
|
+
const column = PROPERTY_TO_COLUMN[ref.property];
|
|
990
|
+
if (!column) {
|
|
991
|
+
throw new CypherExecutionError(`Unknown property '${ref.property}'. Available properties: ${Object.keys(PROPERTY_TO_COLUMN).join(', ')}`, '');
|
|
992
|
+
}
|
|
993
|
+
return `${info.alias}.${column}`;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Resolve a Cypher node label to the internal node type string.
|
|
997
|
+
*/
|
|
998
|
+
resolveNodeLabel(label) {
|
|
999
|
+
const resolved = LABEL_TO_TYPE[label.toLowerCase()];
|
|
1000
|
+
if (!resolved) {
|
|
1001
|
+
throw new CypherExecutionError(`Unknown node label ':${label}'. Available labels: ${Object.keys(LABEL_TO_TYPE).join(', ')}`, '');
|
|
1002
|
+
}
|
|
1003
|
+
return resolved;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Resolve a Cypher edge type label to the internal edge type string.
|
|
1007
|
+
*/
|
|
1008
|
+
resolveEdgeType(edgeType) {
|
|
1009
|
+
const resolved = EDGE_LABEL_TO_TYPE[edgeType.toLowerCase()];
|
|
1010
|
+
if (!resolved) {
|
|
1011
|
+
throw new CypherExecutionError(`Unknown edge type '[:${edgeType}]'. Available types: ${Object.keys(EDGE_LABEL_TO_TYPE).join(', ')}`, '');
|
|
1012
|
+
}
|
|
1013
|
+
return resolved;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Convert a Cypher literal to a SQL-safe value.
|
|
1017
|
+
*/
|
|
1018
|
+
toLiteralSqlValue(value) {
|
|
1019
|
+
if (typeof value === 'boolean')
|
|
1020
|
+
return value ? 1 : 0;
|
|
1021
|
+
return value;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
// ============================================================
|
|
1025
|
+
// Error Classes
|
|
1026
|
+
// ============================================================
|
|
1027
|
+
/**
|
|
1028
|
+
* Syntax error in a Cypher query string.
|
|
1029
|
+
*/
|
|
1030
|
+
export class CypherSyntaxError extends Error {
|
|
1031
|
+
/** Character position where the error occurred */
|
|
1032
|
+
position;
|
|
1033
|
+
constructor(message, position) {
|
|
1034
|
+
super(`Cypher syntax error at position ${position}: ${message}`);
|
|
1035
|
+
this.name = 'CypherSyntaxError';
|
|
1036
|
+
this.position = position;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Runtime error during Cypher query execution.
|
|
1041
|
+
*/
|
|
1042
|
+
export class CypherExecutionError extends Error {
|
|
1043
|
+
/** The original Cypher query that caused the error */
|
|
1044
|
+
query;
|
|
1045
|
+
constructor(message, query) {
|
|
1046
|
+
super(`Cypher execution error: ${message}`);
|
|
1047
|
+
this.name = 'CypherExecutionError';
|
|
1048
|
+
this.query = query;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
//# sourceMappingURL=cypher.js.map
|