Documents: index the file columns and the check date that file deletes and the scheduled scrub look up #1168
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#1168
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This issue holds 2 findings of the audit that share one fix or the same files.
Q6: Documents: deleting a file checks whether another row uses it with full scans of unindexed file columns
What exists today
src/postulo/documents/signals.py:68-79remove_the_file_from_disk(): after commit,any(model.objects.filter(file=name).exists() for model in (RenderedDocument, UploadedDocument)).src/postulo/jobs/signals.py:72-75: the same forCapturedPage.sourceandCapturedPage.rendering.None of these columns is indexed (
documents/models.py:679and:990,jobs/models.py:2125and therenderingfield, nodb_indexand noMeta.indexeson them).EXPLAIN QUERY PLAN SELECT 1 FROM documents_uploadeddocument WHERE file = 'x' LIMIT 1under the test settings:SCAN documents_uploadeddocument(probeaudit/m_scripts/test_probe_queries.py).It runs only on delete. For a single delete it is a scan of one person's-sized table, well under a millisecond. In an account erasure or a bulk delete of N documents it is 2N scans of tables of up to N rows.
What would change
db_index=TrueonUploadedDocument.file,RenderedDocument.file,CapturedPage.sourceandCapturedPage.rendering, in one migration per app. The checks become index lookups. Nothing else changes.What it saves or protects
small: one full table scan per check, two to four per deleted document; quadratic only in an account erasure with many documents
Accessibility
none
Decisions
Q7: Documents: the scheduled scrub sorts every upload to pick the 25 least recently checked
What exists today
src/postulo/documents/scrub.py:55verify_uploads()buildsUploadedDocument.objects.exclude(file="").order_by("verified_at", "pk")and takes[:limit].notifications/management/commands/send_due_reminders.py:230calls it withSCRUB_SLICE = 25(:32) on every scheduler pass.verified_at(documents/models.py:716) has no index.EXPLAIN QUERY PLANof that query under the test settings:SCAN documents_uploadeddocumentthenUSE TEMP B-TREE FOR ORDER BY(probeaudit/m_scripts/test_probe_queries.py).The table holds every upload of every account, so the scan and sort grow with the instance while the work done stays at 25 rows.
What would change
Meta.indexes = [models.Index(fields=("verified_at", "id"))]onUploadedDocument(the order the query asks for, so both databases walk the index and stop at 25). Each scrub then updates 25 index entries as it writesverified_at, which is negligible. NULLverified_at(never checked) sorts first on SQLite and last on PostgreSQL by default; that is today's behaviour too and the index does not change it, but the issue should say whether "never checked first" is meant on PostgreSQL (F("verified_at").asc(nulls_first=True)with a matching index if so).What it saves or protects
small: a full scan and a temporary sort of the uploads table at every scheduler pass, replaced by an index walk of 25 rows
Accessibility
none
Decisions
nulls_first=Truein the query and the index, so a new upload is checked at the next pass on either database.Found by the efficiency and reliability audit of 2026-10-30 (finding Q6, Q7; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).