Skip to content

Commit 19af4d9

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

File tree

7 files changed

+174
-8
lines changed

7 files changed

+174
-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
@@ -278,10 +278,12 @@ protected void prepareIndividual(
278278
CityModel aachen = new CityModel();
279279
aachen.setName("Aachen");
280280
aachen.setExoticProperty("Cars");
281+
aachen.setCompositeProperty(Map.of("language", "German"));
281282

282283
CityModel utrecht = new CityModel();
283284
utrecht.setName("Utrecht");
284285
utrecht.setExoticProperty("Bikes");
286+
utrecht.setCompositeProperty(Map.of("language", "Dutch"));
285287

286288
cityModelRepository.saveAll(List.of(aachen, utrecht));
287289
}
@@ -732,6 +734,29 @@ public void testCityModelProjectionPersistence(
732734
assertThat(reloaded.getCityEmployees()).hasSize(1);
733735
}
734736

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

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ protected final void beforeEach(@Autowired BookmarkCapture bookmarkCapture) {
100100
}
101101

102102
protected static void setupGH2289(QueryRunner queryRunner) {
103+
queryRunner.run("MATCH (s:SKU_RO) DETACH DELETE s").consume();
103104
for (int i = 0; i < 4; ++i) {
104-
queryRunner.run("CREATE (s:SKU_RO {number: $i, name: $n})",
105-
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();
106107
}
107108
}
108109

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

+90
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
import lombok.Setter;
2121

2222
import java.util.HashSet;
23+
import java.util.Map;
2324
import java.util.Set;
2425

2526
import org.springframework.data.annotation.ReadOnlyProperty;
27+
import org.springframework.data.neo4j.core.schema.CompositeProperty;
2628
import org.springframework.data.neo4j.core.schema.GeneratedValue;
2729
import org.springframework.data.neo4j.core.schema.Id;
2830
import org.springframework.data.neo4j.core.schema.Node;
@@ -57,6 +59,9 @@ public class SkuRO {
5759
@Relationship(type = "RANGE_RELATION_TO", direction = Relationship.Direction.INCOMING)
5860
private Set<RangeRelationRO> rangeRelationsIn = new HashSet<>();
5961

62+
@CompositeProperty
63+
private Map<String, Integer> composite;
64+
6065
public SkuRO(Long number, String name) {
6166
this.number = number;
6267
this.name = name;
@@ -67,4 +72,89 @@ public RangeRelationRO rangeRelationTo(SkuRO sku, double minDelta, double maxDel
6772
rangeRelationsOut.add(relationOut);
6873
return relationOut;
6974
}
75+
76+
public Long getId() {
77+
return this.id;
78+
}
79+
80+
public Long getNumber() {
81+
return this.number;
82+
}
83+
84+
public String getName() {
85+
return this.name;
86+
}
87+
88+
public Set<RangeRelationRO> getRangeRelationsOut() {
89+
return this.rangeRelationsOut;
90+
}
91+
92+
public Set<RangeRelationRO> getRangeRelationsIn() {
93+
return this.rangeRelationsIn;
94+
}
95+
96+
public void setId(Long id) {
97+
this.id = id;
98+
}
99+
100+
public void setNumber(Long number) {
101+
this.number = number;
102+
}
103+
104+
public void setName(String name) {
105+
this.name = name;
106+
}
107+
108+
public void setRangeRelationsOut(Set<RangeRelationRO> rangeRelationsOut) {
109+
this.rangeRelationsOut = rangeRelationsOut;
110+
}
111+
112+
public void setRangeRelationsIn(Set<RangeRelationRO> rangeRelationsIn) {
113+
this.rangeRelationsIn = rangeRelationsIn;
114+
}
115+
116+
public boolean equals(final Object o) {
117+
if (o == this) {
118+
return true;
119+
}
120+
if (!(o instanceof SkuRO)) {
121+
return false;
122+
}
123+
final SkuRO other = (SkuRO) o;
124+
if (!other.canEqual((Object) this)) {
125+
return false;
126+
}
127+
final Object this$id = this.getId();
128+
final Object other$id = other.getId();
129+
if (this$id == null ? other$id != null : !this$id.equals(other$id)) {
130+
return false;
131+
}
132+
final Object this$number = this.getNumber();
133+
final Object other$number = other.getNumber();
134+
if (this$number == null ? other$number != null : !this$number.equals(other$number)) {
135+
return false;
136+
}
137+
final Object this$name = this.getName();
138+
final Object other$name = other.getName();
139+
if (this$name == null ? other$name != null : !this$name.equals(other$name)) {
140+
return false;
141+
}
142+
return true;
143+
}
144+
145+
protected boolean canEqual(final Object other) {
146+
return other instanceof SkuRO;
147+
}
148+
149+
public int hashCode() {
150+
final int PRIME = 59;
151+
int result = 1;
152+
final Object $id = this.getId();
153+
result = result * PRIME + ($id == null ? 43 : $id.hashCode());
154+
final Object $number = this.getNumber();
155+
result = result * PRIME + ($number == null ? 43 : $number.hashCode());
156+
final Object $name = this.getName();
157+
result = result * PRIME + ($name == null ? 43 : $name.hashCode());
158+
return result;
159+
}
70160
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919

2020
import java.util.ArrayList;
2121
import java.util.List;
22+
import java.util.Map;
2223
import java.util.UUID;
2324

25+
import org.springframework.data.neo4j.core.schema.CompositeProperty;
2426
import org.springframework.data.neo4j.core.schema.GeneratedValue;
2527
import org.springframework.data.neo4j.core.schema.Id;
2628
import org.springframework.data.neo4j.core.schema.Node;
@@ -50,4 +52,7 @@ public class CityModel {
5052

5153
@Property("exotic.property")
5254
private String exoticProperty;
55+
56+
@CompositeProperty
57+
private Map<String, String> compositeProperty;
5358
}

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)