diff --git a/.jules/bolt.md b/.jules/bolt.md index dcaf4634e..a0774464a 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -54,3 +54,7 @@ ## 2026-06-18 - Route heavy search reads to read replica **Learning:** `backend/api/search.py` needs the primary database session for current provider and tenant configuration, but the heavy email and attachment search query can use the read-only session from `db.session.get_readonly_db`. **Action:** Keep provider/config resolution on `Depends(get_db)` and route only the read-only search query through `Depends(get_readonly_db)` so search load moves to replicas without replica-lagging fresh configuration reads. + +## 2026-06-19 - Email owner/date lookup index +**Learning:** Inbox and reply-wait queries commonly scope `email_records` by `user_id` and `organization_id` before ordering or filtering by `date`, so separate single-column indexes still leave the planner with avoidable sort/filter work. +**Action:** Keep `ix_email_records_owner_date` on `(user_id, organization_id, date)` in both the SQLAlchemy model and bootstrap backfill SQL when optimizing owner-scoped email timelines. diff --git a/backend/db/models.py b/backend/db/models.py index 8be76dc17..762ae122f 100644 --- a/backend/db/models.py +++ b/backend/db/models.py @@ -351,6 +351,12 @@ class Email(Base): "message_id", name="uq_emails_owner_message_id", ), + Index( + "ix_email_records_owner_date", + "user_id", + "organization_id", + "date", + ), ) id: Mapped[int] = mapped_column(primary_key=True) diff --git a/backend/scripts/bootstrap_db.py b/backend/scripts/bootstrap_db.py index f61ab371c..b241380ab 100644 --- a/backend/scripts/bootstrap_db.py +++ b/backend/scripts/bootstrap_db.py @@ -145,6 +145,10 @@ def _get_create_indexes_statements() -> list[Executable]: "CREATE INDEX IF NOT EXISTS ix_email_records_organization_id " "ON email_records (organization_id)" ), + text( + "CREATE INDEX IF NOT EXISTS ix_email_records_owner_date " + "ON email_records (user_id, organization_id, date)" + ), text( "CREATE INDEX IF NOT EXISTS ix_sender_relationships_owner_source " "ON sender_relationships " diff --git a/backend/tests/test_bootstrap_db.py b/backend/tests/test_bootstrap_db.py index 84fb5079a..3b31b6d12 100644 --- a/backend/tests/test_bootstrap_db.py +++ b/backend/tests/test_bootstrap_db.py @@ -10,6 +10,7 @@ from db.models import ( CalendarWritebackSource, ConnectorSignalEvent, + Email, ProjectFolder, SecurityAuditEvent, SenderRelationship, @@ -103,6 +104,11 @@ def test_schema_backfill_adds_email_indexes(monkeypatch): "create index if not exists ix_email_records_organization_id" in statement for statement in statements ) + assert any( + "create index if not exists ix_email_records_owner_date" in statement + and "user_id, organization_id, date" in statement + for statement in statements + ) assert any( "drop index if exists ix_email_records_message_id" in statement for statement in statements @@ -414,6 +420,19 @@ def test_sender_relationship_model_declares_source_unique_index(): assert "source_thread_id" in expression_text +def test_email_model_declares_owner_date_index(): + indexes = {index.name: index for index in Email.__table__.indexes} + + owner_date_index = indexes["ix_email_records_owner_date"] + + assert owner_date_index.unique is False + assert [column.name for column in owner_date_index.columns] == [ + "user_id", + "organization_id", + "date", + ] + + def test_tenant_config_model_declares_owner_scope_unique_index(): indexes = {index.name: index for index in TenantConfig.__table__.indexes}