leangraph 1.0.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 +456 -0
- package/dist/auth.d.ts +66 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +148 -0
- package/dist/auth.js.map +1 -0
- package/dist/backup.d.ts +51 -0
- package/dist/backup.d.ts.map +1 -0
- package/dist/backup.js +201 -0
- package/dist/backup.js.map +1 -0
- package/dist/cli-helpers.d.ts +17 -0
- package/dist/cli-helpers.d.ts.map +1 -0
- package/dist/cli-helpers.js +121 -0
- package/dist/cli-helpers.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +660 -0
- package/dist/cli.js.map +1 -0
- package/dist/db.d.ts +118 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +720 -0
- package/dist/db.js.map +1 -0
- package/dist/executor.d.ts +663 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +8578 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/local.d.ts +7 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +119 -0
- package/dist/local.js.map +1 -0
- package/dist/parser.d.ts +365 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +2711 -0
- package/dist/parser.js.map +1 -0
- package/dist/property-value.d.ts +3 -0
- package/dist/property-value.d.ts.map +1 -0
- package/dist/property-value.js +30 -0
- package/dist/property-value.js.map +1 -0
- package/dist/remote.d.ts +6 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +93 -0
- package/dist/remote.js.map +1 -0
- package/dist/routes.d.ts +31 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +202 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -0
- package/dist/translator.d.ts +330 -0
- package/dist/translator.d.ts.map +1 -0
- package/dist/translator.js +13712 -0
- package/dist/translator.js.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/package.json +77 -0
package/dist/db.js
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
// Database Wrapper for SQLite
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Schema
|
|
5
|
+
// ============================================================================
|
|
6
|
+
const SCHEMA = `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
8
|
+
id TEXT PRIMARY KEY,
|
|
9
|
+
label JSON NOT NULL,
|
|
10
|
+
properties JSON DEFAULT '{}'
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
type TEXT NOT NULL,
|
|
16
|
+
source_id TEXT NOT NULL,
|
|
17
|
+
target_id TEXT NOT NULL,
|
|
18
|
+
properties JSON DEFAULT '{}',
|
|
19
|
+
FOREIGN KEY (source_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
|
20
|
+
FOREIGN KEY (target_id) REFERENCES nodes(id) ON DELETE CASCADE
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
|
|
26
|
+
`;
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Helpers
|
|
29
|
+
// ============================================================================
|
|
30
|
+
/**
|
|
31
|
+
* Convert a parameter value for SQLite binding.
|
|
32
|
+
* Large integers (outside JavaScript's safe integer range) are converted to BigInt
|
|
33
|
+
* to ensure SQLite treats them as INTEGER rather than REAL (which loses precision).
|
|
34
|
+
*
|
|
35
|
+
* Important: We convert via string representation to preserve the value that JavaScript
|
|
36
|
+
* would serialize (e.g., to JSON), rather than the internal floating-point representation
|
|
37
|
+
* which may differ for large integers.
|
|
38
|
+
*/
|
|
39
|
+
function convertParamForSqlite(value) {
|
|
40
|
+
if (typeof value === "number" && Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
41
|
+
// Large integer: convert to BigInt via string to preserve the serialized representation
|
|
42
|
+
// This ensures consistency with JSON.stringify() behavior
|
|
43
|
+
return BigInt(String(value));
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Convert all params in an array for SQLite binding.
|
|
49
|
+
*/
|
|
50
|
+
function convertParamsForSqlite(params) {
|
|
51
|
+
return params.map(convertParamForSqlite);
|
|
52
|
+
}
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Database Class
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Custom SQL Functions for Cypher Semantics
|
|
58
|
+
// ============================================================================
|
|
59
|
+
/**
|
|
60
|
+
* Deep equality comparison with Cypher's three-valued logic.
|
|
61
|
+
* Returns: 1 (true), 0 (false), or null (unknown when comparing with null)
|
|
62
|
+
*/
|
|
63
|
+
function deepCypherEquals(a, b) {
|
|
64
|
+
// Both null/undefined -> null (unknown if null equals null)
|
|
65
|
+
if (a === null && b === null)
|
|
66
|
+
return null;
|
|
67
|
+
// One null -> null (unknown)
|
|
68
|
+
if (a === null || b === null)
|
|
69
|
+
return null;
|
|
70
|
+
// Arrays
|
|
71
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
72
|
+
if (a.length !== b.length)
|
|
73
|
+
return 0; // false
|
|
74
|
+
if (a.length === 0)
|
|
75
|
+
return 1; // true
|
|
76
|
+
let hasNull = false;
|
|
77
|
+
for (let i = 0; i < a.length; i++) {
|
|
78
|
+
const cmp = deepCypherEquals(a[i], b[i]);
|
|
79
|
+
if (cmp === null)
|
|
80
|
+
hasNull = true;
|
|
81
|
+
else if (cmp === 0)
|
|
82
|
+
return 0; // false
|
|
83
|
+
}
|
|
84
|
+
return hasNull ? null : 1;
|
|
85
|
+
}
|
|
86
|
+
// Objects (maps)
|
|
87
|
+
if (typeof a === "object" && typeof b === "object" && a !== null && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
|
|
88
|
+
const keysA = Object.keys(a).sort();
|
|
89
|
+
const keysB = Object.keys(b).sort();
|
|
90
|
+
if (keysA.length !== keysB.length)
|
|
91
|
+
return 0;
|
|
92
|
+
if (keysA.join(",") !== keysB.join(","))
|
|
93
|
+
return 0;
|
|
94
|
+
let hasNull = false;
|
|
95
|
+
for (const k of keysA) {
|
|
96
|
+
const cmp = deepCypherEquals(a[k], b[k]);
|
|
97
|
+
if (cmp === null)
|
|
98
|
+
hasNull = true;
|
|
99
|
+
else if (cmp === 0)
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
return hasNull ? null : 1;
|
|
103
|
+
}
|
|
104
|
+
// Primitives
|
|
105
|
+
return a === b ? 1 : 0;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the Cypher type category for ordering comparisons.
|
|
109
|
+
* Returns a type string for values that can be ordered, or null for non-orderable types.
|
|
110
|
+
*
|
|
111
|
+
* When using SQLite's -> operator for JSON extraction, values come as JSON-formatted strings:
|
|
112
|
+
* - 'true' / 'false' for booleans
|
|
113
|
+
* - '"string"' for strings (with quotes)
|
|
114
|
+
* - '123' or '3.14' for numbers (no quotes)
|
|
115
|
+
* - '[...]' for arrays
|
|
116
|
+
* - '{...}' for objects
|
|
117
|
+
*/
|
|
118
|
+
// Regex patterns for temporal types
|
|
119
|
+
const TIME_PATTERN = /^\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
120
|
+
const LOCALTIME_PATTERN = /^\d{2}:\d{2}(:\d{2})?(\.\d+)?$/;
|
|
121
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
122
|
+
const DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?(\[.+\])?$/;
|
|
123
|
+
const LOCALDATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?$/;
|
|
124
|
+
function getCypherTypeForOrdering(value) {
|
|
125
|
+
if (value === null)
|
|
126
|
+
return null;
|
|
127
|
+
const jsType = typeof value;
|
|
128
|
+
// Numbers (integer and real) are in the same ordering category
|
|
129
|
+
if (jsType === "number" || jsType === "bigint")
|
|
130
|
+
return "number";
|
|
131
|
+
// Strings - could be raw strings OR JSON-formatted values from -> operator
|
|
132
|
+
if (jsType === "string") {
|
|
133
|
+
const s = value;
|
|
134
|
+
// Check for JSON boolean literals (from -> operator)
|
|
135
|
+
if (s === "true" || s === "false")
|
|
136
|
+
return "boolean";
|
|
137
|
+
// Check for JSON null
|
|
138
|
+
if (s === "null")
|
|
139
|
+
return null;
|
|
140
|
+
// Check for JSON array
|
|
141
|
+
if (s.startsWith("[") && s.endsWith("]")) {
|
|
142
|
+
try {
|
|
143
|
+
JSON.parse(s);
|
|
144
|
+
return "array"; // arrays are not orderable
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Not valid JSON, treat as string
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Check for JSON object
|
|
151
|
+
if (s.startsWith("{") && s.endsWith("}")) {
|
|
152
|
+
try {
|
|
153
|
+
JSON.parse(s);
|
|
154
|
+
return "object"; // objects are not orderable
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Not valid JSON, treat as string
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Check for JSON string literal (starts and ends with quotes)
|
|
161
|
+
if (s.startsWith('"') && s.endsWith('"') && s.length >= 2) {
|
|
162
|
+
return "string";
|
|
163
|
+
}
|
|
164
|
+
// Check for JSON number (no quotes, valid number)
|
|
165
|
+
if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(s)) {
|
|
166
|
+
return "number";
|
|
167
|
+
}
|
|
168
|
+
// Check for temporal types (times with timezone need special comparison)
|
|
169
|
+
if (TIME_PATTERN.test(s))
|
|
170
|
+
return "time";
|
|
171
|
+
if (DATETIME_PATTERN.test(s))
|
|
172
|
+
return "datetime";
|
|
173
|
+
if (DATE_PATTERN.test(s))
|
|
174
|
+
return "date";
|
|
175
|
+
if (LOCALTIME_PATTERN.test(s))
|
|
176
|
+
return "localtime";
|
|
177
|
+
if (LOCALDATETIME_PATTERN.test(s))
|
|
178
|
+
return "localdatetime";
|
|
179
|
+
// Otherwise treat as a plain string
|
|
180
|
+
return "string";
|
|
181
|
+
}
|
|
182
|
+
// Booleans - SQLite stores these as integers, but if we somehow get a JS boolean
|
|
183
|
+
if (jsType === "boolean")
|
|
184
|
+
return "boolean";
|
|
185
|
+
// Objects and arrays in JS form (shouldn't normally happen with SQLite)
|
|
186
|
+
if (Array.isArray(value))
|
|
187
|
+
return "array";
|
|
188
|
+
if (jsType === "object")
|
|
189
|
+
return "object";
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if two Cypher types are compatible for ordering comparisons (<, <=, >, >=).
|
|
194
|
+
*/
|
|
195
|
+
function areCypherTypesOrderable(typeA, typeB) {
|
|
196
|
+
if (typeA === null || typeB === null)
|
|
197
|
+
return false;
|
|
198
|
+
// Arrays, objects, nodes, relationships are not orderable
|
|
199
|
+
if (typeA === "array" || typeB === "array")
|
|
200
|
+
return false;
|
|
201
|
+
if (typeA === "object" || typeB === "object")
|
|
202
|
+
return false;
|
|
203
|
+
// Same type is always orderable
|
|
204
|
+
if (typeA === typeB)
|
|
205
|
+
return true;
|
|
206
|
+
// Numbers (integer/real) are orderable with each other - already handled by "number" category
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Parse timezone offset to minutes from UTC
|
|
211
|
+
*/
|
|
212
|
+
function parseTimezoneOffset(tz) {
|
|
213
|
+
if (tz === "Z" || tz === "+00:00")
|
|
214
|
+
return 0;
|
|
215
|
+
const sign = tz[0] === "-" ? -1 : 1;
|
|
216
|
+
const hours = parseInt(tz.slice(1, 3), 10);
|
|
217
|
+
const minutes = parseInt(tz.slice(4, 6), 10);
|
|
218
|
+
return sign * (hours * 60 + minutes);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Convert time string to nanoseconds from midnight UTC for comparison
|
|
222
|
+
*/
|
|
223
|
+
function timeToNanosUTC(timeStr) {
|
|
224
|
+
// Format: HH:MM or HH:MM:SS or HH:MM:SS.nnnnnnnnn followed by Z or +HH:MM or -HH:MM
|
|
225
|
+
// May also have [timezone] suffix - strip it
|
|
226
|
+
const withoutTzName = timeStr.replace(/\[.+\]$/, "");
|
|
227
|
+
// Find timezone part
|
|
228
|
+
let tzOffset = 0;
|
|
229
|
+
let timePart = withoutTzName;
|
|
230
|
+
if (withoutTzName.endsWith("Z")) {
|
|
231
|
+
timePart = withoutTzName.slice(0, -1);
|
|
232
|
+
tzOffset = 0;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const tzMatch = withoutTzName.match(/([+-]\d{2}:\d{2})$/);
|
|
236
|
+
if (tzMatch) {
|
|
237
|
+
tzOffset = parseTimezoneOffset(tzMatch[1]);
|
|
238
|
+
timePart = withoutTzName.slice(0, -6);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Parse time components
|
|
242
|
+
const parts = timePart.split(":");
|
|
243
|
+
const hours = parseInt(parts[0], 10);
|
|
244
|
+
const minutes = parseInt(parts[1], 10);
|
|
245
|
+
let seconds = 0;
|
|
246
|
+
let nanos = 0;
|
|
247
|
+
if (parts[2]) {
|
|
248
|
+
const secParts = parts[2].split(".");
|
|
249
|
+
seconds = parseInt(secParts[0], 10);
|
|
250
|
+
if (secParts[1]) {
|
|
251
|
+
// Pad or truncate to 9 digits
|
|
252
|
+
const fracStr = secParts[1].padEnd(9, "0").slice(0, 9);
|
|
253
|
+
nanos = parseInt(fracStr, 10);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Convert to total nanoseconds from midnight, then adjust for timezone
|
|
257
|
+
const totalMinutes = hours * 60 + minutes - tzOffset;
|
|
258
|
+
const totalNanos = (totalMinutes * 60 + seconds) * 1_000_000_000 + nanos;
|
|
259
|
+
// Normalize to 24-hour range (handle negative from timezone adjustment)
|
|
260
|
+
const dayInNanos = 24 * 60 * 60 * 1_000_000_000;
|
|
261
|
+
return ((totalNanos % dayInNanos) + dayInNanos) % dayInNanos;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Convert a value to its comparable form.
|
|
265
|
+
* For JSON-formatted strings from -> operator, parse to get actual value.
|
|
266
|
+
* For temporal types, convert to a form suitable for comparison.
|
|
267
|
+
*/
|
|
268
|
+
function toComparableValue(value, type) {
|
|
269
|
+
if (typeof value === "string") {
|
|
270
|
+
if (type === "number") {
|
|
271
|
+
return parseFloat(value);
|
|
272
|
+
}
|
|
273
|
+
if (type === "boolean") {
|
|
274
|
+
return value === "true";
|
|
275
|
+
}
|
|
276
|
+
if (type === "string") {
|
|
277
|
+
// JSON string literal - remove outer quotes
|
|
278
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
279
|
+
return value.slice(1, -1);
|
|
280
|
+
}
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
if (type === "time") {
|
|
284
|
+
// Convert to nanoseconds from midnight UTC for comparison
|
|
285
|
+
return timeToNanosUTC(value);
|
|
286
|
+
}
|
|
287
|
+
if (type === "datetime") {
|
|
288
|
+
// Strip [timezone] suffix and compare lexically (ISO format is naturally sortable when in UTC or same TZ)
|
|
289
|
+
// For proper comparison, we'd need to convert to UTC, but for same-offset datetimes, lexical works
|
|
290
|
+
// TODO: Full timezone-aware datetime comparison
|
|
291
|
+
return value.replace(/\[.+\]$/, "");
|
|
292
|
+
}
|
|
293
|
+
// date, localtime, localdatetime can be compared lexically (ISO format is sortable)
|
|
294
|
+
if (type === "date" || type === "localtime" || type === "localdatetime") {
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Helper to convert SQLite boolean representation to JavaScript boolean.
|
|
302
|
+
* SQLite can represent booleans as: 1, 0, 'true', 'false'
|
|
303
|
+
*/
|
|
304
|
+
function toBoolValue(x) {
|
|
305
|
+
if (x === null || x === undefined)
|
|
306
|
+
return null;
|
|
307
|
+
if (x === 1 || x === true || x === 'true')
|
|
308
|
+
return true;
|
|
309
|
+
if (x === 0 || x === false || x === 'false')
|
|
310
|
+
return false;
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Register custom SQL functions for Cypher semantics on a database instance.
|
|
315
|
+
*/
|
|
316
|
+
function registerCypherFunctions(db) {
|
|
317
|
+
// cypher_not: Proper boolean negation that works with both JSON booleans and integers
|
|
318
|
+
// Converts json('true')/1 -> 0, json('false')/0 -> 1, null -> null
|
|
319
|
+
// Returns integers for SQLite compatibility in WHERE clauses
|
|
320
|
+
db.function("cypher_not", { deterministic: true }, (x) => {
|
|
321
|
+
const b = toBoolValue(x);
|
|
322
|
+
if (b === null)
|
|
323
|
+
return null;
|
|
324
|
+
return b ? 0 : 1;
|
|
325
|
+
});
|
|
326
|
+
// cypher_and: Proper boolean AND that works with both JSON booleans and integers
|
|
327
|
+
// Returns integers for SQLite compatibility in WHERE clauses
|
|
328
|
+
db.function("cypher_and", { deterministic: true }, (a, b) => {
|
|
329
|
+
const boolA = toBoolValue(a);
|
|
330
|
+
const boolB = toBoolValue(b);
|
|
331
|
+
// Cypher AND with NULL: false AND NULL = false, true AND NULL = NULL
|
|
332
|
+
if (boolA === false || boolB === false)
|
|
333
|
+
return 0;
|
|
334
|
+
if (boolA === null || boolB === null)
|
|
335
|
+
return null;
|
|
336
|
+
return boolA && boolB ? 1 : 0;
|
|
337
|
+
});
|
|
338
|
+
// cypher_or: Proper boolean OR that works with both JSON booleans and integers
|
|
339
|
+
// Returns integers for SQLite compatibility in WHERE clauses
|
|
340
|
+
db.function("cypher_or", { deterministic: true }, (a, b) => {
|
|
341
|
+
const boolA = toBoolValue(a);
|
|
342
|
+
const boolB = toBoolValue(b);
|
|
343
|
+
// Cypher OR with NULL: true OR NULL = true, false OR NULL = NULL
|
|
344
|
+
if (boolA === true || boolB === true)
|
|
345
|
+
return 1;
|
|
346
|
+
if (boolA === null || boolB === null)
|
|
347
|
+
return null;
|
|
348
|
+
return boolA || boolB ? 1 : 0;
|
|
349
|
+
});
|
|
350
|
+
// cypher_compare: Type-aware comparison for ordering operators (<, <=, >, >=)
|
|
351
|
+
// Returns: 1 if condition is true, 0 if false, null if types are incompatible
|
|
352
|
+
db.function("cypher_lt", { deterministic: true }, (a, b) => {
|
|
353
|
+
if (a === null || a === undefined || b === null || b === undefined)
|
|
354
|
+
return null;
|
|
355
|
+
const typeA = getCypherTypeForOrdering(a);
|
|
356
|
+
const typeB = getCypherTypeForOrdering(b);
|
|
357
|
+
if (!areCypherTypesOrderable(typeA, typeB))
|
|
358
|
+
return null;
|
|
359
|
+
const valA = toComparableValue(a, typeA);
|
|
360
|
+
const valB = toComparableValue(b, typeB);
|
|
361
|
+
return valA < valB ? 1 : 0;
|
|
362
|
+
});
|
|
363
|
+
db.function("cypher_lte", { deterministic: true }, (a, b) => {
|
|
364
|
+
if (a === null || a === undefined || b === null || b === undefined)
|
|
365
|
+
return null;
|
|
366
|
+
const typeA = getCypherTypeForOrdering(a);
|
|
367
|
+
const typeB = getCypherTypeForOrdering(b);
|
|
368
|
+
if (!areCypherTypesOrderable(typeA, typeB))
|
|
369
|
+
return null;
|
|
370
|
+
const valA = toComparableValue(a, typeA);
|
|
371
|
+
const valB = toComparableValue(b, typeB);
|
|
372
|
+
return valA <= valB ? 1 : 0;
|
|
373
|
+
});
|
|
374
|
+
db.function("cypher_gt", { deterministic: true }, (a, b) => {
|
|
375
|
+
if (a === null || a === undefined || b === null || b === undefined)
|
|
376
|
+
return null;
|
|
377
|
+
const typeA = getCypherTypeForOrdering(a);
|
|
378
|
+
const typeB = getCypherTypeForOrdering(b);
|
|
379
|
+
if (!areCypherTypesOrderable(typeA, typeB))
|
|
380
|
+
return null;
|
|
381
|
+
const valA = toComparableValue(a, typeA);
|
|
382
|
+
const valB = toComparableValue(b, typeB);
|
|
383
|
+
return valA > valB ? 1 : 0;
|
|
384
|
+
});
|
|
385
|
+
db.function("cypher_gte", { deterministic: true }, (a, b) => {
|
|
386
|
+
if (a === null || a === undefined || b === null || b === undefined)
|
|
387
|
+
return null;
|
|
388
|
+
const typeA = getCypherTypeForOrdering(a);
|
|
389
|
+
const typeB = getCypherTypeForOrdering(b);
|
|
390
|
+
if (!areCypherTypesOrderable(typeA, typeB))
|
|
391
|
+
return null;
|
|
392
|
+
const valA = toComparableValue(a, typeA);
|
|
393
|
+
const valB = toComparableValue(b, typeB);
|
|
394
|
+
return valA >= valB ? 1 : 0;
|
|
395
|
+
});
|
|
396
|
+
// cypher_equals: Null-aware deep equality for lists and maps
|
|
397
|
+
db.function("cypher_equals", { deterministic: true }, (a, b) => {
|
|
398
|
+
// Handle SQL NULL
|
|
399
|
+
if (a === null && b === null)
|
|
400
|
+
return null;
|
|
401
|
+
if (a === null || b === null)
|
|
402
|
+
return null;
|
|
403
|
+
// Try to parse as JSON (for arrays/objects stored as JSON strings)
|
|
404
|
+
let parsedA, parsedB;
|
|
405
|
+
try {
|
|
406
|
+
parsedA = typeof a === "string" ? JSON.parse(a) : a;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
parsedA = a;
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
parsedB = typeof b === "string" ? JSON.parse(b) : b;
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
parsedB = b;
|
|
416
|
+
}
|
|
417
|
+
return deepCypherEquals(parsedA, parsedB);
|
|
418
|
+
});
|
|
419
|
+
// cypher_case_eq: Type-aware equality for CASE expressions
|
|
420
|
+
// Takes value+type pairs to preserve type information across SQLite's type coercion
|
|
421
|
+
// Returns: 1 if equal (same type and value), 0 if not equal
|
|
422
|
+
db.function("cypher_case_eq", { deterministic: true }, (val1, type1, val2, type2) => {
|
|
423
|
+
// NULL handling: if either is null, return null (unknown)
|
|
424
|
+
if (val1 === null || val2 === null)
|
|
425
|
+
return null;
|
|
426
|
+
if (type1 === "null" || type2 === "null")
|
|
427
|
+
return null;
|
|
428
|
+
// Helper to get runtime type from a value
|
|
429
|
+
const getRuntimeType = (val) => {
|
|
430
|
+
if (val === null)
|
|
431
|
+
return "null";
|
|
432
|
+
const jsType = typeof val;
|
|
433
|
+
if (jsType === "boolean" || val === 0 || val === 1) {
|
|
434
|
+
// SQLite stores booleans as 0/1, so we can't distinguish at runtime
|
|
435
|
+
// We rely on the compile-time type info for this
|
|
436
|
+
return "unknown_number_or_boolean";
|
|
437
|
+
}
|
|
438
|
+
if (jsType === "number" || jsType === "bigint")
|
|
439
|
+
return "number";
|
|
440
|
+
if (jsType === "string") {
|
|
441
|
+
// Check if it's a JSON array/object
|
|
442
|
+
const str = val;
|
|
443
|
+
if (str.startsWith("["))
|
|
444
|
+
return "list";
|
|
445
|
+
if (str.startsWith("{"))
|
|
446
|
+
return "map";
|
|
447
|
+
return "string";
|
|
448
|
+
}
|
|
449
|
+
return "unknown";
|
|
450
|
+
};
|
|
451
|
+
// Resolve "dynamic" types using runtime type detection
|
|
452
|
+
let resolvedType1 = type1;
|
|
453
|
+
let resolvedType2 = type2;
|
|
454
|
+
if (type1 === "dynamic") {
|
|
455
|
+
resolvedType1 = getRuntimeType(val1);
|
|
456
|
+
}
|
|
457
|
+
if (type2 === "dynamic") {
|
|
458
|
+
resolvedType2 = getRuntimeType(val2);
|
|
459
|
+
}
|
|
460
|
+
// Normalize numeric types: integer, float, and number are all comparable
|
|
461
|
+
const normalizeNumericType = (t) => {
|
|
462
|
+
if (t === "integer" || t === "float" || t === "number")
|
|
463
|
+
return "numeric";
|
|
464
|
+
return t;
|
|
465
|
+
};
|
|
466
|
+
const normType1 = normalizeNumericType(resolvedType1);
|
|
467
|
+
const normType2 = normalizeNumericType(resolvedType2);
|
|
468
|
+
// Different types are never equal in CASE expressions
|
|
469
|
+
// (except numeric types which are comparable)
|
|
470
|
+
if (normType1 !== normType2)
|
|
471
|
+
return 0;
|
|
472
|
+
// Same type - compare values
|
|
473
|
+
// For lists/maps, use deep comparison
|
|
474
|
+
if (normType1 === "list" || normType1 === "map") {
|
|
475
|
+
let parsed1, parsed2;
|
|
476
|
+
try {
|
|
477
|
+
parsed1 = typeof val1 === "string" ? JSON.parse(val1) : val1;
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
parsed1 = val1;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
parsed2 = typeof val2 === "string" ? JSON.parse(val2) : val2;
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
parsed2 = val2;
|
|
487
|
+
}
|
|
488
|
+
const result = deepCypherEquals(parsed1, parsed2);
|
|
489
|
+
return result === null ? null : result;
|
|
490
|
+
}
|
|
491
|
+
// For numeric types, compare as numbers
|
|
492
|
+
if (normType1 === "numeric") {
|
|
493
|
+
const num1 = Number(val1);
|
|
494
|
+
const num2 = Number(val2);
|
|
495
|
+
return num1 === num2 ? 1 : 0;
|
|
496
|
+
}
|
|
497
|
+
// For primitives (boolean, string), direct comparison
|
|
498
|
+
return val1 === val2 ? 1 : 0;
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
export class GraphDatabase {
|
|
502
|
+
db;
|
|
503
|
+
initialized = false;
|
|
504
|
+
constructor(path = ":memory:") {
|
|
505
|
+
this.db = new Database(path);
|
|
506
|
+
this.db.pragma("journal_mode = WAL");
|
|
507
|
+
this.db.pragma("foreign_keys = ON");
|
|
508
|
+
// Register custom Cypher functions
|
|
509
|
+
registerCypherFunctions(this.db);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Initialize the database schema
|
|
513
|
+
*/
|
|
514
|
+
initialize() {
|
|
515
|
+
if (this.initialized)
|
|
516
|
+
return;
|
|
517
|
+
this.db.exec(SCHEMA);
|
|
518
|
+
this.initialized = true;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Execute a SQL statement and return results
|
|
522
|
+
*/
|
|
523
|
+
execute(sql, params = []) {
|
|
524
|
+
this.ensureInitialized();
|
|
525
|
+
// Convert large integers to BigInt for proper SQLite INTEGER binding
|
|
526
|
+
const convertedParams = convertParamsForSqlite(params);
|
|
527
|
+
const stmt = this.db.prepare(sql);
|
|
528
|
+
const trimmedSql = sql.trim().toUpperCase();
|
|
529
|
+
// Check if it's a query (SELECT or WITH for CTEs)
|
|
530
|
+
const isQuery = trimmedSql.startsWith("SELECT") || trimmedSql.startsWith("WITH");
|
|
531
|
+
if (isQuery) {
|
|
532
|
+
const rows = stmt.all(...convertedParams);
|
|
533
|
+
return { rows, changes: 0, lastInsertRowid: 0 };
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
const result = stmt.run(...convertedParams);
|
|
537
|
+
return {
|
|
538
|
+
rows: [],
|
|
539
|
+
changes: result.changes,
|
|
540
|
+
lastInsertRowid: result.lastInsertRowid,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Execute multiple statements in a transaction
|
|
546
|
+
*/
|
|
547
|
+
transaction(fn) {
|
|
548
|
+
this.ensureInitialized();
|
|
549
|
+
return this.db.transaction(fn)();
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Insert a node
|
|
553
|
+
*/
|
|
554
|
+
insertNode(id, label, properties = {}) {
|
|
555
|
+
// Normalize label to array format for storage
|
|
556
|
+
const labelArray = Array.isArray(label) ? label : [label];
|
|
557
|
+
this.execute("INSERT INTO nodes (id, label, properties) VALUES (?, ?, ?)", [id, JSON.stringify(labelArray), JSON.stringify(properties)]);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Insert an edge
|
|
561
|
+
*/
|
|
562
|
+
insertEdge(id, type, sourceId, targetId, properties = {}) {
|
|
563
|
+
this.execute("INSERT INTO edges (id, type, source_id, target_id, properties) VALUES (?, ?, ?, ?, ?)", [id, type, sourceId, targetId, JSON.stringify(properties)]);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Get a node by ID
|
|
567
|
+
*/
|
|
568
|
+
getNode(id) {
|
|
569
|
+
const result = this.execute("SELECT * FROM nodes WHERE id = ?", [id]);
|
|
570
|
+
if (result.rows.length === 0)
|
|
571
|
+
return null;
|
|
572
|
+
const row = result.rows[0];
|
|
573
|
+
const labelArray = JSON.parse(row.label);
|
|
574
|
+
return {
|
|
575
|
+
id: row.id,
|
|
576
|
+
label: labelArray,
|
|
577
|
+
properties: JSON.parse(row.properties),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Get an edge by ID
|
|
582
|
+
*/
|
|
583
|
+
getEdge(id) {
|
|
584
|
+
const result = this.execute("SELECT * FROM edges WHERE id = ?", [id]);
|
|
585
|
+
if (result.rows.length === 0)
|
|
586
|
+
return null;
|
|
587
|
+
const row = result.rows[0];
|
|
588
|
+
return {
|
|
589
|
+
id: row.id,
|
|
590
|
+
type: row.type,
|
|
591
|
+
source_id: row.source_id,
|
|
592
|
+
target_id: row.target_id,
|
|
593
|
+
properties: JSON.parse(row.properties),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Get all nodes with a given label
|
|
598
|
+
*/
|
|
599
|
+
getNodesByLabel(label) {
|
|
600
|
+
const result = this.execute("SELECT * FROM nodes WHERE EXISTS (SELECT 1 FROM json_each(label) WHERE value = ?)", [label]);
|
|
601
|
+
return result.rows.map((row) => {
|
|
602
|
+
const r = row;
|
|
603
|
+
const labelArray = JSON.parse(r.label);
|
|
604
|
+
return {
|
|
605
|
+
id: r.id,
|
|
606
|
+
label: labelArray,
|
|
607
|
+
properties: JSON.parse(r.properties),
|
|
608
|
+
};
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Get all edges with a given type
|
|
613
|
+
*/
|
|
614
|
+
getEdgesByType(type) {
|
|
615
|
+
const result = this.execute("SELECT * FROM edges WHERE type = ?", [type]);
|
|
616
|
+
return result.rows.map((row) => {
|
|
617
|
+
const r = row;
|
|
618
|
+
return {
|
|
619
|
+
id: r.id,
|
|
620
|
+
type: r.type,
|
|
621
|
+
source_id: r.source_id,
|
|
622
|
+
target_id: r.target_id,
|
|
623
|
+
properties: JSON.parse(r.properties),
|
|
624
|
+
};
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Delete a node by ID
|
|
629
|
+
*/
|
|
630
|
+
deleteNode(id) {
|
|
631
|
+
const result = this.execute("DELETE FROM nodes WHERE id = ?", [id]);
|
|
632
|
+
return result.changes > 0;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Delete an edge by ID
|
|
636
|
+
*/
|
|
637
|
+
deleteEdge(id) {
|
|
638
|
+
const result = this.execute("DELETE FROM edges WHERE id = ?", [id]);
|
|
639
|
+
return result.changes > 0;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Update node properties
|
|
643
|
+
*/
|
|
644
|
+
updateNodeProperties(id, properties) {
|
|
645
|
+
const result = this.execute("UPDATE nodes SET properties = ? WHERE id = ?", [JSON.stringify(properties), id]);
|
|
646
|
+
return result.changes > 0;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Count nodes
|
|
650
|
+
*/
|
|
651
|
+
countNodes() {
|
|
652
|
+
const result = this.execute("SELECT COUNT(*) as count FROM nodes");
|
|
653
|
+
return result.rows[0].count;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Count edges
|
|
657
|
+
*/
|
|
658
|
+
countEdges() {
|
|
659
|
+
const result = this.execute("SELECT COUNT(*) as count FROM edges");
|
|
660
|
+
return result.rows[0].count;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Close the database connection
|
|
664
|
+
*/
|
|
665
|
+
close() {
|
|
666
|
+
this.db.close();
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Get the underlying database instance (for advanced operations)
|
|
670
|
+
*/
|
|
671
|
+
getRawDatabase() {
|
|
672
|
+
return this.db;
|
|
673
|
+
}
|
|
674
|
+
ensureInitialized() {
|
|
675
|
+
if (!this.initialized) {
|
|
676
|
+
this.initialize();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// ============================================================================
|
|
681
|
+
// Database Manager (for multi-project support)
|
|
682
|
+
// ============================================================================
|
|
683
|
+
export class DatabaseManager {
|
|
684
|
+
databases = new Map();
|
|
685
|
+
basePath;
|
|
686
|
+
constructor(basePath = ":memory:") {
|
|
687
|
+
this.basePath = basePath;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Get or create a database for a project/environment
|
|
691
|
+
*/
|
|
692
|
+
getDatabase(project, env = "production") {
|
|
693
|
+
const key = `${env}/${project}`;
|
|
694
|
+
if (!this.databases.has(key)) {
|
|
695
|
+
const path = this.basePath === ":memory:"
|
|
696
|
+
? ":memory:"
|
|
697
|
+
: `${this.basePath}/${env}/${project}.db`;
|
|
698
|
+
const db = new GraphDatabase(path);
|
|
699
|
+
db.initialize();
|
|
700
|
+
this.databases.set(key, db);
|
|
701
|
+
}
|
|
702
|
+
return this.databases.get(key);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Close all database connections
|
|
706
|
+
*/
|
|
707
|
+
closeAll() {
|
|
708
|
+
for (const db of this.databases.values()) {
|
|
709
|
+
db.close();
|
|
710
|
+
}
|
|
711
|
+
this.databases.clear();
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* List all open databases
|
|
715
|
+
*/
|
|
716
|
+
listDatabases() {
|
|
717
|
+
return Array.from(this.databases.keys());
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
//# sourceMappingURL=db.js.map
|