Conversation
…nd the scan --backfill-message-ids repair pass Migration 2 adds a nullable, indexed rfc_message_id column to seen_mails and populates it for mid: rows from the Message-ID embedded in their key, using the single shared normalization helper (trim whitespace, strip one enclosing <>, case preserved) that the scanner, importer, and backfill all use. Databases from newer binaries are still refused. Both ingesters now record the normalized Message-ID when marking a message seen, and the writer checks it inside the same transaction before incrementing the sender count: a newly inserted row whose Message-ID already exists on any other row is recorded as seen but not counted, in both directions (scan finding an imported message and import finding a scanned one). Messages without a Message-ID keep the old behavior: counted, NULL column. scan --backfill-message-ids is a first-class ingest run (flock, run row with heartbeats and total_estimate, SIGTERM-clean, --quiet/--verbose) that repairs historical scan rows: it iterates bare-Gmail-id rows with NULL rfc_message_id in keyset-paginated batches, fetches format=metadata (Message-ID, From, Return-Path) through the scan's rate-limited retrying fetch machinery, and populates the column. The collapse of historical double counts is fused into the populate: each row's duplicate check and sender decrement (clamped at zero) commit atomically with its populate, so a partially completed run leaves every row either fully repaired or untouched, re-running continues with exactly the remainder, and a second run over a repaired database is a network-free no-op. Rows fetched and found to have no Message-ID get an empty-string marker so they are never fetched again.
…l banner, README /api/status's mixed_sources now means "both seen_mails keyspaces present AND at least one bare-Gmail-id row still lacks its Message-ID", computed from the data itself instead of run history, with unrepaired_count exposed for the UI copy. A pre-Phase-D database without the column counts every scan row as unrepaired; the read-only viewer never 5xxes over it. The banner now explains that counts may include cross-source duplicates and names the exact repair command in a copyable code block (all content via textContent); it disappears once the backfill has repaired every row. Backfill runs get a proper source label in the run UI. README: the "don't mix both modes" warning is replaced by cross-source dedupe documentation and a section on the backfill repair pass. The headless-Chrome harness gains two scenarios asserting the banner shows the command when unrepaired and is absent when repaired.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #32 — the final committed phase (Phase D) of #26: exact cross-source dedupe via
rfc_message_id, and retirement of the interim mixed-sources banner.What's here
Schema (migration 2,
user_version2).seen_mailsgains a nullablerfc_message_idcolumn with a non-unique index.mid:rows are populated during the migration itself from the Message-ID embedded in their key; bare Gmail rows stay NULL until backfilled. A database from a newer binary is still refused. Message-IDs are normalized by one shared helper (trim whitespace, strip one enclosing<>, case preserved) used by the scanner, the importer, the migration, and the backfill.Dual-direction check-before-increment. Both ingesters record the normalized Message-ID when marking a message seen; inside the same single-writer transaction, a newly inserted row whose Message-ID already exists on any other
seen_mailsrow is marked seen (idempotency intact) but not counted — neither the sender count nor the run'smessages_newmoves (there was no room in the run row schema for a distinct deduped counter, so deduped messages read as seen-not-new). Works in both directions: scan-after-import and import-after-scan. Messages without a Message-ID keep the old behavior (counted, NULL column).scan --backfill-message-ids. A first-class ingest run: takes the flock, writes a run row (source: backfill, with heartbeats andtotal_estimate), stops cleanly on SIGTERM/SIGINT, honors--quiet/--verbose, and shows up in the web viewer's progress UI like any other run. It iterates bare-Gmail-id rows with NULLrfc_message_idin keyset-paginated batches (cursor overmail_id, so writer-lag can never re-select in-flight rows), fetchesformat=metadatarestricted to Message-ID/From/Return-Path through the scan's existing rate-limited, retrying fetch machinery, and populates the column.The collapse of historical double counts is fused into the populate rather than run as a separate phase: each row's duplicate check and sender decrement (never below zero) commit atomically with its populate. That is what makes partial completion + resume exact — a killed run leaves every row either fully repaired or untouched, re-running the same command continues with precisely the remainder, and a second run over a repaired database finds nothing to do and never touches the network. Rows fetched and found to have no Message-ID get an
''marker so they are not re-fetched (forward ingestion keeps NULL for such messages, per the issue; the marker exists only so the backfill terminates). No--resumeflag: resumption is inherent, and combining the two is rejected.Banner retirement.
mixed_sourcesin/api/statusnow means "both keyspaces present AND at least one bare-Gmail-id row has NULLrfc_message_id", computed fromseen_mailsitself, withunrepaired_countexposed for the UI copy. A pre-Phase-D database (no column yet) counts every scan row as unrepaired and does not 5xx the read-only viewer. The banner explains that counts may include cross-source duplicates and shows the exact backfill command in a copyable code block (textContent only); it disappears once repaired. README's "don't mix both modes" warning is replaced with dedupe + backfill documentation.Honest limitation
The live Gmail fetch inside the backfill was not run — it needs real OAuth credentials, which this change never touches. It shares
fetch_message's machinery (now parameterized to addformat=metadata+metadataHeaders) and the scan's auth path, and the fetch loop is generic over the fetcher, so everything downstream of the network — iteration/pagination, header extraction, populate, collapse, resume — is covered by tests against fabricated rows and messages.stats.db/credentials.json/tokencache.jsonin the repo root are untouched.Tests
mid:population, newer-version refusal, re-run no-op<id@host>/id@host/ whitespace / case / unbalanced-bracket variantsGates:
cargo build,cargo test(49+41+13 pass),cargo clippy --all-targets -- -D warnings,cargo fmt --check,python3 demo/build_demo.pyinto a temp dir (all anchors intact),python3 tests/frontend/harness.py(all scenarios pass).