leangraph 1.0.1 → 1.1.1
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 -111
- package/dist/auth.d.ts +1 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +5 -15
- 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 +9 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +82 -9
- package/dist/db.js.map +1 -1
- package/dist/executor.d.ts +114 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +1248 -341
- 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 +12 -38
- 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/parser.d.ts +15 -3
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +231 -42
- package/dist/parser.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 +15 -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 +36 -0
- package/dist/translator.d.ts.map +1 -1
- package/dist/translator.js +634 -136
- 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 +8 -2
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
|
|
@@ -3488,51 +3541,51 @@ export class Translator {
|
|
|
3488
3541
|
if (isUndirected2) {
|
|
3489
3542
|
// Undirected: traverse in both directions
|
|
3490
3543
|
if (edgeType2) {
|
|
3491
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3492
|
-
SELECT id, id, 0, json_array() FROM nodes
|
|
3544
|
+
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3545
|
+
SELECT id, id, 0, json_array(), ROW_NUMBER() OVER () FROM nodes
|
|
3493
3546
|
UNION ALL
|
|
3494
|
-
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)))
|
|
3547
|
+
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
|
|
3495
3548
|
FROM ${pathCteName2} p
|
|
3496
3549
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_id)
|
|
3497
|
-
WHERE p.depth < ? AND e.type = ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id)
|
|
3550
|
+
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 < ?
|
|
3498
3551
|
)`;
|
|
3499
|
-
allParams.push(maxHops2, edgeType2);
|
|
3552
|
+
allParams.push(maxHops2, edgeType2, earlyTerminationLimit);
|
|
3500
3553
|
}
|
|
3501
3554
|
else {
|
|
3502
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3503
|
-
SELECT id, id, 0, json_array() FROM nodes
|
|
3555
|
+
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3556
|
+
SELECT id, id, 0, json_array(), ROW_NUMBER() OVER () FROM nodes
|
|
3504
3557
|
UNION ALL
|
|
3505
|
-
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)))
|
|
3558
|
+
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
|
|
3506
3559
|
FROM ${pathCteName2} p
|
|
3507
3560
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_id)
|
|
3508
|
-
WHERE p.depth < ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id)
|
|
3561
|
+
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 < ?
|
|
3509
3562
|
)`;
|
|
3510
|
-
allParams.push(maxHops2);
|
|
3563
|
+
allParams.push(maxHops2, earlyTerminationLimit);
|
|
3511
3564
|
}
|
|
3512
3565
|
}
|
|
3513
3566
|
else {
|
|
3514
3567
|
// Directed
|
|
3515
3568
|
if (edgeType2) {
|
|
3516
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3517
|
-
SELECT id, id, 0, json_array() FROM nodes
|
|
3569
|
+
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3570
|
+
SELECT id, id, 0, json_array(), ROW_NUMBER() OVER () FROM nodes
|
|
3518
3571
|
UNION ALL
|
|
3519
|
-
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)))
|
|
3572
|
+
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
|
|
3520
3573
|
FROM ${pathCteName2} p
|
|
3521
3574
|
JOIN edges e ON p.end_id = e.source_id
|
|
3522
|
-
WHERE p.depth < ? AND e.type = ?
|
|
3575
|
+
WHERE p.depth < ? AND e.type = ? AND p.row_num < ?
|
|
3523
3576
|
)`;
|
|
3524
|
-
allParams.push(maxHops2, edgeType2);
|
|
3577
|
+
allParams.push(maxHops2, edgeType2, earlyTerminationLimit);
|
|
3525
3578
|
}
|
|
3526
3579
|
else {
|
|
3527
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3528
|
-
SELECT id, id, 0, json_array() FROM nodes
|
|
3580
|
+
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3581
|
+
SELECT id, id, 0, json_array(), ROW_NUMBER() OVER () FROM nodes
|
|
3529
3582
|
UNION ALL
|
|
3530
|
-
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)))
|
|
3583
|
+
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
|
|
3531
3584
|
FROM ${pathCteName2} p
|
|
3532
3585
|
JOIN edges e ON p.end_id = e.source_id
|
|
3533
|
-
WHERE p.depth < ?
|
|
3586
|
+
WHERE p.depth < ? AND p.row_num < ?
|
|
3534
3587
|
)`;
|
|
3535
|
-
allParams.push(maxHops2);
|
|
3588
|
+
allParams.push(maxHops2, earlyTerminationLimit);
|
|
3536
3589
|
}
|
|
3537
3590
|
}
|
|
3538
3591
|
}
|
|
@@ -3540,55 +3593,55 @@ export class Translator {
|
|
|
3540
3593
|
if (isUndirected2) {
|
|
3541
3594
|
// Undirected: traverse in both directions
|
|
3542
3595
|
if (edgeType2) {
|
|
3543
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3544
|
-
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 type = ?
|
|
3596
|
+
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3597
|
+
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 type = ?
|
|
3545
3598
|
UNION ALL
|
|
3546
|
-
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 = ?
|
|
3599
|
+
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 = ?
|
|
3547
3600
|
UNION ALL
|
|
3548
|
-
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)))
|
|
3601
|
+
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
|
|
3549
3602
|
FROM ${pathCteName2} p
|
|
3550
3603
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_id)
|
|
3551
|
-
WHERE p.depth < ? AND e.type = ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id)
|
|
3604
|
+
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 < ?
|
|
3552
3605
|
)`;
|
|
3553
|
-
allParams.push(edgeType2, edgeType2, maxHops2, edgeType2);
|
|
3606
|
+
allParams.push(edgeType2, edgeType2, maxHops2, edgeType2, earlyTerminationLimit);
|
|
3554
3607
|
}
|
|
3555
3608
|
else {
|
|
3556
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3557
|
-
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
|
|
3609
|
+
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3610
|
+
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
|
|
3558
3611
|
UNION ALL
|
|
3559
|
-
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
|
|
3612
|
+
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
|
|
3560
3613
|
UNION ALL
|
|
3561
|
-
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)))
|
|
3614
|
+
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
|
|
3562
3615
|
FROM ${pathCteName2} p
|
|
3563
3616
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_id)
|
|
3564
|
-
WHERE p.depth < ? AND NOT EXISTS (SELECT 1 FROM json_each(p.edge_ids) WHERE json_extract(value, '$.id') = e.id)
|
|
3617
|
+
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 < ?
|
|
3565
3618
|
)`;
|
|
3566
|
-
allParams.push(maxHops2);
|
|
3619
|
+
allParams.push(maxHops2, earlyTerminationLimit);
|
|
3567
3620
|
}
|
|
3568
3621
|
}
|
|
3569
3622
|
else {
|
|
3570
3623
|
// Directed
|
|
3571
3624
|
if (edgeType2) {
|
|
3572
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3573
|
-
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 type = ?
|
|
3625
|
+
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3626
|
+
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 type = ?
|
|
3574
3627
|
UNION ALL
|
|
3575
|
-
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)))
|
|
3628
|
+
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
|
|
3576
3629
|
FROM ${pathCteName2} p
|
|
3577
3630
|
JOIN edges e ON p.end_id = e.source_id
|
|
3578
|
-
WHERE p.depth < ? AND e.type = ?
|
|
3631
|
+
WHERE p.depth < ? AND e.type = ? AND p.row_num < ?
|
|
3579
3632
|
)`;
|
|
3580
|
-
allParams.push(edgeType2, maxHops2, edgeType2);
|
|
3633
|
+
allParams.push(edgeType2, maxHops2, edgeType2, earlyTerminationLimit);
|
|
3581
3634
|
}
|
|
3582
3635
|
else {
|
|
3583
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3584
|
-
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
|
|
3636
|
+
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids, row_num) AS (
|
|
3637
|
+
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
|
|
3585
3638
|
UNION ALL
|
|
3586
|
-
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)))
|
|
3639
|
+
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
|
|
3587
3640
|
FROM ${pathCteName2} p
|
|
3588
3641
|
JOIN edges e ON p.end_id = e.source_id
|
|
3589
|
-
WHERE p.depth < ?
|
|
3642
|
+
WHERE p.depth < ? AND p.row_num < ?
|
|
3590
3643
|
)`;
|
|
3591
|
-
allParams.push(maxHops2);
|
|
3644
|
+
allParams.push(maxHops2, earlyTerminationLimit);
|
|
3592
3645
|
}
|
|
3593
3646
|
}
|
|
3594
3647
|
}
|
|
@@ -4026,7 +4079,7 @@ export class Translator {
|
|
|
4026
4079
|
return { sql: "?", params: [expr.value] };
|
|
4027
4080
|
}
|
|
4028
4081
|
if (typeof expr.value === "boolean") {
|
|
4029
|
-
return { sql: expr.value ? "
|
|
4082
|
+
return { sql: expr.value ? "json('true')" : "json('false')", params };
|
|
4030
4083
|
}
|
|
4031
4084
|
if (expr.value === null) {
|
|
4032
4085
|
return { sql: "NULL", params };
|
|
@@ -5356,6 +5409,16 @@ END FROM (SELECT json_group_array(${valueExpr}) as sv))`,
|
|
|
5356
5409
|
}
|
|
5357
5410
|
throw new Error("sqrt requires an argument");
|
|
5358
5411
|
}
|
|
5412
|
+
// SIGN: returns -1, 0, or 1 based on the sign of the number
|
|
5413
|
+
if (expr.functionName === "SIGN") {
|
|
5414
|
+
if (expr.args && expr.args.length > 0) {
|
|
5415
|
+
const argResult = this.translateFunctionArg(expr.args[0]);
|
|
5416
|
+
tables.push(...argResult.tables);
|
|
5417
|
+
params.push(...argResult.params);
|
|
5418
|
+
return { sql: `SIGN(${argResult.sql})`, tables, params };
|
|
5419
|
+
}
|
|
5420
|
+
throw new Error("sign requires an argument");
|
|
5421
|
+
}
|
|
5359
5422
|
// RAND: random float between 0 and 1
|
|
5360
5423
|
if (expr.functionName === "RAND") {
|
|
5361
5424
|
// SQLite's RANDOM() returns integer between -9223372036854775808 and 9223372036854775807
|
|
@@ -7805,7 +7868,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
7805
7868
|
// Strip JSON quotes if input is wrapped in "" (from toString() output)
|
|
7806
7869
|
const argResult = this.translateExpression(arg);
|
|
7807
7870
|
tables.push(...argResult.tables);
|
|
7808
|
-
params.push(...argResult.params
|
|
7871
|
+
params.push(...argResult.params);
|
|
7809
7872
|
return {
|
|
7810
7873
|
sql: `(SELECT CASE WHEN _d IS NULL THEN NULL ELSE CASE
|
|
7811
7874
|
WHEN substr(_d, 1, 1) = '"' AND substr(_d, length(_d), 1) = '"'
|
|
@@ -8134,16 +8197,25 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8134
8197
|
const listResult = this.translateExpression(listArg);
|
|
8135
8198
|
const indexResult = this.translateExpression(indexArg);
|
|
8136
8199
|
tables.push(...listResult.tables, ...indexResult.tables);
|
|
8137
|
-
params.push(...listResult.params, ...indexResult.params);
|
|
8138
8200
|
// For map access with string key, use json_extract with the key
|
|
8139
8201
|
// For list access with integer index, use json_extract with array index
|
|
8140
8202
|
if (isContainerMap || isStringExpression(resolvedIndexArg)) {
|
|
8141
8203
|
// Map access: use key directly
|
|
8204
|
+
params.push(...listResult.params, ...indexResult.params);
|
|
8142
8205
|
return { sql: `json_extract(${listResult.sql}, '$.' || ${indexResult.sql})`, tables, params };
|
|
8143
8206
|
}
|
|
8144
8207
|
// Use -> operator with array index to preserve JSON types (booleans, etc.)
|
|
8145
8208
|
// Cast index to integer to avoid "0.0" in JSON path
|
|
8146
|
-
|
|
8209
|
+
// Handle negative indices by converting to positive using json_array_length
|
|
8210
|
+
// Cypher: list[-1] gets last element, list[-2] gets second to last, etc.
|
|
8211
|
+
const idxCast = `CAST(${indexResult.sql} AS INTEGER)`;
|
|
8212
|
+
// SQL expression uses: list (for ->), idx (for CASE condition), list (for json_array_length), idx (for + in THEN), idx (for ELSE)
|
|
8213
|
+
params.push(...listResult.params, ...indexResult.params, ...listResult.params, ...indexResult.params, ...indexResult.params);
|
|
8214
|
+
return {
|
|
8215
|
+
sql: `(${listResult.sql}) -> ('$[' || (CASE WHEN ${idxCast} < 0 THEN json_array_length(${listResult.sql}) + ${idxCast} ELSE ${idxCast} END) || ']')`,
|
|
8216
|
+
tables,
|
|
8217
|
+
params
|
|
8218
|
+
};
|
|
8147
8219
|
}
|
|
8148
8220
|
throw new Error("INDEX requires list and index arguments");
|
|
8149
8221
|
}
|
|
@@ -8282,8 +8354,14 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8282
8354
|
if (Array.isArray(expr.value)) {
|
|
8283
8355
|
return this.translateArrayLiteral(expr.value);
|
|
8284
8356
|
}
|
|
8285
|
-
//
|
|
8286
|
-
|
|
8357
|
+
// Return JSON booleans to preserve boolean type in results
|
|
8358
|
+
if (expr.value === true) {
|
|
8359
|
+
return { sql: "json('true')", tables, params };
|
|
8360
|
+
}
|
|
8361
|
+
if (expr.value === false) {
|
|
8362
|
+
return { sql: "json('false')", tables, params };
|
|
8363
|
+
}
|
|
8364
|
+
const value = expr.value;
|
|
8287
8365
|
// Preserve float-literal formatting (e.g., 0.0, -0.0, 1.0) so SQLite treats them as REAL.
|
|
8288
8366
|
if (typeof value === "number" && expr.numberLiteralKind === "float" && expr.raw) {
|
|
8289
8367
|
return { sql: expr.raw, tables, params };
|
|
@@ -8324,8 +8402,29 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8324
8402
|
case "patternComprehension": {
|
|
8325
8403
|
return this.translatePatternComprehension(expr);
|
|
8326
8404
|
}
|
|
8405
|
+
case "existsPattern": {
|
|
8406
|
+
return this.translateExistsPattern(expr);
|
|
8407
|
+
}
|
|
8327
8408
|
case "listPredicate": {
|
|
8328
|
-
|
|
8409
|
+
// Wrap list predicate result with cypher_to_json_bool for proper boolean output in RETURN
|
|
8410
|
+
// translateListPredicate returns 0/1 for SQLite WHERE compatibility
|
|
8411
|
+
const result = this.translateListPredicate(expr);
|
|
8412
|
+
return {
|
|
8413
|
+
sql: `cypher_to_json_bool(${result.sql})`,
|
|
8414
|
+
tables: result.tables,
|
|
8415
|
+
params: result.params,
|
|
8416
|
+
};
|
|
8417
|
+
}
|
|
8418
|
+
case "reduce": {
|
|
8419
|
+
return this.translateReduceExpression(expr);
|
|
8420
|
+
}
|
|
8421
|
+
case "filter": {
|
|
8422
|
+
// filter(x IN list WHERE cond) - same as list comprehension without map expression
|
|
8423
|
+
return this.translateFilterExpression(expr);
|
|
8424
|
+
}
|
|
8425
|
+
case "extract": {
|
|
8426
|
+
// extract(x IN list | expr) - same as list comprehension without filter
|
|
8427
|
+
return this.translateExtractExpression(expr);
|
|
8329
8428
|
}
|
|
8330
8429
|
case "unary": {
|
|
8331
8430
|
return this.translateUnaryExpression(expr);
|
|
@@ -8419,7 +8518,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8419
8518
|
if (listExpr.type === "literal" && Array.isArray(listExpr.value)) {
|
|
8420
8519
|
const values = listExpr.value;
|
|
8421
8520
|
if (values.length === 0) {
|
|
8422
|
-
return { sql: "
|
|
8521
|
+
return { sql: "json('false')", tables, params }; // false for empty list
|
|
8423
8522
|
}
|
|
8424
8523
|
// Check if RHS contains complex types (nested arrays/objects)
|
|
8425
8524
|
const rhsHasComplexTypes = values.some(containsComplexTypes);
|
|
@@ -8450,7 +8549,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8450
8549
|
// If LHS contains null, finding a JSON match means comparing null=null → return NULL
|
|
8451
8550
|
params.push(rhsJson, lhsJson);
|
|
8452
8551
|
return {
|
|
8453
|
-
sql: `CASE WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(?)) THEN NULL ELSE
|
|
8552
|
+
sql: `CASE WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(?)) THEN NULL ELSE json('false') END`,
|
|
8454
8553
|
tables,
|
|
8455
8554
|
params,
|
|
8456
8555
|
};
|
|
@@ -8459,7 +8558,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8459
8558
|
// If RHS has top-level null and no match, return NULL
|
|
8460
8559
|
params.push(rhsJson, lhsJson);
|
|
8461
8560
|
return {
|
|
8462
|
-
sql: `CASE WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(?)) THEN
|
|
8561
|
+
sql: `CASE WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(?)) THEN json('true') ELSE NULL END`,
|
|
8463
8562
|
tables,
|
|
8464
8563
|
params,
|
|
8465
8564
|
};
|
|
@@ -8475,7 +8574,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8475
8574
|
params.push(lhsJson, rhsJson);
|
|
8476
8575
|
return {
|
|
8477
8576
|
sql: `(SELECT CASE
|
|
8478
|
-
WHEN EXISTS(SELECT 1 FROM json_each(rhs_param.v) WHERE json(value) = json(lhs_param.v)) THEN
|
|
8577
|
+
WHEN EXISTS(SELECT 1 FROM json_each(rhs_param.v) WHERE json(value) = json(lhs_param.v)) THEN json('true')
|
|
8479
8578
|
WHEN EXISTS(
|
|
8480
8579
|
SELECT 1 FROM (
|
|
8481
8580
|
SELECT rhs.rowid,
|
|
@@ -8489,7 +8588,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8489
8588
|
GROUP BY rhs.rowid
|
|
8490
8589
|
) WHERE mismatches = 0 AND nulls > 0
|
|
8491
8590
|
) THEN NULL
|
|
8492
|
-
ELSE
|
|
8591
|
+
ELSE json('false')
|
|
8493
8592
|
END FROM (SELECT ? AS v) AS lhs_param, (SELECT ? AS v) AS rhs_param)`,
|
|
8494
8593
|
tables,
|
|
8495
8594
|
params,
|
|
@@ -8498,7 +8597,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8498
8597
|
// No null semantics needed - simple JSON comparison
|
|
8499
8598
|
params.push(rhsJson, lhsJson);
|
|
8500
8599
|
return {
|
|
8501
|
-
sql: `EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(?))`,
|
|
8600
|
+
sql: `cypher_to_json_bool(EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(?)))`,
|
|
8502
8601
|
tables,
|
|
8503
8602
|
params,
|
|
8504
8603
|
};
|
|
@@ -8511,47 +8610,63 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8511
8610
|
// So rhsJson comes first (for json_each), then leftResult.params (for leftResult.sql)
|
|
8512
8611
|
params.push(rhsJson);
|
|
8513
8612
|
params.push(...leftResult.params);
|
|
8514
|
-
// When LHS is a scalar expression (like comparison result),
|
|
8515
|
-
//
|
|
8516
|
-
// Use direct value comparison which handles int/real equality correctly
|
|
8613
|
+
// When LHS is a scalar expression (like comparison result), use cypher_bool_eq
|
|
8614
|
+
// to handle type mismatches between JSON boolean strings ('true'/'false') and integers (1/0)
|
|
8517
8615
|
const useDirectComparison = !leftIsComplex;
|
|
8518
8616
|
if (rhsHasTopLevelNull) {
|
|
8519
8617
|
if (useDirectComparison) {
|
|
8618
|
+
// Check for exact match first, then check for null comparison
|
|
8520
8619
|
return {
|
|
8521
|
-
sql: `CASE WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE value = ${leftResult.sql}) THEN
|
|
8620
|
+
sql: `CASE WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE cypher_bool_eq(value, ${leftResult.sql}) = 1) THEN json('true') WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE cypher_bool_eq(value, ${leftResult.sql}) IS NULL) THEN NULL ELSE json('false') END`,
|
|
8522
8621
|
tables,
|
|
8523
|
-
params,
|
|
8622
|
+
params: [...params, ...params], // Duplicate for two json_each usages
|
|
8524
8623
|
};
|
|
8525
8624
|
}
|
|
8526
8625
|
return {
|
|
8527
|
-
sql: `CASE WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(${leftResult.sql})) THEN
|
|
8626
|
+
sql: `CASE WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(${leftResult.sql})) THEN json('true') ELSE NULL END`,
|
|
8528
8627
|
tables,
|
|
8529
8628
|
params,
|
|
8530
8629
|
};
|
|
8531
8630
|
}
|
|
8532
8631
|
if (useDirectComparison) {
|
|
8533
8632
|
return {
|
|
8534
|
-
sql: `EXISTS(SELECT 1 FROM json_each(?) WHERE value
|
|
8633
|
+
sql: `cypher_to_json_bool(EXISTS(SELECT 1 FROM json_each(?) WHERE cypher_bool_eq(value, ${leftResult.sql})))`,
|
|
8535
8634
|
tables,
|
|
8536
8635
|
params,
|
|
8537
8636
|
};
|
|
8538
8637
|
}
|
|
8539
8638
|
return {
|
|
8540
|
-
sql: `EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(${leftResult.sql}))`,
|
|
8639
|
+
sql: `cypher_to_json_bool(EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(${leftResult.sql})))`,
|
|
8541
8640
|
tables,
|
|
8542
8641
|
params,
|
|
8543
8642
|
};
|
|
8544
8643
|
}
|
|
8545
|
-
// Simple scalar values -
|
|
8644
|
+
// Simple scalar values - use json_each with cypher_bool_eq for type-safe comparison
|
|
8645
|
+
// This handles cases where LHS is json('true') (string) and RHS values are integers (1, 0)
|
|
8546
8646
|
const leftResult = this.translateExpression(leftExpr);
|
|
8547
8647
|
tables.push(...leftResult.tables);
|
|
8548
|
-
|
|
8549
|
-
const placeholders = values.map(() => "?").join(", ");
|
|
8550
|
-
params.push(...toSqliteParams(values));
|
|
8648
|
+
const rhsJson = JSON.stringify(values);
|
|
8551
8649
|
// Wrap left side in extra parentheses to ensure correct precedence (e.g., NOT has lower precedence than IN in SQL)
|
|
8552
8650
|
const leftSql = leftExpr.type === "unary" ? `(${leftResult.sql})` : leftResult.sql;
|
|
8651
|
+
// Cypher null semantics: if RHS has top-level null and no exact match is found, return null
|
|
8652
|
+
// e.g., null IN [null] returns null (unknown), not false
|
|
8653
|
+
// e.g., 1 IN [2, null] returns null (unknown) because null could be 1
|
|
8654
|
+
if (hasTopLevelNull(values)) {
|
|
8655
|
+
// IMPORTANT: params order must match SQL placeholder order
|
|
8656
|
+
// SQL: json_each(?) ... ${leftSql} ... json_each(?) ... cypher_bool_eq(${leftSql}, value)
|
|
8657
|
+
params.push(rhsJson, ...leftResult.params, rhsJson, ...leftResult.params);
|
|
8658
|
+
return {
|
|
8659
|
+
sql: `CASE WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE value = ${leftSql}) THEN json('true') WHEN EXISTS(SELECT 1 FROM json_each(?) WHERE cypher_bool_eq(${leftSql}, value) IS NULL) THEN NULL ELSE json('false') END`,
|
|
8660
|
+
tables,
|
|
8661
|
+
params,
|
|
8662
|
+
};
|
|
8663
|
+
}
|
|
8664
|
+
// IMPORTANT: params order must match SQL placeholder order
|
|
8665
|
+
// SQL: json_each(?) ... cypher_bool_eq(${leftSql}, value)
|
|
8666
|
+
// So rhsJson comes first (for json_each), then leftResult.params (for ${leftSql})
|
|
8667
|
+
params.push(rhsJson, ...leftResult.params);
|
|
8553
8668
|
return {
|
|
8554
|
-
sql: `(
|
|
8669
|
+
sql: `cypher_to_json_bool(EXISTS(SELECT 1 FROM json_each(?) WHERE cypher_bool_eq(${leftSql}, value)))`,
|
|
8555
8670
|
tables,
|
|
8556
8671
|
params,
|
|
8557
8672
|
};
|
|
@@ -8560,7 +8675,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8560
8675
|
const paramValue = this.ctx.paramValues[listExpr.name];
|
|
8561
8676
|
if (Array.isArray(paramValue)) {
|
|
8562
8677
|
if (paramValue.length === 0) {
|
|
8563
|
-
return { sql: "
|
|
8678
|
+
return { sql: "json('false')", tables, params }; // false for empty list
|
|
8564
8679
|
}
|
|
8565
8680
|
// Check if RHS contains complex types
|
|
8566
8681
|
const rhsHasComplexTypes = paramValue.some(containsComplexTypes);
|
|
@@ -8573,7 +8688,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8573
8688
|
const lhsJson = JSON.stringify(leftExpr.value);
|
|
8574
8689
|
params.push(rhsJson, lhsJson);
|
|
8575
8690
|
return {
|
|
8576
|
-
sql: `EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(?))`,
|
|
8691
|
+
sql: `cypher_to_json_bool(EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(?)))`,
|
|
8577
8692
|
tables,
|
|
8578
8693
|
params,
|
|
8579
8694
|
};
|
|
@@ -8583,7 +8698,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8583
8698
|
params.push(...leftResult.params);
|
|
8584
8699
|
params.push(rhsJson);
|
|
8585
8700
|
return {
|
|
8586
|
-
sql: `EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(${leftResult.sql}))`,
|
|
8701
|
+
sql: `cypher_to_json_bool(EXISTS(SELECT 1 FROM json_each(?) WHERE json(value) = json(${leftResult.sql})))`,
|
|
8587
8702
|
tables,
|
|
8588
8703
|
params,
|
|
8589
8704
|
};
|
|
@@ -8597,7 +8712,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8597
8712
|
// Wrap left side in extra parentheses to ensure correct precedence (e.g., NOT has lower precedence than IN in SQL)
|
|
8598
8713
|
const leftSql = leftExpr.type === "unary" ? `(${leftResult.sql})` : leftResult.sql;
|
|
8599
8714
|
return {
|
|
8600
|
-
sql: `(${leftSql} IN (${placeholders}))`,
|
|
8715
|
+
sql: `cypher_to_json_bool(${leftSql} IN (${placeholders}))`,
|
|
8601
8716
|
tables,
|
|
8602
8717
|
params,
|
|
8603
8718
|
};
|
|
@@ -8614,7 +8729,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8614
8729
|
params.push(...listResult.params);
|
|
8615
8730
|
params.push(lhsJson);
|
|
8616
8731
|
return {
|
|
8617
|
-
sql: `EXISTS(SELECT 1 FROM json_each(${listResult.sql}) WHERE json(value) = json(?))`,
|
|
8732
|
+
sql: `cypher_to_json_bool(EXISTS(SELECT 1 FROM json_each(${listResult.sql}) WHERE json(value) = json(?)))`,
|
|
8618
8733
|
tables,
|
|
8619
8734
|
params,
|
|
8620
8735
|
};
|
|
@@ -8627,7 +8742,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8627
8742
|
params.push(...listResult.params);
|
|
8628
8743
|
params.push(...leftResult.params);
|
|
8629
8744
|
return {
|
|
8630
|
-
sql: `EXISTS(SELECT 1 FROM json_each(${listResult.sql}) WHERE json(value) = json(${leftResult.sql}))`,
|
|
8745
|
+
sql: `cypher_to_json_bool(EXISTS(SELECT 1 FROM json_each(${listResult.sql}) WHERE json(value) = json(${leftResult.sql})))`,
|
|
8631
8746
|
tables,
|
|
8632
8747
|
params,
|
|
8633
8748
|
};
|
|
@@ -8641,7 +8756,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8641
8756
|
// Wrap left side in extra parentheses to ensure correct precedence (e.g., NOT has lower precedence than IN in SQL)
|
|
8642
8757
|
const leftSql = leftExpr.type === "unary" ? `(${leftResult.sql})` : leftResult.sql;
|
|
8643
8758
|
return {
|
|
8644
|
-
sql: `(${leftSql} IN (SELECT value FROM json_each(${listResult.sql})))`,
|
|
8759
|
+
sql: `cypher_to_json_bool(${leftSql} IN (SELECT value FROM json_each(${listResult.sql})))`,
|
|
8645
8760
|
tables,
|
|
8646
8761
|
params,
|
|
8647
8762
|
};
|
|
@@ -8664,7 +8779,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8664
8779
|
if (stringOp === "CONTAINS") {
|
|
8665
8780
|
// INSTR returns position (1-based) if found, 0 if not found
|
|
8666
8781
|
return {
|
|
8667
|
-
sql: `CASE WHEN ${isString(leftResult.sql)} AND ${isString(rightResult.sql)} THEN INSTR(${leftResult.sql}, ${rightResult.sql}) > 0 ELSE NULL END`,
|
|
8782
|
+
sql: `CASE WHEN ${isString(leftResult.sql)} AND ${isString(rightResult.sql)} THEN cypher_to_json_bool(INSTR(${leftResult.sql}, ${rightResult.sql}) > 0) ELSE NULL END`,
|
|
8668
8783
|
tables,
|
|
8669
8784
|
// leftResult.sql appears 3 times, rightResult.sql appears 3 times
|
|
8670
8785
|
params: [...leftResult.params, ...leftResult.params, ...rightResult.params, ...rightResult.params, ...leftResult.params, ...rightResult.params],
|
|
@@ -8673,7 +8788,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8673
8788
|
else if (stringOp === "STARTS WITH") {
|
|
8674
8789
|
// Use SUBSTR for case-sensitive prefix match
|
|
8675
8790
|
return {
|
|
8676
|
-
sql: `CASE WHEN ${isString(leftResult.sql)} AND ${isString(rightResult.sql)} THEN SUBSTR(${leftResult.sql}, 1, LENGTH(${rightResult.sql})) = ${rightResult.sql} ELSE NULL END`,
|
|
8791
|
+
sql: `CASE WHEN ${isString(leftResult.sql)} AND ${isString(rightResult.sql)} THEN cypher_to_json_bool(SUBSTR(${leftResult.sql}, 1, LENGTH(${rightResult.sql})) = ${rightResult.sql}) ELSE NULL END`,
|
|
8677
8792
|
tables,
|
|
8678
8793
|
// leftResult.sql appears 3 times, rightResult.sql appears 5 times
|
|
8679
8794
|
params: [...leftResult.params, ...leftResult.params, ...rightResult.params, ...rightResult.params, ...leftResult.params, ...rightResult.params, ...rightResult.params],
|
|
@@ -8683,7 +8798,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8683
8798
|
// ENDS WITH
|
|
8684
8799
|
// Use CASE to handle: 1) type check 2) empty suffix edge case, 3) case-sensitive suffix match
|
|
8685
8800
|
return {
|
|
8686
|
-
sql: `CASE WHEN NOT (${isString(leftResult.sql)} AND ${isString(rightResult.sql)}) THEN NULL WHEN LENGTH(${rightResult.sql}) = 0 THEN
|
|
8801
|
+
sql: `CASE WHEN NOT (${isString(leftResult.sql)} AND ${isString(rightResult.sql)}) THEN NULL WHEN LENGTH(${rightResult.sql}) = 0 THEN json('true') ELSE cypher_to_json_bool(SUBSTR(${leftResult.sql}, -LENGTH(${rightResult.sql})) = ${rightResult.sql}) END`,
|
|
8687
8802
|
tables,
|
|
8688
8803
|
// leftResult.sql appears 4 times, rightResult.sql appears 6 times
|
|
8689
8804
|
params: [...leftResult.params, ...leftResult.params, ...rightResult.params, ...rightResult.params, ...rightResult.params, ...leftResult.params, ...rightResult.params, ...rightResult.params],
|
|
@@ -8917,7 +9032,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8917
9032
|
// Use custom cypher_and/cypher_or functions for proper JSON boolean handling
|
|
8918
9033
|
const func = expr.operator === "AND" ? "cypher_and" : "cypher_or";
|
|
8919
9034
|
return {
|
|
8920
|
-
sql:
|
|
9035
|
+
sql: `cypher_to_json_bool(${func}(${leftResult.sql}, ${rightResult.sql}))`,
|
|
8921
9036
|
tables,
|
|
8922
9037
|
params,
|
|
8923
9038
|
};
|
|
@@ -8930,13 +9045,11 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8930
9045
|
const leftSql = leftResult.sql;
|
|
8931
9046
|
const rightSql = rightResult.sql;
|
|
8932
9047
|
// XOR with NULL semantics: (a XOR b) = (a AND NOT b) OR (NOT a AND b)
|
|
8933
|
-
//
|
|
8934
|
-
// NULL OR NULL = NULL, NULL OR FALSE = NULL, so result is NULL when either input is NULL
|
|
9048
|
+
// Use cypher_* functions to handle both JSON booleans and integers properly
|
|
8935
9049
|
// Note: params are duplicated because the formula uses each operand twice:
|
|
8936
|
-
// ((left AND NOT right) OR (NOT left AND right))
|
|
8937
9050
|
const xorParams = [...leftResult.params, ...rightResult.params, ...leftResult.params, ...rightResult.params];
|
|
8938
9051
|
return {
|
|
8939
|
-
sql: `((${leftSql}
|
|
9052
|
+
sql: `cypher_to_json_bool(cypher_or(cypher_and(${leftSql}, cypher_not(${rightSql})), cypher_and(cypher_not(${leftSql}), ${rightSql})))`,
|
|
8940
9053
|
tables,
|
|
8941
9054
|
params: xorParams,
|
|
8942
9055
|
};
|
|
@@ -9561,8 +9674,13 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9561
9674
|
if (Array.isArray(expr.value)) {
|
|
9562
9675
|
return this.translateArrayLiteral(expr.value);
|
|
9563
9676
|
}
|
|
9564
|
-
|
|
9565
|
-
|
|
9677
|
+
if (expr.value === true) {
|
|
9678
|
+
return { sql: "json('true')", params };
|
|
9679
|
+
}
|
|
9680
|
+
if (expr.value === false) {
|
|
9681
|
+
return { sql: "json('false')", params };
|
|
9682
|
+
}
|
|
9683
|
+
params.push(expr.value);
|
|
9566
9684
|
return { sql: "?", params };
|
|
9567
9685
|
case "property": {
|
|
9568
9686
|
// Use subquery to get property from the created node
|
|
@@ -9693,22 +9811,23 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9693
9811
|
if (leftCouldBeNaN || rightCouldBeNaN) {
|
|
9694
9812
|
const op = expr.comparisonOperator;
|
|
9695
9813
|
// For = and <>, NaN semantics always apply (return false/true respectively)
|
|
9814
|
+
// Note: The expression appears twice in the CASE, so we need to duplicate params
|
|
9696
9815
|
if (op === "=") {
|
|
9697
9816
|
// NaN = anything is false (including NaN = NaN)
|
|
9698
|
-
// If the comparison returns NULL (because of NaN), return false
|
|
9817
|
+
// If the comparison returns NULL (because of NaN), return false
|
|
9699
9818
|
return {
|
|
9700
|
-
sql: `
|
|
9819
|
+
sql: `CASE WHEN (${leftSql} ${op} ${rightSql}) IS NULL THEN json('false') WHEN (${leftSql} ${op} ${rightSql}) THEN json('true') ELSE json('false') END`,
|
|
9701
9820
|
tables,
|
|
9702
|
-
params,
|
|
9821
|
+
params: [...params, ...params],
|
|
9703
9822
|
};
|
|
9704
9823
|
}
|
|
9705
9824
|
else if (op === "<>") {
|
|
9706
9825
|
// NaN <> anything is true (including NaN <> NaN)
|
|
9707
|
-
// If the comparison returns NULL (because of NaN), return true
|
|
9826
|
+
// If the comparison returns NULL (because of NaN), return true
|
|
9708
9827
|
return {
|
|
9709
|
-
sql: `
|
|
9828
|
+
sql: `CASE WHEN (${leftSql} ${op} ${rightSql}) IS NULL THEN json('true') WHEN (${leftSql} ${op} ${rightSql}) THEN json('true') ELSE json('false') END`,
|
|
9710
9829
|
tables,
|
|
9711
|
-
params,
|
|
9830
|
+
params: [...params, ...params],
|
|
9712
9831
|
};
|
|
9713
9832
|
}
|
|
9714
9833
|
else {
|
|
@@ -9722,9 +9841,9 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9722
9841
|
else {
|
|
9723
9842
|
// NaN compared to numeric via range operators returns false
|
|
9724
9843
|
return {
|
|
9725
|
-
sql: `
|
|
9844
|
+
sql: `CASE WHEN (${leftSql} ${op} ${rightSql}) IS NULL THEN json('false') WHEN (${leftSql} ${op} ${rightSql}) THEN json('true') ELSE json('false') END`,
|
|
9726
9845
|
tables,
|
|
9727
|
-
params,
|
|
9846
|
+
params: [...params, ...params],
|
|
9728
9847
|
};
|
|
9729
9848
|
}
|
|
9730
9849
|
}
|
|
@@ -9743,7 +9862,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9743
9862
|
};
|
|
9744
9863
|
const func = opToFunc[expr.comparisonOperator];
|
|
9745
9864
|
return {
|
|
9746
|
-
sql:
|
|
9865
|
+
sql: `cypher_to_json_bool(${func}(${leftSql}, ${rightSql}))`,
|
|
9747
9866
|
tables,
|
|
9748
9867
|
params,
|
|
9749
9868
|
};
|
|
@@ -9756,7 +9875,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9756
9875
|
if ((expr.comparisonOperator === "=" || expr.comparisonOperator === "<>") && needsCypherEquals) {
|
|
9757
9876
|
if (expr.comparisonOperator === "=") {
|
|
9758
9877
|
return {
|
|
9759
|
-
sql: `cypher_equals(${leftSql}, ${rightSql})`,
|
|
9878
|
+
sql: `cypher_to_json_bool(cypher_equals(${leftSql}, ${rightSql}))`,
|
|
9760
9879
|
tables,
|
|
9761
9880
|
params,
|
|
9762
9881
|
};
|
|
@@ -9765,14 +9884,31 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9765
9884
|
// <> is NOT equals: invert the result, but preserve null
|
|
9766
9885
|
// We need to duplicate params because cypher_equals appears twice in the SQL
|
|
9767
9886
|
return {
|
|
9768
|
-
sql: `CASE WHEN cypher_equals(${leftSql}, ${rightSql}) IS NULL THEN NULL WHEN cypher_equals(${leftSql}, ${rightSql}) = 1 THEN
|
|
9887
|
+
sql: `CASE WHEN cypher_equals(${leftSql}, ${rightSql}) IS NULL THEN NULL WHEN cypher_equals(${leftSql}, ${rightSql}) = 1 THEN json('false') ELSE json('true') END`,
|
|
9769
9888
|
tables,
|
|
9770
9889
|
params: [...params, ...params],
|
|
9771
9890
|
};
|
|
9772
9891
|
}
|
|
9773
9892
|
}
|
|
9893
|
+
// For equality comparisons, use cypher_bool_eq to handle mixed boolean representations
|
|
9894
|
+
// (JSON boolean strings 'true'/'false' vs SQLite integers 1/0)
|
|
9895
|
+
if (expr.comparisonOperator === "=") {
|
|
9896
|
+
return {
|
|
9897
|
+
sql: `cypher_to_json_bool(cypher_bool_eq(${leftSql}, ${rightSql}))`,
|
|
9898
|
+
tables,
|
|
9899
|
+
params,
|
|
9900
|
+
};
|
|
9901
|
+
}
|
|
9902
|
+
if (expr.comparisonOperator === "<>") {
|
|
9903
|
+
// <> is NOT equals: use cypher_bool_eq and invert the result
|
|
9904
|
+
return {
|
|
9905
|
+
sql: `cypher_to_json_bool(CASE WHEN cypher_bool_eq(${leftSql}, ${rightSql}) IS NULL THEN NULL ELSE 1 - cypher_bool_eq(${leftSql}, ${rightSql}) END)`,
|
|
9906
|
+
tables,
|
|
9907
|
+
params: [...params, ...params], // Duplicate params for the two uses
|
|
9908
|
+
};
|
|
9909
|
+
}
|
|
9774
9910
|
return {
|
|
9775
|
-
sql: `(${leftSql} ${expr.comparisonOperator} ${rightSql})`,
|
|
9911
|
+
sql: `cypher_to_json_bool(${leftSql} ${expr.comparisonOperator} ${rightSql})`,
|
|
9776
9912
|
tables,
|
|
9777
9913
|
params,
|
|
9778
9914
|
};
|
|
@@ -9899,6 +10035,197 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9899
10035
|
params.push(...mapParams, ...listResult.params, ...filterParams);
|
|
9900
10036
|
return { sql, tables, params };
|
|
9901
10037
|
}
|
|
10038
|
+
/**
|
|
10039
|
+
* Translate a reduce expression.
|
|
10040
|
+
* Syntax: reduce(acc = init, x IN list | expr)
|
|
10041
|
+
*
|
|
10042
|
+
* Uses a recursive CTE to iterate through the list and accumulate a value.
|
|
10043
|
+
*
|
|
10044
|
+
* Example: reduce(acc = 0, x IN [1,2,3,4] | acc + x) returns 10
|
|
10045
|
+
*/
|
|
10046
|
+
translateReduceExpression(expr) {
|
|
10047
|
+
const tables = [];
|
|
10048
|
+
const params = [];
|
|
10049
|
+
const accumulator = expr.accumulator;
|
|
10050
|
+
const initialValue = expr.initialValue;
|
|
10051
|
+
const variable = expr.variable;
|
|
10052
|
+
const listExpr = expr.listExpr;
|
|
10053
|
+
const reduceExpr = expr.reduceExpr;
|
|
10054
|
+
// Translate the initial value
|
|
10055
|
+
const initResult = this.translateExpression(initialValue);
|
|
10056
|
+
tables.push(...initResult.tables);
|
|
10057
|
+
// Translate the source list expression
|
|
10058
|
+
const listResult = this.translateExpression(listExpr);
|
|
10059
|
+
tables.push(...listResult.tables);
|
|
10060
|
+
// Build the reduce expression with variable substitutions
|
|
10061
|
+
// Replace accumulator with "__red__.acc" and variable with "__red_elem.value"
|
|
10062
|
+
const reduceResult = this.translateReduceBodyExpr(reduceExpr, accumulator, variable, "__red__", "__red_elem");
|
|
10063
|
+
// Build recursive CTE:
|
|
10064
|
+
// WITH RECURSIVE __red__(idx, acc) AS (
|
|
10065
|
+
// SELECT 0, <init>
|
|
10066
|
+
// UNION ALL
|
|
10067
|
+
// SELECT idx + 1, <reduceExpr>
|
|
10068
|
+
// FROM __red__, json_each(<list>) AS __red_elem__
|
|
10069
|
+
// WHERE __red__.idx = __red_elem__.key
|
|
10070
|
+
// )
|
|
10071
|
+
// SELECT acc FROM __red__ ORDER BY idx DESC LIMIT 1
|
|
10072
|
+
const sql = `(WITH RECURSIVE __red__(idx, acc) AS (
|
|
10073
|
+
SELECT 0, ${initResult.sql}
|
|
10074
|
+
UNION ALL
|
|
10075
|
+
SELECT __red__.idx + 1, ${reduceResult.sql}
|
|
10076
|
+
FROM __red__, json_each(${listResult.sql}) AS __red_elem
|
|
10077
|
+
WHERE __red__.idx = __red_elem.key
|
|
10078
|
+
)
|
|
10079
|
+
SELECT acc FROM __red__ ORDER BY idx DESC LIMIT 1)`;
|
|
10080
|
+
// Params order: init params, then list params (once for each occurrence), then reduce params
|
|
10081
|
+
params.push(...initResult.params, ...listResult.params, ...reduceResult.params);
|
|
10082
|
+
return { sql, tables, params };
|
|
10083
|
+
}
|
|
10084
|
+
/**
|
|
10085
|
+
* Translate a filter expression.
|
|
10086
|
+
* Syntax: filter(x IN list WHERE predicate)
|
|
10087
|
+
*
|
|
10088
|
+
* Returns a list of elements that satisfy the predicate.
|
|
10089
|
+
* Equivalent to list comprehension [x IN list WHERE predicate] without mapping.
|
|
10090
|
+
*/
|
|
10091
|
+
translateFilterExpression(expr) {
|
|
10092
|
+
const tables = [];
|
|
10093
|
+
const params = [];
|
|
10094
|
+
const variable = expr.variable;
|
|
10095
|
+
const listExpr = expr.listExpr;
|
|
10096
|
+
const filterCondition = expr.filterCondition;
|
|
10097
|
+
// Translate the source list expression
|
|
10098
|
+
const listResult = this.translateExpression(listExpr);
|
|
10099
|
+
tables.push(...listResult.tables);
|
|
10100
|
+
// Wrap the source expression for json_each
|
|
10101
|
+
let sourceExpr = listResult.sql;
|
|
10102
|
+
if (listExpr.type === "property") {
|
|
10103
|
+
// For property access, use json_extract
|
|
10104
|
+
const varInfo = this.ctx.variables.get(listExpr.variable);
|
|
10105
|
+
if (varInfo) {
|
|
10106
|
+
sourceExpr = `json_extract(${varInfo.alias}.properties, '$.${listExpr.property}')`;
|
|
10107
|
+
}
|
|
10108
|
+
}
|
|
10109
|
+
// Build the WHERE clause from the filter condition
|
|
10110
|
+
const filterResult = this.translateListComprehensionCondition(filterCondition, variable, "__flt__");
|
|
10111
|
+
const filterParams = filterResult.params;
|
|
10112
|
+
const whereClause = ` WHERE ${filterResult.sql}`;
|
|
10113
|
+
// Build the final SQL using json_group_array
|
|
10114
|
+
const sql = `(SELECT json_group_array(__flt__.value) FROM json_each(${sourceExpr}) AS __flt__${whereClause})`;
|
|
10115
|
+
// Params must match SQL order: source params, then filter params
|
|
10116
|
+
params.push(...listResult.params, ...filterParams);
|
|
10117
|
+
return { sql, tables, params };
|
|
10118
|
+
}
|
|
10119
|
+
/**
|
|
10120
|
+
* Translate an extract expression.
|
|
10121
|
+
* Syntax: extract(x IN list | expr)
|
|
10122
|
+
*
|
|
10123
|
+
* Returns a list of mapped values.
|
|
10124
|
+
* Equivalent to list comprehension [x IN list | expr] without filtering.
|
|
10125
|
+
*/
|
|
10126
|
+
translateExtractExpression(expr) {
|
|
10127
|
+
const tables = [];
|
|
10128
|
+
const params = [];
|
|
10129
|
+
const variable = expr.variable;
|
|
10130
|
+
const listExpr = expr.listExpr;
|
|
10131
|
+
const mapExpr = expr.mapExpr;
|
|
10132
|
+
// Translate the source list expression
|
|
10133
|
+
const listResult = this.translateExpression(listExpr);
|
|
10134
|
+
tables.push(...listResult.tables);
|
|
10135
|
+
// Wrap the source expression for json_each
|
|
10136
|
+
let sourceExpr = listResult.sql;
|
|
10137
|
+
if (listExpr.type === "property") {
|
|
10138
|
+
// For property access, use json_extract
|
|
10139
|
+
const varInfo = this.ctx.variables.get(listExpr.variable);
|
|
10140
|
+
if (varInfo) {
|
|
10141
|
+
sourceExpr = `json_extract(${varInfo.alias}.properties, '$.${listExpr.property}')`;
|
|
10142
|
+
}
|
|
10143
|
+
}
|
|
10144
|
+
// Translate the map expression
|
|
10145
|
+
const mapResult = this.translateListComprehensionExpr(mapExpr, variable, "__ext__");
|
|
10146
|
+
const mapParams = mapResult.params;
|
|
10147
|
+
const selectExpr = mapResult.sql;
|
|
10148
|
+
// Build the final SQL using json_group_array
|
|
10149
|
+
const sql = `(SELECT json_group_array(${selectExpr}) FROM json_each(${sourceExpr}) AS __ext__)`;
|
|
10150
|
+
// Params must match SQL order: select params, then source params
|
|
10151
|
+
params.push(...mapParams, ...listResult.params);
|
|
10152
|
+
return { sql, tables, params };
|
|
10153
|
+
}
|
|
10154
|
+
/**
|
|
10155
|
+
* Translate an expression within a reduce body, substituting variables.
|
|
10156
|
+
*/
|
|
10157
|
+
translateReduceBodyExpr(expr, accVar, iterVar, tableAlias, elemAlias) {
|
|
10158
|
+
const params = [];
|
|
10159
|
+
switch (expr.type) {
|
|
10160
|
+
case "variable": {
|
|
10161
|
+
if (expr.variable === accVar) {
|
|
10162
|
+
return { sql: `${tableAlias}.acc`, params };
|
|
10163
|
+
}
|
|
10164
|
+
if (expr.variable === iterVar) {
|
|
10165
|
+
return { sql: `${elemAlias}.value`, params };
|
|
10166
|
+
}
|
|
10167
|
+
// Other variables - use the standard translation
|
|
10168
|
+
const varInfo = this.ctx.variables.get(expr.variable);
|
|
10169
|
+
if (varInfo) {
|
|
10170
|
+
return { sql: `${varInfo.alias}.${expr.property || "id"}`, params };
|
|
10171
|
+
}
|
|
10172
|
+
return { sql: expr.variable, params };
|
|
10173
|
+
}
|
|
10174
|
+
case "property": {
|
|
10175
|
+
if (expr.variable === iterVar) {
|
|
10176
|
+
// Property access on the iterator variable: x.name
|
|
10177
|
+
return { sql: `json_extract(${elemAlias}.value, '$.${expr.property}')`, params };
|
|
10178
|
+
}
|
|
10179
|
+
if (expr.variable === accVar) {
|
|
10180
|
+
// Property access on accumulator (if acc is an object)
|
|
10181
|
+
return { sql: `json_extract(${tableAlias}.acc, '$.${expr.property}')`, params };
|
|
10182
|
+
}
|
|
10183
|
+
// Standard property translation
|
|
10184
|
+
const varInfo = this.ctx.variables.get(expr.variable);
|
|
10185
|
+
if (varInfo) {
|
|
10186
|
+
return { sql: `json_extract(${varInfo.alias}.properties, '$.${expr.property}')`, params };
|
|
10187
|
+
}
|
|
10188
|
+
return { sql: `json_extract(${expr.variable}, '$.${expr.property}')`, params };
|
|
10189
|
+
}
|
|
10190
|
+
case "literal": {
|
|
10191
|
+
if (expr.value === null)
|
|
10192
|
+
return { sql: "NULL", params };
|
|
10193
|
+
if (typeof expr.value === "boolean")
|
|
10194
|
+
return { sql: expr.value ? "json('true')" : "json('false')", params };
|
|
10195
|
+
if (typeof expr.value === "number")
|
|
10196
|
+
return { sql: String(expr.value), params };
|
|
10197
|
+
if (typeof expr.value === "string") {
|
|
10198
|
+
params.push(expr.value);
|
|
10199
|
+
return { sql: "?", params };
|
|
10200
|
+
}
|
|
10201
|
+
if (Array.isArray(expr.value)) {
|
|
10202
|
+
return { sql: `json('${JSON.stringify(expr.value)}')`, params };
|
|
10203
|
+
}
|
|
10204
|
+
return { sql: `json('${JSON.stringify(expr.value)}')`, params };
|
|
10205
|
+
}
|
|
10206
|
+
case "binary": {
|
|
10207
|
+
const left = this.translateReduceBodyExpr(expr.left, accVar, iterVar, tableAlias, elemAlias);
|
|
10208
|
+
const right = this.translateReduceBodyExpr(expr.right, accVar, iterVar, tableAlias, elemAlias);
|
|
10209
|
+
params.push(...left.params, ...right.params);
|
|
10210
|
+
return { sql: `(${left.sql} ${expr.operator} ${right.sql})`, params };
|
|
10211
|
+
}
|
|
10212
|
+
case "function": {
|
|
10213
|
+
const funcName = expr.functionName.toUpperCase();
|
|
10214
|
+
const args = expr.args || [];
|
|
10215
|
+
const argResults = args.map(arg => this.translateReduceBodyExpr(arg, accVar, iterVar, tableAlias, elemAlias));
|
|
10216
|
+
for (const arg of argResults) {
|
|
10217
|
+
params.push(...arg.params);
|
|
10218
|
+
}
|
|
10219
|
+
const argsSql = argResults.map(r => r.sql).join(", ");
|
|
10220
|
+
return { sql: `${funcName}(${argsSql})`, params };
|
|
10221
|
+
}
|
|
10222
|
+
default: {
|
|
10223
|
+
// For other expression types, fall back to standard translation
|
|
10224
|
+
const result = this.translateExpression(expr);
|
|
10225
|
+
return { sql: result.sql, params: result.params };
|
|
10226
|
+
}
|
|
10227
|
+
}
|
|
10228
|
+
}
|
|
9902
10229
|
/**
|
|
9903
10230
|
* Translate a pattern comprehension expression.
|
|
9904
10231
|
* Syntax: [(pattern) WHERE filterCondition | mapExpr]
|
|
@@ -10044,6 +10371,118 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10044
10371
|
tables.push(boundVarInfo.alias);
|
|
10045
10372
|
return { sql, tables, params };
|
|
10046
10373
|
}
|
|
10374
|
+
/**
|
|
10375
|
+
* Translate an exists() pattern expression.
|
|
10376
|
+
* Returns true if the pattern has at least one match, false otherwise.
|
|
10377
|
+
*
|
|
10378
|
+
* Example: exists((p)-[:KNOWS]->()) returns true if node p has any outgoing KNOWS edges
|
|
10379
|
+
*/
|
|
10380
|
+
translateExistsPattern(expr) {
|
|
10381
|
+
const tables = [];
|
|
10382
|
+
const params = [];
|
|
10383
|
+
const patterns = expr.patterns;
|
|
10384
|
+
// Pattern structure: patterns from parsePatternChain
|
|
10385
|
+
// The first element is always a NodePattern or RelationshipPattern
|
|
10386
|
+
const firstPattern = patterns[0];
|
|
10387
|
+
const isRelPattern = (p) => {
|
|
10388
|
+
return typeof p === "object" && p !== null && "edge" in p;
|
|
10389
|
+
};
|
|
10390
|
+
let startVar;
|
|
10391
|
+
let relPattern;
|
|
10392
|
+
let startNodePattern;
|
|
10393
|
+
let targetNodePattern;
|
|
10394
|
+
if (isRelPattern(firstPattern)) {
|
|
10395
|
+
// First pattern is a RelationshipPattern
|
|
10396
|
+
relPattern = firstPattern;
|
|
10397
|
+
startNodePattern = relPattern.source;
|
|
10398
|
+
targetNodePattern = relPattern.target;
|
|
10399
|
+
startVar = startNodePattern.variable;
|
|
10400
|
+
}
|
|
10401
|
+
else {
|
|
10402
|
+
// First pattern is a NodePattern, look for RelationshipPattern in rest
|
|
10403
|
+
startNodePattern = firstPattern;
|
|
10404
|
+
startVar = startNodePattern.variable;
|
|
10405
|
+
for (let i = 1; i < patterns.length; i++) {
|
|
10406
|
+
if (isRelPattern(patterns[i])) {
|
|
10407
|
+
relPattern = patterns[i];
|
|
10408
|
+
targetNodePattern = relPattern.target;
|
|
10409
|
+
break;
|
|
10410
|
+
}
|
|
10411
|
+
}
|
|
10412
|
+
}
|
|
10413
|
+
if (!startVar) {
|
|
10414
|
+
throw new Error("exists() pattern must start with a bound variable");
|
|
10415
|
+
}
|
|
10416
|
+
// Get the bound variable info from outer context
|
|
10417
|
+
const boundVarInfo = this.ctx.variables.get(startVar);
|
|
10418
|
+
if (!boundVarInfo) {
|
|
10419
|
+
throw new Error(`Unknown variable in exists() pattern: ${startVar}`);
|
|
10420
|
+
}
|
|
10421
|
+
if (!relPattern) {
|
|
10422
|
+
throw new Error("exists() pattern must include a relationship pattern");
|
|
10423
|
+
}
|
|
10424
|
+
// Build the correlated subquery
|
|
10425
|
+
const edgeAlias = `__ex_e_${this.ctx.aliasCounter++}`;
|
|
10426
|
+
const targetAlias = `__ex_t_${this.ctx.aliasCounter++}`;
|
|
10427
|
+
const edge = relPattern.edge;
|
|
10428
|
+
// Build edge type filter
|
|
10429
|
+
const edgeTypes = edge.types || (edge.type ? [edge.type] : []);
|
|
10430
|
+
let edgeTypeFilter = "";
|
|
10431
|
+
const edgeTypeParams = [];
|
|
10432
|
+
if (edgeTypes.length > 0) {
|
|
10433
|
+
const typeConditions = edgeTypes.map((t) => `${edgeAlias}.type = ?`);
|
|
10434
|
+
edgeTypeFilter = ` AND (${typeConditions.join(" OR ")})`;
|
|
10435
|
+
edgeTypeParams.push(...edgeTypes);
|
|
10436
|
+
}
|
|
10437
|
+
// Build direction filter
|
|
10438
|
+
let directionFilter = "";
|
|
10439
|
+
const direction = edge.direction || "right";
|
|
10440
|
+
if (direction === "right") {
|
|
10441
|
+
directionFilter = `${edgeAlias}.source_id = ${boundVarInfo.alias}.id`;
|
|
10442
|
+
}
|
|
10443
|
+
else if (direction === "left") {
|
|
10444
|
+
directionFilter = `${edgeAlias}.target_id = ${boundVarInfo.alias}.id`;
|
|
10445
|
+
}
|
|
10446
|
+
else {
|
|
10447
|
+
// "none" means either direction
|
|
10448
|
+
directionFilter = `(${edgeAlias}.source_id = ${boundVarInfo.alias}.id OR ${edgeAlias}.target_id = ${boundVarInfo.alias}.id)`;
|
|
10449
|
+
}
|
|
10450
|
+
// Build target node filter if labels specified
|
|
10451
|
+
let targetFilter = "";
|
|
10452
|
+
const targetFilterParams = [];
|
|
10453
|
+
if (targetNodePattern && targetNodePattern.label) {
|
|
10454
|
+
const labels = Array.isArray(targetNodePattern.label)
|
|
10455
|
+
? targetNodePattern.label
|
|
10456
|
+
: [targetNodePattern.label];
|
|
10457
|
+
const labelConditions = labels.map((l) => `EXISTS(SELECT 1 FROM json_each(${targetAlias}.label) WHERE value = ?)`);
|
|
10458
|
+
targetFilter = ` AND ${labelConditions.join(" AND ")}`;
|
|
10459
|
+
targetFilterParams.push(...labels);
|
|
10460
|
+
}
|
|
10461
|
+
// Build the from clause
|
|
10462
|
+
let fromClause = `edges ${edgeAlias}`;
|
|
10463
|
+
if (targetNodePattern && (targetNodePattern.label || targetNodePattern.variable)) {
|
|
10464
|
+
// Need to join with nodes for target filtering
|
|
10465
|
+
let targetJoin;
|
|
10466
|
+
if (direction === "right") {
|
|
10467
|
+
targetJoin = `${edgeAlias}.target_id = ${targetAlias}.id`;
|
|
10468
|
+
}
|
|
10469
|
+
else if (direction === "left") {
|
|
10470
|
+
targetJoin = `${edgeAlias}.source_id = ${targetAlias}.id`;
|
|
10471
|
+
}
|
|
10472
|
+
else {
|
|
10473
|
+
// For undirected, target is the "other" node
|
|
10474
|
+
targetJoin = `(CASE WHEN ${edgeAlias}.source_id = ${boundVarInfo.alias}.id THEN ${edgeAlias}.target_id ELSE ${edgeAlias}.source_id END) = ${targetAlias}.id`;
|
|
10475
|
+
}
|
|
10476
|
+
fromClause = `edges ${edgeAlias} JOIN nodes ${targetAlias} ON ${targetJoin}`;
|
|
10477
|
+
}
|
|
10478
|
+
// Build the EXISTS subquery and wrap result for proper boolean output
|
|
10479
|
+
const sql = `cypher_to_json_bool(EXISTS(SELECT 1 FROM ${fromClause} WHERE ${directionFilter}${edgeTypeFilter}${targetFilter}))`;
|
|
10480
|
+
// Params must be in SQL order: edgeType, then targetFilter
|
|
10481
|
+
params.push(...edgeTypeParams, ...targetFilterParams);
|
|
10482
|
+
// Add outer table reference
|
|
10483
|
+
tables.push(boundVarInfo.alias);
|
|
10484
|
+
return { sql, tables, params };
|
|
10485
|
+
}
|
|
10047
10486
|
/**
|
|
10048
10487
|
* Translate an expression within a pattern comprehension.
|
|
10049
10488
|
*/
|
|
@@ -10172,9 +10611,14 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10172
10611
|
if (expr.value === null) {
|
|
10173
10612
|
return { sql: "NULL", params };
|
|
10174
10613
|
}
|
|
10175
|
-
//
|
|
10176
|
-
|
|
10177
|
-
|
|
10614
|
+
// Use 0/1 for booleans in list comprehension context (used in WHERE clauses)
|
|
10615
|
+
if (expr.value === true) {
|
|
10616
|
+
return { sql: "1", params };
|
|
10617
|
+
}
|
|
10618
|
+
if (expr.value === false) {
|
|
10619
|
+
return { sql: "0", params };
|
|
10620
|
+
}
|
|
10621
|
+
params.push(expr.value);
|
|
10178
10622
|
return { sql: "?", params };
|
|
10179
10623
|
case "parameter": {
|
|
10180
10624
|
const paramValue = this.ctx.paramValues[expr.name];
|
|
@@ -10299,10 +10743,34 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10299
10743
|
}
|
|
10300
10744
|
case "expression": {
|
|
10301
10745
|
// Handle bare expressions used as boolean conditions (e.g., all(x IN list WHERE x))
|
|
10746
|
+
// Need to convert JSON boolean strings to integers for SQLite WHERE clause evaluation
|
|
10747
|
+
// JSON booleans in collected arrays are stored as strings "true"/"false" which SQLite
|
|
10748
|
+
// treats as falsy (value 0) since they're not integers
|
|
10302
10749
|
const exprResult = this.translateListComprehensionExpr(condition.left, compVar, tableAlias, scopes);
|
|
10750
|
+
// Wrap with CASE to handle JSON boolean strings, integers, and regular values
|
|
10751
|
+
// - JSON "true" string -> 1
|
|
10752
|
+
// - JSON "false" string -> 0
|
|
10753
|
+
// - JSON true literal (rarely occurs) -> 1
|
|
10754
|
+
// - JSON false literal (rarely occurs) -> 0
|
|
10755
|
+
// - Integer 1 -> 1 (already truthy)
|
|
10756
|
+
// - Integer 0 -> 0 (already falsy)
|
|
10757
|
+
// - NULL -> NULL
|
|
10758
|
+
// - Other truthy values pass through
|
|
10759
|
+
const wrappedSql = `(CASE WHEN ${exprResult.sql} = 'true' OR ${exprResult.sql} = 1 OR ${exprResult.sql} IS TRUE THEN 1 WHEN ${exprResult.sql} = 'false' OR ${exprResult.sql} = 0 OR ${exprResult.sql} IS FALSE THEN 0 WHEN ${exprResult.sql} IS NULL THEN NULL ELSE ${exprResult.sql} END)`;
|
|
10760
|
+
// Need to duplicate params for each use of exprResult.sql (6 uses total)
|
|
10761
|
+
const allParams = [
|
|
10762
|
+
...exprResult.params, // first condition check (= 'true')
|
|
10763
|
+
...exprResult.params, // second condition check (= 1)
|
|
10764
|
+
...exprResult.params, // third condition check (IS TRUE)
|
|
10765
|
+
...exprResult.params, // fourth condition check (= 'false')
|
|
10766
|
+
...exprResult.params, // fifth condition check (= 0)
|
|
10767
|
+
...exprResult.params, // sixth condition check (IS FALSE)
|
|
10768
|
+
...exprResult.params, // seventh condition check (IS NULL)
|
|
10769
|
+
...exprResult.params, // ELSE fallback
|
|
10770
|
+
];
|
|
10303
10771
|
return {
|
|
10304
|
-
sql:
|
|
10305
|
-
params:
|
|
10772
|
+
sql: wrappedSql,
|
|
10773
|
+
params: allParams,
|
|
10306
10774
|
};
|
|
10307
10775
|
}
|
|
10308
10776
|
case "listPredicate": {
|
|
@@ -10737,7 +11205,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10737
11205
|
params.push(...operandResult.params);
|
|
10738
11206
|
// Use custom cypher_not function that properly handles JSON booleans and integers
|
|
10739
11207
|
return {
|
|
10740
|
-
sql: `cypher_not(${operandResult.sql})`,
|
|
11208
|
+
sql: `cypher_to_json_bool(cypher_not(${operandResult.sql}))`,
|
|
10741
11209
|
tables,
|
|
10742
11210
|
params,
|
|
10743
11211
|
};
|
|
@@ -10763,6 +11231,15 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10763
11231
|
params: [...left.params, ...right.params],
|
|
10764
11232
|
};
|
|
10765
11233
|
}
|
|
11234
|
+
// For equality comparisons involving CASE expressions (which return json('true')/json('false')),
|
|
11235
|
+
// use cypher_bool_eq to handle JSON boolean vs integer comparison
|
|
11236
|
+
if (condition.operator === "=" &&
|
|
11237
|
+
(condition.left?.type === "case" || condition.right?.type === "case")) {
|
|
11238
|
+
return {
|
|
11239
|
+
sql: `cypher_bool_eq(${left.sql}, ${right.sql})`,
|
|
11240
|
+
params: [...left.params, ...right.params],
|
|
11241
|
+
};
|
|
11242
|
+
}
|
|
10766
11243
|
return {
|
|
10767
11244
|
sql: `${left.sql} ${condition.operator} ${right.sql}`,
|
|
10768
11245
|
params: [...left.params, ...right.params],
|
|
@@ -10821,6 +11298,15 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10821
11298
|
params: [...left.params, ...right.params, ...right.params, ...left.params, ...right.params, ...right.params],
|
|
10822
11299
|
};
|
|
10823
11300
|
}
|
|
11301
|
+
case "regex": {
|
|
11302
|
+
const left = this.translateWhereExpression(condition.left);
|
|
11303
|
+
const right = this.translateWhereExpression(condition.right);
|
|
11304
|
+
// Use cypher_regex custom function for regex matching
|
|
11305
|
+
return {
|
|
11306
|
+
sql: `cypher_regex(${left.sql}, ${right.sql})`,
|
|
11307
|
+
params: [...left.params, ...right.params],
|
|
11308
|
+
};
|
|
11309
|
+
}
|
|
10824
11310
|
case "isNull": {
|
|
10825
11311
|
const left = this.translateWhereExpression(condition.left);
|
|
10826
11312
|
return {
|
|
@@ -11677,6 +12163,11 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
11677
12163
|
params: [],
|
|
11678
12164
|
};
|
|
11679
12165
|
}
|
|
12166
|
+
case "case": {
|
|
12167
|
+
// CASE expression in WHERE clause
|
|
12168
|
+
const result = this.translateCaseExpression(expr);
|
|
12169
|
+
return { sql: result.sql, params: result.params };
|
|
12170
|
+
}
|
|
11680
12171
|
default:
|
|
11681
12172
|
throw new Error(`Unknown expression type in WHERE: ${expr.type}`);
|
|
11682
12173
|
}
|
|
@@ -11705,7 +12196,14 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
11705
12196
|
};
|
|
11706
12197
|
}
|
|
11707
12198
|
case "literal": {
|
|
11708
|
-
|
|
12199
|
+
// Return JSON booleans to preserve boolean type in results
|
|
12200
|
+
if (expr.value === true) {
|
|
12201
|
+
return { sql: "json('true')", tables, params };
|
|
12202
|
+
}
|
|
12203
|
+
if (expr.value === false) {
|
|
12204
|
+
return { sql: "json('false')", tables, params };
|
|
12205
|
+
}
|
|
12206
|
+
const value = expr.value;
|
|
11709
12207
|
if (typeof value === "number" && expr.numberLiteralKind === "float" && expr.raw) {
|
|
11710
12208
|
return { sql: expr.raw, tables, params };
|
|
11711
12209
|
}
|