Two code-review-derived backfill-correctness bugs (from the PR #46
review). Both govern where MailBackfiller stops and resumes paging a
folder, so they are fixed together.
(MIN(timestampMillis)), but paging descends by UID. One high-UID
message with an old Date header (moved/imported mail) dragged the
cached minimum below the cutoff and marked the folder complete while
lower-UID within-retention messages were still unfetched — a silent,
permanent gap (completion is sticky). The age floor is now decided from
each page actually fetched: only a page ENTIRELY older than the cutoff
(or folder exhaustion) ends paging, and such a prune-fodder page is not
persisted. The count floor keeps its cheap cache check — it orders by
UID like paging, so inversions can't bite it. oldestSyncedTimestamp had
no remaining caller and is removed.
migrated before the uid column existed, or a UIDFolder.getUID -1
fetch); fetchOlderThan treats beforeUid <= 1 as "nothing older", so the
folder was falsely marked fully backfilled. lowestSyncedUid now ignores
uid <= 0 rows (matching MailSyncer's minWindowUid guard), a stale
persisted boundary <= 0 is discarded on resume, and the per-page
descent takes min over positive UIDs only. A page of entirely
unresolved UIDs stalls the folder — it stays incomplete (a future
scheduled run retries) but reports no immediate more-work, so
BackfillWorker's slice-chaining loop can't busy-spin on it.
Together: #95 guarantees paging always descends with a real positive
UID boundary, and #94 makes the stop decision independent of cached
aggregates, so a placeholder or old-Dated row can no longer end
backfill early through either path. Completion stays sticky and is
declared only on positive evidence, preserving the #12/#13
backfill/pruner non-interference.
Tests (JVM, GreenMail + the existing in-memory DAO-fake harness; all
four fail against the pre-fix code): a high-UID/old-Date message must
not gap within-retention history (#94); an entirely-old page ends
paging without persisting prune-fodder (#94); a uid=0 row must not
poison the boundary (#95); a page of unresolvable UIDs stalls instead
of falsely completing (#95). MessageDaoRetentionTest pins the uid > 0
SQL guard against real SQLite and drops the removed oldestSyncedTimestamp
probe.
Closes#94Closes#95
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>