From 416f24eff6982892872e2c431759b02bdbe463da Mon Sep 17 00:00:00 2001 From: Eric Rizzo Date: Thu, 4 Jun 2026 14:58:16 -0400 Subject: [PATCH 1/4] Added tests to cover more of the SameCalendarDay implementations --- .../java/expresspecs/TCTestConfig.java | 6 +- .../SpecificationIntegrationTests.java | 11 ++ .../datetime/OnDateTypeCoverageTests.java | 103 +++++++++++++++--- .../java/expresspecs/datetime/Socialite.java | 60 ++++++++++ .../datetime/SocialiteRepository.java | 7 ++ .../java/expresspecs/example/Customer.java | 11 -- .../java/expresspecs/SB3JpaTestConfig.java | 9 +- .../java/expresspecs/SB4JPATestConfig.java | 9 +- 8 files changed, 183 insertions(+), 33 deletions(-) create mode 100644 src/test/java/expresspecs/datetime/Socialite.java create mode 100644 src/test/java/expresspecs/datetime/SocialiteRepository.java diff --git a/src/test/containers/java/expresspecs/TCTestConfig.java b/src/test/containers/java/expresspecs/TCTestConfig.java index ed0ecc6..b427cf1 100644 --- a/src/test/containers/java/expresspecs/TCTestConfig.java +++ b/src/test/containers/java/expresspecs/TCTestConfig.java @@ -2,11 +2,14 @@ import org.jspecify.annotations.NonNull; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.persistence.autoconfigure.EntityScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import expresspecs.datetime.Socialite; +import expresspecs.datetime.SocialiteRepository; import expresspecs.example.ComponentScanMarker; import jakarta.persistence.EntityManager; import rizzoweb.spring.jpa.EntityManagerWrapper; @@ -25,7 +28,8 @@ @Configuration @EnableAutoConfiguration @ComponentScan(basePackageClasses = ComponentScanMarker.class) -@EnableJpaRepositories(basePackageClasses = ComponentScanMarker.class) +@EntityScan(basePackageClasses = { ComponentScanMarker.class, Socialite.class }) +@EnableJpaRepositories(basePackageClasses = { ComponentScanMarker.class, SocialiteRepository.class }) public class TCTestConfig { @Bean diff --git a/src/test/java/expresspecs/SpecificationIntegrationTests.java b/src/test/java/expresspecs/SpecificationIntegrationTests.java index 30a63c8..366fb78 100644 --- a/src/test/java/expresspecs/SpecificationIntegrationTests.java +++ b/src/test/java/expresspecs/SpecificationIntegrationTests.java @@ -1,9 +1,12 @@ package expresspecs; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import expresspecs.datetime.DateTimeSpecificationIntegrationTests; import expresspecs.datetime.OnDateTypeCoverageTests; +import expresspecs.datetime.Socialite; +import expresspecs.datetime.SocialiteRepository; import expresspecs.example.Customer; import expresspecs.example.CustomerRepository; import lombok.Getter; @@ -22,4 +25,12 @@ public abstract class SpecificationIntegrationTests extends BaseJPAIntegrationTe @Getter protected CustomerRepository repo; + @Autowired + protected SocialiteRepository socialiteRepository; + + @Override + public JpaSpecificationExecutor getSocialiteRepo() { + return socialiteRepository; + } + } diff --git a/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java b/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java index 36527ee..442b0f3 100644 --- a/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java +++ b/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java @@ -2,14 +2,17 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.sql.Timestamp; import java.time.Instant; import java.time.LocalDate; import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.transaction.annotation.Transactional; import expresspecs.example.Customer; @@ -18,23 +21,26 @@ /** * Integration tests that exercise {@link DateTimeSpecifications#onDate} against common temporal - * field types ({@code LocalDate}, {@code Instant}, legacy {@code Date}, {@code OffsetDateTime}). + * field types ({@code LocalDate}, {@code Instant}, legacy {@code Date}, {@code OffsetDateTime}, + * {@code ZonedDateTime}) using {@link Socialite} and {@link Customer} persistence fixtures. */ @Transactional public interface OnDateTypeCoverageTests extends DataIntegrationTest { + JpaSpecificationExecutor getSocialiteRepo(); + @Test default void onDate_LocalDateField() { - var dec3 = persistAndFlush(Customer.builder() - .localDateOnly(LocalDate.of(2007, 12, 3)) + var dec3 = persistAndFlush(Socialite.builder() + .localDate(LocalDate.of(2007, 12, 3)) .build()); - persistAndFlush(Customer.builder() - .localDateOnly(LocalDate.of(2007, 12, 4)) + persistAndFlush(Socialite.builder() + .localDate(LocalDate.of(2007, 12, 4)) .build()); - Specification spec = DateTimeSpecifications.onDate(Fields.localDateOnly, LocalDate.of(2007, 12, 3)); - List results = getRepo().findAll(spec); + Specification spec = DateTimeSpecifications.onDate(Socialite.Fields.localDate, LocalDate.of(2007, 12, 3)); + List results = getSocialiteRepo().findAll(spec); assertThat(results).containsExactly(dec3); } @@ -56,17 +62,65 @@ default void onDate_InstantField() { } @Test - default void onDate_LegacyDateField() { - var dec3 = persistAndFlush(Customer.builder() - .legacyDate(Date.from(Instant.parse("2007-12-03T10:15:30Z"))) + default void onDate_JavaUtilDateAsTimestampField() { + var dec3 = persistAndFlush(Socialite.builder() + .javaUtilDateAsTimestamp(Date.from(Instant.parse("2007-12-03T10:15:30Z"))) .build()); - persistAndFlush(Customer.builder() - .legacyDate(Date.from(Instant.parse("2007-12-04T10:15:30Z"))) + persistAndFlush(Socialite.builder() + .javaUtilDateAsTimestamp(Date.from(Instant.parse("2007-12-04T10:15:30Z"))) .build()); - Specification spec = DateTimeSpecifications.onDate(Fields.legacyDate, LocalDate.of(2007, 12, 3)); - List results = getRepo().findAll(spec); + Specification spec = DateTimeSpecifications.onDate(Socialite.Fields.javaUtilDateAsTimestamp, LocalDate.of(2007, 12, 3)); + List results = getSocialiteRepo().findAll(spec); + + assertThat(results).containsExactly(dec3); + } + + @Test + default void onDate_SqlDateField() { + var dec3 = persistAndFlush(Socialite.builder() + .sqlDate(java.sql.Date.valueOf("2007-12-03")) + .build()); + + persistAndFlush(Socialite.builder() + .sqlDate(java.sql.Date.valueOf("2007-12-04")) + .build()); + + Specification spec = DateTimeSpecifications.onDate(Socialite.Fields.sqlDate, LocalDate.of(2007, 12, 3)); + List results = getSocialiteRepo().findAll(spec); + + assertThat(results).containsExactly(dec3); + } + + @Test + default void onDate_JavaUtilDateAsDateField() { + var dec3 = persistAndFlush(Socialite.builder() + .javaUtilDateAsDate(Date.from(Instant.parse("2007-12-03T10:15:30Z"))) + .build()); + + persistAndFlush(Socialite.builder() + .javaUtilDateAsDate(Date.from(Instant.parse("2007-12-04T10:15:30Z"))) + .build()); + + Specification spec = DateTimeSpecifications.onDate(Socialite.Fields.javaUtilDateAsDate, LocalDate.of(2007, 12, 3)); + List results = getSocialiteRepo().findAll(spec); + + assertThat(results).containsExactly(dec3); + } + + @Test + default void onDate_SqlTimestampField() { + var dec3 = persistAndFlush(Socialite.builder() + .sqlTimestamp(Timestamp.from(Instant.parse("2007-12-03T10:15:30Z"))) + .build()); + + persistAndFlush(Socialite.builder() + .sqlTimestamp(Timestamp.from(Instant.parse("2007-12-04T10:15:30Z"))) + .build()); + + Specification spec = DateTimeSpecifications.onDate(Socialite.Fields.sqlTimestamp, LocalDate.of(2007, 12, 3)); + List results = getSocialiteRepo().findAll(spec); assertThat(results).containsExactly(dec3); } @@ -82,7 +136,6 @@ default void onDate_OffsetDateTimeField_offsetCrossesMidnight() { .createdTimestamp(OffsetDateTime.parse("2007-12-03T10:00:00+00:00")) .build()); - // Local time is Dec 3, but UTC equivalent is Dec 4 at 00:30; must NOT appear in a Dec-3 query persistAndFlush(Customer.builder() .createdTimestamp(OffsetDateTime.parse("2007-12-03T23:30:00-01:00")) .build()); @@ -92,4 +145,24 @@ default void onDate_OffsetDateTimeField_offsetCrossesMidnight() { assertThat(results).containsExactly(clearlyDec3); } + + /** + * Same UTC-boundary behavior as {@link #onDate_OffsetDateTimeField_offsetCrossesMidnight()}, + * for {@link ZonedDateTime} columns. + */ + @Test + default void onDate_ZonedDateTimeField_offsetCrossesMidnight() { + var clearlyDec3 = persistAndFlush(Socialite.builder() + .zonedDateTime(ZonedDateTime.parse("2007-12-03T10:00:00+00:00")) + .build()); + + persistAndFlush(Socialite.builder() + .zonedDateTime(ZonedDateTime.parse("2007-12-03T23:30:00-01:00")) + .build()); + + Specification spec = DateTimeSpecifications.onDate(Socialite.Fields.zonedDateTime, LocalDate.of(2007, 12, 3)); + List results = getSocialiteRepo().findAll(spec); + + assertThat(results).containsExactly(clearlyDec3); + } } diff --git a/src/test/java/expresspecs/datetime/Socialite.java b/src/test/java/expresspecs/datetime/Socialite.java new file mode 100644 index 0000000..2b8d690 --- /dev/null +++ b/src/test/java/expresspecs/datetime/Socialite.java @@ -0,0 +1,60 @@ +package expresspecs.datetime; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.util.Date; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Temporal; +import jakarta.persistence.TemporalType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.FieldNameConstants; + +@Entity +@Table(name = "socialites") +@Getter +@Setter +@ToString +@FieldNameConstants +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +/** + * Socialite is an entity that has a lot if dates. Get it? + */ +public class Socialite { + + @Id + @GeneratedValue + @EqualsAndHashCode.Include + private Long id; + + private LocalDate localDate; + + @SuppressWarnings("deprecation") + @Temporal(TemporalType.TIMESTAMP) + private Date javaUtilDateAsTimestamp; + + @SuppressWarnings("deprecation") + @Temporal(TemporalType.DATE) + private Date javaUtilDateAsDate; + + @SuppressWarnings("deprecation") + @Temporal(TemporalType.DATE) + private java.sql.Date sqlDate; + + private Timestamp sqlTimestamp; + + private ZonedDateTime zonedDateTime; +} diff --git a/src/test/java/expresspecs/datetime/SocialiteRepository.java b/src/test/java/expresspecs/datetime/SocialiteRepository.java new file mode 100644 index 0000000..c79d62d --- /dev/null +++ b/src/test/java/expresspecs/datetime/SocialiteRepository.java @@ -0,0 +1,7 @@ +package expresspecs.datetime; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface SocialiteRepository extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/src/test/java/expresspecs/example/Customer.java b/src/test/java/expresspecs/example/Customer.java index 9a9a51d..d1447fa 100644 --- a/src/test/java/expresspecs/example/Customer.java +++ b/src/test/java/expresspecs/example/Customer.java @@ -3,15 +3,10 @@ import static lombok.AccessLevel.NONE; import java.time.Instant; -import java.time.LocalDate; import java.time.OffsetDateTime; -import java.util.Date; import java.util.HashSet; import java.util.Set; -import jakarta.persistence.Temporal; -import jakarta.persistence.TemporalType; - import jakarta.persistence.CascadeType; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -57,12 +52,6 @@ public class Customer { private Instant instantTimestamp; - private LocalDate localDateOnly; - - @SuppressWarnings("deprecation") - @Temporal(TemporalType.TIMESTAMP) - private Date legacyDate; - @OneToOne private Address address; diff --git a/src/test/springboot3/java/expresspecs/SB3JpaTestConfig.java b/src/test/springboot3/java/expresspecs/SB3JpaTestConfig.java index 181ada2..0e9a992 100644 --- a/src/test/springboot3/java/expresspecs/SB3JpaTestConfig.java +++ b/src/test/springboot3/java/expresspecs/SB3JpaTestConfig.java @@ -9,14 +9,17 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import expresspecs.datetime.Socialite; +import expresspecs.datetime.SocialiteRepository; +import expresspecs.example.ComponentScanMarker; import rizzoweb.spring.jpa.EntityManagerWrapper; @Configuration @EnableAutoConfiguration -@ComponentScan(basePackageClasses = expresspecs.example.ComponentScanMarker.class) -@EntityScan(basePackageClasses = expresspecs.example.ComponentScanMarker.class) -@EnableJpaRepositories(basePackageClasses = expresspecs.example.ComponentScanMarker.class) +@ComponentScan(basePackageClasses = ComponentScanMarker.class) +@EntityScan(basePackageClasses = { ComponentScanMarker.class, Socialite.class }) +@EnableJpaRepositories(basePackageClasses = { ComponentScanMarker.class, SocialiteRepository.class }) public class SB3JpaTestConfig { @Bean diff --git a/src/test/springboot4/java/expresspecs/SB4JPATestConfig.java b/src/test/springboot4/java/expresspecs/SB4JPATestConfig.java index c436a6a..1bf7ae3 100644 --- a/src/test/springboot4/java/expresspecs/SB4JPATestConfig.java +++ b/src/test/springboot4/java/expresspecs/SB4JPATestConfig.java @@ -9,14 +9,17 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import expresspecs.datetime.Socialite; +import expresspecs.datetime.SocialiteRepository; +import expresspecs.example.ComponentScanMarker; import rizzoweb.spring.jpa.EntityManagerWrapper; @Configuration @EnableAutoConfiguration -@ComponentScan(basePackageClasses = expresspecs.example.ComponentScanMarker.class) -@EntityScan(basePackageClasses = expresspecs.example.ComponentScanMarker.class) -@EnableJpaRepositories(basePackageClasses = expresspecs.example.ComponentScanMarker.class) +@ComponentScan(basePackageClasses = ComponentScanMarker.class) +@EntityScan(basePackageClasses = { ComponentScanMarker.class, Socialite.class }) +@EnableJpaRepositories(basePackageClasses = { ComponentScanMarker.class, SocialiteRepository.class }) public class SB4JPATestConfig { @Bean From 894fa82bb34390c21d20e89ce5068b3eb8a633b9 Mon Sep 17 00:00:00 2001 From: Eric Rizzo Date: Thu, 4 Jun 2026 16:43:57 -0400 Subject: [PATCH 2/4] Changed the fallback strategy for onDate to throw an exception. --- .cursor/rules/api-refactor-approval.mdc | 25 ++++++++ .cursor/rules/plan-before-code.mdc | 8 ++- CLAUDE.md | 8 +++ docs/usage-guide.md | 4 +- .../datetime/DateTimeSpecifications.java | 12 ++-- .../expresspecs/datetime/SameCalendarDay.java | 2 +- .../datetime/SameCalendarDayFallback.java | 10 ++- .../datetime/OnDateTypeCoverageTests.java | 64 ++++++++++++++++++- .../java/expresspecs/datetime/Socialite.java | 6 ++ 9 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 .cursor/rules/api-refactor-approval.mdc diff --git a/.cursor/rules/api-refactor-approval.mdc b/.cursor/rules/api-refactor-approval.mdc new file mode 100644 index 0000000..ec288c3 --- /dev/null +++ b/.cursor/rules/api-refactor-approval.mdc @@ -0,0 +1,25 @@ +--- +description: No public or multi-site API refactors without explicit user approval; propose narrower fixes first +alwaysApply: true +--- + +# API and cross-cutting refactors require explicit approval + +## Do not do without explicit user go-ahead + +Do **not** implement or merge edits that: + +- Change **public** library surface (exported types, public methods, `module-info` exports, or anything consumers rely on), or +- Change **shared contracts** touched in many places at once (for example a **public interface** with **multiple implementations** in this repo, or a new parameter on such an interface), or +- Refactor **across many modules or packages** solely to support a small feature, when a **local** change would work. + +If the user’s goal can be met by changing **one call site**, **one package-private class**, or **one strategy implementation** without widening the blast radius, **prefer that** and say so briefly. + +## When the user asks for a behavior change + +1. Prefer **minimal diffs** that preserve existing contracts. +2. If the clean solution **would** require a wide contract change, **stop and ask**: describe the tradeoff (local hack vs API change) and wait for approval before editing across implementations. + +## After approval + +If the user explicitly approves a contract change, follow the rest of the project rules (tests, Javadoc, `CLAUDE.md`, and workspace sync as applicable). diff --git a/.cursor/rules/plan-before-code.mdc b/.cursor/rules/plan-before-code.mdc index ccf47ff..68465ec 100644 --- a/.cursor/rules/plan-before-code.mdc +++ b/.cursor/rules/plan-before-code.mdc @@ -1,5 +1,5 @@ --- -description: Answer-first for design questions; no repo edits without explicit implementation ask +description: Answer-first for design questions; no unasked edits; ask when repo state or intent is unclear alwaysApply: true --- @@ -25,3 +25,9 @@ For non-trivial implementation the user *does* request, if several files or desi ## No rabbit holes Do **not** expand scope (extra APIs, example rewrites, new helpers, docs, broad test matrix runs) unless the user asked for that scope or agreed to it in the same thread. + +## Do not assume the working tree is wrong + +When what you read in the repo **differs** from an earlier turn, from another path, or from what you expected, **do not treat that as an automatic error** to correct without confirmation. Omitted annotations, different names, or unusual structure may be **deliberate**. + +If you are not sure whether to **restore**, **add**, or **change** something, **ask the user** in one short question before editing. diff --git a/CLAUDE.md b/CLAUDE.md index b07b81b..b3b9332 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,6 +84,14 @@ See **[docs/testing.md](docs/testing.md)** for Maven commands, profiles, Testcon The `sb3` Maven profile switches the Spring Boot BOM version and activates the `src/test/springboot3/java` test source tree. When adding new features, verify behaviour under both profiles (H2 integration tests). Testcontainers profiles are excluded - they require `sb4`. +## API and cross-cutting refactors + +Do **not** change public library API or shared contracts that ripple across many implementations (for example new parameters on a **public interface** implemented in multiple places) **unless the user explicitly approves that design** in the thread. Prefer a **narrow** change (single class, package-private helper, or one call site) when it satisfies the request. If the right fix needs a wide contract change, **propose options and ask first** before editing. + +## Trust the working tree; ask when unsure + +If the code on disk **differs** from what you remember or expected, **do not assume** that gap is a mistake to fix on your own. When it is unclear whether a difference is intentional, **ask the user** before changing it. + ## Style rules **No em dashes:** Do not use the em dash character (U+2014, `—`) anywhere: documentation, Markdown files, Javadoc, or code comments (`//`, `/* */`, `/** */`). Use a comma, colon, semicolon, parentheses, or a hyphen with spaces instead. diff --git a/docs/usage-guide.md b/docs/usage-guide.md index 03cd4bc..f0926bc 100644 --- a/docs/usage-guide.md +++ b/docs/usage-guide.md @@ -263,6 +263,8 @@ Specification spec = yearIs(Customer.Fields.createdTimestamp, 2024); | `java.util.Date` / `java.sql.Timestamp` | UTC half-open range (same UTC window, compared as `java.util.Date`) | | `LocalDateTime` | Wall-clock half-open range: `[targetDate at midnight, targetDate+1 at midnight)`, no zone conversion | +Any other mapped Java type causes `IllegalArgumentException` when the specification runs (for example `java.util.Calendar`). + **Date-only types** (`LocalDate`, `java.sql.Date`) use a simple equality check because the stored value already represents just a calendar date. **Zone-aware types** (`Instant`, `OffsetDateTime`, `ZonedDateTime`, `java.util.Date`, `java.sql.Timestamp`) use a UTC midnight-to-midnight window. "On 2025-03-15" means any instant in `[2025-03-15T00:00:00Z, 2025-03-16T00:00:00Z)`. A value stored as `2025-03-16T01:00+02:00` (which is `2025-03-15T23:00Z`) matches; a value stored as `2025-03-16T00:00:00Z` does not. @@ -270,7 +272,7 @@ Specification spec = yearIs(Customer.Fields.createdTimestamp, 2024); **Zone-naive types** (`LocalDateTime`) compare the stored value directly against the wall-clock midnight boundaries with no zone conversion. `2025-03-15T23:45` matches but `2025-03-16T00:00` does not, regardless of where the server or database is located. > [!NOTE] -> If the property type is not one of the above, `onDate` falls back to wall-clock `LocalDateTime` bounds. Whether the resulting predicate behaves correctly depends on how your JPA provider coerces `LocalDateTime` values to the mapped column type. +> Unsupported property types throw `IllegalArgumentException` with a message that names the leaf type and lists supported alternatives. Through Spring Data JPA, that exception may be wrapped in a `DataAccessException` (for example `InvalidDataAccessApiUsageException`). For all available predicates and their descriptions, see [RangeSpecifications.java](../src/main/java/expresspecs/RangeSpecifications.java) and [DateTimeSpecifications.java](../src/main/java/expresspecs/datetime/DateTimeSpecifications.java). diff --git a/src/main/java/expresspecs/datetime/DateTimeSpecifications.java b/src/main/java/expresspecs/datetime/DateTimeSpecifications.java index 6d12df5..ec340ca 100644 --- a/src/main/java/expresspecs/datetime/DateTimeSpecifications.java +++ b/src/main/java/expresspecs/datetime/DateTimeSpecifications.java @@ -177,15 +177,16 @@ private HibernateCriteriaBuilder hibernateBuilder(CriteriaBuilder builder) { *
  • {@link LocalDateTime}: half-open range using {@linkplain LocalDate#atStartOfDay() start of day} * through the following midnight in the same {@link LocalDateTime} calendar (no zone * conversion).
  • - *
  • Other types: half-open range using {@link LocalDateTime} bounds - * {@code targetDate.atStartOfDay()} (inclusive) through {@code targetDate.plusDays(1).atStartOfDay()} - * (exclusive). The property is compared to those bound values; the effective filter depends on the - * JPA provider and how JDBC coerces {@link LocalDateTime} to the mapped column type.
  • + *
  • Any other leaf property type: {@link IllegalArgumentException} when the specification is + * evaluated, because {@code onDate} does not define calendar-day semantics for that type.
  • * * * @param The entity type being queried. * @param propertyPath Resolved property path to compare. * @param targetDate The date to match. + * @throws IllegalArgumentException if the leaf property type is not one of the supported temporal types + * (see list in this method's description). When the specification is run through Spring Data JPA, + * this exception may be wrapped in a {@link org.springframework.dao.DataAccessException}. */ public static @NonNull Specification onDate(PropertyPath propertyPath, LocalDate targetDate) { if (targetDate == null) { @@ -204,8 +205,7 @@ private static SameCalendarDay getSameCalendarDayStrategy(Class propertyType) .stream() .filter(s -> s.supports(propertyType)) .findFirst() - // This would be a programming error in the fallback strategy - .orElseThrow(() -> new AssertionError("Fallback strategy must support any leaf type")); + .orElseThrow(() -> new AssertionError("SameCalendarDay strategy list must end with a catch-all")); } } diff --git a/src/main/java/expresspecs/datetime/SameCalendarDay.java b/src/main/java/expresspecs/datetime/SameCalendarDay.java index 171a79b..cc6451e 100644 --- a/src/main/java/expresspecs/datetime/SameCalendarDay.java +++ b/src/main/java/expresspecs/datetime/SameCalendarDay.java @@ -7,7 +7,7 @@ import jakarta.persistence.criteria.Predicate; /** - * A strategy interface, implemenations of which build a JPA predicate that checks whether a temporal attribute + * A strategy interface, implementations of which build a JPA predicate that checks whether a temporal attribute * refers to the same calendar day as {@code targetDate}. */ public interface SameCalendarDay { diff --git a/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java b/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java index 91ea021..bca8552 100644 --- a/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java +++ b/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java @@ -1,7 +1,6 @@ package expresspecs.datetime; import java.time.LocalDate; -import java.time.LocalDateTime; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Path; @@ -16,10 +15,9 @@ public boolean supports(Class javaType) { @Override public Predicate toPredicate(Path path, LocalDate targetDate, CriteriaBuilder cb) { - LocalDateTime start = targetDate.atStartOfDay(); - LocalDateTime end = targetDate.plusDays(1).atStartOfDay(); - @SuppressWarnings("unchecked") - Path typed = (Path) path; - return cb.and(cb.greaterThanOrEqualTo(typed, start), cb.lessThan(typed, end)); + Class javaType = path.getJavaType(); + String typeName = javaType == null ? "(unknown)" : javaType.getName(); + throw new IllegalArgumentException( + "DateTimeSpecifications.onDate does not support leaf property type "+ typeName); } } diff --git a/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java b/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java index 442b0f3..3925f13 100644 --- a/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java +++ b/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java @@ -1,16 +1,21 @@ package expresspecs.datetime; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.sql.Timestamp; import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZonedDateTime; +import java.util.Calendar; import java.util.Date; import java.util.List; +import java.util.TimeZone; import org.junit.jupiter.api.Test; +import org.springframework.dao.DataAccessException; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.transaction.annotation.Transactional; @@ -21,8 +26,8 @@ /** * Integration tests that exercise {@link DateTimeSpecifications#onDate} against common temporal - * field types ({@code LocalDate}, {@code Instant}, legacy {@code Date}, {@code OffsetDateTime}, - * {@code ZonedDateTime}) using {@link Socialite} and {@link Customer} persistence fixtures. + * field types ({@code LocalDate}, {@code LocalDateTime}, {@code Instant}, legacy {@code Date}, + * {@code OffsetDateTime}, {@code ZonedDateTime}) using {@link Socialite} and {@link Customer} persistence fixtures. */ @Transactional public interface OnDateTypeCoverageTests extends DataIntegrationTest { @@ -45,6 +50,42 @@ default void onDate_LocalDateField() { assertThat(results).containsExactly(dec3); } + @Test + default void onDate_LocalDateTimeField() { + var dec3 = persistAndFlush(Socialite.builder() + .localDateTime(LocalDateTime.parse("2007-12-03T10:15:30")) + .build()); + + persistAndFlush(Socialite.builder() + .localDateTime(LocalDateTime.parse("2007-12-04T10:15:30")) + .build()); + + Specification spec = DateTimeSpecifications.onDate(Socialite.Fields.localDateTime, LocalDate.of(2007, 12, 3)); + List results = getSocialiteRepo().findAll(spec); + + assertThat(results).containsExactly(dec3); + } + + /** + * {@code LocalDateTime} uses wall-clock midnight bounds, not UTC. A value at the start of the + * next calendar day must not match the previous {@code onDate} target. + */ + @Test + default void onDate_LocalDateTimeField_midnightBoundary() { + var clearlyDec3 = persistAndFlush(Socialite.builder() + .localDateTime(LocalDateTime.parse("2007-12-03T23:45")) + .build()); + + persistAndFlush(Socialite.builder() + .localDateTime(LocalDateTime.parse("2007-12-04T00:00")) + .build()); + + Specification spec = DateTimeSpecifications.onDate(Socialite.Fields.localDateTime, LocalDate.of(2007, 12, 3)); + List results = getSocialiteRepo().findAll(spec); + + assertThat(results).containsExactly(clearlyDec3); + } + @Test default void onDate_InstantField() { var dec3 = persistAndFlush(Customer.builder() @@ -125,6 +166,25 @@ default void onDate_SqlTimestampField() { assertThat(results).containsExactly(dec3); } + @Test + default void onDate_Fallback_throwsIllegalArgumentException() { + Calendar dec3 = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + dec3.set(2007, Calendar.DECEMBER, 3, 10, 15, 30); + dec3.set(Calendar.MILLISECOND, 0); + persistAndFlush(Socialite.builder() + .javaUtilCalendar(dec3) + .build()); + + Specification spec = DateTimeSpecifications.onDate(Socialite.Fields.javaUtilCalendar, LocalDate.of(2007, 12, 3)); + + assertThatThrownBy(() -> getSocialiteRepo().findAll(spec)) + .isInstanceOf(DataAccessException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .cause() + .hasMessageContaining("java.util.Calendar") + .hasMessageContaining("DateTimeSpecifications.onDate"); + } + /** * Stores a timestamp that is Dec 3 in local time but Dec 4 in UTC * ({@code 2007-12-03T23:30:00-01:00 == 2007-12-04T00:30:00Z}), alongside a value diff --git a/src/test/java/expresspecs/datetime/Socialite.java b/src/test/java/expresspecs/datetime/Socialite.java index 2b8d690..b5ae928 100644 --- a/src/test/java/expresspecs/datetime/Socialite.java +++ b/src/test/java/expresspecs/datetime/Socialite.java @@ -2,7 +2,9 @@ import java.sql.Timestamp; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.ZonedDateTime; +import java.util.Calendar; import java.util.Date; import jakarta.persistence.Entity; @@ -42,6 +44,8 @@ public class Socialite { private LocalDate localDate; + private LocalDateTime localDateTime; + @SuppressWarnings("deprecation") @Temporal(TemporalType.TIMESTAMP) private Date javaUtilDateAsTimestamp; @@ -56,5 +60,7 @@ public class Socialite { private Timestamp sqlTimestamp; + private Calendar javaUtilCalendar; + private ZonedDateTime zonedDateTime; } From 2e7ab216b2073484553658467996a5d75c0cd9de Mon Sep 17 00:00:00 2001 From: Eric Rizzo Date: Thu, 4 Jun 2026 17:20:46 -0400 Subject: [PATCH 3/4] Added UnsupportedDatePropertyException --- .../datetime/SameCalendarDayFallback.java | 3 +-- .../UnsupportedDatePropertyException.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 src/main/java/expresspecs/datetime/UnsupportedDatePropertyException.java diff --git a/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java b/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java index bca8552..1dc0fd2 100644 --- a/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java +++ b/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java @@ -17,7 +17,6 @@ public boolean supports(Class javaType) { public Predicate toPredicate(Path path, LocalDate targetDate, CriteriaBuilder cb) { Class javaType = path.getJavaType(); String typeName = javaType == null ? "(unknown)" : javaType.getName(); - throw new IllegalArgumentException( - "DateTimeSpecifications.onDate does not support leaf property type "+ typeName); + throw new UnsupportedDatePropertyException(javaType, "onDate does not support property type "+ typeName); } } diff --git a/src/main/java/expresspecs/datetime/UnsupportedDatePropertyException.java b/src/main/java/expresspecs/datetime/UnsupportedDatePropertyException.java new file mode 100644 index 0000000..846c4ff --- /dev/null +++ b/src/main/java/expresspecs/datetime/UnsupportedDatePropertyException.java @@ -0,0 +1,18 @@ +package expresspecs.datetime; + +import lombok.Getter; + +public class UnsupportedDatePropertyException extends IllegalArgumentException { + + private static final long serialVersionUID = 1L; + + @Getter + private Class type; + + + public UnsupportedDatePropertyException(Class type, String message) { + super(message); + this.type = type; + } + +} From f857281546ff853a121e00a8e418eb9a83c1ceb2 Mon Sep 17 00:00:00 2001 From: Eric Rizzo Date: Mon, 15 Jun 2026 14:28:44 -0400 Subject: [PATCH 4/4] More coverage improvements --- .cursor/rules/lombok-boilerplate.mdc | 21 ++++++ CLAUDE.md | 2 + docs/usage-guide.md | 3 +- src/main/java/expresspecs/PropertyPath.java | 7 ++ .../datetime/DateTimeSpecifications.java | 35 ++------- .../datetime/OnDateSpecification.java | 75 +++++++++++++++++++ .../datetime/SameCalendarDayFallback.java | 22 ------ .../UnsupportedDatePropertyException.java | 10 ++- .../datetime/OnDateTypeCoverageTests.java | 7 +- 9 files changed, 123 insertions(+), 59 deletions(-) create mode 100644 .cursor/rules/lombok-boilerplate.mdc create mode 100644 src/main/java/expresspecs/datetime/OnDateSpecification.java delete mode 100644 src/main/java/expresspecs/datetime/SameCalendarDayFallback.java diff --git a/.cursor/rules/lombok-boilerplate.mdc b/.cursor/rules/lombok-boilerplate.mdc new file mode 100644 index 0000000..7dc2291 --- /dev/null +++ b/.cursor/rules/lombok-boilerplate.mdc @@ -0,0 +1,21 @@ +--- +description: Prefer Lombok for constructors, getters, and setters instead of hand-written boilerplate +alwaysApply: true +--- + +# Lombok over hand-written accessors and constructors + +When adding or editing Java types in this repository, **prefer Lombok** to generate repetitive members instead of spelling them out in source. + +**Use Lombok for** + +- **Getters and setters:** `@Getter`, `@Setter` (type or field level), or `@Data` / `@Value` when the rest of the bundle matches the design. +- **Constructors:** `@RequiredArgsConstructor`, `@NoArgsConstructor`, `@AllArgsConstructor`, or `@Builder` (with `@Builder.Default` where needed) instead of manual constructor bodies that only assign fields. + +**When explicit code is still appropriate** + +- Logic in a constructor (validation, copying defensively, computing fields). +- `record` types: rely on the compact canonical constructor and built-in accessors; do not duplicate accessors with Lombok unless there is a clear need. +- Members Lombok cannot express, or APIs that must stay stable and are intentionally hand-written. + +After introducing or changing Lombok annotations on a type, ensure the project still compiles (`jdt problems` or `./mvnw clean test` as appropriate). diff --git a/CLAUDE.md b/CLAUDE.md index b3b9332..74191eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,3 +95,5 @@ If the code on disk **differs** from what you remember or expected, **do not ass ## Style rules **No em dashes:** Do not use the em dash character (U+2014, `—`) anywhere: documentation, Markdown files, Javadoc, or code comments (`//`, `/* */`, `/** */`). Use a comma, colon, semicolon, parentheses, or a hyphen with spaces instead. + +**Lombok:** Prefer Lombok (`@Getter`, `@Setter`, `@RequiredArgsConstructor`, `@NoArgsConstructor`, `@AllArgsConstructor`, `@Builder`, `@Data`, `@Value`, etc.) to generate constructors, getters, and setters instead of hand-written boilerplate. Keep explicit constructors when they contain real logic (validation, defensive copies). For `record` types, use the language features instead of duplicating accessors with Lombok unless there is a clear need. diff --git a/docs/usage-guide.md b/docs/usage-guide.md index f0926bc..ebe161a 100644 --- a/docs/usage-guide.md +++ b/docs/usage-guide.md @@ -227,7 +227,6 @@ Specification spec = isNotNullOrEmpty(Customer.Fields.name); > condition another way (for example, checking only `isNotNull`, since Oracle cannot store an > empty string and a non-null column value is therefore guaranteed to be non-empty). - ### Range & DateTime Specifications For numbers, dates, and comparisons. @@ -263,7 +262,7 @@ Specification spec = yearIs(Customer.Fields.createdTimestamp, 2024); | `java.util.Date` / `java.sql.Timestamp` | UTC half-open range (same UTC window, compared as `java.util.Date`) | | `LocalDateTime` | Wall-clock half-open range: `[targetDate at midnight, targetDate+1 at midnight)`, no zone conversion | -Any other mapped Java type causes `IllegalArgumentException` when the specification runs (for example `java.util.Calendar`). +Any other mapped Java type causes an runtime exception when the specification predicate is evaluated (for example `java.util.Calendar`). **Date-only types** (`LocalDate`, `java.sql.Date`) use a simple equality check because the stored value already represents just a calendar date. diff --git a/src/main/java/expresspecs/PropertyPath.java b/src/main/java/expresspecs/PropertyPath.java index ac6b868..52de6ba 100644 --- a/src/main/java/expresspecs/PropertyPath.java +++ b/src/main/java/expresspecs/PropertyPath.java @@ -7,6 +7,8 @@ import java.util.Arrays; import java.util.List; +import org.apache.commons.lang3.StringUtils; + import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.Path; @@ -124,6 +126,11 @@ public static PropertyPath of(SingularAttribute first, Plural return new PropertyPath(List.of(first.getName(), second.getName(), third.getName())); } + @Override + public String toString() { + return StringUtils.join(properties, '.'); + } + @SuppressWarnings("unchecked") public Path asPath(Root root) { List intermediates = properties.subList(0, properties.size() - 1); diff --git a/src/main/java/expresspecs/datetime/DateTimeSpecifications.java b/src/main/java/expresspecs/datetime/DateTimeSpecifications.java index ec340ca..44e659b 100644 --- a/src/main/java/expresspecs/datetime/DateTimeSpecifications.java +++ b/src/main/java/expresspecs/datetime/DateTimeSpecifications.java @@ -8,7 +8,6 @@ import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.util.Date; -import java.util.List; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.jspecify.annotations.NonNull; @@ -17,7 +16,6 @@ import expresspecs.PropertyPath; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Expression; -import jakarta.persistence.criteria.Path; import lombok.experimental.UtilityClass; /** @@ -35,18 +33,6 @@ @UtilityClass public class DateTimeSpecifications { - // The ordering of this list is important - private static final List SAME_CALENDAR_DAY_STRATEGIES = List.of( - new SameCalendarDayForLocalDate(), - new SameCalendarDayForSqlDate(), - new SameCalendarDayForInstant(), - new SameCalendarDayForOffsetDateTime(), - new SameCalendarDayForZonedDateTime(), - new SameCalendarDayForLocalDateTime(), - new SameCalendarDayForUtilDate(), - new SameCalendarDayFallback()); - - private HibernateCriteriaBuilder hibernateBuilder(CriteriaBuilder builder) { return (HibernateCriteriaBuilder) builder; } @@ -177,14 +163,15 @@ private HibernateCriteriaBuilder hibernateBuilder(CriteriaBuilder builder) { *
  • {@link LocalDateTime}: half-open range using {@linkplain LocalDate#atStartOfDay() start of day} * through the following midnight in the same {@link LocalDateTime} calendar (no zone * conversion).
  • - *
  • Any other leaf property type: {@link IllegalArgumentException} when the specification is - * evaluated, because {@code onDate} does not define calendar-day semantics for that type.
  • + *
  • Any other leaf property type: {@link UnsupportedDatePropertyException} (a subtype of + * {@link IllegalArgumentException}) when the specification is evaluated, because {@code onDate} + * does not define calendar-day semantics for that type.
  • * * * @param The entity type being queried. * @param propertyPath Resolved property path to compare. * @param targetDate The date to match. - * @throws IllegalArgumentException if the leaf property type is not one of the supported temporal types + * @throws UnsupportedDatePropertyException if the leaf property type is not one of the supported temporal types * (see list in this method's description). When the specification is run through Spring Data JPA, * this exception may be wrapped in a {@link org.springframework.dao.DataAccessException}. */ @@ -193,19 +180,7 @@ private HibernateCriteriaBuilder hibernateBuilder(CriteriaBuilder builder) { return unrestricted(); } - return (root, query, cb) -> { - Path path = propertyPath.asPath(root); - Class javaType = path.getJavaType(); - return getSameCalendarDayStrategy(javaType).toPredicate(path, targetDate, cb); - }; - } - - private static SameCalendarDay getSameCalendarDayStrategy(Class propertyType) { - return SAME_CALENDAR_DAY_STRATEGIES - .stream() - .filter(s -> s.supports(propertyType)) - .findFirst() - .orElseThrow(() -> new AssertionError("SameCalendarDay strategy list must end with a catch-all")); + return new OnDateSpecification<>(propertyPath, targetDate); } } diff --git a/src/main/java/expresspecs/datetime/OnDateSpecification.java b/src/main/java/expresspecs/datetime/OnDateSpecification.java new file mode 100644 index 0000000..60d3108 --- /dev/null +++ b/src/main/java/expresspecs/datetime/OnDateSpecification.java @@ -0,0 +1,75 @@ +package expresspecs.datetime; + +import java.time.LocalDate; +import java.util.List; + +import org.jspecify.annotations.NonNull; +import org.springframework.data.jpa.domain.Specification; + +import expresspecs.PropertyPath; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import lombok.RequiredArgsConstructor; + +/** + * {@link Specification} that matches entities whose temporal property falls on a given calendar + * {@link LocalDate}. + * + *

    When the specification is translated to a {@linkplain jakarta.persistence.criteria.Predicate predicate}, + * the property path's Java type selects the first {@link SameCalendarDay} implementation that + * {@linkplain SameCalendarDay#supports(Class) supports} that type. Unsupported types yield + * {@link UnsupportedDatePropertyException} at evaluation time. + * + *

    {@link DateTimeSpecifications#onDate(PropertyPath, LocalDate)} is the typical entry point: it returns + * an instance of this class for a non-null date, and {@link expresspecs.BasicSpecifications#unrestricted()} + * when the date is {@code null}. The detailed semantics per supported property type are documented on that method. + * + * @param entity root type + */ +@RequiredArgsConstructor +public final class OnDateSpecification implements Specification { + + private static final long serialVersionUID = 1L; + + // The ordering of this list is important + private static final List SAME_CALENDAR_DAY_STRATEGIES = List.of( + new SameCalendarDayForLocalDate(), + new SameCalendarDayForSqlDate(), + new SameCalendarDayForInstant(), + new SameCalendarDayForOffsetDateTime(), + new SameCalendarDayForZonedDateTime(), + new SameCalendarDayForLocalDateTime(), + new SameCalendarDayForUtilDate() + ); + + private final PropertyPath propertyPath; + private final LocalDate targetDate; + + + /** + * @throws UnsupportedDatePropertyException if the property is of a type not supported (eg, java.util.Calendar) + */ + @Override + public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery query, @NonNull CriteriaBuilder cb) { + Path path = propertyPath.asPath(root); + Class javaType = path.getJavaType(); + SameCalendarDay strategy = getSameCalendarDayStrategy(javaType); + + if (strategy == null) { + throw new UnsupportedDatePropertyException(propertyPath, javaType, root); + } + + return strategy.toPredicate(path, targetDate, cb); + } + + private static SameCalendarDay getSameCalendarDayStrategy(Class propertyType) { + return SAME_CALENDAR_DAY_STRATEGIES + .stream() + .filter(s -> s.supports(propertyType)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java b/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java deleted file mode 100644 index 1dc0fd2..0000000 --- a/src/main/java/expresspecs/datetime/SameCalendarDayFallback.java +++ /dev/null @@ -1,22 +0,0 @@ -package expresspecs.datetime; - -import java.time.LocalDate; - -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.Path; -import jakarta.persistence.criteria.Predicate; - -final class SameCalendarDayFallback implements SameCalendarDay { - - @Override - public boolean supports(Class javaType) { - return true; - } - - @Override - public Predicate toPredicate(Path path, LocalDate targetDate, CriteriaBuilder cb) { - Class javaType = path.getJavaType(); - String typeName = javaType == null ? "(unknown)" : javaType.getName(); - throw new UnsupportedDatePropertyException(javaType, "onDate does not support property type "+ typeName); - } -} diff --git a/src/main/java/expresspecs/datetime/UnsupportedDatePropertyException.java b/src/main/java/expresspecs/datetime/UnsupportedDatePropertyException.java index 846c4ff..091ecc4 100644 --- a/src/main/java/expresspecs/datetime/UnsupportedDatePropertyException.java +++ b/src/main/java/expresspecs/datetime/UnsupportedDatePropertyException.java @@ -1,5 +1,7 @@ package expresspecs.datetime; +import expresspecs.PropertyPath; +import jakarta.persistence.criteria.Root; import lombok.Getter; public class UnsupportedDatePropertyException extends IllegalArgumentException { @@ -10,8 +12,12 @@ public class UnsupportedDatePropertyException extends IllegalArgumentException { private Class type; - public UnsupportedDatePropertyException(Class type, String message) { - super(message); + public UnsupportedDatePropertyException(PropertyPath path, Class type, Root root) { + super("Property %s.%s is type %s which is not supported" + .formatted(root.getJavaType().getSimpleName(), + path, + type == null ? "(unknown)" : type.getName() + )); this.type = type; } diff --git a/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java b/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java index 3925f13..774bf99 100644 --- a/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java +++ b/src/test/java/expresspecs/datetime/OnDateTypeCoverageTests.java @@ -167,7 +167,7 @@ default void onDate_SqlTimestampField() { } @Test - default void onDate_Fallback_throwsIllegalArgumentException() { + default void onDate_UndupportedPropertyType() { Calendar dec3 = Calendar.getInstance(TimeZone.getTimeZone("UTC")); dec3.set(2007, Calendar.DECEMBER, 3, 10, 15, 30); dec3.set(Calendar.MILLISECOND, 0); @@ -179,10 +179,11 @@ default void onDate_Fallback_throwsIllegalArgumentException() { assertThatThrownBy(() -> getSocialiteRepo().findAll(spec)) .isInstanceOf(DataAccessException.class) - .hasCauseInstanceOf(IllegalArgumentException.class) + .hasCauseInstanceOf(UnsupportedDatePropertyException.class) .cause() .hasMessageContaining("java.util.Calendar") - .hasMessageContaining("DateTimeSpecifications.onDate"); + .hasMessageContaining(Socialite.class.getSimpleName()) + .hasMessageContaining(Socialite.Fields.javaUtilCalendar); } /**