flowquery 1.0.41 → 1.0.43
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/flowquery.min.js +1 -1
- package/dist/graph/node_reference.d.ts.map +1 -1
- package/dist/graph/node_reference.js +7 -3
- package/dist/graph/node_reference.js.map +1 -1
- package/dist/graph/relationship_match_collector.d.ts +1 -0
- package/dist/graph/relationship_match_collector.d.ts.map +1 -1
- package/dist/graph/relationship_match_collector.js +2 -6
- package/dist/graph/relationship_match_collector.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/graph/node_reference.py +5 -4
- package/flowquery-py/src/graph/relationship_match_collector.py +7 -8
- package/flowquery-py/tests/compute/test_runner.py +228 -1
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/node_reference.ts +5 -1
- package/src/graph/relationship_match_collector.ts +4 -1
- package/tests/compute/runner.test.ts +205 -0
|
@@ -32,10 +32,11 @@ class NodeReference(Node):
|
|
|
32
32
|
async def next(self) -> None:
|
|
33
33
|
"""Process next using the referenced node's value."""
|
|
34
34
|
ref_value = self._reference.value()
|
|
35
|
-
if ref_value is
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
if ref_value is None:
|
|
36
|
+
return
|
|
37
|
+
self.set_value(dict(ref_value))
|
|
38
|
+
if self._outgoing and self._value:
|
|
39
|
+
await self._outgoing.find(self._value['id'])
|
|
39
40
|
await self.run_todo_next()
|
|
40
41
|
|
|
41
42
|
async def find(self, id_: str, hop: int = 0) -> None:
|
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Dict, List, Optional,
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from .node import Node
|
|
9
9
|
from .relationship import Relationship
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
endNode: Any
|
|
17
|
-
properties: Dict[str, Any]
|
|
11
|
+
# A relationship match record is a plain dict with known keys (type,
|
|
12
|
+
# startNode, endNode, properties) plus any extra relationship-property
|
|
13
|
+
# keys spread at the top level – mirroring the TypeScript version that
|
|
14
|
+
# uses an index signature ``[key: string]: any``.
|
|
15
|
+
RelationshipMatchRecord = Dict[str, Any]
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
class RelationshipMatchCollector:
|
|
@@ -36,6 +34,7 @@ class RelationshipMatchCollector:
|
|
|
36
34
|
actual_type = default_type
|
|
37
35
|
rel_props: Dict[str, Any] = (rel_data.properties() or {}) if rel_data else {}
|
|
38
36
|
match: RelationshipMatchRecord = {
|
|
37
|
+
**rel_props,
|
|
39
38
|
"type": actual_type,
|
|
40
39
|
"startNode": start_node_value or {},
|
|
41
40
|
"endNode": None,
|
|
@@ -3542,6 +3542,76 @@ class TestRunner:
|
|
|
3542
3542
|
assert len(results) == 1
|
|
3543
3543
|
assert results[0] == {"sum": 0}
|
|
3544
3544
|
|
|
3545
|
+
@pytest.mark.asyncio
|
|
3546
|
+
async def test_relationship_properties_direct_dot_notation(self):
|
|
3547
|
+
"""Test relationship properties can be accessed directly via dot notation."""
|
|
3548
|
+
await Runner(
|
|
3549
|
+
"""
|
|
3550
|
+
CREATE VIRTUAL (:RCity) AS {
|
|
3551
|
+
unwind [
|
|
3552
|
+
{id: 1, name: 'NYC'},
|
|
3553
|
+
{id: 2, name: 'LA'}
|
|
3554
|
+
] as record
|
|
3555
|
+
RETURN record.id as id, record.name as name
|
|
3556
|
+
}
|
|
3557
|
+
"""
|
|
3558
|
+
).run()
|
|
3559
|
+
await Runner(
|
|
3560
|
+
"""
|
|
3561
|
+
CREATE VIRTUAL (:RCity)-[:RFLIGHT]-(:RCity) AS {
|
|
3562
|
+
unwind [
|
|
3563
|
+
{left_id: 1, right_id: 2, airline: 'Delta', duration: 5}
|
|
3564
|
+
] as record
|
|
3565
|
+
RETURN record.left_id as left_id, record.right_id as right_id, record.airline as airline, record.duration as duration
|
|
3566
|
+
}
|
|
3567
|
+
"""
|
|
3568
|
+
).run()
|
|
3569
|
+
match = Runner(
|
|
3570
|
+
"""
|
|
3571
|
+
MATCH (a:RCity)-[r:RFLIGHT]->(b:RCity)
|
|
3572
|
+
RETURN a.name AS from, b.name AS to, r.airline AS airline, r.duration AS duration
|
|
3573
|
+
"""
|
|
3574
|
+
)
|
|
3575
|
+
await match.run()
|
|
3576
|
+
results = match.results
|
|
3577
|
+
assert len(results) == 1
|
|
3578
|
+
assert results[0] == {"from": "NYC", "to": "LA", "airline": "Delta", "duration": 5}
|
|
3579
|
+
|
|
3580
|
+
@pytest.mark.asyncio
|
|
3581
|
+
async def test_relationship_properties_direct_and_via_properties_function(self):
|
|
3582
|
+
"""Test relationship properties accessible via both direct access and properties()."""
|
|
3583
|
+
await Runner(
|
|
3584
|
+
"""
|
|
3585
|
+
CREATE VIRTUAL (:RPerson) AS {
|
|
3586
|
+
unwind [
|
|
3587
|
+
{id: 1, name: 'Alice'},
|
|
3588
|
+
{id: 2, name: 'Bob'}
|
|
3589
|
+
] as record
|
|
3590
|
+
RETURN record.id as id, record.name as name
|
|
3591
|
+
}
|
|
3592
|
+
"""
|
|
3593
|
+
).run()
|
|
3594
|
+
await Runner(
|
|
3595
|
+
"""
|
|
3596
|
+
CREATE VIRTUAL (:RPerson)-[:RKNOWS]-(:RPerson) AS {
|
|
3597
|
+
unwind [
|
|
3598
|
+
{left_id: 1, right_id: 2, since: 2020, strength: 'strong'}
|
|
3599
|
+
] as record
|
|
3600
|
+
RETURN record.left_id as left_id, record.right_id as right_id, record.since as since, record.strength as strength
|
|
3601
|
+
}
|
|
3602
|
+
"""
|
|
3603
|
+
).run()
|
|
3604
|
+
match = Runner(
|
|
3605
|
+
"""
|
|
3606
|
+
MATCH (a:RPerson)-[r:RKNOWS]->(b:RPerson)
|
|
3607
|
+
RETURN a.name AS from, b.name AS to, r.since AS since, r.strength AS strength, properties(r).since AS propSince
|
|
3608
|
+
"""
|
|
3609
|
+
)
|
|
3610
|
+
await match.run()
|
|
3611
|
+
results = match.results
|
|
3612
|
+
assert len(results) == 1
|
|
3613
|
+
assert results[0] == {"from": "Alice", "to": "Bob", "since": 2020, "strength": "strong", "propSince": 2020}
|
|
3614
|
+
|
|
3545
3615
|
@pytest.mark.asyncio
|
|
3546
3616
|
async def test_coalesce_returns_first_non_null(self):
|
|
3547
3617
|
"""Test coalesce returns first non-null value."""
|
|
@@ -4394,4 +4464,161 @@ class TestRunner:
|
|
|
4394
4464
|
"mentor": "Bob Jones",
|
|
4395
4465
|
"mentorJobTitle": "Staff Engineer",
|
|
4396
4466
|
"mentorDepartment": "Engineering",
|
|
4397
|
-
}
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
@pytest.mark.asyncio
|
|
4470
|
+
async def test_chained_optional_match_with_null_intermediate_node(self):
|
|
4471
|
+
"""Test chained OPTIONAL MATCH where intermediate node is null doesn't crash."""
|
|
4472
|
+
# Chain: Alice -> Bob -> Charlie (no outgoing)
|
|
4473
|
+
await Runner(
|
|
4474
|
+
"""
|
|
4475
|
+
CREATE VIRTUAL (:ChainEmp) AS {
|
|
4476
|
+
unwind [
|
|
4477
|
+
{id: 1, name: 'Alice'},
|
|
4478
|
+
{id: 2, name: 'Bob'},
|
|
4479
|
+
{id: 3, name: 'Charlie'}
|
|
4480
|
+
] as record
|
|
4481
|
+
RETURN record.id as id, record.name as name
|
|
4482
|
+
}
|
|
4483
|
+
"""
|
|
4484
|
+
).run()
|
|
4485
|
+
await Runner(
|
|
4486
|
+
"""
|
|
4487
|
+
CREATE VIRTUAL (:ChainEmp)-[:REPORTS_TO]-(:ChainEmp) AS {
|
|
4488
|
+
unwind [
|
|
4489
|
+
{left_id: 1, right_id: 2},
|
|
4490
|
+
{left_id: 2, right_id: 3}
|
|
4491
|
+
] as record
|
|
4492
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
4493
|
+
}
|
|
4494
|
+
"""
|
|
4495
|
+
).run()
|
|
4496
|
+
|
|
4497
|
+
# Alice -> Bob -> Charlie -> null -> null
|
|
4498
|
+
runner = Runner(
|
|
4499
|
+
"""
|
|
4500
|
+
MATCH (u:ChainEmp)
|
|
4501
|
+
WHERE u.name = "Alice"
|
|
4502
|
+
OPTIONAL MATCH (u)-[:REPORTS_TO]->(m1:ChainEmp)
|
|
4503
|
+
OPTIONAL MATCH (m1)-[:REPORTS_TO]->(m2:ChainEmp)
|
|
4504
|
+
OPTIONAL MATCH (m2)-[:REPORTS_TO]->(m3:ChainEmp)
|
|
4505
|
+
OPTIONAL MATCH (m3)-[:REPORTS_TO]->(m4:ChainEmp)
|
|
4506
|
+
RETURN
|
|
4507
|
+
u.name AS user,
|
|
4508
|
+
m1.name AS manager1,
|
|
4509
|
+
m2.name AS manager2,
|
|
4510
|
+
m3.name AS manager3,
|
|
4511
|
+
m4.name AS manager4
|
|
4512
|
+
"""
|
|
4513
|
+
)
|
|
4514
|
+
await runner.run()
|
|
4515
|
+
results = runner.results
|
|
4516
|
+
|
|
4517
|
+
assert len(results) == 1
|
|
4518
|
+
assert results[0]["user"] == "Alice"
|
|
4519
|
+
assert results[0]["manager1"] == "Bob"
|
|
4520
|
+
assert results[0]["manager2"] == "Charlie"
|
|
4521
|
+
assert results[0]["manager3"] is None
|
|
4522
|
+
assert results[0]["manager4"] is None
|
|
4523
|
+
|
|
4524
|
+
@pytest.mark.asyncio
|
|
4525
|
+
async def test_chained_optional_match_all_null_from_first(self):
|
|
4526
|
+
"""Test chained OPTIONAL MATCH where first optional returns null propagates nulls."""
|
|
4527
|
+
await Runner(
|
|
4528
|
+
"""
|
|
4529
|
+
CREATE VIRTUAL (:ChainWorker) AS {
|
|
4530
|
+
unwind [
|
|
4531
|
+
{id: 1, name: 'Solo'}
|
|
4532
|
+
] as record
|
|
4533
|
+
RETURN record.id as id, record.name as name
|
|
4534
|
+
}
|
|
4535
|
+
"""
|
|
4536
|
+
).run()
|
|
4537
|
+
await Runner(
|
|
4538
|
+
"""
|
|
4539
|
+
CREATE VIRTUAL (:ChainWorker)-[:MANAGES]-(:ChainWorker) AS {
|
|
4540
|
+
unwind [] as record
|
|
4541
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
4542
|
+
}
|
|
4543
|
+
"""
|
|
4544
|
+
).run()
|
|
4545
|
+
|
|
4546
|
+
# Solo has no MANAGES relationship
|
|
4547
|
+
runner = Runner(
|
|
4548
|
+
"""
|
|
4549
|
+
MATCH (u:ChainWorker)
|
|
4550
|
+
OPTIONAL MATCH (u)-[:MANAGES]->(m1:ChainWorker)
|
|
4551
|
+
OPTIONAL MATCH (m1)-[:MANAGES]->(m2:ChainWorker)
|
|
4552
|
+
OPTIONAL MATCH (m2)-[:MANAGES]->(m3:ChainWorker)
|
|
4553
|
+
RETURN
|
|
4554
|
+
u.name AS user,
|
|
4555
|
+
m1.name AS mgr1,
|
|
4556
|
+
m2.name AS mgr2,
|
|
4557
|
+
m3.name AS mgr3
|
|
4558
|
+
"""
|
|
4559
|
+
)
|
|
4560
|
+
await runner.run()
|
|
4561
|
+
results = runner.results
|
|
4562
|
+
|
|
4563
|
+
assert len(results) == 1
|
|
4564
|
+
assert results[0]["user"] == "Solo"
|
|
4565
|
+
assert results[0]["mgr1"] is None
|
|
4566
|
+
assert results[0]["mgr2"] is None
|
|
4567
|
+
assert results[0]["mgr3"] is None
|
|
4568
|
+
|
|
4569
|
+
@pytest.mark.asyncio
|
|
4570
|
+
async def test_chained_optional_match_mixed_null_and_non_null(self):
|
|
4571
|
+
"""Test chained OPTIONAL MATCH with multiple start nodes having different chain depths."""
|
|
4572
|
+
await Runner(
|
|
4573
|
+
"""
|
|
4574
|
+
CREATE VIRTUAL (:ChainStaff) AS {
|
|
4575
|
+
unwind [
|
|
4576
|
+
{id: 1, name: 'Dev'},
|
|
4577
|
+
{id: 2, name: 'Lead'},
|
|
4578
|
+
{id: 3, name: 'Director'},
|
|
4579
|
+
{id: 4, name: 'Intern'}
|
|
4580
|
+
] as record
|
|
4581
|
+
RETURN record.id as id, record.name as name
|
|
4582
|
+
}
|
|
4583
|
+
"""
|
|
4584
|
+
).run()
|
|
4585
|
+
await Runner(
|
|
4586
|
+
"""
|
|
4587
|
+
CREATE VIRTUAL (:ChainStaff)-[:REPORTS_TO]-(:ChainStaff) AS {
|
|
4588
|
+
unwind [
|
|
4589
|
+
{left_id: 1, right_id: 2},
|
|
4590
|
+
{left_id: 2, right_id: 3}
|
|
4591
|
+
] as record
|
|
4592
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
4593
|
+
}
|
|
4594
|
+
"""
|
|
4595
|
+
).run()
|
|
4596
|
+
|
|
4597
|
+
# Dev -> Lead -> Director -> null
|
|
4598
|
+
# Intern -> null -> null -> null
|
|
4599
|
+
runner = Runner(
|
|
4600
|
+
"""
|
|
4601
|
+
MATCH (u:ChainStaff)
|
|
4602
|
+
WHERE u.name = "Dev" OR u.name = "Intern"
|
|
4603
|
+
OPTIONAL MATCH (u)-[:REPORTS_TO]->(m1:ChainStaff)
|
|
4604
|
+
OPTIONAL MATCH (m1)-[:REPORTS_TO]->(m2:ChainStaff)
|
|
4605
|
+
OPTIONAL MATCH (m2)-[:REPORTS_TO]->(m3:ChainStaff)
|
|
4606
|
+
RETURN
|
|
4607
|
+
u.name AS user,
|
|
4608
|
+
m1.name AS mgr1,
|
|
4609
|
+
m2.name AS mgr2,
|
|
4610
|
+
m3.name AS mgr3
|
|
4611
|
+
"""
|
|
4612
|
+
)
|
|
4613
|
+
await runner.run()
|
|
4614
|
+
results = runner.results
|
|
4615
|
+
|
|
4616
|
+
assert len(results) == 2
|
|
4617
|
+
dev = next(r for r in results if r["user"] == "Dev")
|
|
4618
|
+
assert dev["mgr1"] == "Lead"
|
|
4619
|
+
assert dev["mgr2"] == "Director"
|
|
4620
|
+
assert dev["mgr3"] is None
|
|
4621
|
+
intern = next(r for r in results if r["user"] == "Intern")
|
|
4622
|
+
assert intern["mgr1"] is None
|
|
4623
|
+
assert intern["mgr2"] is None
|
|
4624
|
+
assert intern["mgr3"] is None
|