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.
Files changed (49) hide show
  1. package/README.md +198 -118
  2. package/dist/auth.d.ts +1 -4
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +3 -13
  5. package/dist/auth.js.map +1 -1
  6. package/dist/backup.d.ts +1 -3
  7. package/dist/backup.d.ts.map +1 -1
  8. package/dist/backup.js +10 -15
  9. package/dist/backup.js.map +1 -1
  10. package/dist/cli-helpers.d.ts +2 -3
  11. package/dist/cli-helpers.d.ts.map +1 -1
  12. package/dist/cli-helpers.js +11 -30
  13. package/dist/cli-helpers.js.map +1 -1
  14. package/dist/cli.js +82 -129
  15. package/dist/cli.js.map +1 -1
  16. package/dist/db.d.ts +8 -2
  17. package/dist/db.d.ts.map +1 -1
  18. package/dist/db.js +34 -8
  19. package/dist/db.js.map +1 -1
  20. package/dist/executor.d.ts +6 -0
  21. package/dist/executor.d.ts.map +1 -1
  22. package/dist/executor.js +92 -22
  23. package/dist/executor.js.map +1 -1
  24. package/dist/index.d.ts +8 -34
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +20 -37
  27. package/dist/index.js.map +1 -1
  28. package/dist/local.d.ts +3 -3
  29. package/dist/local.d.ts.map +1 -1
  30. package/dist/local.js +13 -15
  31. package/dist/local.js.map +1 -1
  32. package/dist/remote.d.ts +3 -3
  33. package/dist/remote.d.ts.map +1 -1
  34. package/dist/remote.js +8 -10
  35. package/dist/remote.js.map +1 -1
  36. package/dist/routes.d.ts +0 -1
  37. package/dist/routes.d.ts.map +1 -1
  38. package/dist/routes.js +12 -39
  39. package/dist/routes.js.map +1 -1
  40. package/dist/server.js +1 -1
  41. package/dist/server.js.map +1 -1
  42. package/dist/translator.d.ts.map +1 -1
  43. package/dist/translator.js +85 -32
  44. package/dist/translator.js.map +1 -1
  45. package/dist/types.d.ts +24 -26
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/types.js +2 -2
  48. package/dist/types.js.map +1 -1
  49. package/package.json +16 -3
@@ -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
- return this.translateVariableLengthPath(clause, relPatterns, selectParts, returnColumns, exprParams, whereParams);
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(maxHops, edgeType);
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(maxHops);
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(maxHops, edgeType); // for recursive
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(maxHops); // for recursive
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