Skip to content

Commit 0df2e39

Browse files
committed
fix: support undirected relationship patterns
1 parent 31a5b27 commit 0df2e39

7 files changed

Lines changed: 78 additions & 26 deletions

File tree

cypher/models/cypher/format/format.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,11 @@ func (s Emitter) formatNodePattern(output io.Writer, nodePattern *cypher.NodePat
8080

8181
func (s Emitter) formatRelationshipPattern(output io.Writer, relationshipPattern *cypher.RelationshipPattern) error {
8282
switch relationshipPattern.Direction {
83-
case graph.DirectionOutbound:
83+
case graph.DirectionOutbound, graph.DirectionBoth:
8484
if _, err := io.WriteString(output, "-["); err != nil {
8585
return err
8686
}
8787

88-
case graph.DirectionBoth:
89-
fallthrough
90-
9188
case graph.DirectionInbound:
9289
if _, err := io.WriteString(output, "<-["); err != nil {
9390
return err
@@ -147,14 +144,11 @@ func (s Emitter) formatRelationshipPattern(output io.Writer, relationshipPattern
147144
}
148145

149146
switch relationshipPattern.Direction {
150-
case graph.DirectionInbound:
147+
case graph.DirectionInbound, graph.DirectionBoth:
151148
if _, err := io.WriteString(output, "]-"); err != nil {
152149
return err
153150
}
154151

155-
case graph.DirectionBoth:
156-
fallthrough
157-
158152
case graph.DirectionOutbound:
159153
if _, err := io.WriteString(output, "]->"); err != nil {
160154
return err

cypher/models/cypher/format/format_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/specterops/dawgs/cypher/models/cypher"
88
"github.com/specterops/dawgs/cypher/models/cypher/format"
9+
"github.com/specterops/dawgs/graph"
910

1011
"github.com/specterops/dawgs/cypher/frontend"
1112
"github.com/stretchr/testify/require"
@@ -27,6 +28,55 @@ func TestCypherEmitter_StripLiterals(t *testing.T) {
2728
require.Equal(t, "match (n {value: $STRIPPED}) where n.other = $STRIPPED and n.number = $STRIPPED return n.name, n", buffer.String())
2829
}
2930

31+
func TestCypherEmitter_RelationshipDirections(t *testing.T) {
32+
testCases := []struct {
33+
name string
34+
direction graph.Direction
35+
expected string
36+
}{
37+
{
38+
name: "outbound",
39+
direction: graph.DirectionOutbound,
40+
expected: "match (a)-[r]->(b) return r",
41+
},
42+
{
43+
name: "inbound",
44+
direction: graph.DirectionInbound,
45+
expected: "match (a)<-[r]-(b) return r",
46+
},
47+
{
48+
name: "both",
49+
direction: graph.DirectionBoth,
50+
expected: "match (a)-[r]-(b) return r",
51+
},
52+
}
53+
54+
for _, testCase := range testCases {
55+
t.Run(testCase.name, func(t *testing.T) {
56+
regularQuery, singlePartQuery := cypher.NewRegularQueryWithSingleQuery()
57+
match := singlePartQuery.NewReadingClause().NewMatch(false)
58+
match.NewPatternPart().AddPatternElements(
59+
&cypher.NodePattern{
60+
Variable: cypher.NewVariableWithSymbol("a"),
61+
},
62+
&cypher.RelationshipPattern{
63+
Variable: cypher.NewVariableWithSymbol("r"),
64+
Direction: testCase.direction,
65+
},
66+
&cypher.NodePattern{
67+
Variable: cypher.NewVariableWithSymbol("b"),
68+
},
69+
)
70+
71+
singlePartQuery.NewProjection(false).AddItem(cypher.NewProjectionItemWithExpr(cypher.NewVariableWithSymbol("r")))
72+
73+
rendered, err := format.RegularQuery(regularQuery, false)
74+
require.NoError(t, err)
75+
require.Equal(t, testCase.expected, rendered)
76+
})
77+
}
78+
}
79+
3080
func TestCypherEmitter_HappyPath(t *testing.T) {
3181
test.LoadFixture(t, test.MutationTestCases).Run(t)
3282
test.LoadFixture(t, test.PositiveTestCases).Run(t)

cypher/test/cases/mutation_tests.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "Multipart query with mutation",
55
"type": "string_match",
66
"details": {
7-
"query": "match (s:Ship {name: 'Nebuchadnezzar'}) with s as ship merge p = (c:Crew {name: 'Neo'})\u003c-[:CrewOf]-\u003e(ship) set c.title = 'The One' return p",
7+
"query": "match (s:Ship {name: 'Nebuchadnezzar'}) with s as ship merge p = (c:Crew {name: 'Neo'})-[:CrewOf]-(ship) set c.title = 'The One' return p",
88
"fitness": 7
99
}
1010
},

cypher/test/cases/positive_tests.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@
189189
"name": "Specify bi-directional relationship",
190190
"type": "string_match",
191191
"details": {
192-
"query": "match (p:Person)\u003c-[]-\u003e(m:Movie) return m",
192+
"query": "match (p:Person)-[]-(m:Movie) return m",
193193
"fitness": 0
194194
}
195195
},
@@ -437,7 +437,7 @@
437437
"name": "built-in shortestPaths()",
438438
"type": "string_match",
439439
"details": {
440-
"query": "match p = shortestPath((p1:Person)\u003c-[*]-\u003e(p2:Person)) where p1.name = 'tom' and p2.name = 'jerry' return p",
440+
"query": "match p = shortestPath((p1:Person)-[*]-(p2:Person)) where p1.name = 'tom' and p2.name = 'jerry' return p",
441441
"fitness": 17
442442
}
443443
},
@@ -453,15 +453,15 @@
453453
"name": "Find nodes with relationships",
454454
"type": "string_match",
455455
"details": {
456-
"query": "match (b) where (b)\u003c-[]-\u003e() return b",
456+
"query": "match (b) where (b)-[]-() return b",
457457
"fitness": -4
458458
}
459459
},
460460
{
461461
"name": "Find nodes with no relationships",
462462
"type": "string_match",
463463
"details": {
464-
"query": "match (b) where not ((b)\u003c-[]-\u003e()) return b",
464+
"query": "match (b) where not ((b)-[]-()) return b",
465465
"fitness": -5
466466
}
467467
},
@@ -898,4 +898,4 @@
898898
}
899899
}
900900
]
901-
}
901+
}

query/neo4j/neo4j_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ func TestQueryBuilder_Render(t *testing.T) {
422422
query.Returning(
423423
query.Node(),
424424
),
425-
), "match (n) where (n)<-[]->() return n"))
425+
), "match (n) where (n)-[]-() return n"))
426426

427427
t.Run("Node has Relationships Order by Node Item", assertQueryResult(query.SinglePartQuery(
428428
query.Where(
@@ -436,7 +436,7 @@ func TestQueryBuilder_Render(t *testing.T) {
436436
query.OrderBy(
437437
query.Order(query.NodeProperty("value"), query.Ascending()),
438438
),
439-
), "match (n) where (n)<-[]->() return n order by n.value asc"))
439+
), "match (n) where (n)-[]-() return n order by n.value asc"))
440440

441441
t.Run("Node has Relationships Order by Node Item", assertQueryResult(query.SinglePartQuery(
442442
query.Where(
@@ -451,7 +451,7 @@ func TestQueryBuilder_Render(t *testing.T) {
451451
query.Order(query.NodeProperty("value_1"), query.Ascending()),
452452
query.Order(query.NodeProperty("value_2"), query.Descending()),
453453
),
454-
), "match (n) where (n)<-[]->() return n order by n.value_1 asc, n.value_2 desc"))
454+
), "match (n) where (n)-[]-() return n order by n.value_1 asc, n.value_2 desc"))
455455

456456
t.Run("Node has Relationships Order by Node Item with Limit and Offset", assertQueryResult(query.SinglePartQuery(
457457
query.Where(
@@ -469,7 +469,7 @@ func TestQueryBuilder_Render(t *testing.T) {
469469

470470
query.Limit(10),
471471
query.Offset(20),
472-
), "match (n) where (n)<-[]->() return n order by n.value_1 asc, n.value_2 desc skip 20 limit 10"))
472+
), "match (n) where (n)-[]-() return n order by n.value_1 asc, n.value_2 desc skip 20 limit 10"))
473473

474474
t.Run("Node has no Relationships", assertQueryResult(query.SinglePartQuery(
475475
query.Where(
@@ -479,7 +479,7 @@ func TestQueryBuilder_Render(t *testing.T) {
479479
query.Returning(
480480
query.Node(),
481481
),
482-
), "match (n) where not ((n)<-[]->()) return n"))
482+
), "match (n) where not ((n)-[]-()) return n"))
483483

484484
t.Run("Node Datetime Before", assertQueryResult(query.SinglePartQuery(
485485
query.Where(

query/v2/query_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,13 @@ func TestUnicodeCypherSymbols(t *testing.T) {
274274
func TestInvalidRelationshipDirectionReturnsError(t *testing.T) {
275275
_, err := v2.New().WithRelationshipDirection(graph.Direction(99)).Return(v2.Relationship()).Build()
276276
require.ErrorContains(t, err, "unsupported relationship direction: invalid")
277+
}
278+
279+
func TestRelationshipDirectionBoth(t *testing.T) {
280+
preparedQuery, err := v2.New().WithRelationshipDirection(graph.DirectionBoth).Return(v2.Relationship()).Build()
281+
require.NoError(t, err)
277282

278-
_, err = v2.New().WithRelationshipDirection(graph.DirectionBoth).Return(v2.Relationship()).Build()
279-
require.ErrorContains(t, err, "unsupported relationship direction: both")
283+
require.Equal(t, "match ()-[r]-() return r", renderPrepared(t, preparedQuery))
280284
}
281285

282286
func TestShortestPathControls(t *testing.T) {
@@ -500,13 +504,17 @@ func TestEmptyLogicalHelpersReturnBuildErrors(t *testing.T) {
500504
require.ErrorContains(t, err, "or requires at least one operand")
501505
}
502506

503-
func TestInvalidExplicitRelationshipPatternDirectionReturnsError(t *testing.T) {
504-
_, err := v2.New().Create(
507+
func TestExplicitRelationshipPatternDirectionBoth(t *testing.T) {
508+
preparedQuery, err := v2.New().Create(
505509
v2.RelationshipPattern(graph.StringKind("Edge"), nil, graph.DirectionBoth),
506510
).Build()
507-
require.ErrorContains(t, err, "unsupported relationship direction: both")
511+
require.NoError(t, err)
512+
513+
require.Equal(t, "create (s)-[r:Edge]-(e)", renderPrepared(t, preparedQuery))
514+
}
508515

509-
_, err = v2.New().Create(
516+
func TestInvalidExplicitRelationshipPatternDirectionReturnsError(t *testing.T) {
517+
_, err := v2.New().Create(
510518
v2.Relationship().RelationshipPattern(graph.StringKind("Edge"), nil, graph.Direction(99)),
511519
).Build()
512520
require.ErrorContains(t, err, "unsupported relationship direction: invalid")

query/v2/util.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func prepareNodePattern(match *cypher.Match, seen *identifierSet, identifiers ru
9595

9696
func validateRelationshipDirection(direction graph.Direction) error {
9797
switch direction {
98-
case graph.DirectionInbound, graph.DirectionOutbound:
98+
case graph.DirectionInbound, graph.DirectionOutbound, graph.DirectionBoth:
9999
return nil
100100
default:
101101
return fmt.Errorf("unsupported relationship direction: %s", direction)

0 commit comments

Comments
 (0)