Skip to content

Commit 0561029

Browse files
GH-2273 - Allow function calls in Sort / Pageable objects in custom queries.
This closes #2273.
1 parent d0013f1 commit 0561029

File tree

2 files changed

+40
-17
lines changed

2 files changed

+40
-17
lines changed

src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java

+8-5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.List;
3131
import java.util.function.Predicate;
3232
import java.util.function.UnaryOperator;
33+
import java.util.regex.Pattern;
3334

3435
import org.apiguardian.api.API;
3536
import org.neo4j.cypherdsl.core.Condition;
@@ -76,6 +77,7 @@ public enum CypherGenerator {
7677
private static final SymbolicName END_NODE_NAME = Cypher.name("endNode");
7778

7879
private static final SymbolicName RELATIONSHIP_NAME = Cypher.name("relProps");
80+
private static final Pattern LOOKS_LIKE_A_FUNCTION = Pattern.compile(".+\\(.*\\)");
7981

8082
/**
8183
* @param nodeDescription The node description for which a match clause should be generated
@@ -466,12 +468,13 @@ public Expression[] createReturnStatementForMatch(NodeDescription<?> nodeDescrip
466468
if (sort == null || sort.isUnsorted()) {
467469
return null;
468470
}
469-
470-
Statement statement = Cypher.match(Cypher.anyNode()).returning("n")
471-
.orderBy(sort.stream().filter(order -> order != null).map(order -> {
472-
String property = order.getProperty();
471+
Statement statement = match(anyNode()).returning("n")
472+
.orderBy(sort.stream().filter(order -> order != null && order.getProperty() != null).map(order -> {
473+
String property = order.getProperty().trim();
473474
Expression expression;
474-
if (property.contains(".")) {
475+
if (LOOKS_LIKE_A_FUNCTION.matcher(property).matches()) {
476+
expression = Cypher.raw(property);
477+
} else if (property.contains(".")) {
475478
String[] path = property.split("\\.");
476479
if (path.length != 2) {
477480
throw new IllegalArgumentException(String.format(

src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java

+32-12
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,22 @@
1515
*/
1616
package org.springframework.data.neo4j.core.mapping;
1717

18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
20+
import static org.mockito.Mockito.doReturn;
21+
import static org.mockito.Mockito.when;
22+
23+
import java.util.Collection;
24+
import java.util.Map;
25+
import java.util.Optional;
26+
import java.util.regex.Pattern;
27+
import java.util.stream.Stream;
28+
1829
import org.junit.Assert;
1930
import org.junit.jupiter.api.Test;
2031
import org.junit.jupiter.params.ParameterizedTest;
2132
import org.junit.jupiter.params.provider.Arguments;
33+
import org.junit.jupiter.params.provider.CsvSource;
2234
import org.junit.jupiter.params.provider.MethodSource;
2335
import org.mockito.Mockito;
2436
import org.neo4j.cypherdsl.core.Cypher;
@@ -28,17 +40,6 @@
2840
import org.springframework.data.neo4j.core.schema.Id;
2941
import org.springframework.data.neo4j.core.schema.Node;
3042

31-
import java.util.Collection;
32-
import java.util.Map;
33-
import java.util.Optional;
34-
import java.util.regex.Pattern;
35-
import java.util.stream.Stream;
36-
37-
import static org.assertj.core.api.Assertions.assertThat;
38-
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
39-
import static org.mockito.Mockito.doReturn;
40-
import static org.mockito.Mockito.when;
41-
4243
/**
4344
* @author Davide Fantuzzi
4445
* @author Andrea Santurbano
@@ -175,10 +176,29 @@ void shouldFailOnInvalidPathWithMultipleHops() {
175176
.withMessageMatching("Cannot handle order property `.*`, it must be a simple property or one-hop path\\.");
176177
}
177178

179+
@CsvSource(delimiterString = "|", value = {
180+
"apoc.text.clean(department.name) |false| ORDER BY apoc.text.clean(department.name) ASC",
181+
"apoc.text.clean(department.name) |true | ORDER BY apoc.text.clean(department.name) DESC",
182+
"apoc.text.clean() |true | ORDER BY apoc.text.clean() DESC",
183+
"date() |false| ORDER BY date() ASC",
184+
"date({year:1984, month:10, day:11})|false| ORDER BY date({year:1984, month:10, day:11}) ASC",
185+
"round(3.141592, 3) |false| ORDER BY round(3.141592, 3) ASC"
186+
})
187+
@ParameterizedTest // GH-2273
188+
void functionCallsShouldWork(String input, boolean descending, String expected) {
189+
190+
Sort sort = Sort.by(input);
191+
if (descending) {
192+
sort = sort.descending();
193+
}
194+
String orderByFragment = CypherGenerator.INSTANCE.createOrderByFragment(sort);
195+
assertThat(orderByFragment).isEqualTo(expected);
196+
}
197+
178198
@Test
179199
void shouldFailOnInvalidSymbolicNames() {
180200

181-
assertThatIllegalArgumentException().isThrownBy(() -> CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("n()")))
201+
assertThatIllegalArgumentException().isThrownBy(() -> CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("()")))
182202
.withMessage("Name must be a valid identifier.");
183203
}
184204

0 commit comments

Comments
 (0)