flowquery 1.0.41 → 1.0.42

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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowquery"
3
- version = "1.0.31"
3
+ version = "1.0.32"
4
4
  description = "A declarative query language for data processing pipelines"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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 not None:
36
- self.set_value(dict(ref_value))
37
- if self._outgoing and self._value:
38
- await self._outgoing.find(self._value['id'])
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:
@@ -4394,4 +4394,161 @@ class TestRunner:
4394
4394
  "mentor": "Bob Jones",
4395
4395
  "mentorJobTitle": "Staff Engineer",
4396
4396
  "mentorDepartment": "Engineering",
4397
- }
4397
+ }
4398
+
4399
+ @pytest.mark.asyncio
4400
+ async def test_chained_optional_match_with_null_intermediate_node(self):
4401
+ """Test chained OPTIONAL MATCH where intermediate node is null doesn't crash."""
4402
+ # Chain: Alice -> Bob -> Charlie (no outgoing)
4403
+ await Runner(
4404
+ """
4405
+ CREATE VIRTUAL (:ChainEmp) AS {
4406
+ unwind [
4407
+ {id: 1, name: 'Alice'},
4408
+ {id: 2, name: 'Bob'},
4409
+ {id: 3, name: 'Charlie'}
4410
+ ] as record
4411
+ RETURN record.id as id, record.name as name
4412
+ }
4413
+ """
4414
+ ).run()
4415
+ await Runner(
4416
+ """
4417
+ CREATE VIRTUAL (:ChainEmp)-[:REPORTS_TO]-(:ChainEmp) AS {
4418
+ unwind [
4419
+ {left_id: 1, right_id: 2},
4420
+ {left_id: 2, right_id: 3}
4421
+ ] as record
4422
+ RETURN record.left_id as left_id, record.right_id as right_id
4423
+ }
4424
+ """
4425
+ ).run()
4426
+
4427
+ # Alice -> Bob -> Charlie -> null -> null
4428
+ runner = Runner(
4429
+ """
4430
+ MATCH (u:ChainEmp)
4431
+ WHERE u.name = "Alice"
4432
+ OPTIONAL MATCH (u)-[:REPORTS_TO]->(m1:ChainEmp)
4433
+ OPTIONAL MATCH (m1)-[:REPORTS_TO]->(m2:ChainEmp)
4434
+ OPTIONAL MATCH (m2)-[:REPORTS_TO]->(m3:ChainEmp)
4435
+ OPTIONAL MATCH (m3)-[:REPORTS_TO]->(m4:ChainEmp)
4436
+ RETURN
4437
+ u.name AS user,
4438
+ m1.name AS manager1,
4439
+ m2.name AS manager2,
4440
+ m3.name AS manager3,
4441
+ m4.name AS manager4
4442
+ """
4443
+ )
4444
+ await runner.run()
4445
+ results = runner.results
4446
+
4447
+ assert len(results) == 1
4448
+ assert results[0]["user"] == "Alice"
4449
+ assert results[0]["manager1"] == "Bob"
4450
+ assert results[0]["manager2"] == "Charlie"
4451
+ assert results[0]["manager3"] is None
4452
+ assert results[0]["manager4"] is None
4453
+
4454
+ @pytest.mark.asyncio
4455
+ async def test_chained_optional_match_all_null_from_first(self):
4456
+ """Test chained OPTIONAL MATCH where first optional returns null propagates nulls."""
4457
+ await Runner(
4458
+ """
4459
+ CREATE VIRTUAL (:ChainWorker) AS {
4460
+ unwind [
4461
+ {id: 1, name: 'Solo'}
4462
+ ] as record
4463
+ RETURN record.id as id, record.name as name
4464
+ }
4465
+ """
4466
+ ).run()
4467
+ await Runner(
4468
+ """
4469
+ CREATE VIRTUAL (:ChainWorker)-[:MANAGES]-(:ChainWorker) AS {
4470
+ unwind [] as record
4471
+ RETURN record.left_id as left_id, record.right_id as right_id
4472
+ }
4473
+ """
4474
+ ).run()
4475
+
4476
+ # Solo has no MANAGES relationship
4477
+ runner = Runner(
4478
+ """
4479
+ MATCH (u:ChainWorker)
4480
+ OPTIONAL MATCH (u)-[:MANAGES]->(m1:ChainWorker)
4481
+ OPTIONAL MATCH (m1)-[:MANAGES]->(m2:ChainWorker)
4482
+ OPTIONAL MATCH (m2)-[:MANAGES]->(m3:ChainWorker)
4483
+ RETURN
4484
+ u.name AS user,
4485
+ m1.name AS mgr1,
4486
+ m2.name AS mgr2,
4487
+ m3.name AS mgr3
4488
+ """
4489
+ )
4490
+ await runner.run()
4491
+ results = runner.results
4492
+
4493
+ assert len(results) == 1
4494
+ assert results[0]["user"] == "Solo"
4495
+ assert results[0]["mgr1"] is None
4496
+ assert results[0]["mgr2"] is None
4497
+ assert results[0]["mgr3"] is None
4498
+
4499
+ @pytest.mark.asyncio
4500
+ async def test_chained_optional_match_mixed_null_and_non_null(self):
4501
+ """Test chained OPTIONAL MATCH with multiple start nodes having different chain depths."""
4502
+ await Runner(
4503
+ """
4504
+ CREATE VIRTUAL (:ChainStaff) AS {
4505
+ unwind [
4506
+ {id: 1, name: 'Dev'},
4507
+ {id: 2, name: 'Lead'},
4508
+ {id: 3, name: 'Director'},
4509
+ {id: 4, name: 'Intern'}
4510
+ ] as record
4511
+ RETURN record.id as id, record.name as name
4512
+ }
4513
+ """
4514
+ ).run()
4515
+ await Runner(
4516
+ """
4517
+ CREATE VIRTUAL (:ChainStaff)-[:REPORTS_TO]-(:ChainStaff) AS {
4518
+ unwind [
4519
+ {left_id: 1, right_id: 2},
4520
+ {left_id: 2, right_id: 3}
4521
+ ] as record
4522
+ RETURN record.left_id as left_id, record.right_id as right_id
4523
+ }
4524
+ """
4525
+ ).run()
4526
+
4527
+ # Dev -> Lead -> Director -> null
4528
+ # Intern -> null -> null -> null
4529
+ runner = Runner(
4530
+ """
4531
+ MATCH (u:ChainStaff)
4532
+ WHERE u.name = "Dev" OR u.name = "Intern"
4533
+ OPTIONAL MATCH (u)-[:REPORTS_TO]->(m1:ChainStaff)
4534
+ OPTIONAL MATCH (m1)-[:REPORTS_TO]->(m2:ChainStaff)
4535
+ OPTIONAL MATCH (m2)-[:REPORTS_TO]->(m3:ChainStaff)
4536
+ RETURN
4537
+ u.name AS user,
4538
+ m1.name AS mgr1,
4539
+ m2.name AS mgr2,
4540
+ m3.name AS mgr3
4541
+ """
4542
+ )
4543
+ await runner.run()
4544
+ results = runner.results
4545
+
4546
+ assert len(results) == 2
4547
+ dev = next(r for r in results if r["user"] == "Dev")
4548
+ assert dev["mgr1"] == "Lead"
4549
+ assert dev["mgr2"] == "Director"
4550
+ assert dev["mgr3"] is None
4551
+ intern = next(r for r in results if r["user"] == "Intern")
4552
+ assert intern["mgr1"] is None
4553
+ assert intern["mgr2"] is None
4554
+ assert intern["mgr3"] is None