From 7e075f78c12377a9cd61366ae93e2a57d650c6de Mon Sep 17 00:00:00 2001 From: Peter Aisher Date: Thu, 25 Sep 2025 17:21:16 +0200 Subject: [PATCH 1/3] Update test for Specification.not() to ignore unrestricted() See #4203 Signed-off-by: Peter Aisher --- .../data/jpa/domain/SpecificationUnitTests.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java index cee8386513..4082596884 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java @@ -43,6 +43,7 @@ * @author Mark Paluch * @author Daniel Shuy * @author Heeeun Cho + * @author Peter Aisher */ @SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @@ -127,15 +128,13 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } - @Test // GH-3849 + @Test // GH-3849, GH-4023 void notWithNullPredicate() { - when(builder.disjunction()).thenReturn(mock(Predicate.class)); + Specification notSpec = Specification.not(Specification.unrestricted()); - Specification notSpec = Specification.not((r, q, cb) -> null); - - assertThat(notSpec.toPredicate(root, query, builder)).isNotNull(); - verify(builder).disjunction(); + assertThat(notSpec.toPredicate(root, query, builder)).isNull(); + verifyNoInteractions(builder); } @Test // GH-3992 From 899f2dd58cb9d09f6b8d6b9c5c66cadc4985bed2 Mon Sep 17 00:00:00 2001 From: Peter Aisher Date: Thu, 25 Sep 2025 17:47:34 +0200 Subject: [PATCH 2/3] Update Specification.not() to ignore unrestricted() See #4203 Signed-off-by: Peter Aisher --- .../data/jpa/domain/Specification.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index 8427c5e8aa..b0b2c943b2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java @@ -36,9 +36,10 @@ *

* Specifications can be composed into higher order functions from other specifications using * {@link #and(Specification)}, {@link #or(Specification)} or factory methods such as {@link #allOf(Iterable)}. + *

* Composition considers whether one or more specifications contribute to the overall predicate by returning a - * {@link Predicate} or {@literal null}. Specifications returning {@literal null} are considered to not contribute to - * the overall predicate and their result is not considered in the final predicate. + * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, are + * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * * @author Oliver Gierke * @author Thomas Darimont @@ -49,12 +50,22 @@ * @author Daniel Shuy * @author Sergey Rukin * @author Heeeun Cho + * @author Peter Aisher */ @FunctionalInterface public interface Specification extends Serializable { /** - * Simple static factory method to create a specification matching all objects. + * Simple static factory method to create a specification which does not participate in matching. The specification + * returned is {@code null}-like, and is elided in all operations. + * + *

+	 * {@code
+	 * unrestricted().and(other) // consider only `other`
+	 * unrestricted().or(other) // consider only `other`
+	 * not(unrestricted()) // equivalent to `unrestricted()`
+	 * }
+	 * 
* * @param the type of the {@link Root} the resulting {@literal Specification} operates on. * @return guaranteed to be not {@literal null}. @@ -175,7 +186,7 @@ static Specification not(Specification spec) { return (root, query, builder) -> { Predicate predicate = spec.toPredicate(root, query, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); + return predicate != null ? builder.not(predicate) : null; }; } From 8e11e18c7a2b966272198b90dc992633d7de4e66 Mon Sep 17 00:00:00 2001 From: Peter Aisher Date: Thu, 25 Sep 2025 18:02:12 +0200 Subject: [PATCH 3/3] Constistent unrestricted() behaviour for all *Specification types Closes #4203 Signed-off-by: Peter Aisher --- .../data/jpa/domain/DeleteSpecification.java | 24 ++++++++++++++----- .../jpa/domain/PredicateSpecification.java | 24 ++++++++++++++----- .../data/jpa/domain/UpdateSpecification.java | 24 ++++++++++++++----- .../domain/DeleteSpecificationUnitTests.java | 11 ++++----- .../PredicateSpecificationUnitTests.java | 11 ++++----- .../domain/UpdateSpecificationUnitTests.java | 11 ++++----- 6 files changed, 69 insertions(+), 36 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java index 4c7deb638d..79a3865afe 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,18 +35,30 @@ *

* Specifications can be composed into higher order functions from other specifications using * {@link #and(DeleteSpecification)}, {@link #or(DeleteSpecification)} or factory methods such as - * {@link #allOf(Iterable)}. Composition considers whether one or more specifications contribute to the overall - * predicate by returning a {@link Predicate} or {@literal null}. Specifications returning {@literal null} are - * considered to not contribute to the overall predicate and their result is not considered in the final predicate. + * {@link #allOf(Iterable)}. + *

+ * Composition considers whether one or more specifications contribute to the overall predicate by returning a + * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, are + * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * * @author Mark Paluch + * @author Peter Aisher * @since 4.0 */ @FunctionalInterface public interface DeleteSpecification extends Serializable { /** - * Simple static factory method to create a specification deleting all objects. + * Simple static factory method to create a specification which does not participate in matching. The specification + * returned is {@code null}-like, and is elided in all operations. + * + *

+	 * {@code
+	 * unrestricted().and(other) // consider only `other`
+	 * unrestricted().or(other) // consider only `other`
+	 * not(unrestricted()) // equivalent to `unrestricted()`
+	 * }
+	 * 
* * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on. * @return guaranteed to be not {@literal null}. @@ -159,7 +171,7 @@ static DeleteSpecification not(DeleteSpecification spec) { return (root, delete, builder) -> { Predicate predicate = spec.toPredicate(root, delete, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); + return predicate != null ? builder.not(predicate) : null; }; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java index daa39b9ba7..3c7e048b81 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,18 +34,30 @@ *

* Specifications can be composed into higher order functions from other specifications using * {@link #and(PredicateSpecification)}, {@link #or(PredicateSpecification)} or factory methods such as - * {@link #allOf(Iterable)}. Composition considers whether one or more specifications contribute to the overall - * predicate by returning a {@link Predicate} or {@literal null}. Specifications returning {@literal null} are - * considered to not contribute to the overall predicate and their result is not considered in the final predicate. + * {@link #allOf(Iterable)}. + *

+ * Composition considers whether one or more specifications contribute to the overall predicate by returning a + * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, are + * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * * @author Mark Paluch + * @author Peter Aisher * @since 4.0 */ @FunctionalInterface public interface PredicateSpecification extends Serializable { /** - * Simple static factory method to create a specification matching all objects. + * Simple static factory method to create a specification which does not participate in matching. The specification + * returned is {@code null}-like, and is elided in all operations. + * + *

+	 * {@code
+	 * unrestricted().and(other) // consider only `other`
+	 * unrestricted().or(other) // consider only `other`
+	 * not(unrestricted()) // equivalent to `unrestricted()`
+	 * }
+	 * 
* * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on. * @return guaranteed to be not {@literal null}. @@ -113,7 +125,7 @@ static PredicateSpecification not(PredicateSpecification spec) { return (root, builder) -> { Predicate predicate = spec.toPredicate(root, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); + return predicate != null ? builder.not(predicate) : null; }; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java index 1a27d428a4..02bff0c3d6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,18 +35,30 @@ *

* Specifications can be composed into higher order functions from other specifications using * {@link #and(UpdateSpecification)}, {@link #or(UpdateSpecification)} or factory methods such as - * {@link #allOf(Iterable)}. Composition considers whether one or more specifications contribute to the overall - * predicate by returning a {@link Predicate} or {@literal null}. Specifications returning {@literal null} are - * considered to not contribute to the overall predicate and their result is not considered in the final predicate. + * {@link #allOf(Iterable)}. + *

+ * Composition considers whether one or more specifications contribute to the overall predicate by returning a + * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, are + * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * * @author Mark Paluch + * @author Peter Aisher * @since 4.0 */ @FunctionalInterface public interface UpdateSpecification extends Serializable { /** - * Simple static factory method to create a specification updating all objects. + * Simple static factory method to create a specification which does not participate in matching. The specification + * returned is {@code null}-like, and is elided in all operations. + * + *

+	 * {@code
+	 * unrestricted().and(other) // consider only `other`
+	 * unrestricted().or(other) // consider only `other`
+	 * not(unrestricted()) // equivalent to `unrestricted()`
+	 * }
+	 * 
* * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. * @return guaranteed to be not {@literal null}. @@ -180,7 +192,7 @@ static UpdateSpecification not(UpdateSpecification spec) { return (root, update, builder) -> { Predicate predicate = spec.toPredicate(root, update, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); + return predicate != null ? builder.not(predicate) : null; }; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java index 99e6bb80ac..13e051c46f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ * Unit tests for {@link DeleteSpecification}. * * @author Mark Paluch + * @author Peter Aisher */ @SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @@ -158,15 +159,13 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } - @Test // GH-3849 + @Test // GH-3849, GH-4023 void notWithNullPredicate() { - when(builder.disjunction()).thenReturn(mock(Predicate.class)); - DeleteSpecification notSpec = DeleteSpecification.not((r, q, cb) -> null); - assertThat(notSpec.toPredicate(root, delete, builder)).isNotNull(); - verify(builder).disjunction(); + assertThat(notSpec.toPredicate(root, delete, builder)).isNull(); + verifyNoInteractions(builder); } static class SerializableSpecification implements Serializable, DeleteSpecification { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java index 0bcefc79ae..0bce2b5a2c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ * Unit tests for {@link PredicateSpecification}. * * @author Mark Paluch + * @author Peter Aisher */ @SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @@ -156,15 +157,13 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } - @Test // GH-3849 + @Test // GH-3849, GH-4023 void notWithNullPredicate() { - when(builder.disjunction()).thenReturn(mock(Predicate.class)); - PredicateSpecification notSpec = PredicateSpecification.not((r, cb) -> null); - assertThat(notSpec.toPredicate(root, builder)).isNotNull(); - verify(builder).disjunction(); + assertThat(notSpec.toPredicate(root, builder)).isNull(); + verifyNoInteractions(builder); } static class SerializableSpecification implements Serializable, PredicateSpecification { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java index f65ab6ecaa..a5415a3bd1 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ * Unit tests for {@link UpdateSpecification}. * * @author Mark Paluch + * @author Peter Aisher */ @SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @@ -158,15 +159,13 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } - @Test // GH-3849 + @Test // GH-3849, GH-4023 void notWithNullPredicate() { - when(builder.disjunction()).thenReturn(mock(Predicate.class)); - UpdateSpecification notSpec = UpdateSpecification.not((r, q, cb) -> null); - assertThat(notSpec.toPredicate(root, update, builder)).isNotNull(); - verify(builder).disjunction(); + assertThat(notSpec.toPredicate(root, update, builder)).isNull(); + verifyNoInteractions(builder); } static class SerializableSpecification implements Serializable, UpdateSpecification {