leangraph 1.0.0 → 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/README.md +198 -118
- package/dist/auth.d.ts +1 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +3 -13
- package/dist/auth.js.map +1 -1
- package/dist/backup.d.ts +1 -3
- package/dist/backup.d.ts.map +1 -1
- package/dist/backup.js +10 -15
- package/dist/backup.js.map +1 -1
- package/dist/cli-helpers.d.ts +2 -3
- package/dist/cli-helpers.d.ts.map +1 -1
- package/dist/cli-helpers.js +11 -30
- package/dist/cli-helpers.js.map +1 -1
- package/dist/cli.js +82 -129
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts +8 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +34 -8
- package/dist/db.js.map +1 -1
- package/dist/executor.d.ts +6 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +92 -22
- package/dist/executor.js.map +1 -1
- package/dist/index.d.ts +8 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -37
- package/dist/index.js.map +1 -1
- package/dist/local.d.ts +3 -3
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +13 -15
- package/dist/local.js.map +1 -1
- package/dist/remote.d.ts +3 -3
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +8 -10
- package/dist/remote.js.map +1 -1
- package/dist/routes.d.ts +0 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +12 -39
- package/dist/routes.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/translator.d.ts.map +1 -1
- package/dist/translator.js +85 -32
- package/dist/translator.js.map +1 -1
- package/dist/types.d.ts +24 -26
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/package.json +16 -3
package/dist/translator.js
CHANGED
|
@@ -1278,7 +1278,9 @@ export class Translator {
|
|
|
1278
1278
|
const hasVariableLengthPattern = relPatterns?.some(p => p.isVariableLength);
|
|
1279
1279
|
if (hasVariableLengthPattern && relPatterns) {
|
|
1280
1280
|
// Use recursive CTE for variable-length paths
|
|
1281
|
-
|
|
1281
|
+
// Pass the limit value for early termination optimization
|
|
1282
|
+
const limitValue = clause.limit !== undefined && typeof clause.limit === 'number' ? clause.limit : undefined;
|
|
1283
|
+
return this.translateVariableLengthPath(clause, relPatterns, selectParts, returnColumns, exprParams, whereParams, limitValue);
|
|
1282
1284
|
}
|
|
1283
1285
|
if (relPatterns && relPatterns.length > 0) {
|
|
1284
1286
|
// Track which node aliases we've already added to FROM/JOIN
|
|
@@ -2952,7 +2954,7 @@ export class Translator {
|
|
|
2952
2954
|
// ============================================================================
|
|
2953
2955
|
// Variable-length paths
|
|
2954
2956
|
// ============================================================================
|
|
2955
|
-
translateVariableLengthPath(clause, relPatterns, selectParts, returnColumns, exprParams, whereParams) {
|
|
2957
|
+
translateVariableLengthPath(clause, relPatterns, selectParts, returnColumns, exprParams, whereParams, limitValue) {
|
|
2956
2958
|
// For variable-length paths, we use SQLite's recursive CTEs
|
|
2957
2959
|
// Pattern: WITH RECURSIVE path(start_id, end_id, depth) AS (
|
|
2958
2960
|
// SELECT source_id, target_id, 1 FROM edges WHERE ...
|
|
@@ -2980,6 +2982,10 @@ export class Translator {
|
|
|
2980
2982
|
const edgeProperties = varLengthPattern.edge.properties;
|
|
2981
2983
|
const varLengthSourceAlias = varLengthPattern.sourceAlias;
|
|
2982
2984
|
const varLengthTargetAlias = varLengthPattern.targetAlias;
|
|
2985
|
+
// Early termination optimization: if there's a LIMIT clause, use it to bound recursion
|
|
2986
|
+
// This prevents the CTE from generating millions of paths only to return a few
|
|
2987
|
+
const effectiveLimit = limitValue ?? 1000; // Default limit for safety
|
|
2988
|
+
const earlyTerminationLimit = Math.min(effectiveLimit * 10, 10000); // Cap at 10k for safety
|
|
2983
2989
|
const allParams = [...exprParams];
|
|
2984
2990
|
// Build edge property conditions for variable-length paths
|
|
2985
2991
|
// These conditions need to be applied to every edge in the path
|
|
@@ -2996,6 +3002,39 @@ export class Translator {
|
|
|
2996
3002
|
// Build the condition string for recursive case (with 'e.' table alias)
|
|
2997
3003
|
const recursivePropConditions = edgePropConditions.map(c => c.replace("properties", "e.properties"));
|
|
2998
3004
|
const recursivePropCondition = recursivePropConditions.length > 0 ? " AND " + recursivePropConditions.join(" AND ") : "";
|
|
3005
|
+
// Build source node filter for CTE optimization
|
|
3006
|
+
// This pushes the source filter INTO the CTE base case instead of filtering after
|
|
3007
|
+
// which dramatically improves performance for large graphs
|
|
3008
|
+
const sourcePattern = this.ctx[`pattern_${varLengthSourceAlias}`];
|
|
3009
|
+
const sourceFilterParts = [];
|
|
3010
|
+
const sourceFilterParams = [];
|
|
3011
|
+
if (sourcePattern?.label) {
|
|
3012
|
+
const labelMatch = this.generateLabelMatchCondition("src_n", sourcePattern.label);
|
|
3013
|
+
sourceFilterParts.push(labelMatch.sql);
|
|
3014
|
+
sourceFilterParams.push(...labelMatch.params);
|
|
3015
|
+
}
|
|
3016
|
+
if (sourcePattern?.properties) {
|
|
3017
|
+
for (const [key, value] of Object.entries(sourcePattern.properties)) {
|
|
3018
|
+
if (this.isParameterRef(value)) {
|
|
3019
|
+
sourceFilterParts.push(`json_extract(src_n.properties, '$.${key}') = ?`);
|
|
3020
|
+
sourceFilterParams.push(this.ctx.paramValues[value.name]);
|
|
3021
|
+
}
|
|
3022
|
+
else {
|
|
3023
|
+
sourceFilterParts.push(`json_extract(src_n.properties, '$.${key}') = ?`);
|
|
3024
|
+
sourceFilterParams.push(value);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
// Build the source filter subquery if there are any constraints
|
|
3029
|
+
// This filters source_id IN (SELECT id FROM nodes WHERE <constraints>)
|
|
3030
|
+
const hasSourceFilter = sourceFilterParts.length > 0;
|
|
3031
|
+
const sourceFilterSubquery = hasSourceFilter
|
|
3032
|
+
? ` AND source_id IN (SELECT src_n.id FROM nodes src_n WHERE ${sourceFilterParts.join(" AND ")})`
|
|
3033
|
+
: "";
|
|
3034
|
+
// For undirected, we also need to filter target_id for reverse direction
|
|
3035
|
+
const sourceFilterSubqueryReverse = hasSourceFilter
|
|
3036
|
+
? ` AND target_id IN (SELECT src_n.id FROM nodes src_n WHERE ${sourceFilterParts.join(" AND ")})`
|
|
3037
|
+
: "";
|
|
2999
3038
|
// Check if a path expression already allocated a CTE name for this variable-length pattern
|
|
3000
3039
|
// This allows length(p) to reference the correct CTE
|
|
3001
3040
|
let pathCteName;
|
|
@@ -3047,44 +3086,52 @@ export class Translator {
|
|
|
3047
3086
|
}
|
|
3048
3087
|
else if (minHops === 0) {
|
|
3049
3088
|
// Need to include zero-length paths (source = target) plus longer paths
|
|
3089
|
+
// Build source filter for the base case (filters which nodes we start from)
|
|
3090
|
+
const minHops0SourceFilter = hasSourceFilter
|
|
3091
|
+
? ` WHERE ${sourceFilterParts.join(" AND ").replace(/src_n\./g, "")}`
|
|
3092
|
+
: "";
|
|
3050
3093
|
if (isUndirected) {
|
|
3051
3094
|
// For undirected with minHops=0, traverse edges in both directions with edge tracking
|
|
3052
3095
|
if (edgeType) {
|
|
3053
|
-
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids) AS (
|
|
3054
|
-
SELECT id, id, 0, json_array() FROM nodes
|
|
3096
|
+
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3097
|
+
SELECT id, id, 0, json_array(), ROW_NUMBER() OVER () FROM nodes${minHops0SourceFilter}
|
|
3055
3098
|
UNION ALL
|
|
3056
|
-
SELECT p.start_id, CASE WHEN p.end_id = e.source_id THEN e.target_id ELSE e.source_id END, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties)))
|
|
3099
|
+
SELECT p.start_id, CASE WHEN p.end_id = e.source_id THEN e.target_id ELSE e.source_id END, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties))), p.row_num + 1
|
|
3057
3100
|
FROM ${pathCteName} p
|
|
3058
3101
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_id)
|
|
3059
|
-
WHERE p.depth < ? AND e.type = ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id)
|
|
3102
|
+
WHERE p.depth < ? AND e.type = ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id) AND p.row_num < ?
|
|
3060
3103
|
)`;
|
|
3061
|
-
allParams.push(
|
|
3104
|
+
allParams.push(...sourceFilterParams); // for base case
|
|
3105
|
+
allParams.push(maxHops, edgeType, earlyTerminationLimit);
|
|
3062
3106
|
}
|
|
3063
3107
|
else {
|
|
3064
|
-
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids) AS (
|
|
3065
|
-
SELECT id, id, 0, json_array() FROM nodes
|
|
3108
|
+
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3109
|
+
SELECT id, id, 0, json_array(), ROW_NUMBER() OVER () FROM nodes${minHops0SourceFilter}
|
|
3066
3110
|
UNION ALL
|
|
3067
|
-
SELECT p.start_id, CASE WHEN p.end_id = e.source_id THEN e.target_id ELSE e.source_id END, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties)))
|
|
3111
|
+
SELECT p.start_id, CASE WHEN p.end_id = e.source_id THEN e.target_id ELSE e.source_id END, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties))), p.row_num + 1
|
|
3068
3112
|
FROM ${pathCteName} p
|
|
3069
3113
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_id)
|
|
3070
|
-
WHERE p.depth < ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id)
|
|
3114
|
+
WHERE p.depth < ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id) AND p.row_num < ?
|
|
3071
3115
|
)`;
|
|
3072
|
-
allParams.push(
|
|
3116
|
+
allParams.push(...sourceFilterParams); // for base case
|
|
3117
|
+
allParams.push(maxHops, earlyTerminationLimit);
|
|
3073
3118
|
}
|
|
3074
3119
|
}
|
|
3075
3120
|
else {
|
|
3076
|
-
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids) AS (
|
|
3077
|
-
SELECT id, id, 0, json_array() FROM nodes
|
|
3121
|
+
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3122
|
+
SELECT id, id, 0, json_array(), ROW_NUMBER() OVER () FROM nodes${minHops0SourceFilter}
|
|
3078
3123
|
UNION ALL
|
|
3079
|
-
SELECT p.start_id, e.target_id, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties)))
|
|
3124
|
+
SELECT p.start_id, e.target_id, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties))), p.row_num + 1
|
|
3080
3125
|
FROM ${pathCteName} p
|
|
3081
3126
|
JOIN edges e ON p.end_id = e.source_id
|
|
3082
|
-
WHERE p.depth < ?${edgeType ? " AND e.type = ?" : ""}
|
|
3127
|
+
WHERE p.depth < ?${edgeType ? " AND e.type = ?" : ""} AND p.row_num < ?
|
|
3083
3128
|
)`;
|
|
3129
|
+
allParams.push(...sourceFilterParams); // for base case
|
|
3084
3130
|
allParams.push(maxHops);
|
|
3085
3131
|
if (edgeType) {
|
|
3086
3132
|
allParams.push(edgeType);
|
|
3087
3133
|
}
|
|
3134
|
+
allParams.push(earlyTerminationLimit);
|
|
3088
3135
|
}
|
|
3089
3136
|
}
|
|
3090
3137
|
else {
|
|
@@ -3100,49 +3147,55 @@ export class Translator {
|
|
|
3100
3147
|
// The base case includes both directions, and recursive step does too
|
|
3101
3148
|
// We need to avoid revisiting the same edge (tracked in edge_ids)
|
|
3102
3149
|
if (edgeType) {
|
|
3103
|
-
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids) AS (
|
|
3104
|
-
SELECT source_id, target_id, 1, json_array(json_object('id', id, 'type', type, 'source_id', source_id, 'target_id', target_id, 'properties', json(properties))) FROM edges WHERE ${edgeCondition}
|
|
3150
|
+
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3151
|
+
SELECT source_id, target_id, 1, json_array(json_object('id', id, 'type', type, 'source_id', source_id, 'target_id', target_id, 'properties', json(properties))), ROW_NUMBER() OVER () FROM edges WHERE ${edgeCondition}${sourceFilterSubquery}
|
|
3105
3152
|
UNION ALL
|
|
3106
|
-
SELECT target_id, source_id, 1, json_array(json_object('id', id, 'type', type, 'source_id', source_id, 'target_id', target_id, 'properties', json(properties))) FROM edges WHERE type =
|
|
3153
|
+
SELECT target_id, source_id, 1, json_array(json_object('id', id, 'type', type, 'source_id', source_id, 'target_id', target_id, 'properties', json(properties))), ROW_NUMBER() OVER () FROM edges WHERE type = ?${sourceFilterSubqueryReverse}
|
|
3107
3154
|
UNION ALL
|
|
3108
|
-
SELECT p.start_id, CASE WHEN p.end_id = e.source_id THEN e.target_id ELSE e.source_id END, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties)))
|
|
3155
|
+
SELECT p.start_id, CASE WHEN p.end_id = e.source_id THEN e.target_id ELSE e.source_id END, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties))), p.row_num + 1
|
|
3109
3156
|
FROM ${pathCteName} p
|
|
3110
3157
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_id)
|
|
3111
|
-
WHERE p.depth < ? AND e.type = ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id)
|
|
3158
|
+
WHERE p.depth < ? AND e.type = ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id) AND p.row_num < ?
|
|
3112
3159
|
)`;
|
|
3160
|
+
allParams.push(...sourceFilterParams); // for forward base case
|
|
3113
3161
|
allParams.push(edgeType); // for reverse base case
|
|
3114
|
-
allParams.push(
|
|
3162
|
+
allParams.push(...sourceFilterParams); // for reverse base case
|
|
3163
|
+
allParams.push(maxHops, edgeType, earlyTerminationLimit); // for recursive
|
|
3115
3164
|
}
|
|
3116
3165
|
else {
|
|
3117
|
-
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids) AS (
|
|
3118
|
-
SELECT source_id, target_id, 1, json_array(json_object('id', id, 'type', type, 'source_id', source_id, 'target_id', target_id, 'properties', json(properties))) FROM edges
|
|
3166
|
+
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3167
|
+
SELECT source_id, target_id, 1, json_array(json_object('id', id, 'type', type, 'source_id', source_id, 'target_id', target_id, 'properties', json(properties))), ROW_NUMBER() OVER () FROM edges WHERE 1=1${sourceFilterSubquery}
|
|
3119
3168
|
UNION ALL
|
|
3120
|
-
SELECT target_id, source_id, 1, json_array(json_object('id', id, 'type', type, 'source_id', source_id, 'target_id', target_id, 'properties', json(properties))) FROM edges
|
|
3169
|
+
SELECT target_id, source_id, 1, json_array(json_object('id', id, 'type', type, 'source_id', source_id, 'target_id', target_id, 'properties', json(properties))), ROW_NUMBER() OVER () FROM edges WHERE 1=1${sourceFilterSubqueryReverse}
|
|
3121
3170
|
UNION ALL
|
|
3122
|
-
SELECT p.start_id, CASE WHEN p.end_id = e.source_id THEN e.target_id ELSE e.source_id END, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties)))
|
|
3171
|
+
SELECT p.start_id, CASE WHEN p.end_id = e.source_id THEN e.target_id ELSE e.source_id END, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties))), p.row_num + 1
|
|
3123
3172
|
FROM ${pathCteName} p
|
|
3124
3173
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_id)
|
|
3125
|
-
WHERE p.depth < ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id)
|
|
3174
|
+
WHERE p.depth < ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id) AND p.row_num < ?
|
|
3126
3175
|
)`;
|
|
3127
|
-
allParams.push(
|
|
3176
|
+
allParams.push(...sourceFilterParams); // for forward base case
|
|
3177
|
+
allParams.push(...sourceFilterParams); // for reverse base case
|
|
3178
|
+
allParams.push(maxHops, earlyTerminationLimit); // for recursive
|
|
3128
3179
|
}
|
|
3129
3180
|
}
|
|
3130
3181
|
else {
|
|
3131
|
-
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids) AS (
|
|
3132
|
-
SELECT source_id, target_id, 1, json_array(json_object('id', id, 'type', type, 'source_id', source_id, 'target_id', target_id, 'properties', json(properties))) FROM edges WHERE ${edgeCondition}${basePropCondition}
|
|
3182
|
+
cte = `WITH RECURSIVE ${pathCteName}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3183
|
+
SELECT source_id, target_id, 1, json_array(json_object('id', id, 'type', type, 'source_id', source_id, 'target_id', target_id, 'properties', json(properties))), ROW_NUMBER() OVER () FROM edges WHERE ${edgeCondition}${basePropCondition}${sourceFilterSubquery}
|
|
3133
3184
|
UNION ALL
|
|
3134
|
-
SELECT p.start_id, e.target_id, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties)))
|
|
3185
|
+
SELECT p.start_id, e.target_id, p.depth + 1, json_insert(p.edge_ids, '$[#]', json_object('id', e.id, 'type', e.type, 'source_id', e.source_id, 'target_id', e.target_id, 'properties', json(e.properties))), p.row_num + 1
|
|
3135
3186
|
FROM ${pathCteName} p
|
|
3136
3187
|
JOIN edges e ON p.end_id = e.source_id
|
|
3137
|
-
WHERE p.depth < ?${edgeType ? " AND e.type = ?" : ""}${recursivePropCondition} AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id)
|
|
3188
|
+
WHERE p.depth < ?${edgeType ? " AND e.type = ?" : ""}${recursivePropCondition} AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id) AND p.row_num < ?
|
|
3138
3189
|
)`;
|
|
3139
3190
|
// For maxHops=2, we need depth to reach 2, so recursion limit should be maxHops
|
|
3140
3191
|
allParams.push(...edgePropParams); // for base case
|
|
3192
|
+
allParams.push(...sourceFilterParams); // for source filter subquery
|
|
3141
3193
|
allParams.push(maxHops);
|
|
3142
3194
|
if (edgeType) {
|
|
3143
3195
|
allParams.push(edgeType);
|
|
3144
3196
|
}
|
|
3145
3197
|
allParams.push(...edgePropParams); // for recursive case
|
|
3198
|
+
allParams.push(earlyTerminationLimit);
|
|
3146
3199
|
}
|
|
3147
3200
|
}
|
|
3148
3201
|
// Build FROM and JOIN clauses for fixed-length patterns before the variable-length
|