Skip to content

Commit 5e25282

Browse files
wip
Signed-off-by: Michael Simons <[email protected]>
1 parent 3649ed0 commit 5e25282

File tree

7 files changed

+201
-14
lines changed

7 files changed

+201
-14
lines changed

neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/Cypher.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,33 @@ public static Node node(LabelExpression labelExpression) {
148148
return new InternalNodeImpl(Objects.requireNonNull(labelExpression), null);
149149
}
150150

151+
/**
152+
* @param expression
153+
* @return
154+
* @since 2025.1.0
155+
*/
156+
public static DynamicLabels allLabels(Expression expression) {
157+
return DynamicLabels.all(expression);
158+
}
159+
160+
/**
161+
* @param expression
162+
* @return
163+
* @since 2025.1.0
164+
*/
165+
public static DynamicLabels anyLabel(Expression expression) {
166+
return DynamicLabels.any(expression);
167+
}
168+
169+
/**
170+
* @param dynamicLabels
171+
* @return a node matching a dynamic label expression
172+
* @since 2025.1.0
173+
*/
174+
public static Node node(DynamicLabels dynamicLabels) {
175+
return new InternalNodeImpl(Objects.requireNonNull(dynamicLabels), null);
176+
}
177+
151178
/**
152179
* {@return the '*' wildcard literal}
153180
*/
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright (c) 2019-2025 "Neo4j,"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* https://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
package org.neo4j.cypherdsl.core;
20+
21+
import org.neo4j.cypherdsl.core.ast.Visitable;
22+
import org.neo4j.cypherdsl.core.ast.Visitor;
23+
24+
/**
25+
* This expression is modelled to represent matching <a href=
26+
* "https://neo4j.com/docs/cypher-manual/current/clauses/match/#dynamic-match">using
27+
* dynamic node labels and relationship types</a>. The underlying expression must in
28+
* someway resolve to <code>
29+
* STRING NOT NULL | LIST&lt;STRING NOT NULL&gt; NOT NULL
30+
* </code>
31+
*
32+
* @author Michael J. Simons
33+
* @since 2025.1.0
34+
*/
35+
public final class DynamicLabels implements Visitable {
36+
37+
/**
38+
* Returns a new dynamic label expression matching all labels.
39+
* @param expression the labels to match
40+
* @return a new dynamic label expression matching all labels
41+
*/
42+
static DynamicLabels all(Expression expression) {
43+
return new DynamicLabels(Modifier.ALL, expression);
44+
}
45+
46+
/**
47+
* Returns a new dynamic label expression matching any labels.
48+
* @param expression the labels to match
49+
* @return a new dynamic label expression matching any labels
50+
*/
51+
static DynamicLabels any(Expression expression) {
52+
return new DynamicLabels(Modifier.ANY, expression);
53+
}
54+
55+
private final Modifier modifier;
56+
57+
/**
58+
* The expression that should be dynamically matched. It may resolve to
59+
* <ul>
60+
* <li>A single string</li>
61+
* <li>A list of strings</li>
62+
* <li>A parameter holding either a single or a list of strings</li>
63+
* </ul>
64+
*/
65+
private final Expression labels;
66+
67+
DynamicLabels(Modifier modifier, Expression labels) {
68+
this.modifier = modifier;
69+
this.labels = labels;
70+
}
71+
72+
public Modifier getModifier() {
73+
return this.modifier;
74+
}
75+
76+
@Override
77+
public void accept(Visitor visitor) {
78+
visitor.enter(this);
79+
this.labels.accept(visitor);
80+
visitor.leave(this);
81+
}
82+
83+
/**
84+
* An enum describing whether a {@link DynamicLabels} should match all or any labels
85+
* the expression resolves to.
86+
*/
87+
public enum Modifier {
88+
89+
/** Matching all labels. */
90+
ALL,
91+
/** Matching any label. */
92+
ANY
93+
94+
}
95+
96+
}

neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/InternalNodeImpl.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,20 @@ final class InternalNodeImpl extends NodeBase<InternalNodeImpl> {
4242
}
4343

4444
InternalNodeImpl(LabelExpression labelExpression, Where innerPredicate) {
45-
super(null, null, labelExpression, null, innerPredicate);
45+
super(null, null, labelExpression, null, null, innerPredicate);
46+
}
47+
48+
InternalNodeImpl(DynamicLabels dynamicLabels, Where innerPredicate) {
49+
super(null, null, null, dynamicLabels, null, innerPredicate);
4650
}
4751

4852
InternalNodeImpl(String primaryLabel, String... additionalLabels) {
4953
super(primaryLabel, additionalLabels);
5054
}
5155

5256
InternalNodeImpl(SymbolicName symbolicName, List<NodeLabel> labels, LabelExpression labelExpression,
53-
Properties properties, Where innerPredicate) {
54-
super(symbolicName, labels, labelExpression, properties, innerPredicate);
57+
DynamicLabels dynamicLabels, Properties properties, Where innerPredicate) {
58+
super(symbolicName, labels, labelExpression, dynamicLabels, properties, innerPredicate);
5559
}
5660

5761
InternalNodeImpl(SymbolicName symbolicName, String primaryLabel, MapExpression properties,
@@ -63,16 +67,16 @@ final class InternalNodeImpl extends NodeBase<InternalNodeImpl> {
6367
public InternalNodeImpl named(SymbolicName newSymbolicName) {
6468

6569
Assertions.notNull(newSymbolicName, "Symbolic name is required.");
66-
return new InternalNodeImpl(newSymbolicName, this.labels, this.labelExpression, this.properties,
67-
this.innerPredicate);
70+
return new InternalNodeImpl(newSymbolicName, this.labels, this.labelExpression, this.dynamicLabels,
71+
this.properties, this.innerPredicate);
6872

6973
}
7074

7175
@Override
7276
public InternalNodeImpl withProperties(MapExpression newProperties) {
7377

7478
return new InternalNodeImpl(this.getSymbolicName().orElse(null), this.labels, this.labelExpression,
75-
Properties.create(newProperties), this.innerPredicate);
79+
this.dynamicLabels, Properties.create(newProperties), this.innerPredicate);
7680
}
7781

7882
@Override
@@ -82,7 +86,7 @@ public Node where(Expression predicate) {
8286
}
8387

8488
return new InternalNodeImpl(this.getSymbolicName().orElse(null), this.labels, this.labelExpression,
85-
this.properties, Where.from(predicate));
89+
this.dynamicLabels, this.properties, Where.from(predicate));
8690
}
8791

8892
}

neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/NodeBase.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public abstract class NodeBase<SELF extends Node> extends AbstractNode implement
4848

4949
final LabelExpression labelExpression;
5050

51+
final DynamicLabels dynamicLabels;
52+
5153
final Properties properties;
5254

5355
final Where innerPredicate;
@@ -78,7 +80,7 @@ protected NodeBase(String primaryLabel, String... additionalLabels) {
7880
* @param properties a set of properties
7981
*/
8082
protected NodeBase(SymbolicName symbolicName, List<NodeLabel> labels, Properties properties) {
81-
this(symbolicName, new ArrayList<>(labels), null, properties, null);
83+
this(symbolicName, new ArrayList<>(labels), null, null, properties, null);
8284
}
8385

8486
NodeBase() {
@@ -91,12 +93,13 @@ protected NodeBase(SymbolicName symbolicName, List<NodeLabel> labels, Properties
9193
this(symbolicName, assertLabels(primaryLabel, additionalLabels), Properties.create(properties));
9294
}
9395

94-
NodeBase(SymbolicName symbolicName, List<NodeLabel> labels, LabelExpression labelExpression, Properties properties,
95-
Where innerPredicate) {
96+
NodeBase(SymbolicName symbolicName, List<NodeLabel> labels, LabelExpression labelExpression,
97+
DynamicLabels dynamicLabels, Properties properties, Where innerPredicate) {
9698

9799
this.symbolicName = symbolicName;
98100
this.labels = labels;
99101
this.labelExpression = labelExpression;
102+
this.dynamicLabels = dynamicLabels;
100103
this.properties = properties;
101104
this.innerPredicate = innerPredicate;
102105
}
@@ -215,6 +218,7 @@ public final void accept(Visitor visitor) {
215218
this.labels.forEach(label -> label.accept(visitor));
216219
}
217220
Visitable.visitIfNotNull(this.labelExpression, visitor);
221+
Visitable.visitIfNotNull(this.dynamicLabels, visitor);
218222
Visitable.visitIfNotNull(this.properties, visitor);
219223
Visitable.visitIfNotNull(this.innerPredicate, visitor);
220224
visitor.leave(this);

neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/NodeLabels.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,15 @@
2020

2121
import java.util.List;
2222

23-
import org.apiguardian.api.API;
2423
import org.neo4j.cypherdsl.core.ast.Visitable;
2524
import org.neo4j.cypherdsl.core.ast.Visitor;
2625

27-
import static org.apiguardian.api.API.Status.STABLE;
28-
2926
/**
3027
* Makes a list of {@link NodeLabel node labels} visitable.
3128
*
3229
* @author Michael J. Simons
3330
* @since 1.0
3431
*/
35-
@API(status = STABLE, since = "1.0")
3632
final class NodeLabels implements Visitable {
3733

3834
private final List<NodeLabel> values;

neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/renderer/DefaultVisitor.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.neo4j.cypherdsl.core.CountExpression;
4141
import org.neo4j.cypherdsl.core.Create;
4242
import org.neo4j.cypherdsl.core.Delete;
43+
import org.neo4j.cypherdsl.core.DynamicLabels;
4344
import org.neo4j.cypherdsl.core.ExistentialSubquery;
4445
import org.neo4j.cypherdsl.core.Foreach;
4546
import org.neo4j.cypherdsl.core.FunctionInvocation;
@@ -611,6 +612,21 @@ void renderLabelExpression(LabelExpression l, LabelExpression.Type parent) {
611612
}
612613
}
613614

615+
void enter(DynamicLabels dynamicLabels) {
616+
617+
var modifier = switch (dynamicLabels.getModifier()) {
618+
case ALL -> "";
619+
case ANY -> "any";
620+
};
621+
622+
this.builder.append(":$").append(modifier).append("(");
623+
}
624+
625+
void leave(DynamicLabels dynamicLabels) {
626+
627+
this.builder.append(")");
628+
}
629+
614630
void enter(Properties properties) {
615631

616632
this.builder.append(" ");
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2019-2025 "Neo4j,"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* https://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
package org.neo4j.cypherdsl.core;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
class DynamicLabelsTests {
26+
27+
@Test
28+
void shouldMatchLabelsDynamically() {
29+
var labels = Cypher.listOf(Cypher.literalOf("Person"), Cypher.literalOf("Director")).as("labels");
30+
var directors = Cypher.node(Cypher.allLabels(labels)).named("directors");
31+
var stmt = Cypher.with(labels).match(directors).returning(directors).build();
32+
assertThat(stmt.getCypher())
33+
.isEqualTo("WITH ['Person', 'Director'] AS labels MATCH (directors:$(labels)) RETURN directors");
34+
}
35+
36+
@Test
37+
void shouldMatchNodesDynamicallyUsingAny() {
38+
var labels = Cypher.listOf(Cypher.literalOf("Movie"), Cypher.literalOf("Actor"));
39+
var n = Cypher.node(Cypher.anyLabel(labels)).named("n");
40+
var stmt = Cypher.match(n).returning(n.as("nodes")).build();
41+
assertThat(stmt.getCypher()).isEqualTo("MATCH (n:$any(['Movie', 'Actor'])) RETURN n AS nodes");
42+
}
43+
44+
}

0 commit comments

Comments
 (0)