leangraph 1.1.0 → 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/dist/auth.js +2 -2
- package/dist/auth.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +56 -9
- package/dist/db.js.map +1 -1
- package/dist/executor.d.ts +108 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +1160 -323
- package/dist/executor.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/routes.d.ts.map +1 -1
- package/dist/routes.js +3 -0
- package/dist/routes.js.map +1 -1
- package/dist/translator.d.ts +36 -0
- package/dist/translator.d.ts.map +1 -1
- package/dist/translator.js +549 -104
- package/dist/translator.js.map +1 -1
- package/package.json +1 -1
package/dist/translator.js
CHANGED
|
@@ -3541,51 +3541,51 @@ export class Translator {
|
|
|
3541
3541
|
if (isUndirected2) {
|
|
3542
3542
|
// Undirected: traverse in both directions
|
|
3543
3543
|
if (edgeType2) {
|
|
3544
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3545
|
-
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
|
|
3546
3546
|
UNION ALL
|
|
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)))
|
|
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
|
|
3548
3548
|
FROM ${pathCteName2} p
|
|
3549
3549
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_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)
|
|
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 < ?
|
|
3551
3551
|
)`;
|
|
3552
|
-
allParams.push(maxHops2, edgeType2);
|
|
3552
|
+
allParams.push(maxHops2, edgeType2, earlyTerminationLimit);
|
|
3553
3553
|
}
|
|
3554
3554
|
else {
|
|
3555
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3556
|
-
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
|
|
3557
3557
|
UNION ALL
|
|
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)))
|
|
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
|
|
3559
3559
|
FROM ${pathCteName2} p
|
|
3560
3560
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_id)
|
|
3561
|
-
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 < ?
|
|
3562
3562
|
)`;
|
|
3563
|
-
allParams.push(maxHops2);
|
|
3563
|
+
allParams.push(maxHops2, earlyTerminationLimit);
|
|
3564
3564
|
}
|
|
3565
3565
|
}
|
|
3566
3566
|
else {
|
|
3567
3567
|
// Directed
|
|
3568
3568
|
if (edgeType2) {
|
|
3569
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3570
|
-
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
|
|
3571
3571
|
UNION ALL
|
|
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)))
|
|
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
|
|
3573
3573
|
FROM ${pathCteName2} p
|
|
3574
3574
|
JOIN edges e ON p.end_id = e.source_id
|
|
3575
|
-
WHERE p.depth < ? AND e.type = ?
|
|
3575
|
+
WHERE p.depth < ? AND e.type = ? AND p.row_num < ?
|
|
3576
3576
|
)`;
|
|
3577
|
-
allParams.push(maxHops2, edgeType2);
|
|
3577
|
+
allParams.push(maxHops2, edgeType2, earlyTerminationLimit);
|
|
3578
3578
|
}
|
|
3579
3579
|
else {
|
|
3580
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) AS (
|
|
3581
|
-
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
|
|
3582
3582
|
UNION ALL
|
|
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)))
|
|
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
|
|
3584
3584
|
FROM ${pathCteName2} p
|
|
3585
3585
|
JOIN edges e ON p.end_id = e.source_id
|
|
3586
|
-
WHERE p.depth < ?
|
|
3586
|
+
WHERE p.depth < ? AND p.row_num < ?
|
|
3587
3587
|
)`;
|
|
3588
|
-
allParams.push(maxHops2);
|
|
3588
|
+
allParams.push(maxHops2, earlyTerminationLimit);
|
|
3589
3589
|
}
|
|
3590
3590
|
}
|
|
3591
3591
|
}
|
|
@@ -3593,55 +3593,55 @@ export class Translator {
|
|
|
3593
3593
|
if (isUndirected2) {
|
|
3594
3594
|
// Undirected: traverse in both directions
|
|
3595
3595
|
if (edgeType2) {
|
|
3596
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) 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))) 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 = ?
|
|
3598
3598
|
UNION ALL
|
|
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))) 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 = ?
|
|
3600
3600
|
UNION ALL
|
|
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)))
|
|
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
|
|
3602
3602
|
FROM ${pathCteName2} p
|
|
3603
3603
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_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)
|
|
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 < ?
|
|
3605
3605
|
)`;
|
|
3606
|
-
allParams.push(edgeType2, edgeType2, maxHops2, edgeType2);
|
|
3606
|
+
allParams.push(edgeType2, edgeType2, maxHops2, edgeType2, earlyTerminationLimit);
|
|
3607
3607
|
}
|
|
3608
3608
|
else {
|
|
3609
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) 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))) 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
|
|
3611
3611
|
UNION ALL
|
|
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))) 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
|
|
3613
3613
|
UNION ALL
|
|
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)))
|
|
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
|
|
3615
3615
|
FROM ${pathCteName2} p
|
|
3616
3616
|
JOIN edges e ON (p.end_id = e.source_id OR p.end_id = e.target_id)
|
|
3617
|
-
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 < ?
|
|
3618
3618
|
)`;
|
|
3619
|
-
allParams.push(maxHops2);
|
|
3619
|
+
allParams.push(maxHops2, earlyTerminationLimit);
|
|
3620
3620
|
}
|
|
3621
3621
|
}
|
|
3622
3622
|
else {
|
|
3623
3623
|
// Directed
|
|
3624
3624
|
if (edgeType2) {
|
|
3625
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) 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))) 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 = ?
|
|
3627
3627
|
UNION ALL
|
|
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)))
|
|
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
|
|
3629
3629
|
FROM ${pathCteName2} p
|
|
3630
3630
|
JOIN edges e ON p.end_id = e.source_id
|
|
3631
|
-
WHERE p.depth < ? AND e.type = ?
|
|
3631
|
+
WHERE p.depth < ? AND e.type = ? AND p.row_num < ?
|
|
3632
3632
|
)`;
|
|
3633
|
-
allParams.push(edgeType2, maxHops2, edgeType2);
|
|
3633
|
+
allParams.push(edgeType2, maxHops2, edgeType2, earlyTerminationLimit);
|
|
3634
3634
|
}
|
|
3635
3635
|
else {
|
|
3636
|
-
cte2 = `, ${pathCteName2}(start_id, end_id, depth, edge_ids) 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))) 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
|
|
3638
3638
|
UNION ALL
|
|
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)))
|
|
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
|
|
3640
3640
|
FROM ${pathCteName2} p
|
|
3641
3641
|
JOIN edges e ON p.end_id = e.source_id
|
|
3642
|
-
WHERE p.depth < ?
|
|
3642
|
+
WHERE p.depth < ? AND p.row_num < ?
|
|
3643
3643
|
)`;
|
|
3644
|
-
allParams.push(maxHops2);
|
|
3644
|
+
allParams.push(maxHops2, earlyTerminationLimit);
|
|
3645
3645
|
}
|
|
3646
3646
|
}
|
|
3647
3647
|
}
|
|
@@ -4079,7 +4079,7 @@ export class Translator {
|
|
|
4079
4079
|
return { sql: "?", params: [expr.value] };
|
|
4080
4080
|
}
|
|
4081
4081
|
if (typeof expr.value === "boolean") {
|
|
4082
|
-
return { sql: expr.value ? "
|
|
4082
|
+
return { sql: expr.value ? "json('true')" : "json('false')", params };
|
|
4083
4083
|
}
|
|
4084
4084
|
if (expr.value === null) {
|
|
4085
4085
|
return { sql: "NULL", params };
|
|
@@ -5409,6 +5409,16 @@ END FROM (SELECT json_group_array(${valueExpr}) as sv))`,
|
|
|
5409
5409
|
}
|
|
5410
5410
|
throw new Error("sqrt requires an argument");
|
|
5411
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
|
+
}
|
|
5412
5422
|
// RAND: random float between 0 and 1
|
|
5413
5423
|
if (expr.functionName === "RAND") {
|
|
5414
5424
|
// SQLite's RANDOM() returns integer between -9223372036854775808 and 9223372036854775807
|
|
@@ -7858,7 +7868,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
7858
7868
|
// Strip JSON quotes if input is wrapped in "" (from toString() output)
|
|
7859
7869
|
const argResult = this.translateExpression(arg);
|
|
7860
7870
|
tables.push(...argResult.tables);
|
|
7861
|
-
params.push(...argResult.params
|
|
7871
|
+
params.push(...argResult.params);
|
|
7862
7872
|
return {
|
|
7863
7873
|
sql: `(SELECT CASE WHEN _d IS NULL THEN NULL ELSE CASE
|
|
7864
7874
|
WHEN substr(_d, 1, 1) = '"' AND substr(_d, length(_d), 1) = '"'
|
|
@@ -8187,16 +8197,25 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8187
8197
|
const listResult = this.translateExpression(listArg);
|
|
8188
8198
|
const indexResult = this.translateExpression(indexArg);
|
|
8189
8199
|
tables.push(...listResult.tables, ...indexResult.tables);
|
|
8190
|
-
params.push(...listResult.params, ...indexResult.params);
|
|
8191
8200
|
// For map access with string key, use json_extract with the key
|
|
8192
8201
|
// For list access with integer index, use json_extract with array index
|
|
8193
8202
|
if (isContainerMap || isStringExpression(resolvedIndexArg)) {
|
|
8194
8203
|
// Map access: use key directly
|
|
8204
|
+
params.push(...listResult.params, ...indexResult.params);
|
|
8195
8205
|
return { sql: `json_extract(${listResult.sql}, '$.' || ${indexResult.sql})`, tables, params };
|
|
8196
8206
|
}
|
|
8197
8207
|
// Use -> operator with array index to preserve JSON types (booleans, etc.)
|
|
8198
8208
|
// Cast index to integer to avoid "0.0" in JSON path
|
|
8199
|
-
|
|
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
|
+
};
|
|
8200
8219
|
}
|
|
8201
8220
|
throw new Error("INDEX requires list and index arguments");
|
|
8202
8221
|
}
|
|
@@ -8335,8 +8354,14 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8335
8354
|
if (Array.isArray(expr.value)) {
|
|
8336
8355
|
return this.translateArrayLiteral(expr.value);
|
|
8337
8356
|
}
|
|
8338
|
-
//
|
|
8339
|
-
|
|
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;
|
|
8340
8365
|
// Preserve float-literal formatting (e.g., 0.0, -0.0, 1.0) so SQLite treats them as REAL.
|
|
8341
8366
|
if (typeof value === "number" && expr.numberLiteralKind === "float" && expr.raw) {
|
|
8342
8367
|
return { sql: expr.raw, tables, params };
|
|
@@ -8377,8 +8402,29 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8377
8402
|
case "patternComprehension": {
|
|
8378
8403
|
return this.translatePatternComprehension(expr);
|
|
8379
8404
|
}
|
|
8405
|
+
case "existsPattern": {
|
|
8406
|
+
return this.translateExistsPattern(expr);
|
|
8407
|
+
}
|
|
8380
8408
|
case "listPredicate": {
|
|
8381
|
-
|
|
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);
|
|
8382
8428
|
}
|
|
8383
8429
|
case "unary": {
|
|
8384
8430
|
return this.translateUnaryExpression(expr);
|
|
@@ -8472,7 +8518,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8472
8518
|
if (listExpr.type === "literal" && Array.isArray(listExpr.value)) {
|
|
8473
8519
|
const values = listExpr.value;
|
|
8474
8520
|
if (values.length === 0) {
|
|
8475
|
-
return { sql: "
|
|
8521
|
+
return { sql: "json('false')", tables, params }; // false for empty list
|
|
8476
8522
|
}
|
|
8477
8523
|
// Check if RHS contains complex types (nested arrays/objects)
|
|
8478
8524
|
const rhsHasComplexTypes = values.some(containsComplexTypes);
|
|
@@ -8503,7 +8549,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8503
8549
|
// If LHS contains null, finding a JSON match means comparing null=null → return NULL
|
|
8504
8550
|
params.push(rhsJson, lhsJson);
|
|
8505
8551
|
return {
|
|
8506
|
-
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`,
|
|
8507
8553
|
tables,
|
|
8508
8554
|
params,
|
|
8509
8555
|
};
|
|
@@ -8512,7 +8558,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8512
8558
|
// If RHS has top-level null and no match, return NULL
|
|
8513
8559
|
params.push(rhsJson, lhsJson);
|
|
8514
8560
|
return {
|
|
8515
|
-
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`,
|
|
8516
8562
|
tables,
|
|
8517
8563
|
params,
|
|
8518
8564
|
};
|
|
@@ -8528,7 +8574,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8528
8574
|
params.push(lhsJson, rhsJson);
|
|
8529
8575
|
return {
|
|
8530
8576
|
sql: `(SELECT CASE
|
|
8531
|
-
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')
|
|
8532
8578
|
WHEN EXISTS(
|
|
8533
8579
|
SELECT 1 FROM (
|
|
8534
8580
|
SELECT rhs.rowid,
|
|
@@ -8542,7 +8588,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8542
8588
|
GROUP BY rhs.rowid
|
|
8543
8589
|
) WHERE mismatches = 0 AND nulls > 0
|
|
8544
8590
|
) THEN NULL
|
|
8545
|
-
ELSE
|
|
8591
|
+
ELSE json('false')
|
|
8546
8592
|
END FROM (SELECT ? AS v) AS lhs_param, (SELECT ? AS v) AS rhs_param)`,
|
|
8547
8593
|
tables,
|
|
8548
8594
|
params,
|
|
@@ -8551,7 +8597,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8551
8597
|
// No null semantics needed - simple JSON comparison
|
|
8552
8598
|
params.push(rhsJson, lhsJson);
|
|
8553
8599
|
return {
|
|
8554
|
-
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(?)))`,
|
|
8555
8601
|
tables,
|
|
8556
8602
|
params,
|
|
8557
8603
|
};
|
|
@@ -8564,47 +8610,63 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8564
8610
|
// So rhsJson comes first (for json_each), then leftResult.params (for leftResult.sql)
|
|
8565
8611
|
params.push(rhsJson);
|
|
8566
8612
|
params.push(...leftResult.params);
|
|
8567
|
-
// When LHS is a scalar expression (like comparison result),
|
|
8568
|
-
//
|
|
8569
|
-
// 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)
|
|
8570
8615
|
const useDirectComparison = !leftIsComplex;
|
|
8571
8616
|
if (rhsHasTopLevelNull) {
|
|
8572
8617
|
if (useDirectComparison) {
|
|
8618
|
+
// Check for exact match first, then check for null comparison
|
|
8573
8619
|
return {
|
|
8574
|
-
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`,
|
|
8575
8621
|
tables,
|
|
8576
|
-
params,
|
|
8622
|
+
params: [...params, ...params], // Duplicate for two json_each usages
|
|
8577
8623
|
};
|
|
8578
8624
|
}
|
|
8579
8625
|
return {
|
|
8580
|
-
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`,
|
|
8581
8627
|
tables,
|
|
8582
8628
|
params,
|
|
8583
8629
|
};
|
|
8584
8630
|
}
|
|
8585
8631
|
if (useDirectComparison) {
|
|
8586
8632
|
return {
|
|
8587
|
-
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})))`,
|
|
8588
8634
|
tables,
|
|
8589
8635
|
params,
|
|
8590
8636
|
};
|
|
8591
8637
|
}
|
|
8592
8638
|
return {
|
|
8593
|
-
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})))`,
|
|
8594
8640
|
tables,
|
|
8595
8641
|
params,
|
|
8596
8642
|
};
|
|
8597
8643
|
}
|
|
8598
|
-
// 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)
|
|
8599
8646
|
const leftResult = this.translateExpression(leftExpr);
|
|
8600
8647
|
tables.push(...leftResult.tables);
|
|
8601
|
-
|
|
8602
|
-
const placeholders = values.map(() => "?").join(", ");
|
|
8603
|
-
params.push(...toSqliteParams(values));
|
|
8648
|
+
const rhsJson = JSON.stringify(values);
|
|
8604
8649
|
// Wrap left side in extra parentheses to ensure correct precedence (e.g., NOT has lower precedence than IN in SQL)
|
|
8605
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);
|
|
8606
8668
|
return {
|
|
8607
|
-
sql: `(
|
|
8669
|
+
sql: `cypher_to_json_bool(EXISTS(SELECT 1 FROM json_each(?) WHERE cypher_bool_eq(${leftSql}, value)))`,
|
|
8608
8670
|
tables,
|
|
8609
8671
|
params,
|
|
8610
8672
|
};
|
|
@@ -8613,7 +8675,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8613
8675
|
const paramValue = this.ctx.paramValues[listExpr.name];
|
|
8614
8676
|
if (Array.isArray(paramValue)) {
|
|
8615
8677
|
if (paramValue.length === 0) {
|
|
8616
|
-
return { sql: "
|
|
8678
|
+
return { sql: "json('false')", tables, params }; // false for empty list
|
|
8617
8679
|
}
|
|
8618
8680
|
// Check if RHS contains complex types
|
|
8619
8681
|
const rhsHasComplexTypes = paramValue.some(containsComplexTypes);
|
|
@@ -8626,7 +8688,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8626
8688
|
const lhsJson = JSON.stringify(leftExpr.value);
|
|
8627
8689
|
params.push(rhsJson, lhsJson);
|
|
8628
8690
|
return {
|
|
8629
|
-
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(?)))`,
|
|
8630
8692
|
tables,
|
|
8631
8693
|
params,
|
|
8632
8694
|
};
|
|
@@ -8636,7 +8698,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8636
8698
|
params.push(...leftResult.params);
|
|
8637
8699
|
params.push(rhsJson);
|
|
8638
8700
|
return {
|
|
8639
|
-
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})))`,
|
|
8640
8702
|
tables,
|
|
8641
8703
|
params,
|
|
8642
8704
|
};
|
|
@@ -8650,7 +8712,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8650
8712
|
// Wrap left side in extra parentheses to ensure correct precedence (e.g., NOT has lower precedence than IN in SQL)
|
|
8651
8713
|
const leftSql = leftExpr.type === "unary" ? `(${leftResult.sql})` : leftResult.sql;
|
|
8652
8714
|
return {
|
|
8653
|
-
sql: `(${leftSql} IN (${placeholders}))`,
|
|
8715
|
+
sql: `cypher_to_json_bool(${leftSql} IN (${placeholders}))`,
|
|
8654
8716
|
tables,
|
|
8655
8717
|
params,
|
|
8656
8718
|
};
|
|
@@ -8667,7 +8729,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8667
8729
|
params.push(...listResult.params);
|
|
8668
8730
|
params.push(lhsJson);
|
|
8669
8731
|
return {
|
|
8670
|
-
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(?)))`,
|
|
8671
8733
|
tables,
|
|
8672
8734
|
params,
|
|
8673
8735
|
};
|
|
@@ -8680,7 +8742,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8680
8742
|
params.push(...listResult.params);
|
|
8681
8743
|
params.push(...leftResult.params);
|
|
8682
8744
|
return {
|
|
8683
|
-
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})))`,
|
|
8684
8746
|
tables,
|
|
8685
8747
|
params,
|
|
8686
8748
|
};
|
|
@@ -8694,7 +8756,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8694
8756
|
// Wrap left side in extra parentheses to ensure correct precedence (e.g., NOT has lower precedence than IN in SQL)
|
|
8695
8757
|
const leftSql = leftExpr.type === "unary" ? `(${leftResult.sql})` : leftResult.sql;
|
|
8696
8758
|
return {
|
|
8697
|
-
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})))`,
|
|
8698
8760
|
tables,
|
|
8699
8761
|
params,
|
|
8700
8762
|
};
|
|
@@ -8717,7 +8779,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8717
8779
|
if (stringOp === "CONTAINS") {
|
|
8718
8780
|
// INSTR returns position (1-based) if found, 0 if not found
|
|
8719
8781
|
return {
|
|
8720
|
-
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`,
|
|
8721
8783
|
tables,
|
|
8722
8784
|
// leftResult.sql appears 3 times, rightResult.sql appears 3 times
|
|
8723
8785
|
params: [...leftResult.params, ...leftResult.params, ...rightResult.params, ...rightResult.params, ...leftResult.params, ...rightResult.params],
|
|
@@ -8726,7 +8788,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8726
8788
|
else if (stringOp === "STARTS WITH") {
|
|
8727
8789
|
// Use SUBSTR for case-sensitive prefix match
|
|
8728
8790
|
return {
|
|
8729
|
-
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`,
|
|
8730
8792
|
tables,
|
|
8731
8793
|
// leftResult.sql appears 3 times, rightResult.sql appears 5 times
|
|
8732
8794
|
params: [...leftResult.params, ...leftResult.params, ...rightResult.params, ...rightResult.params, ...leftResult.params, ...rightResult.params, ...rightResult.params],
|
|
@@ -8736,7 +8798,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8736
8798
|
// ENDS WITH
|
|
8737
8799
|
// Use CASE to handle: 1) type check 2) empty suffix edge case, 3) case-sensitive suffix match
|
|
8738
8800
|
return {
|
|
8739
|
-
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`,
|
|
8740
8802
|
tables,
|
|
8741
8803
|
// leftResult.sql appears 4 times, rightResult.sql appears 6 times
|
|
8742
8804
|
params: [...leftResult.params, ...leftResult.params, ...rightResult.params, ...rightResult.params, ...rightResult.params, ...leftResult.params, ...rightResult.params, ...rightResult.params],
|
|
@@ -8970,7 +9032,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8970
9032
|
// Use custom cypher_and/cypher_or functions for proper JSON boolean handling
|
|
8971
9033
|
const func = expr.operator === "AND" ? "cypher_and" : "cypher_or";
|
|
8972
9034
|
return {
|
|
8973
|
-
sql:
|
|
9035
|
+
sql: `cypher_to_json_bool(${func}(${leftResult.sql}, ${rightResult.sql}))`,
|
|
8974
9036
|
tables,
|
|
8975
9037
|
params,
|
|
8976
9038
|
};
|
|
@@ -8983,13 +9045,11 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
8983
9045
|
const leftSql = leftResult.sql;
|
|
8984
9046
|
const rightSql = rightResult.sql;
|
|
8985
9047
|
// XOR with NULL semantics: (a XOR b) = (a AND NOT b) OR (NOT a AND b)
|
|
8986
|
-
//
|
|
8987
|
-
// 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
|
|
8988
9049
|
// Note: params are duplicated because the formula uses each operand twice:
|
|
8989
|
-
// ((left AND NOT right) OR (NOT left AND right))
|
|
8990
9050
|
const xorParams = [...leftResult.params, ...rightResult.params, ...leftResult.params, ...rightResult.params];
|
|
8991
9051
|
return {
|
|
8992
|
-
sql: `((${leftSql}
|
|
9052
|
+
sql: `cypher_to_json_bool(cypher_or(cypher_and(${leftSql}, cypher_not(${rightSql})), cypher_and(cypher_not(${leftSql}), ${rightSql})))`,
|
|
8993
9053
|
tables,
|
|
8994
9054
|
params: xorParams,
|
|
8995
9055
|
};
|
|
@@ -9614,8 +9674,13 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9614
9674
|
if (Array.isArray(expr.value)) {
|
|
9615
9675
|
return this.translateArrayLiteral(expr.value);
|
|
9616
9676
|
}
|
|
9617
|
-
|
|
9618
|
-
|
|
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);
|
|
9619
9684
|
return { sql: "?", params };
|
|
9620
9685
|
case "property": {
|
|
9621
9686
|
// Use subquery to get property from the created node
|
|
@@ -9746,22 +9811,23 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9746
9811
|
if (leftCouldBeNaN || rightCouldBeNaN) {
|
|
9747
9812
|
const op = expr.comparisonOperator;
|
|
9748
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
|
|
9749
9815
|
if (op === "=") {
|
|
9750
9816
|
// NaN = anything is false (including NaN = NaN)
|
|
9751
|
-
// If the comparison returns NULL (because of NaN), return false
|
|
9817
|
+
// If the comparison returns NULL (because of NaN), return false
|
|
9752
9818
|
return {
|
|
9753
|
-
sql: `
|
|
9819
|
+
sql: `CASE WHEN (${leftSql} ${op} ${rightSql}) IS NULL THEN json('false') WHEN (${leftSql} ${op} ${rightSql}) THEN json('true') ELSE json('false') END`,
|
|
9754
9820
|
tables,
|
|
9755
|
-
params,
|
|
9821
|
+
params: [...params, ...params],
|
|
9756
9822
|
};
|
|
9757
9823
|
}
|
|
9758
9824
|
else if (op === "<>") {
|
|
9759
9825
|
// NaN <> anything is true (including NaN <> NaN)
|
|
9760
|
-
// If the comparison returns NULL (because of NaN), return true
|
|
9826
|
+
// If the comparison returns NULL (because of NaN), return true
|
|
9761
9827
|
return {
|
|
9762
|
-
sql: `
|
|
9828
|
+
sql: `CASE WHEN (${leftSql} ${op} ${rightSql}) IS NULL THEN json('true') WHEN (${leftSql} ${op} ${rightSql}) THEN json('true') ELSE json('false') END`,
|
|
9763
9829
|
tables,
|
|
9764
|
-
params,
|
|
9830
|
+
params: [...params, ...params],
|
|
9765
9831
|
};
|
|
9766
9832
|
}
|
|
9767
9833
|
else {
|
|
@@ -9775,9 +9841,9 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9775
9841
|
else {
|
|
9776
9842
|
// NaN compared to numeric via range operators returns false
|
|
9777
9843
|
return {
|
|
9778
|
-
sql: `
|
|
9844
|
+
sql: `CASE WHEN (${leftSql} ${op} ${rightSql}) IS NULL THEN json('false') WHEN (${leftSql} ${op} ${rightSql}) THEN json('true') ELSE json('false') END`,
|
|
9779
9845
|
tables,
|
|
9780
|
-
params,
|
|
9846
|
+
params: [...params, ...params],
|
|
9781
9847
|
};
|
|
9782
9848
|
}
|
|
9783
9849
|
}
|
|
@@ -9796,7 +9862,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9796
9862
|
};
|
|
9797
9863
|
const func = opToFunc[expr.comparisonOperator];
|
|
9798
9864
|
return {
|
|
9799
|
-
sql:
|
|
9865
|
+
sql: `cypher_to_json_bool(${func}(${leftSql}, ${rightSql}))`,
|
|
9800
9866
|
tables,
|
|
9801
9867
|
params,
|
|
9802
9868
|
};
|
|
@@ -9809,7 +9875,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9809
9875
|
if ((expr.comparisonOperator === "=" || expr.comparisonOperator === "<>") && needsCypherEquals) {
|
|
9810
9876
|
if (expr.comparisonOperator === "=") {
|
|
9811
9877
|
return {
|
|
9812
|
-
sql: `cypher_equals(${leftSql}, ${rightSql})`,
|
|
9878
|
+
sql: `cypher_to_json_bool(cypher_equals(${leftSql}, ${rightSql}))`,
|
|
9813
9879
|
tables,
|
|
9814
9880
|
params,
|
|
9815
9881
|
};
|
|
@@ -9818,14 +9884,31 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9818
9884
|
// <> is NOT equals: invert the result, but preserve null
|
|
9819
9885
|
// We need to duplicate params because cypher_equals appears twice in the SQL
|
|
9820
9886
|
return {
|
|
9821
|
-
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`,
|
|
9822
9888
|
tables,
|
|
9823
9889
|
params: [...params, ...params],
|
|
9824
9890
|
};
|
|
9825
9891
|
}
|
|
9826
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
|
+
}
|
|
9827
9910
|
return {
|
|
9828
|
-
sql: `(${leftSql} ${expr.comparisonOperator} ${rightSql})`,
|
|
9911
|
+
sql: `cypher_to_json_bool(${leftSql} ${expr.comparisonOperator} ${rightSql})`,
|
|
9829
9912
|
tables,
|
|
9830
9913
|
params,
|
|
9831
9914
|
};
|
|
@@ -9952,6 +10035,197 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
9952
10035
|
params.push(...mapParams, ...listResult.params, ...filterParams);
|
|
9953
10036
|
return { sql, tables, params };
|
|
9954
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
|
+
}
|
|
9955
10229
|
/**
|
|
9956
10230
|
* Translate a pattern comprehension expression.
|
|
9957
10231
|
* Syntax: [(pattern) WHERE filterCondition | mapExpr]
|
|
@@ -10097,6 +10371,118 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10097
10371
|
tables.push(boundVarInfo.alias);
|
|
10098
10372
|
return { sql, tables, params };
|
|
10099
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
|
+
}
|
|
10100
10486
|
/**
|
|
10101
10487
|
* Translate an expression within a pattern comprehension.
|
|
10102
10488
|
*/
|
|
@@ -10225,9 +10611,14 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10225
10611
|
if (expr.value === null) {
|
|
10226
10612
|
return { sql: "NULL", params };
|
|
10227
10613
|
}
|
|
10228
|
-
//
|
|
10229
|
-
|
|
10230
|
-
|
|
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);
|
|
10231
10622
|
return { sql: "?", params };
|
|
10232
10623
|
case "parameter": {
|
|
10233
10624
|
const paramValue = this.ctx.paramValues[expr.name];
|
|
@@ -10352,10 +10743,34 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10352
10743
|
}
|
|
10353
10744
|
case "expression": {
|
|
10354
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
|
|
10355
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
|
+
];
|
|
10356
10771
|
return {
|
|
10357
|
-
sql:
|
|
10358
|
-
params:
|
|
10772
|
+
sql: wrappedSql,
|
|
10773
|
+
params: allParams,
|
|
10359
10774
|
};
|
|
10360
10775
|
}
|
|
10361
10776
|
case "listPredicate": {
|
|
@@ -10790,7 +11205,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10790
11205
|
params.push(...operandResult.params);
|
|
10791
11206
|
// Use custom cypher_not function that properly handles JSON booleans and integers
|
|
10792
11207
|
return {
|
|
10793
|
-
sql: `cypher_not(${operandResult.sql})`,
|
|
11208
|
+
sql: `cypher_to_json_bool(cypher_not(${operandResult.sql}))`,
|
|
10794
11209
|
tables,
|
|
10795
11210
|
params,
|
|
10796
11211
|
};
|
|
@@ -10816,6 +11231,15 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10816
11231
|
params: [...left.params, ...right.params],
|
|
10817
11232
|
};
|
|
10818
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
|
+
}
|
|
10819
11243
|
return {
|
|
10820
11244
|
sql: `${left.sql} ${condition.operator} ${right.sql}`,
|
|
10821
11245
|
params: [...left.params, ...right.params],
|
|
@@ -10874,6 +11298,15 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
10874
11298
|
params: [...left.params, ...right.params, ...right.params, ...left.params, ...right.params, ...right.params],
|
|
10875
11299
|
};
|
|
10876
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
|
+
}
|
|
10877
11310
|
case "isNull": {
|
|
10878
11311
|
const left = this.translateWhereExpression(condition.left);
|
|
10879
11312
|
return {
|
|
@@ -11730,6 +12163,11 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
11730
12163
|
params: [],
|
|
11731
12164
|
};
|
|
11732
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
|
+
}
|
|
11733
12171
|
default:
|
|
11734
12172
|
throw new Error(`Unknown expression type in WHERE: ${expr.type}`);
|
|
11735
12173
|
}
|
|
@@ -11758,7 +12196,14 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
|
|
|
11758
12196
|
};
|
|
11759
12197
|
}
|
|
11760
12198
|
case "literal": {
|
|
11761
|
-
|
|
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;
|
|
11762
12207
|
if (typeof value === "number" && expr.numberLiteralKind === "float" && expr.raw) {
|
|
11763
12208
|
return { sql: expr.raw, tables, params };
|
|
11764
12209
|
}
|