Skip to content

Commit ee635b0

Browse files
committed
GH-2884 - Support composite property in sort.
Should work for map projection and node returns now. Closes #2884
1 parent 1cdba99 commit ee635b0

File tree

7 files changed

+104
-8
lines changed

7 files changed

+104
-8
lines changed

src/main/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtils.java

+24-6
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,43 @@ public static Function<Sort.Order, SortItem> sortAdapterFor(NodeDescription<?> n
6464
return order -> {
6565

6666
String domainProperty = order.getProperty();
67-
boolean propertyIsQualified = domainProperty.contains(".");
67+
boolean propertyIsQualifiedOrComposite = domainProperty.contains(".");
6868
SymbolicName root;
69-
if (!propertyIsQualified) {
69+
if (!propertyIsQualifiedOrComposite) {
7070
root = Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription);
7171
} else {
72-
int indexOfSeparator = domainProperty.indexOf(".");
73-
root = Cypher.name(domainProperty.substring(0, indexOfSeparator));
74-
domainProperty = domainProperty.substring(indexOfSeparator + 1);
72+
// need to check first if this is really a qualified name or the "qualifier" is a composite property
73+
if (nodeDescription.getGraphProperty(domainProperty.split("\\.")[0]).isEmpty()) {
74+
int indexOfSeparator = domainProperty.indexOf(".");
75+
root = Cypher.name(domainProperty.substring(0, indexOfSeparator));
76+
domainProperty = domainProperty.substring(indexOfSeparator + 1);
77+
} else {
78+
root = Constants.NAME_OF_TYPED_ROOT_NODE.apply(nodeDescription);
79+
}
7580
}
7681

7782
var optionalGraphProperty = nodeDescription.getGraphProperty(domainProperty);
83+
// try to resolve if this is a composite property
84+
if (optionalGraphProperty.isEmpty()) {
85+
var domainPropertyPrefix = domainProperty.split("\\.")[0];
86+
optionalGraphProperty = nodeDescription.getGraphProperty(domainPropertyPrefix);
87+
}
7888
if (optionalGraphProperty.isEmpty()) {
79-
throw new IllegalStateException(String.format("Cannot order by the unknown graph property: '%s'", order.getProperty()));
89+
throw new IllegalStateException(String.format("Cannot order by the unknown graph property: '%s'", domainProperty));
8090
}
8191
var graphProperty = optionalGraphProperty.get();
8292
Expression expression;
8393
if (graphProperty.isInternalIdProperty()) {
8494
// Not using the id expression here, as the root will be referring to the constructed map being returned.
8595
expression = property(root, Constants.NAME_OF_INTERNAL_ID);
96+
} else if (graphProperty.isComposite() && !domainProperty.contains(".")) {
97+
throw new IllegalStateException(String.format("Cannot order by composite property: '%s'. Only ordering by its nested fields is allowed.", domainProperty));
98+
} else if (graphProperty.isComposite()) {
99+
if (nodeDescription.containsPossibleCircles(rpp -> true)) {
100+
expression = property(root, domainProperty);
101+
} else {
102+
expression = property(root, Constants.NAME_OF_ALL_PROPERTIES, domainProperty);
103+
}
86104
} else {
87105
expression = property(root, graphProperty.getPropertyName());
88106
if (order.isIgnoreCase()) {

src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java

+25
Original file line numberDiff line numberDiff line change
@@ -281,10 +281,12 @@ protected void prepareIndividual(
281281
CityModel aachen = new CityModel();
282282
aachen.setName("Aachen");
283283
aachen.setExoticProperty("Cars");
284+
aachen.setCompositeProperty(Map.of("language", "German"));
284285

285286
CityModel utrecht = new CityModel();
286287
utrecht.setName("Utrecht");
287288
utrecht.setExoticProperty("Bikes");
289+
utrecht.setCompositeProperty(Map.of("language", "Dutch"));
288290

289291
cityModelRepository.saveAll(List.of(aachen, utrecht));
290292
}
@@ -736,6 +738,29 @@ public void testCityModelProjectionPersistence(
736738
assertThat(reloaded.getCityEmployees()).hasSize(1);
737739
}
738740

741+
@Test
742+
@Tag("GH-2884")
743+
void sortByCompositeProperty(@Autowired CityModelRepository repository) {
744+
Sort sort = Sort.by(Sort.Order.asc("compositeProperty.language"));
745+
List<CityModel> models = repository.findAll(sort);
746+
747+
assertThat(models).extracting("name").containsExactly("Utrecht", "Aachen");
748+
749+
Sort sortDesc = Sort.by(Sort.Order.desc("compositeProperty.language"));
750+
models = repository.findAll(sortDesc);
751+
752+
assertThat(models).extracting("name").containsExactly("Aachen", "Utrecht");
753+
}
754+
755+
756+
@Test
757+
@Tag("GH-2884")
758+
void sortByCompositePropertyForCyclicDomainReturn(@Autowired SkuRORepository repository) {
759+
List<SkuRO> result = repository.findAll(Sort.by("composite.a"));
760+
761+
assertThat(result).extracting("number").containsExactly(3L, 2L, 1L, 0L);
762+
}
763+
739764
@Test
740765
@Tag("GH-2493")
741766
void saveOneShouldWork(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture,

src/test/java/org/springframework/data/neo4j/integration/issues/TestBase.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ protected final void beforeEach(@Autowired BookmarkCapture bookmarkCapture) {
102102
protected static void setupGH2289(QueryRunner queryRunner) {
103103
queryRunner.run("MATCH (s:SKU_RO) DETACH DELETE s").consume();
104104
for (int i = 0; i < 4; ++i) {
105-
queryRunner.run("CREATE (s:SKU_RO {number: $i, name: $n})",
106-
Values.parameters("i", i, "n", new String(new char[]{(char) ('A' + i)}))).consume();
105+
queryRunner.run("CREATE (s:SKU_RO {number: $i, name: $n, `composite.a`: $a})",
106+
Values.parameters("i", i, "n", new String(new char[]{(char) ('A' + i)}), "a", 10 - i)).consume();
107107
}
108108
}
109109

src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java

+13
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
package org.springframework.data.neo4j.integration.issues.gh2289;
1717

1818
import org.springframework.data.annotation.ReadOnlyProperty;
19+
import org.springframework.data.neo4j.core.schema.CompositeProperty;
1920
import org.springframework.data.neo4j.core.schema.GeneratedValue;
2021
import org.springframework.data.neo4j.core.schema.Id;
2122
import org.springframework.data.neo4j.core.schema.Node;
2223
import org.springframework.data.neo4j.core.schema.Property;
2324
import org.springframework.data.neo4j.core.schema.Relationship;
2425

2526
import java.util.HashSet;
27+
import java.util.Map;
2628
import java.util.Set;
2729

2830
/**
@@ -48,6 +50,9 @@ public class SkuRO {
4850
@Relationship(type = "RANGE_RELATION_TO", direction = Relationship.Direction.INCOMING)
4951
private Set<RangeRelationRO> rangeRelationsIn = new HashSet<>();
5052

53+
@CompositeProperty
54+
private Map<String, Integer> composite;
55+
5156
public SkuRO(Long number, String name) {
5257
this.number = number;
5358
this.name = name;
@@ -99,6 +104,14 @@ public void setRangeRelationsIn(Set<RangeRelationRO> rangeRelationsIn) {
99104
this.rangeRelationsIn = rangeRelationsIn;
100105
}
101106

107+
public Map<String, Integer> getComposite() {
108+
return composite;
109+
}
110+
111+
public void setComposite(Map<String, Integer> composite) {
112+
this.composite = composite;
113+
}
114+
102115
public boolean equals(final Object o) {
103116
if (o == this) {
104117
return true;

src/test/java/org/springframework/data/neo4j/integration/issues/gh2474/CityModel.java

+13
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.neo4j.integration.issues.gh2474;
1717

18+
import org.springframework.data.neo4j.core.schema.CompositeProperty;
1819
import org.springframework.data.neo4j.core.schema.GeneratedValue;
1920
import org.springframework.data.neo4j.core.schema.Id;
2021
import org.springframework.data.neo4j.core.schema.Node;
@@ -23,6 +24,7 @@
2324

2425
import java.util.ArrayList;
2526
import java.util.List;
27+
import java.util.Map;
2628
import java.util.UUID;
2729

2830
/**
@@ -48,6 +50,9 @@ public class CityModel {
4850
@Property("exotic.property")
4951
private String exoticProperty;
5052

53+
@CompositeProperty
54+
private Map<String, String> compositeProperty;
55+
5156
public CityModel() {
5257
}
5358

@@ -99,6 +104,14 @@ public void setExoticProperty(String exoticProperty) {
99104
this.exoticProperty = exoticProperty;
100105
}
101106

107+
public Map<String, String> getCompositeProperty() {
108+
return compositeProperty;
109+
}
110+
111+
public void setCompositeProperty(Map<String, String> compositeProperty) {
112+
this.compositeProperty = compositeProperty;
113+
}
114+
102115
public boolean equals(final Object o) {
103116
if (o == this) {
104117
return true;

src/test/java/org/springframework/data/neo4j/integration/shared/common/ScrollingEntity.java

+5
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
package org.springframework.data.neo4j.integration.shared.common;
1717

1818
import java.time.LocalDateTime;
19+
import java.util.Map;
1920
import java.util.UUID;
2021

2122
import org.neo4j.driver.QueryRunner;
2223
import org.springframework.data.domain.Sort;
24+
import org.springframework.data.neo4j.core.schema.CompositeProperty;
2325
import org.springframework.data.neo4j.core.schema.GeneratedValue;
2426
import org.springframework.data.neo4j.core.schema.Id;
2527
import org.springframework.data.neo4j.core.schema.Node;
@@ -78,6 +80,9 @@ public static void createTestDataWithoutDuplicates(QueryRunner queryRunner) {
7880

7981
private LocalDateTime c;
8082

83+
@CompositeProperty
84+
private Map<String, String> basicComposite;
85+
8186
public UUID getId() {
8287
return id;
8388
}

src/test/java/org/springframework/data/neo4j/repository/query/CypherAdapterUtilsTest.java

+22
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.neo4j.repository.query;
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
1920

2021
import java.time.LocalDateTime;
2122
import java.util.Map;
@@ -62,4 +63,25 @@ void shouldCombineSortKeysetProper() {
6263
assertThat(Renderer.getRenderer(Configuration.prettyPrinting()).render(Cypher.match(Cypher.anyNode(n)).where(condition).returning(n).build()))
6364
.isEqualTo(expected);
6465
}
66+
67+
@Test
68+
void sortByCompositePropertyField() {
69+
var mappingContext = new Neo4jMappingContext();
70+
var entity = mappingContext.getPersistentEntity(ScrollingEntity.class);
71+
72+
var sortItem = CypherAdapterUtils.sortAdapterFor(entity).apply(Sort.Order.asc("basicComposite.blubb"));
73+
var node = Cypher.anyNode("scrollingEntity");
74+
var statement = Cypher.match(node).returning(node).orderBy(sortItem).build();
75+
assertThat(Renderer.getDefaultRenderer().render(statement))
76+
.isEqualTo("MATCH (scrollingEntity) RETURN scrollingEntity ORDER BY scrollingEntity.__allProperties__.`basicComposite.blubb`");
77+
}
78+
79+
@Test
80+
void failOnDirectCompositePropertyAccess() {
81+
var mappingContext = new Neo4jMappingContext();
82+
var entity = mappingContext.getPersistentEntity(ScrollingEntity.class);
83+
84+
assertThatIllegalStateException().isThrownBy(() -> CypherAdapterUtils.sortAdapterFor(entity).apply(Sort.Order.asc("basicComposite")))
85+
.withMessage("Cannot order by composite property: 'basicComposite'. Only ordering by its nested fields is allowed.");
86+
}
6587
}

0 commit comments

Comments
 (0)