diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java index 5362fb301..fc2331324 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/DefaultStatementBuilder.java @@ -27,6 +27,7 @@ import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatch; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithWhere; import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchWithoutWhere; import org.springframework.data.neo4j.core.cypher.support.Visitable; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -225,14 +226,20 @@ class DefaultStatementBuilder } @Override - public final OngoingMatchAndReturn skip(Number number) { - skip = Skip.of(number); + public final OngoingMatchAndReturn skip(@Nullable Number number) { + + if (number != null) { + skip = Skip.create(number); + } return this; } @Override - public final OngoingMatchAndReturn limit(Number number) { - limit = Limit.of(number); + public final OngoingMatchAndReturn limit(@Nullable Number number) { + + if (number != null) { + limit = Limit.create(number); + } return this; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java index 5fc7aa7f8..8fed0dc97 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Limit.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.util.Assert; /** * @author Gerrit Meier @@ -30,7 +31,10 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; @API(status = API.Status.INTERNAL, since = "1.0") public final class Limit implements Visitable { - public static Limit of(Number value) { + static Limit create(Number value) { + + Assert.notNull(value, "A limit cannot have a null value."); + return new Limit(new NumberLiteral(value)); } @@ -44,5 +48,6 @@ public final class Limit implements Visitable { public void accept(Visitor visitor) { visitor.enter(this); limitAmount.accept(visitor); + visitor.leave(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java index 9e3d6946f..0f71fd6d3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/Skip.java @@ -21,6 +21,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; import org.springframework.data.neo4j.core.cypher.support.Visitable; import org.springframework.data.neo4j.core.cypher.support.Visitor; +import org.springframework.util.Assert; /** * @author Gerrit Meier @@ -30,7 +31,10 @@ import org.springframework.data.neo4j.core.cypher.support.Visitor; @API(status = API.Status.INTERNAL, since = "1.0") public final class Skip implements Visitable { - public static Skip of(Number value) { + static Skip create(Number value) { + + Assert.notNull(value, "Cannot skip a value of null."); + return new Skip(new NumberLiteral(value)); } @@ -44,5 +48,6 @@ public final class Skip implements Visitable { public void accept(Visitor visitor) { visitor.enter(this); skipAmount.accept(visitor); + visitor.leave(this); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java index 4b0b8bf64..6051e95c9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/cypher/StatementBuilder.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.core.cypher; import org.apiguardian.api.API; +import org.springframework.lang.Nullable; /** * @author Michael J. Simons @@ -236,10 +237,10 @@ public interface StatementBuilder { /** * Adds a skip clause, skipping the given number of records. * - * @param number How many records to skip - * @return A step that only allows the limit of records to be specified + * @param number How many records to skip. If this is null, then no records are skipped. + * @return A step that only allows the limit of records to be specified. */ - ExposesLimit skip(Number number); + ExposesLimit skip(@Nullable Number number); } @@ -250,10 +251,10 @@ public interface StatementBuilder { /** * Limits the number of returned records. - * @param number How many records to return - * @return A buildable match statement + * @param number How many records to return. If this is null, all the records are returned. + * @return A buildable match statement. */ - BuildableMatch limit(Number number); + BuildableMatch limit(@Nullable Number number); } /** diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java index 8e3a74f7a..2efbbc50a 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/CypherQueryCreator.java @@ -79,6 +79,11 @@ final class CypherQueryCreator extends AbstractQueryCreator { private Iterator formalParameters; private final Queue lastParameter = new LinkedList<>(); + /** + * Stores the number of max results, if the {@link PartTree tree} is limiting. + */ + private final Number maxResults; + /** * Sort items may already be needed for some parts, i.e. of type NEAR. */ @@ -93,6 +98,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { this.domainType = domainType; this.nodeDescription = this.mappingContext.getRequiredNodeDescription(this.domainType); this.formalParameters = formalParameters.iterator(); + this.maxResults = tree.isLimiting() ? tree.getMaxResults() : null; } @Override @@ -127,6 +133,7 @@ final class CypherQueryCreator extends AbstractQueryCreator { sort.stream().map(sortAdapterFor(nodeDescription)) ).toArray(SortItem[]::new) ) + .limit(maxResults) .build(); return CypherRenderer.create().render(statement); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 425d10782..8e84f68ce 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -105,6 +105,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { mappingContext, domainType, tree, formalParameters, actualParameters ); + String cypherQuery = queryCreator.createQuery(); Map boundedParameters = formalParameters .getBindableParameters().stream() diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java index 9c6de7f23..6df72815f 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/cypher/CypherTest.java @@ -241,6 +241,15 @@ public class CypherTest { "MATCH (u:`User`) RETURN u SKIP 1"); } + @Test + void nullSkip() { + Statement statement = Cypher.match(userNode).returning(userNode).skip(null).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + } + @Test void limit() { Statement statement = Cypher.match(userNode).returning(userNode).limit(1).build(); @@ -250,6 +259,15 @@ public class CypherTest { "MATCH (u:`User`) RETURN u LIMIT 1"); } + @Test + void nullLimit() { + Statement statement = Cypher.match(userNode).returning(userNode).limit(null).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + } + @Test void skipAndLimit() { Statement statement = Cypher.match(userNode).returning(userNode).skip(1).limit(1).build(); @@ -259,6 +277,16 @@ public class CypherTest { "MATCH (u:`User`) RETURN u SKIP 1 LIMIT 1"); } + @Test + void nullskipAndLimit() { + Statement statement = Cypher.match(userNode).returning(userNode).skip(null).limit(null).build(); + + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + "MATCH (u:`User`) RETURN u"); + } + + @Test void distinct() { Statement statement = Cypher.match(userNode).returningDistinct(userNode).skip(1).limit(1).build(); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index 000155533..5acf5a402 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.stream.IntStream; import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.BeforeEach; @@ -125,6 +126,10 @@ class RepositoryIT { transaction.run("CREATE (n:PersonWithWither) SET n.name = '" + TEST_PERSON1_NAME + "'"); transaction.run("CREATE (n:KotlinPerson) SET n.name = '" + TEST_PERSON1_NAME + "'"); transaction.run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})"); + + IntStream.rangeClosed(1, 20).forEach(i -> + transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", Values.parameters("i", String.format("%02d", i)))); + transaction.success(); transaction.close(); @@ -784,13 +789,30 @@ class RepositoryIT { .containsExactly(person1); } + @Test + void limitClauseShouldWork() { + + List things; + + things = thingRepository.findTop5ByOrderByNameDesc(); + assertThat(things) + .hasSize(5) + .extracting(ThingWithAssignedId::getName) + .containsExactlyInAnyOrder("name20", "name19", "name18", "name17", "name16"); + + things = thingRepository.findFirstByOrderByNameDesc(); + assertThat(things) + .extracting(ThingWithAssignedId::getName) + .containsExactlyInAnyOrder("name20"); + } + @Configuration @EnableNeo4jRepositories @EnableTransactionManagement static class Config { @Bean - public Driver driver() { + public static Driver driver() { return neo4jConnectionSupport.openConnection(); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java index 76496dec0..57343e84b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/ThingRepository.java @@ -18,10 +18,15 @@ */ package org.springframework.data.neo4j.integration; +import java.util.List; + import org.springframework.data.repository.CrudRepository; /** * @author Michael J. Simons */ public interface ThingRepository extends CrudRepository { + List findFirstByOrderByNameDesc(); + + List findTop5ByOrderByNameDesc(); }