Skip to content

Commit ba3496e

Browse files
authored
Merge pull request #23 from microsoft/fix_node_and_relationship_references
Fix node and relationship references
2 parents ea1ee75 + a89e60f commit ba3496e

File tree

5 files changed

+126
-10
lines changed

5 files changed

+126
-10
lines changed

flowquery-py/src/parsing/parser.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,12 @@ def _parse_node(self) -> Optional[Node]:
474474
self._variables[identifier] = node
475475
elif identifier is not None:
476476
reference = self._variables.get(identifier)
477+
# Resolve through Expression -> Reference -> Node (e.g., after WITH)
478+
ref_child = reference.first_child() if isinstance(reference, Expression) else None
479+
if isinstance(ref_child, Reference):
480+
inner = ref_child.referred
481+
if isinstance(inner, Node):
482+
reference = inner
477483
if reference is None or not isinstance(reference, Node):
478484
raise ValueError(f"Undefined node reference: {identifier}")
479485
node = NodeReference(node, reference)
@@ -524,6 +530,13 @@ def _parse_relationship(self) -> Optional[Relationship]:
524530
self._variables[variable] = relationship
525531
elif variable is not None:
526532
reference = self._variables.get(variable)
533+
# Resolve through Expression -> Reference -> Relationship (e.g., after WITH)
534+
if isinstance(reference, Expression) and isinstance(
535+
reference.first_child(), Reference
536+
):
537+
inner = reference.first_child().referred
538+
if isinstance(inner, Relationship):
539+
reference = inner
527540
if reference is None or not isinstance(reference, Relationship):
528541
raise ValueError(f"Undefined relationship reference: {variable}")
529542
relationship = RelationshipReference(relationship, reference)

flowquery-py/tests/compute/test_runner.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1669,4 +1669,44 @@ async def test_reserved_keywords_as_relationship_types_and_labels(self):
16691669
await runner.run()
16701670
results = runner.results
16711671
assert len(results) == 1
1672-
assert results[0] == {"name1": "Node 1", "name2": "Node 2"}
1672+
assert results[0] == {"name1": "Node 1", "name2": "Node 2"}
1673+
1674+
@pytest.mark.asyncio
1675+
async def test_match_with_node_reference_passed_through_with(self):
1676+
"""Test that node variables passed through WITH can be re-referenced in subsequent MATCH."""
1677+
await Runner("""
1678+
CREATE VIRTUAL (:WithRefUser) AS {
1679+
UNWIND [
1680+
{id: 1, name: 'Alice', mail: 'alice@test.com', jobTitle: 'CEO'},
1681+
{id: 2, name: 'Bob', mail: 'bob@test.com', jobTitle: 'VP'},
1682+
{id: 3, name: 'Carol', mail: 'carol@test.com', jobTitle: 'VP'},
1683+
{id: 4, name: 'Dave', mail: 'dave@test.com', jobTitle: 'Engineer'}
1684+
] AS record
1685+
RETURN record.id AS id, record.name AS name, record.mail AS mail, record.jobTitle AS jobTitle
1686+
}
1687+
""").run()
1688+
await Runner("""
1689+
CREATE VIRTUAL (:WithRefUser)-[:MANAGES]-(:WithRefUser) AS {
1690+
UNWIND [
1691+
{left_id: 1, right_id: 2},
1692+
{left_id: 1, right_id: 3},
1693+
{left_id: 2, right_id: 4}
1694+
] AS record
1695+
RETURN record.left_id AS left_id, record.right_id AS right_id
1696+
}
1697+
""").run()
1698+
runner = Runner("""
1699+
MATCH (ceo:WithRefUser)-[:MANAGES]->(dr1:WithRefUser)
1700+
WHERE ceo.jobTitle = 'CEO'
1701+
WITH ceo, dr1
1702+
MATCH (ceo)-[:MANAGES]->(dr2:WithRefUser)
1703+
WHERE dr1.mail <> dr2.mail
1704+
RETURN ceo.name AS ceo, dr1.name AS dr1, dr2.name AS dr2
1705+
""")
1706+
await runner.run()
1707+
results = runner.results
1708+
# CEO (Alice) manages Bob and Carol. All distinct pairs:
1709+
# (Alice, Bob, Carol) and (Alice, Carol, Bob)
1710+
assert len(results) == 2
1711+
assert results[0] == {"ceo": "Alice", "dr1": "Bob", "dr2": "Carol"}
1712+
assert results[1] == {"ceo": "Alice", "dr1": "Carol", "dr2": "Bob"}

src/parsing/expressions/reference.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import Identifier from "./identifier";
33

44
/**
55
* Represents a reference to a previously defined variable or expression.
6-
*
6+
*
77
* References point to values defined earlier in the query (e.g., in WITH or LOAD statements).
8-
*
8+
*
99
* @example
1010
* ```typescript
1111
* const ref = new Reference("myVar", previousNode);
@@ -14,17 +14,20 @@ import Identifier from "./identifier";
1414
*/
1515
class Reference extends Identifier {
1616
private _referred: ASTNode | undefined = undefined;
17-
17+
1818
/**
1919
* Creates a new Reference to a variable.
20-
*
20+
*
2121
* @param value - The identifier name
2222
* @param referred - The node this reference points to (optional)
2323
*/
2424
constructor(value: string, referred: ASTNode | undefined = undefined) {
2525
super(value);
2626
this._referred = referred;
2727
}
28+
public get referred(): ASTNode | undefined {
29+
return this._referred;
30+
}
2831
public set referred(node: ASTNode) {
2932
this._referred = node;
3033
}
@@ -39,4 +42,4 @@ class Reference extends Identifier {
3942
}
4043
}
4144

42-
export default Reference;
45+
export default Reference;

src/parsing/parser.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,15 @@ class Parser extends BaseParser {
438438
node.identifier = identifier;
439439
this.variables.set(identifier, node);
440440
} else if (identifier !== null) {
441-
const reference = this.variables.get(identifier);
442-
if (reference === undefined || reference.constructor !== Node) {
441+
let reference = this.variables.get(identifier);
442+
// Resolve through Expression -> Reference -> Node (e.g., after WITH)
443+
if (reference instanceof Expression && reference.firstChild() instanceof Reference) {
444+
const inner = (reference.firstChild() as Reference).referred;
445+
if (inner instanceof Node) {
446+
reference = inner;
447+
}
448+
}
449+
if (reference === undefined || !(reference instanceof Node)) {
443450
throw new Error(`Undefined node reference: ${identifier}`);
444451
}
445452
node = new NodeReference(node, reference);
@@ -629,8 +636,15 @@ class Parser extends BaseParser {
629636
relationship.identifier = variable;
630637
this.variables.set(variable, relationship);
631638
} else if (variable !== null) {
632-
const reference = this.variables.get(variable);
633-
if (reference === undefined || reference.constructor !== Relationship) {
639+
let reference = this.variables.get(variable);
640+
// Resolve through Expression -> Reference -> Relationship (e.g., after WITH)
641+
if (reference instanceof Expression && reference.firstChild() instanceof Reference) {
642+
const inner = (reference.firstChild() as Reference).referred;
643+
if (inner instanceof Relationship) {
644+
reference = inner;
645+
}
646+
}
647+
if (reference === undefined || !(reference instanceof Relationship)) {
634648
throw new Error(`Undefined relationship reference: ${variable}`);
635649
}
636650
relationship = new RelationshipReference(relationship, reference);

tests/compute/runner.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,3 +1556,49 @@ test("Test reserved keywords as relationship types and labels", async () => {
15561556
expect(results.length).toBe(1);
15571557
expect(results[0]).toEqual({ name1: "Node 1", name2: "Node 2" });
15581558
});
1559+
1560+
test("Test match with node reference passed through WITH", async () => {
1561+
await new Runner(`
1562+
CREATE VIRTUAL (:User) AS {
1563+
UNWIND [
1564+
{id: 1, name: 'Alice', mail: 'alice@test.com', jobTitle: 'CEO'},
1565+
{id: 2, name: 'Bob', mail: 'bob@test.com', jobTitle: 'VP'},
1566+
{id: 3, name: 'Carol', mail: 'carol@test.com', jobTitle: 'VP'},
1567+
{id: 4, name: 'Dave', mail: 'dave@test.com', jobTitle: 'Engineer'}
1568+
] AS record
1569+
RETURN record.id AS id, record.name AS name, record.mail AS mail, record.jobTitle AS jobTitle
1570+
}
1571+
`).run();
1572+
await new Runner(`
1573+
CREATE VIRTUAL (:User)-[:MANAGES]-(:User) AS {
1574+
UNWIND [
1575+
{left_id: 1, right_id: 2},
1576+
{left_id: 1, right_id: 3},
1577+
{left_id: 2, right_id: 4}
1578+
] AS record
1579+
RETURN record.left_id AS left_id, record.right_id AS right_id
1580+
}
1581+
`).run();
1582+
// Equivalent to:
1583+
// MATCH (ceo:User)-[:MANAGES]->(dr1:User)
1584+
// WHERE ceo.jobTitle = 'CEO'
1585+
// WITH ceo, dr1
1586+
// MATCH (ceo)-[:MANAGES]->(dr2:User)
1587+
// WHERE dr1.mail <> dr2.mail
1588+
// RETURN ceo, dr1, dr2
1589+
const match = new Runner(`
1590+
MATCH (ceo:User)-[:MANAGES]->(dr1:User)
1591+
WHERE ceo.jobTitle = 'CEO'
1592+
WITH ceo, dr1
1593+
MATCH (ceo)-[:MANAGES]->(dr2:User)
1594+
WHERE dr1.mail <> dr2.mail
1595+
RETURN ceo.name AS ceo, dr1.name AS dr1, dr2.name AS dr2
1596+
`);
1597+
await match.run();
1598+
const results = match.results;
1599+
// CEO (Alice) manages Bob and Carol. All distinct pairs:
1600+
// (Alice, Bob, Carol) and (Alice, Carol, Bob)
1601+
expect(results.length).toBe(2);
1602+
expect(results[0]).toEqual({ ceo: "Alice", dr1: "Bob", dr2: "Carol" });
1603+
expect(results[1]).toEqual({ ceo: "Alice", dr1: "Carol", dr2: "Bob" });
1604+
});

0 commit comments

Comments
 (0)