Interface CleanupCampaignItemDAO
- All Superinterfaces:
org.springframework.data.repository.CrudRepository<CleanupCampaignItemEntity,,Long> org.springframework.data.jpa.repository.JpaRepository<CleanupCampaignItemEntity,,Long> org.springframework.data.repository.ListCrudRepository<CleanupCampaignItemEntity,,Long> org.springframework.data.repository.ListPagingAndSortingRepository<CleanupCampaignItemEntity,,Long> org.springframework.data.repository.PagingAndSortingRepository<CleanupCampaignItemEntity,,Long> org.springframework.data.repository.query.QueryByExampleExecutor<CleanupCampaignItemEntity>,org.springframework.data.repository.Repository<CleanupCampaignItemEntity,Long>
-
Field Summary
Fields -
Method Summary
Modifier and TypeMethodDescriptionaggregateItemsAbsentFromCampaign(long campaignId, long otherCampaignId) Count and reclaimable-bytes sum of the items ofcampaignIdwhose node uuid is ABSENT fromotherCampaignId.aggregateItemsSharedWithCampaign(long campaignId, long otherCampaignId) Count and reclaimable-bytes sum of the items ofcampaignIdwhose node uuid ALSO belongs tootherCampaignId— the 'persisting' bucket of a campaign comparison, computed set-based by the database (never by loading both candidate sets in memory).longcountByCampaignIdAndOwnerIdentityIdInAndState(long campaignId, List<Long> ownerIdentityIds, String state) longcountByCampaignIdAndState(long campaignId, String state) countFailuresByReason(long campaignId, String state) Per-reason item counts of a campaign's failures, in ONE grouped query (rows: failure reason, item count).voiddeleteByCampaignId(long campaignId) Drops every item row of a campaign in ONE statement.booleanexistsByCampaignId(long campaignId) findAggregatesByCampaignIds(List<Long> campaignIds) Per-campaign, per-state item aggregates of a whole campaigns list in ONE grouped query (rows: campaignId, state, item count, reclaimable bytes sum, reclaimed bytes sum), folded by the Storage layer into one aggregate per campaign — replaces up to 4 aggregate queries PER campaign on the list endpoint.findByCampaignIdAndIdGreaterThanOrderByIdAsc(long campaignId, long lastId, org.springframework.data.domain.Pageable pageable) KEYSET page of a campaign's items in id order — the CSV export walk.findByCampaignIdAndNodeUuidIn(long campaignId, Collection<String> nodeUuids) findByCampaignIdAndPathTouchedBy(long campaignId, List<String> ancestorPaths, String escapedEventPath) Items touched by a JCR event path, in two index-friendly directions (the column always stays on the LEFT of the LIKE, the parameter on the right): items that are the event node or one of its ancestors: exact match against the ancestor chain computed in Java by the Storage layer items below the event node (a deleted/moved FOLDER fires a single JCR event for the top-most node only): prefix LIKE on the event path, with its '_'/'%' wildcards escaped by the Storage layer ('|' escape character), so padded segments like /Users/j___/ never wildcard-match unrelated rowsorg.springframework.data.domain.Page<CleanupCampaignItemEntity> findByCampaignIdAndState(long campaignId, String state, org.springframework.data.domain.Pageable pageable) org.springframework.data.domain.Page<CleanupCampaignItemEntity> findByFilters(long campaignId, Long ownerIdentityId, String state, String action, Long minSize, String searchPattern, org.springframework.data.domain.Pageable pageable) Campaign items matching every provided filter, each one null-tolerant so a single query serves any combination.org.springframework.data.domain.Page<CleanupCampaignItemEntity> findByOwnersAndSearch(long campaignId, List<Long> ownerIdentityIds, String searchPattern, org.springframework.data.domain.Pageable pageable) The user-review counterpart offindByFilters(long, java.lang.Long, java.lang.String, java.lang.String, java.lang.Long, java.lang.String, org.springframework.data.domain.Pageable): the items of the campaign owned by the given identities (the user plus the spaces they manage), optionally narrowed by the same null-tolerant path search — see that method's javadoc for the pattern contract and the index note.findByStateOrderedByReclaimableBytes(long campaignId, String state, Long lastReclaimableBytes, long lastId, org.springframework.data.domain.Pageable pageable) KEYSET page of the items of a campaign in a given state, BIGGEST FIRST — the order the purge worker consumes them in.Campaign ids that have item rows but NO campaign row — rows nothing can ever read again, every item query being scoped by campaign id.findRetryableFailures(long campaignId, String state, Collection<String> failureReasons, long maxAttemptCount, long lastId, org.springframework.data.domain.Pageable pageable) KEYSET page of the RETRYABLE failures of a campaign: SKIPPED items whose failure reason belongs to the allowlist the Service owns and whose attempt count is still under the bound.longsumReclaimableBytesByOwnersAndState(long campaignId, List<Long> ownerIdentityIds, String state) longsumReclaimableBytesByState(long campaignId, String state) longsumReclaimedBytes(long campaignId) longsumReclaimedBytesByOwners(long campaignId, List<Long> ownerIdentityIds) Methods inherited from interface org.springframework.data.repository.CrudRepository
count, delete, deleteAll, deleteAll, deleteAllById, deleteById, existsById, findById, saveMethods inherited from interface org.springframework.data.jpa.repository.JpaRepository
deleteAllByIdInBatch, deleteAllInBatch, deleteAllInBatch, deleteInBatch, findAll, findAll, flush, getById, getOne, getReferenceById, saveAllAndFlush, saveAndFlushMethods inherited from interface org.springframework.data.repository.ListCrudRepository
findAll, findAllById, saveAllMethods inherited from interface org.springframework.data.repository.ListPagingAndSortingRepository
findAllMethods inherited from interface org.springframework.data.repository.PagingAndSortingRepository
findAllMethods inherited from interface org.springframework.data.repository.query.QueryByExampleExecutor
count, exists, findAll, findBy, findOne
-
Field Details
-
RECLAIMABLE_BYTES_PROPERTY
The persisted reclaimable figure of an item — the bytes the item's OWN action frees:- DELETE: the content size PLUS
versionsSize. A hard delete destroys the file's whole version history along with its content, andCleanupJcrStorage#deleteNodereports exactly that sum back asreclaimedBytes— summing content alone under-reported every DELETE candidate by the entire weight of its versions, on the very number an administrator publishes a campaign on - PURGE_VERSIONS:
versionsSizealone, the content being untouched
versionsSizemeans 'the version bytes THIS action reclaims', not 'the whole history' nor 'the purgeable subset': the scan writes the whole history on a DELETE row and the removal set on a PURGE_VERSIONS one (seeCleanupJcrStorage#toCandidate), so prediction and execution report the same figure.IT WAS A JPQL
CASEEXPRESSION AND IS NOW A COLUMN, because no index can serve an ORDER BY over an expression: the purge consumes its candidates biggest first, which turned every batch into a filesort over the campaign's remaining rows — 318 ms against 4 ms per batch on 200k rows, tens of thousands of batches per run, and the cost growing with the square of the item count. Exactly the shape keyset paging replaced offset paging to avoid, reintroduced through the ordering. Stored, ONE index (IDX_DOC_CLEANUP_ITEM_RECLAIMABLE) serves the keyset predicate and its order together.The denormalization removes a duplicate rather than adding one: the definition lives in
CleanupSizeUtil, applied byCleanupCampaignStorage#toEntityon every save, and this column is now the single thing SQL and the in-memory comparator both read — where the CASE expression and the Java method used to mirror each other and could drift.Named as the ENTITY ATTRIBUTE here (no query alias), which is also the logical sort key the REST layer accepts (
CleanupConstants#RECLAIMABLE_SORT_KEY) — an equality pinned by a test, since it is what lets the review list be ordered by an ordinary property sort instead of an unsafe one.- See Also:
- DELETE: the content size PLUS
-
RECLAIMABLE_BYTES
The same column spliced into JPQL text, i.e. WITH the query alias — and the alias is why these are two constants rather than one. Spring Data appends the alias itself to a safeSortproperty, so handing iti.reclaimableBytesrendersi.i.reclaimableBytesand the query does not parse; JPQL text, conversely, needs the alias. Derived by concatenation so the column can only be renamed in one place (and so both stay compile-time constants, which the switch over sort keys requires).- See Also:
-
-
Method Details
-
findByCampaignIdAndIdGreaterThanOrderByIdAsc
List<CleanupCampaignItemEntity> findByCampaignIdAndIdGreaterThanOrderByIdAsc(long campaignId, long lastId, org.springframework.data.domain.Pageable pageable) KEYSET page of a campaign's items in id order — the CSV export walk. APage-returning offset walk here cost onecount(*)per page and a linearly deepening OFFSET scan: on a million-row report that is a thousand count queries over the very table the purge keyset-pages to avoid scanning — this is its export sibling, id-keyset because the export order is the id order and nothing mutates the rows underneath it. -
findByCampaignIdAndState
org.springframework.data.domain.Page<CleanupCampaignItemEntity> findByCampaignIdAndState(long campaignId, String state, org.springframework.data.domain.Pageable pageable) -
findByStateOrderedByReclaimableBytes
@Query("SELECT i FROM CleanupCampaignItem i WHERE i.campaignId = :campaignId AND i.state = :state AND (:lastReclaimableBytes IS NULL OR i.reclaimableBytes < :lastReclaimableBytes OR (i.reclaimableBytes = :lastReclaimableBytes AND i.id > :lastId)) ORDER BY i.reclaimableBytes DESC, i.id ASC") List<CleanupCampaignItemEntity> findByStateOrderedByReclaimableBytes(@Param("campaignId") long campaignId, @Param("state") String state, @Param("lastReclaimableBytes") Long lastReclaimableBytes, @Param("lastId") long lastId, org.springframework.data.domain.Pageable pageable) KEYSET page of the items of a campaign in a given state, BIGGEST FIRST — the order the purge worker consumes them in.Ordered by what each row actually frees (
RECLAIMABLE_BYTES, the one definition, so this can never disagree with the report the administrator published on) and NOT by id. A purge of a 14-year corpus runs for hours, and the id order is the order the scan happened to walk the tree in — so the space came back in no particular order and an administrator watching 4 GB of a promised 234 GB could not tell whether the big wins were still ahead. This front-loads them: the first batches free the most, which is also what makes stopping early a decision rather than a gamble.THE CURSOR IS COMPOSITE and it has to be: ordering by a computed value means the id alone no longer identifies a position. It is
(reclaimableBytes DESC, id ASC)— strictly monotonic over a total order, so the worker cannot revisit a row within a run, which is the property the id-ascending keyset was there for (an offset page-0 re-read relies on every processed item leaving the state, so one item that can never be persisted feeds it back forever). The tie branch onidis what makes the order total: without it a block of equal-sized rows spanning a batch boundary would repeat some rows and drop others.SAFE because the key cannot move under the cursor mid-run: the values it is computed from (
action,fileSize,versionsSize) are only ever refreshed on the item being processed, which then leaves the CANDIDATE state; and the freshness listener that could refresh a pending one is unregistered before the campaign reaches EXECUTING. A row whose save FAILED keeps its stored key, and the cursor is already past it.- Parameters:
campaignId- campaign identifierstate- item statelastReclaimableBytes- reclaimable bytes of the last row seen, NULL to start from the biggestlastId- id of the last row seen, breaking ties on equal sizespageable- page size- Returns:
- the next items, biggest first, empty when the state is exhausted
-
findRetryableFailures
@Query("SELECT i FROM CleanupCampaignItem i\nWHERE i.campaignId = :campaignId\nAND i.state = :state\nAND i.failureReason IN :failureReasons\nAND i.attemptCount < :maxAttemptCount\nAND i.id > :lastId\n") List<CleanupCampaignItemEntity> findRetryableFailures(@Param("campaignId") long campaignId, @Param("state") String state, @Param("failureReasons") Collection<String> failureReasons, @Param("maxAttemptCount") long maxAttemptCount, @Param("lastId") long lastId, org.springframework.data.domain.Pageable pageable) KEYSET page of the RETRYABLE failures of a campaign: SKIPPED items whose failure reason belongs to the allowlist the Service owns and whose attempt count is still under the bound. Keyset-paged for the same reason as above, and one that bites harder here: the requeue MUTATES the very state the filter matches on, so an offset page would skip rows as the result set shrinks underneath it. -
countFailuresByReason
@Query("SELECT i.failureReason, COUNT(i) FROM CleanupCampaignItem i WHERE i.campaignId = :campaignId AND i.state = :state GROUP BY i.failureReason") List<Object[]> countFailuresByReason(@Param("campaignId") long campaignId, @Param("state") String state) Per-reason item counts of a campaign's failures, in ONE grouped query (rows: failure reason, item count). Never by loading the SKIPPED rows: a campaign can hold hundreds of thousands of them, and the console only ever displays the handful of distinct reasons behind them. -
findByCampaignIdAndNodeUuidIn
List<CleanupCampaignItemEntity> findByCampaignIdAndNodeUuidIn(long campaignId, Collection<String> nodeUuids) -
findByFilters
@Query("SELECT i FROM CleanupCampaignItem i\nWHERE i.campaignId = :campaignId\nAND (:ownerIdentityId IS NULL OR i.ownerIdentityId = :ownerIdentityId)\nAND (:state IS NULL OR i.state = :state)\nAND (:action IS NULL OR i.action = :action)\nAND (:minSize IS NULL OR i.fileSize >= :minSize)\nAND (:searchPattern IS NULL OR LOWER(i.path) LIKE :searchPattern ESCAPE \'|\')\n") org.springframework.data.domain.Page<CleanupCampaignItemEntity> findByFilters(@Param("campaignId") long campaignId, @Param("ownerIdentityId") Long ownerIdentityId, @Param("state") String state, @Param("action") String action, @Param("minSize") Long minSize, @Param("searchPattern") String searchPattern, org.springframework.data.domain.Pageable pageable) Campaign items matching every provided filter, each one null-tolerant so a single query serves any combination.searchPatternis a case-insensitive contains match on the item PATH. The item table has NO name column: aCleanupCampaignItem#getName()is derived from the path's last segment, so matching the path covers the file name AND the folders above it — which is what a cleanup reviewer looks for. The pattern is built by the Storage layer (lower-cased, wildcards escaped with the '|' escape character, wrapped in '%'), never by the caller.The leading '%' makes the search unindexable: it scans the campaign's rows, bounded by the CAMPAIGN_ID index (and, for the user-side query below, by OWNER_IDENTITY_ID too).
-
findByOwnersAndSearch
@Query("SELECT i FROM CleanupCampaignItem i\nWHERE i.campaignId = :campaignId\nAND i.ownerIdentityId IN :ownerIdentityIds\nAND (:searchPattern IS NULL OR LOWER(i.path) LIKE :searchPattern ESCAPE \'|\')\n") org.springframework.data.domain.Page<CleanupCampaignItemEntity> findByOwnersAndSearch(@Param("campaignId") long campaignId, @Param("ownerIdentityIds") List<Long> ownerIdentityIds, @Param("searchPattern") String searchPattern, org.springframework.data.domain.Pageable pageable) The user-review counterpart offindByFilters(long, java.lang.Long, java.lang.String, java.lang.String, java.lang.Long, java.lang.String, org.springframework.data.domain.Pageable): the items of the campaign owned by the given identities (the user plus the spaces they manage), optionally narrowed by the same null-tolerant path search — see that method's javadoc for the pattern contract and the index note. -
findByCampaignIdAndPathTouchedBy
@Query("SELECT i FROM CleanupCampaignItem i\nWHERE i.campaignId = :campaignId\nAND (i.path IN :ancestorPaths\n OR i.path LIKE CONCAT(:escapedEventPath, \'/%\') ESCAPE \'|\')\n") List<CleanupCampaignItemEntity> findByCampaignIdAndPathTouchedBy(@Param("campaignId") long campaignId, @Param("ancestorPaths") List<String> ancestorPaths, @Param("escapedEventPath") String escapedEventPath) Items touched by a JCR event path, in two index-friendly directions (the column always stays on the LEFT of the LIKE, the parameter on the right):- items that are the event node or one of its ancestors: exact match against the ancestor chain computed in Java by the Storage layer
- items below the event node (a deleted/moved FOLDER fires a single JCR event for the top-most node only): prefix LIKE on the event path, with its '_'/'%' wildcards escaped by the Storage layer ('|' escape character), so padded segments like /Users/j___/ never wildcard-match unrelated rows
-
findAggregatesByCampaignIds
@Query("SELECT i.campaignId, i.state, COUNT(i), COALESCE(SUM(i.reclaimableBytes), 0), COALESCE(SUM(i.reclaimedBytes), 0) FROM CleanupCampaignItem i WHERE i.campaignId IN :campaignIds GROUP BY i.campaignId, i.state") List<Object[]> findAggregatesByCampaignIds(@Param("campaignIds") List<Long> campaignIds) Per-campaign, per-state item aggregates of a whole campaigns list in ONE grouped query (rows: campaignId, state, item count, reclaimable bytes sum, reclaimed bytes sum), folded by the Storage layer into one aggregate per campaign — replaces up to 4 aggregate queries PER campaign on the list endpoint. -
aggregateItemsAbsentFromCampaign
@Query("SELECT COUNT(i), COALESCE(SUM(i.reclaimableBytes), 0) FROM CleanupCampaignItem i WHERE i.campaignId = :campaignId AND NOT EXISTS (SELECT o.id FROM CleanupCampaignItem o WHERE o.campaignId = :otherCampaignId AND o.nodeUuid = i.nodeUuid)") List<Object[]> aggregateItemsAbsentFromCampaign(@Param("campaignId") long campaignId, @Param("otherCampaignId") long otherCampaignId) Count and reclaimable-bytes sum of the items ofcampaignIdwhose node uuid is ABSENT fromotherCampaignId. Serves BOTH asymmetric buckets of a campaign comparison by swapping the two arguments: 'new' is (base, other), 'gone' is (other, base) — the bytes are always those of the campaign passed first, exactly as the previous in-memory diff computed them.- Returns:
- a single row: item count, reclaimable bytes sum
-
countByCampaignIdAndState
-
countByCampaignIdAndOwnerIdentityIdInAndState
-
sumReclaimableBytesByState
@Query("SELECT COALESCE(SUM(i.reclaimableBytes), 0) FROM CleanupCampaignItem i WHERE i.campaignId = :campaignId AND i.state = :state") long sumReclaimableBytesByState(@Param("campaignId") long campaignId, @Param("state") String state) -
sumReclaimableBytesByOwnersAndState
@Query("SELECT COALESCE(SUM(i.reclaimableBytes), 0) FROM CleanupCampaignItem i WHERE i.campaignId = :campaignId AND i.ownerIdentityId IN :ownerIdentityIds AND i.state = :state") long sumReclaimableBytesByOwnersAndState(@Param("campaignId") long campaignId, @Param("ownerIdentityIds") List<Long> ownerIdentityIds, @Param("state") String state) -
sumReclaimedBytes
@Query("SELECT COALESCE(SUM(i.reclaimedBytes), 0) FROM CleanupCampaignItem i WHERE i.campaignId = :campaignId") long sumReclaimedBytes(@Param("campaignId") long campaignId) -
sumReclaimedBytesByOwners
-
existsByCampaignId
boolean existsByCampaignId(long campaignId) -
deleteByCampaignId
@Modifying @Transactional @Query("DELETE FROM CleanupCampaignItem i WHERE i.campaignId = :campaignId") void deleteByCampaignId(@Param("campaignId") long campaignId) Drops every item row of a campaign in ONE statement. DECLARED rather than derived, and that is the point: a deriveddeleteBy...SELECTs the matching entities and removes them one at a time, so dropping the report of a simulated campaign loaded every one of its rows into a persistence context and issued one DELETE per row — hundreds of thousands of both, on the corpus this feature exists for. One bulk statement is one round trip and no persistence context at all.Bulk JPQL bypasses the persistence context by design, so entities already loaded in the calling transaction are NOT evicted. Harmless at both call sites: the campaign is being deleted outright, or its report has just been archived and nothing reads those rows again.
-
findOrphanCampaignIds
@Query("SELECT DISTINCT i.campaignId FROM CleanupCampaignItem i WHERE i.campaignId NOT IN (SELECT c.id FROM CleanupCampaign c)") List<Long> findOrphanCampaignIds()Campaign ids that have item rows but NO campaign row — rows nothing can ever read again, every item query being scoped by campaign id.They are what a JVM death in the middle of a delete leaves behind (the campaign row goes first, so that a half-deleted campaign can never be acted on), and they would otherwise sit there forever: invisible, and holding the very space this feature exists to reclaim. Swept at startup.
Cheap despite the shape, for two reasons and NOT for the one first written here: the subquery reads the campaign table, which holds one row per campaign ever created — nothing trims it,
MAX_CAMPAIGNSbeing the list endpoint's page size and not a constraint — but a campaign is a deliberate administrative act on a grace cycle measured in weeks, so it stays in the hundreds for years; and the outer DISTINCT is served by IDX_DOC_CLEANUP_ITEM_CAMPAIGN, which is a real index. If that table ever does grow, this query is worth re-examining — a comment claiming an enforced bound is what would stop someone doing so.
-