Description
I have the following PostgreSQL table DDL:
Partitioned table "service.my_values"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
--------------------+-----------------------------+-----------+----------+---------+---------+-------------+--------------+-------------
hour | timestamp(6) with time zone | | not null | | plain | | |
client_id | uuid | | not null | | plain | | |
number_of_invoices | bigint | | not null | | plain | | |
gross_revenue | numeric(18,3) | | not null | | main | | |
Partition key: RANGE (hour)
Indexes:
"my_values_pkey" PRIMARY KEY, btree (client_id, hour)
Partitions: ... (monthly partitions for a given time frame)
And the following data (note: uuid is random):
INSERT INTO service.my_values (hour, client_id, number_of_invoices, gross_revenue)
VALUES
('2024-03-19T00:00:00Z', 'b5aeae94-5e69-4775-a8ea-b5265619cda7', 1, 1),
('2024-03-19T11:00:00Z', 'b5aeae94-5e69-4775-a8ea-b5265619cda7', 1, 1),
('2024-03-20T01:00:00Z', 'b5aeae94-5e69-4775-a8ea-b5265619cda7', 1, 1),
('2024-03-20T04:00:00Z', 'b5aeae94-5e69-4775-a8ea-b5265619cda7', 1, 1);
When I run the following native query repository method:
@Repository
public interface MyRepository extends JpaRepository<MyValue, UUID> {
/**
* @implNote JPA behaviour seems broken when grouping by the {@code date_trunc} function,
* hence we are grouping on the second select "column".
*/
@Query(value = """
SELECT
client_id,
date_trunc(:#{#unitOfTime.name()}, hour AT TIME ZONE 'UTC' - (INTERVAL '1 hour') * :offset) AS hour,
SUM(gross_revenue) AS gross_revenue,
SUM(number_of_invoices) AS number_of_invoices
FROM
service.my_values
WHERE
client_id = :clientId
AND hour >= :from
AND hour < :until
GROUP BY
client_id,
2
ORDER BY
2 ASC;
""", nativeQuery = true)
Stream<MyValue> findMyValues(
@Param("clientId") UUID clientId,
@Param("from") Instant from,
@Param("until") Instant until,
@Param("offset") int offset,
@Param("unitOfTime") UnitOfTime unitOfTime);
}
I expect to get the value: MyValue(clientId=b5aeae94-5e69-4775-a8ea-b5265619cda7, hour=2024-03-19T00:00:00Z, grossRevenue=2, numberOfInvoices=2)
.
The actual value I get is: MyValue(clientId=b5aeae94-5e69-4775-a8ea-b5265619cda7, hour=2024-03-19T00:00:00Z, grossRevenue=1, numberOfInvoices=1)
.
If I run the same query using entityManager.createNativeQuery()
(note: SPeL part switched for a string as createNativeQuery
does not seem to support it):
var query = entityManager.createNativeQuery("""
SELECT
client_id,
date_trunc(:unitOfTimeString, hour AT TIME ZONE 'UTC' - (INTERVAL '1 hour') * :offset) AS hour,
SUM(gross_revenue) AS gross_revenue,
SUM(number_of_invoices) AS number_of_invoices
FROM
service.my_values
WHERE
client_id = :clientId
AND hour >= :from
AND hour < :until
GROUP BY
client_id,
2
ORDER BY
2 ASC;
""")
.setParameter("clientId", clientId)
.setParameter("from", timeSpan.from())
.setParameter("until", timeSpan.until())
.setParameter("offset", timeUtils.getCurrentTZOffset(dt_2024_03_19T10_00))
.setParameter("unitOfTimeString", DAY.name());
var emResult = query.getResultList();
assertThat(emResult).hasSize(1);
Object[] row = (Object[]) emResult.get(0);
assertThat(row[0]).isEqualTo(clientId);
assertThat(row[1]).isEqualTo(Timestamp.from(expectedDayBoundary));
assertThat(row[2]).isEqualTo(BigDecimal.valueOf(2));
assertThat(row[3]).isEqualTo(BigDecimal.valueOf(2));
Then I get the expected result. If I run the same query in the console (using e.g. psql
), I also get the expected result.
Using spring.jpa.show-sql=true
and spring.jpa.properties.hibernate.format_sql=true
, I see the following SQL call logged:
Hibernate:
/* dynamic native SQL query */ SELECT
client_id,
date_trunc(?, hour AT TIME ZONE 'UTC' - (INTERVAL '1 hour') * ?) AS hour,
SUM(gross_revenue) AS gross_revenue,
SUM(number_of_invoices) AS number_of_invoices
FROM
service.my_values
WHERE
client_id = ?
AND hour >= ?
AND hour < ?
GROUP BY
client_id,
2
ORDER BY
2 ASC;
2025-03-07 17:05:26.311 TRACE 1007964 --- [ Test worker] org.hibernate.orm.jdbc.bind : binding parameter (1:VARCHAR) <- [DAY]
2025-03-07 17:05:26.311 TRACE 1007964 --- [ Test worker] org.hibernate.orm.jdbc.bind : binding parameter (2:INTEGER) <- [3]
2025-03-07 17:05:26.311 TRACE 1007964 --- [ Test worker] org.hibernate.orm.jdbc.bind : binding parameter (3:UUID) <- [b5aeae94-5e69-4775-a8ea-b5265619cda7]
2025-03-07 17:05:26.311 TRACE 1007964 --- [ Test worker] org.hibernate.orm.jdbc.bind : binding parameter (4:TIMESTAMP_UTC) <- [2024-03-19T03:00:00Z]
2025-03-07 17:05:26.312 TRACE 1007964 --- [ Test worker] org.hibernate.orm.jdbc.bind : binding parameter (5:TIMESTAMP_UTC) <- [2024-03-20T02:59:59.999999999Z]
which is exactly the query I was expecting.
Here are my entity definitions:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@IdClass(MyKey.class)
@Table(name = "my_values")
public class MyValue {
@Id
@JsonIgnore
private UUID clientId;
@Id
private Instant hour;
@Column(precision = BigDecimalColumnDef.PRECISION, scale = BigDecimalColumnDef.SCALE)
private BigDecimal grossRevenue;
private long numberOfInvoices;
}
Id class:
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class MyKey {
private UUID clientId;
private Instant hour;
}
And this is how the failing test which contains this issue creates the database entries:
List<MyValue> entries = List.of(
/* not in day */ new MyValue(clientId, dt_2024_03_19T01_00, BigDecimal.ONE, 1L),
new MyValue(clientId, dt_2024_03_19T12_00, BigDecimal.ONE, 1L),
new MyValue(clientId, dt_2024_03_20T02_00, BigDecimal.ONE, 1L),
/* not in day */ new MyValue(clientId, dt_2024_03_20T05_00, BigDecimal.ONE, 1L)
);
repository.saveAll(entries);
repository.flush();
(note to avoid confusion: the datetime variables represent times in a +01:00
time zone, but are Instant
s that match the above UTC times in the INSERT
statements)
Seeing how:
- JPA generates the query I was expecting, and
- The native query via
entityManager.createNativeQuery()
returns the correct results
this seems to be Spring Data JPA issue.
I have almost exactly the same query (using different tables with very similar structure) in different repositories which works, which is why I'm a bit baffled at this behaviour. Another query that looks practically identical, is as follows:
@Query(value = """
SELECT
client_id,
date_trunc(:#{#unitOfTime.name()}, hour AT TIME ZONE 'UTC' - (INTERVAL '1 hour') * :offset) AS hour,
SUM(gross_revenue) AS gross_revenue,
SUM(net_revenue) AS net_revenue
FROM
service.my_other_values
WHERE
client_id = :clientId
AND hour >= :from
AND hour < :until
GROUP BY
client_id,
2
ORDER BY
2 ASC;
""", nativeQuery = true)
Stream<MyOtherValue> findOtherValues(
@Param("clientId") UUID clientId,
@Param("from") Instant from,
@Param("until") Instant until,
@Param("offset") int offset,
@Param("unitOfTime") UnitOfTime unitOfTime);
with the following entity:
@Data
@NoArgsConstructor // Required by JPA
@AllArgsConstructor
@Entity
@IdClass(MyOtherKey.class)
@Table(name = "my_other_values")
@NamedNativeQueries({ /* named native queries used for other methods */ })
public class MyOtherValue {
@Id
@JsonIgnore
private UUID clientId;
@Id
private Instant hour;
@Column(precision = BigDecimalColumnDef.PRECISION, scale = BigDecimalColumnDef.SCALE)
private BigDecimal grossRevenue;
@Column(precision = BigDecimalColumnDef.PRECISION, scale = BigDecimalColumnDef.SCALE)
private BigDecimal netRevenue;
}
and Id class:
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class MyOtherKey {
private UUID clientId;
private Instant hour;
}
Unfortunately, I'm not sure how to debug this as JPA/Hibernate stacks are very deep and difficult (IMO) to debug unless you have a lot of experience with it.
I unfortunately cannot provide a reproducible sample at this time, but if that's the only way to move forward, I can spend some time next week preparing an MRP. In the meantime, if there's anything I could potentially try, I would appreciate it.