Skip to content

feat(email): Set email sender as Signal/Noise with email_filters#2190

Merged
evanhutnik merged 4 commits intomainfrom
evan/signal-noise-sender
Mar 26, 2026
Merged

feat(email): Set email sender as Signal/Noise with email_filters#2190
evanhutnik merged 4 commits intomainfrom
evan/signal-noise-sender

Conversation

@evanhutnik
Copy link
Copy Markdown
Contributor

Summary

  • New email_filters table with DB migration — stores per-sender importance overrides by email address or domain, with address rules taking precedence over domain rules
  • Backend endpoints (PUT /email/filters, GET /email/filters, DELETE /email/filters/{id}) for upserting, listing, and deleting sender filters, with input validation (trim, lowercase, format checks) in the service layer
  • Dynamic query integrationImportance(true/false) SQL filters now incorporate email_filters overrides, with explicit exclusion of the opposite override to prevent threads appearing in both Signal and Noise
  • Frontend email client methods for the new endpoints, with generated TypeScript types from OpenAPI spec
  • Context menu actions — "Sender → Signal" (in Noise tab) and "Sender → Noise" (in Signal tab) with undo-able toasts
  • Command palette — "Mark sender as Signal" and "Mark sender as Noise" commands registered in the email block hotkey scope
  • Feature flag ENABLE_CLIENT_EMAIL_SIGNAL_FILTER (default false) to disable redundant client-side signal/noise email filtering, deferring to backend filtering

@evanhutnik evanhutnik requested review from a team as code owners March 25, 2026 20:54
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

Adds sender-based email filtering end-to-end: frontend actions, UI menu items, hotkeys, context handlers, and client queries; server-side API, DB migration, repo, service, dynamic-query logic, fixtures, and tests to persist and evaluate "signal"/"noise" sender filters.

Changes

Cohort / File(s) Summary
Frontend Action Factories
js/app/packages/app/component/next-soup/actions/index.ts, js/app/packages/app/component/next-soup/actions/make-mark-sender-important-action.ts, js/app/packages/app/component/next-soup/actions/make-mark-sender-noise-action.ts, js/app/packages/app/component/next-soup/actions/make-sender-filter-action.ts
Added sender-filter factory and two concrete actions (makeMarkSenderSignalAction, makeMarkSenderNoiseAction) that deduplicate sender emails and invoke provided async action.
Frontend UI & Hotkeys
js/app/packages/app/component/next-soup/soup-view/soup-entity-actions-menu.tsx, js/app/packages/block-email/component/Email.tsx, js/app/packages/block-email/util/emailHotkeys.ts, js/app/packages/block-email/component/EmailContext.tsx, js/app/packages/core/hotkey/tokens.ts
Wired menu entries scoped by tab, registered hotkeys/tokens, and exposed markSenderSignal/markSenderNoise in EmailContext with sender-resolution and toast-backed mutation calls.
Frontend Filters & Flags
js/app/packages/app/component/next-soup/filters/inbox-filters.ts, js/app/packages/core/constant/featureFlags.ts
Introduced ENABLE_CLIENT_EMAIL_SIGNAL_FILTER flag and gated inbox email-entity filter behavior on it.
Frontend Service & Queries
js/app/packages/queries/email/thread.ts, js/app/packages/service-clients/service-email/client.ts, js/app/packages/service-clients/service-email/openapi.json
Added toast-wrapped mutations markSenderSignalWithToast/markSenderNoiseWithToast, client methods listEmailFilters/upsertEmailFilter/deleteEmailFilter, and OpenAPI endpoint/schema definitions for filters.
Backend Domain & Traits
rust/cloud-storage/email/src/domain/models/email_filter.rs, rust/cloud-storage/email/src/domain/models/error.rs, rust/cloud-storage/email/src/domain/models/mod.rs, rust/cloud-storage/email/src/domain/ports.rs
Added EmailFilter/UpsertEmailFilterInput models, InvalidEmailFilter error variant, and new EmailRepo/EmailService trait methods for upsert/delete/list filters.
Backend Service Implementation
rust/cloud-storage/email/src/domain/service/mod.rs
Implemented validation/normalization helper and EmailService methods for upserting, deleting, and listing email filters; maps repo errors into domain errors.
Backend HTTP Router & API
rust/cloud-storage/email/src/inbound/axum.rs, rust/cloud-storage/email/src/inbound/axum/email_filter_router.rs, rust/cloud-storage/email_service/src/api/email/filters/mod.rs, rust/cloud-storage/email_service/src/api/email/mod.rs, rust/cloud-storage/email_service/src/api/swagger.rs
Added Axum router, handlers (PUT/DELETE/GET), API request/response types, error mapping, router registration, and Swagger/OpenAPI entries for email filters.
Backend Repo & DB Access
rust/cloud-storage/email/src/outbound/email_pg_repo/email_filter.rs, rust/cloud-storage/email/src/outbound/email_pg_repo/mod.rs
Added sqlx-backed upsert-by-address/upsert-by-domain, delete, and list functions; integrated into EmailPgRepo with routing based on input fields.
Query / Dynamic Filters
rust/cloud-storage/email/src/outbound/email_pg_repo/dynamic/filters.rs, rust/cloud-storage/email/src/outbound/email_pg_repo/dynamic/tests.rs
Added sender-importance override SQL helper and integrated into importance literal generation; updated unit tests asserting email_filters usage and correctness in generated SQL.
DB Migration & Fixtures
rust/cloud-storage/macro_db_client/migrations/20260325102949_email_filters.sql, rust/cloud-storage/email/fixtures/email_dynamic_query_email_filters.sql, rust/cloud-storage/email/src/outbound/email_pg_repo/test/dynamic_query.rs
Created email_filters table with constraints and unique partial indexes; added fixtures and integration tests covering importance-override scenarios.
Misc Tests / Mocks
rust/cloud-storage/soup/src/domain/service/tests.rs, rust/cloud-storage/soup/src/inbound/axum_router/tests.rs
Expanded Noop/Mock EmailService test implementations to include upsert/delete/list_email_filters (test stubs returning RepoErr or placeholder results).

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed Description is comprehensive and directly related to the changeset, covering database schema, backend endpoints, dynamic query integration, frontend implementation, UI features, and feature flagging—all reflected in the raw summary.
Title check ✅ Passed Title follows conventional commits format (feat:) and is under 72 characters (64 chars), clearly describing the main feature addition of sender signal/noise filtering via email filters.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 25, 2026

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@js/app/packages/app/component/next-soup/actions/make-sender-filter-action.ts`:
- Around line 11-16: The dedupe uses raw entity.senderEmail so mixed-case or
whitespace variants slip through; compute a normalized key (e.g., const key =
entity.senderEmail.trim().toLowerCase()) and use that for seen.has(...) and
seen.add(...), while still passing the original entity.senderEmail to await
action(...) if you want to preserve original casing in the UI/API call.

In `@js/app/packages/queries/email/thread.ts`:
- Around line 431-449: The undo currently always deletes the created/updated
rule using filterId (result[1].filter.id) which loses any previous preference;
change the logic in the toast success undo handler (the onClick that calls
emailClient.deleteEmailFilter) to capture the prior rule state before upserting
(e.g., previous is_important) and on undo either restore the previous row via an
upsert/update call (re-apply the prior is_important and other fields) or delete
only if the rule was newly created; reference the upsert response
(result[1].filter), the variable filterId, and replace the delete-only call to
emailClient.deleteEmailFilter with conditional logic that restores the previous
override using emailClient.upsertEmailFilter or updates the existing filter to
the previous is_important value.
- Around line 421-449: The upsertEmailFilter/deleteEmailFilter flows update
which tab a thread belongs to but never refresh the Signal/Noise lists; after a
successful upsert (successful result in upsertEmailFilter) and after a
successful undo (successful result in deleteEmailFilter inside the toast
onClick), call the cache refresh/invalidate routine that the app uses (e.g.,
queryClient.invalidateQueries or the app's refresh helper) for the preview and
soup queries so the Signal/Noise lists immediately reflect the change; add those
invalidations in the success branch after using result[1].filter.id and in the
else branch where undo succeeds.

In `@rust/cloud-storage/email/src/outbound/email_pg_repo/mod.rs`:
- Around line 261-287: The upsert_email_filter implementation currently prefers
email_address when both fields are set and panics with unreachable! when neither
is set; change it to explicitly validate UpsertEmailFilterInput shapes and
return an error for invalid shapes instead of panicking or silently choosing one
field. Update async fn upsert_email_filter to: handle the exact two valid cases
(Some(address) && None => call email_filter::upsert_email_filter_by_address,
None && Some(domain) => call email_filter::upsert_email_filter_by_domain), and
for the other shapes (both Some or both None) return an appropriate
Err(Self::Err) (create or reuse a descriptive error variant such as
InvalidFilterShape or BadRequest) so callers get a recoverable error rather than
an unwind or silent preference. Ensure the returned error flows through the
function signature and tests cover both invalid-shape cases.

In
`@rust/cloud-storage/macro_db_client/migrations/20260325102949_email_filters.sql`:
- Around line 10-13: The XOR constraint allows empty strings; update the CHECK
for email_filters_address_xor_domain_chk to require a non-empty value by
ensuring (email_address IS NOT NULL AND trim(email_address) <> '') is exclusive
with (email_domain IS NOT NULL AND trim(email_domain) <> ''); also tighten
email_filters_email_domain_format_chk to only evaluate the position check when
email_domain is non-empty/null (e.g., require email_domain IS NULL OR
(trim(email_domain) <> '' AND position('@' IN email_domain) = 0)) so empty
strings are rejected for both email_address and email_domain.
- Around line 1-25: The migration added the email_filters table and indexes but
there are no Rust queries to exercise SQLx and generate its cache; add query
implementations in macro_db_client/src (for example functions named
upsert_email_filter, list_email_filters, delete_email_filter) that contain the
SQL statements (INSERT ... ON CONFLICT ... RETURNING / SELECT ... WHERE link_id
= $1 / DELETE ... WHERE id = $1) matching the new schema, ensure they use the
exact column names (email_address, email_domain, is_important, link_id) and any
lower(...) index usage in WHERE clauses, then run just prepare_db in the
macro_db_client directory to update SQLx cache artifacts; if the table is
intentionally schema-only, add a short note in the PR explaining that instead of
adding queries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0333dd5b-c062-40c7-a23d-6ce91be2fe37

📥 Commits

Reviewing files that changed from the base of the PR and between 2078094 and 586e153.

⛔ Files ignored due to path filters (24)
  • js/app/packages/service-clients/service-email/generated/client.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/apiEmailFilter.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/apiEmailFilterEmailAddress.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/apiEmailFilterEmailDomain.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/index.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/listEmailFiltersResponse.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/upsertEmailFilterRequest.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/upsertEmailFilterRequestEmailAddress.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/upsertEmailFilterRequestEmailDomain.ts is excluded by !**/generated/**
  • js/app/packages/service-clients/service-email/generated/schemas/upsertEmailFilterResponse.ts is excluded by !**/generated/**
  • rust/cloud-storage/email/.sqlx/query-37b5168f739c490a3373c65eab35f247a34af0a8e659cbad61cb99e3094e5a60.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-3a7be99469ed150e0506efa786b672198a6d3419c6bd5548418520cd3e6a8e95.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-48df79c41ba4ec7fa2454ac962aee9ff97875c9e2335b46fe13b2bbdee721186.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-4afa79ca95bdbcebe9d3de7ebb204636419e6cf89e650a8716c40ab2e588aa5b.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-4c695b011d49ba7d5fce4b309875ed952dc386ac760444d7883c5a76718f15be.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-54e3755d63a676a7fa70da77321e618032f0130c73eb6b0ae985f07c5c6c448b.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-679d0dd86957b91b759135cf9fc7314630cfd377b004a8c1dfedbf5764137ebd.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-7617f808df94ffe77c8195a911ee6b230a21fe9935e099d1310d244248ca253a.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-79d3bc72e6f424765706f7deea426379ae49cbd70dd122daedd95343618b9ac6.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-96d853b887f2e6d7aac6b15ddd2b71a8d8900d2ef30047da7ca1a5940503b6d0.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-b3aeddd507e0efee0e0cecef6eae7503820f0f3302e95a459790c5cb78e5dfb0.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-c4d2fed1a0fb95c20ef9b4049cc5cacfb7f0a441c630528d2cde076211ff63f9.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-e70d5acce88125ae894d9d57356559c6e59e236c9650211a52fdc90602318cc3.json is excluded by !**/.sqlx/**
  • rust/cloud-storage/email/.sqlx/query-e7235d6ad2033a2ff7be0bf57ab042cd71e3b3d0e1f8a9750e55cdda92e70c6f.json is excluded by !**/.sqlx/**
📒 Files selected for processing (31)
  • js/app/packages/app/component/next-soup/actions/index.ts
  • js/app/packages/app/component/next-soup/actions/make-mark-sender-important-action.ts
  • js/app/packages/app/component/next-soup/actions/make-mark-sender-noise-action.ts
  • js/app/packages/app/component/next-soup/actions/make-sender-filter-action.ts
  • js/app/packages/app/component/next-soup/filters/inbox-filters.ts
  • js/app/packages/app/component/next-soup/soup-view/soup-entity-actions-menu.tsx
  • js/app/packages/block-email/component/Email.tsx
  • js/app/packages/block-email/component/EmailContext.tsx
  • js/app/packages/block-email/util/emailHotkeys.ts
  • js/app/packages/core/constant/featureFlags.ts
  • js/app/packages/core/hotkey/tokens.ts
  • js/app/packages/queries/email/thread.ts
  • js/app/packages/service-clients/service-email/client.ts
  • js/app/packages/service-clients/service-email/openapi.json
  • rust/cloud-storage/email/fixtures/email_dynamic_query_email_filters.sql
  • rust/cloud-storage/email/src/domain/models/email_filter.rs
  • rust/cloud-storage/email/src/domain/models/error.rs
  • rust/cloud-storage/email/src/domain/models/mod.rs
  • rust/cloud-storage/email/src/domain/ports.rs
  • rust/cloud-storage/email/src/domain/service/mod.rs
  • rust/cloud-storage/email/src/inbound/axum.rs
  • rust/cloud-storage/email/src/inbound/axum/email_filter_router.rs
  • rust/cloud-storage/email/src/outbound/email_pg_repo/dynamic/filters.rs
  • rust/cloud-storage/email/src/outbound/email_pg_repo/dynamic/tests.rs
  • rust/cloud-storage/email/src/outbound/email_pg_repo/email_filter.rs
  • rust/cloud-storage/email/src/outbound/email_pg_repo/mod.rs
  • rust/cloud-storage/email/src/outbound/email_pg_repo/test/dynamic_query.rs
  • rust/cloud-storage/email_service/src/api/email/filters/mod.rs
  • rust/cloud-storage/email_service/src/api/email/mod.rs
  • rust/cloud-storage/email_service/src/api/swagger.rs
  • rust/cloud-storage/macro_db_client/migrations/20260325102949_email_filters.sql

Comment on lines +431 to +449
const filterId = result[1].filter.id;

toast.success(
`Sender marked as ${label}`,
`Messages from ${senderEmail} will appear in ${label}`,
{
text: 'Undo',
onClick: async () => {
const undoResult = await emailClient.deleteEmailFilter({
id: filterId,
});
if (isErr(undoResult)) {
toast.failure('Failed to undo', senderEmail);
} else {
toast.success('Sender filter removed');
}
},
}
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Undo needs to restore an existing override, not always delete it.

Line 431 only captures the returned filter.id. If this upsert updated an existing sender rule, the undo handler on Lines 439-446 deletes that row and loses the previous preference instead of restoring the prior is_important value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/app/packages/queries/email/thread.ts` around lines 431 - 449, The undo
currently always deletes the created/updated rule using filterId
(result[1].filter.id) which loses any previous preference; change the logic in
the toast success undo handler (the onClick that calls
emailClient.deleteEmailFilter) to capture the prior rule state before upserting
(e.g., previous is_important) and on undo either restore the previous row via an
upsert/update call (re-apply the prior is_important and other fields) or delete
only if the rule was newly created; reference the upsert response
(result[1].filter), the variable filterId, and replace the delete-only call to
emailClient.deleteEmailFilter with conditional logic that restores the previous
override using emailClient.upsertEmailFilter or updates the existing filter to
the previous is_important value.

Comment on lines +261 to +287
async fn upsert_email_filter(
&self,
link_id: Uuid,
input: UpsertEmailFilterInput,
) -> Result<EmailFilter, Self::Err> {
if let Some(address) = &input.email_address {
email_filter::upsert_email_filter_by_address(
&self.pool,
link_id,
address,
input.is_important,
)
.await
} else if let Some(domain) = &input.email_domain {
email_filter::upsert_email_filter_by_domain(
&self.pool,
link_id,
domain,
input.is_important,
)
.await
} else {
unreachable!(
"UpsertEmailFilterInput must have either email_address or email_domain; validated by service layer"
)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject invalid filter shapes instead of panicking or silently preferring one field.

This dispatch accepts { email_address: Some, email_domain: Some } by ignoring the domain, and { None, None } hits unreachable!. Please match the two options explicitly and return an error for both invalid shapes so the repo can't panic—or write the wrong rule—if service-side validation regresses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/cloud-storage/email/src/outbound/email_pg_repo/mod.rs` around lines 261
- 287, The upsert_email_filter implementation currently prefers email_address
when both fields are set and panics with unreachable! when neither is set;
change it to explicitly validate UpsertEmailFilterInput shapes and return an
error for invalid shapes instead of panicking or silently choosing one field.
Update async fn upsert_email_filter to: handle the exact two valid cases
(Some(address) && None => call email_filter::upsert_email_filter_by_address,
None && Some(domain) => call email_filter::upsert_email_filter_by_domain), and
for the other shapes (both Some or both None) return an appropriate
Err(Self::Err) (create or reuse a descriptive error variant such as
InvalidFilterShape or BadRequest) so callers get a recoverable error rather than
an unwind or silent preference. Ensure the returned error flows through the
function signature and tests cover both invalid-shape cases.

@evanhutnik evanhutnik changed the title feat(email): sender signal/noise filtering with email_filters feat(email): Set email sender as Signal/Noise with email_filters Mar 25, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
js/app/packages/queries/email/thread.ts (1)

440-450: ⚠️ Potential issue | 🟠 Major

Undo deletes filter instead of restoring prior state.

If this upsert updated an existing filter (e.g., changing a sender from Signal to Noise), the undo action deletes the filter entirely rather than restoring the previous is_important value. This loses the user's prior preference.

Consider capturing the previous filter state before upserting. One approach:

  1. Query existing filters to find if one already exists for this sender
  2. Store the previous is_important value (if any)
  3. On undo: if there was a prior filter, upsert the old value; otherwise delete
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/app/packages/queries/email/thread.ts` around lines 440 - 450, The undo
handler currently always calls emailClient.deleteEmailFilter (in the onClick
block) which deletes a filter instead of restoring a prior is_important state;
change the flow so before calling the upsert you read and cache the existing
filter (e.g., via emailClient.getFilters or the existing query that finds a
filter by sender) and store its previous is_important (or a flag that no prior
filter existed) alongside filterId/senderEmail; then update the onClick undo
logic to: if a previous filter existed, call emailClient.upsertEmailFilter with
the previous is_important value to restore it, otherwise call
emailClient.deleteEmailFilter to remove the newly created filter, and keep the
invalidateAllSoup() / toast.* calls as appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@rust/cloud-storage/macro_db_client/migrations/20260325102949_email_filters.sql`:
- Around line 1-29: Add a reversible DOWN migration or rollback notes for the
new email_filters schema: provide SQL to drop the table and associated
indexes/constraints (DROP TABLE IF EXISTS email_filters CASCADE or explicit DROP
INDEX for email_filters_link_id_email_address_uq,
email_filters_link_id_email_domain_uq, idx_email_filters_link_id and then DROP
TABLE email_filters) or add a documented rollback section describing the manual
steps to remove the email_filters table and its constraints/indexes; reference
the created objects email_filters, email_filters_pkey,
email_filters_address_xor_domain_chk, email_filters_email_domain_format_chk and
the three index names to ensure the DOWN migration fully reverses the UP
migration.

---

Duplicate comments:
In `@js/app/packages/queries/email/thread.ts`:
- Around line 440-450: The undo handler currently always calls
emailClient.deleteEmailFilter (in the onClick block) which deletes a filter
instead of restoring a prior is_important state; change the flow so before
calling the upsert you read and cache the existing filter (e.g., via
emailClient.getFilters or the existing query that finds a filter by sender) and
store its previous is_important (or a flag that no prior filter existed)
alongside filterId/senderEmail; then update the onClick undo logic to: if a
previous filter existed, call emailClient.upsertEmailFilter with the previous
is_important value to restore it, otherwise call emailClient.deleteEmailFilter
to remove the newly created filter, and keep the invalidateAllSoup() / toast.*
calls as appropriate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0275a467-2fcf-4cdb-a765-170506e0786a

📥 Commits

Reviewing files that changed from the base of the PR and between 7c76aa7 and d718c24.

📒 Files selected for processing (3)
  • js/app/packages/app/component/next-soup/actions/make-sender-filter-action.ts
  • js/app/packages/queries/email/thread.ts
  • rust/cloud-storage/macro_db_client/migrations/20260325102949_email_filters.sql

Comment on lines +1 to +29
CREATE TABLE IF NOT EXISTS email_filters
(
id UUID DEFAULT gen_random_uuid() NOT NULL,
link_id UUID NOT NULL REFERENCES email_links (id) ON DELETE CASCADE,
email_address VARCHAR(320),
email_domain VARCHAR(255),
is_important BOOLEAN NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT email_filters_pkey PRIMARY KEY (id),
CONSTRAINT email_filters_address_xor_domain_chk
CHECK (
(email_address IS NOT NULL AND trim(email_address) <> '')
<>
(email_domain IS NOT NULL AND trim(email_domain) <> '')
),
CONSTRAINT email_filters_email_domain_format_chk
CHECK (email_domain IS NULL OR (trim(email_domain) <> '' AND position('@' IN email_domain) = 0))
);

CREATE UNIQUE INDEX IF NOT EXISTS email_filters_link_id_email_address_uq
ON email_filters (link_id, lower(email_address))
WHERE email_address IS NOT NULL;

CREATE UNIQUE INDEX IF NOT EXISTS email_filters_link_id_email_domain_uq
ON email_filters (link_id, lower(email_domain))
WHERE email_domain IS NOT NULL;

CREATE INDEX IF NOT EXISTS idx_email_filters_link_id
ON email_filters (link_id); No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding a DOWN migration for reversibility.

The migration lacks a corresponding DOWN migration (or explicit reversibility strategy). While this may align with project conventions, documenting the rollback approach (manual DROP TABLE, schema version reset, etc.) helps operational teams handle deployment issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@rust/cloud-storage/macro_db_client/migrations/20260325102949_email_filters.sql`
around lines 1 - 29, Add a reversible DOWN migration or rollback notes for the
new email_filters schema: provide SQL to drop the table and associated
indexes/constraints (DROP TABLE IF EXISTS email_filters CASCADE or explicit DROP
INDEX for email_filters_link_id_email_address_uq,
email_filters_link_id_email_domain_uq, idx_email_filters_link_id and then DROP
TABLE email_filters) or add a documented rollback section describing the manual
steps to remove the email_filters table and its constraints/indexes; reference
the created objects email_filters, email_filters_pkey,
email_filters_address_xor_domain_chk, email_filters_email_domain_format_chk and
the three index names to ensure the DOWN migration fully reverses the UP
migration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant