feat(documents): add density and date-range repository queries (#385)

Native SQL aggregations backing GET /api/documents/density:
- findDensityByMonth groups documents by truncated meta_date with optional
  from/to bounds (frontend fills zero-count gaps).
- findMinMaxDocumentDate returns the earliest/latest meta_date via projection,
  null on empty archive.

Covered by DocumentDensityIntegrationTest (Testcontainers PostgreSQL): empty
archive, single+multi-month grouping, from/to bounds, null meta_date exclusion,
min/max edge cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-07 21:47:59 +02:00
parent e61e3797d1
commit c90b42d045
3 changed files with 138 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package org.raddatz.familienarchiv.document;
import java.time.LocalDate;
/**
* Spring Data projection for the document_date min/max query backing the timeline
* density widget (issue #385). Column aliases in the native SQL must match these
* getter names exactly. Both values are {@code null} when the documents table is empty.
*/
public interface DocumentDateRangeProjection {
LocalDate getMinDate();
LocalDate getMaxDate();
}

View File

@@ -240,4 +240,28 @@ public interface DocumentRepository extends JpaRepository<Document, UUID>, JpaSp
""")
TranscriptionWeeklyStatsProjection findWeeklyStats();
/**
* Document count grouped by calendar month for the timeline density widget (issue #385).
* Each row is {@code Object[]{ String yearMonth, long count }}.
* Months without documents are not present — the frontend fills gaps from minDate/maxDate.
*/
@Query(nativeQuery = true, value = """
SELECT TO_CHAR(DATE_TRUNC('month', meta_date), 'YYYY-MM') AS month,
COUNT(*) AS doc_count
FROM documents
WHERE meta_date IS NOT NULL
AND (CAST(:from AS date) IS NULL OR meta_date >= :from)
AND (CAST(:to AS date) IS NULL OR meta_date <= :to)
GROUP BY 1
ORDER BY 1
""")
List<Object[]> findDensityByMonth(@Param("from") LocalDate from, @Param("to") LocalDate to);
/**
* Earliest and latest {@code meta_date} across all documents. Both getters return
* {@code null} when the table holds no documents.
*/
@Query(nativeQuery = true, value = "SELECT MIN(meta_date) AS minDate, MAX(meta_date) AS maxDate FROM documents")
DocumentDateRangeProjection findMinMaxDocumentDate();
}