From d3f8020cf8e21a50c7568d2638d7482778ec56de Mon Sep 17 00:00:00 2001 From: teernisse Date: Fri, 6 Mar 2026 13:36:26 -0500 Subject: [PATCH] perf(me): optimize mentions query with materialized CTEs scoped to candidates The `query_mentioned_in` SQL previously joined notes directly against the full issues/merge_requests tables, with per-row subqueries for author/assignee/reviewer exclusion. On large databases this produced pathological query plans where SQLite scanned the entire notes table before filtering to relevant entities. Refactor into a dedicated `build_mentioned_in_sql()` builder that: 1. Pre-filters candidate issues and MRs into MATERIALIZED CTEs (state open OR recently closed, not authored by user, not assigned/reviewing). This narrows the working set before any notes join. 2. Computes note timestamps (my_ts, others_ts, any_ts) as separate MATERIALIZED CTEs scoped to candidate entities only, rather than scanning all notes. 3. Joins mention-bearing notes against the pre-filtered candidates, avoiding the full-table scans. Also adds a test verifying that authored issues are excluded from the mentions results, and a unit test asserting all four CTEs are materialized. Co-Authored-By: Claude Opus 4.6 --- src/cli/commands/me/me_tests.rs | 36 ++++++++ src/cli/commands/me/queries.rs | 154 +++++++++++++++++--------------- 2 files changed, 119 insertions(+), 71 deletions(-) diff --git a/src/cli/commands/me/me_tests.rs b/src/cli/commands/me/me_tests.rs index 4e4ed58..5750717 100644 --- a/src/cli/commands/me/me_tests.rs +++ b/src/cli/commands/me/me_tests.rs @@ -880,6 +880,21 @@ fn mentioned_in_excludes_assigned_issue() { assert!(results.is_empty(), "should exclude assigned issues"); } +#[test] +fn mentioned_in_excludes_authored_issue() { + let conn = setup_test_db(); + insert_project(&conn, 1, "group/repo"); + insert_issue(&conn, 10, 1, 42, "alice"); // alice IS author + let disc_id = 100; + insert_discussion(&conn, disc_id, 1, None, Some(10)); + let t = now_ms() - 1000; + insert_note_at(&conn, 200, disc_id, 1, "bob", false, "hey @alice", t); + + let recency_cutoff = now_ms() - 7 * 24 * 3600 * 1000; + let results = query_mentioned_in(&conn, "alice", &[], recency_cutoff).unwrap(); + assert!(results.is_empty(), "should exclude authored issues"); +} + #[test] fn mentioned_in_finds_mention_on_non_authored_mr() { let conn = setup_test_db(); @@ -1093,6 +1108,27 @@ fn mentioned_in_rejects_false_positive_email() { // ─── Helper Tests ────────────────────────────────────────────────────────── +#[test] +fn mentioned_in_sql_materializes_core_ctes() { + let sql = build_mentioned_in_sql(""); + assert!( + sql.contains("candidate_issues AS MATERIALIZED"), + "candidate_issues should be materialized" + ); + assert!( + sql.contains("candidate_mrs AS MATERIALIZED"), + "candidate_mrs should be materialized" + ); + assert!( + sql.contains("note_ts_issue AS MATERIALIZED"), + "note_ts_issue should be materialized" + ); + assert!( + sql.contains("note_ts_mr AS MATERIALIZED"), + "note_ts_mr should be materialized" + ); +} + #[test] fn parse_attention_state_all_variants() { assert_eq!( diff --git a/src/cli/commands/me/queries.rs b/src/cli/commands/me/queries.rs index 4248e12..a67e22c 100644 --- a/src/cli/commands/me/queries.rs +++ b/src/cli/commands/me/queries.rs @@ -789,6 +789,87 @@ struct RawMentionRow { mention_body: String, } +fn build_mentioned_in_sql(project_clause: &str) -> String { + format!( + "WITH candidate_issues AS MATERIALIZED ( + SELECT i.id, i.iid, i.title, p.path_with_namespace, i.state, + i.updated_at, i.web_url + FROM issues i + JOIN projects p ON i.project_id = p.id + WHERE (i.state = 'opened' OR (i.state = 'closed' AND i.updated_at > ?2)) + AND (i.author_username IS NULL OR i.author_username != ?1) + AND NOT EXISTS ( + SELECT 1 FROM issue_assignees ia + WHERE ia.issue_id = i.id AND ia.username = ?1 + ) + {project_clause} + ), + candidate_mrs AS MATERIALIZED ( + SELECT m.id, m.iid, m.title, p.path_with_namespace, m.state, + m.updated_at, m.web_url + FROM merge_requests m + JOIN projects p ON m.project_id = p.id + WHERE (m.state = 'opened' + OR (m.state IN ('merged', 'closed') AND m.updated_at > ?2)) + AND m.author_username != ?1 + AND NOT EXISTS ( + SELECT 1 FROM mr_reviewers rv + WHERE rv.merge_request_id = m.id AND rv.username = ?1 + ) + {project_clause} + ), + note_ts_issue AS MATERIALIZED ( + SELECT d.issue_id, + MAX(CASE WHEN n.author_username = ?1 THEN n.created_at END) AS my_ts, + MAX(CASE WHEN n.author_username != ?1 THEN n.created_at END) AS others_ts, + MAX(n.created_at) AS any_ts + FROM notes n + JOIN discussions d ON n.discussion_id = d.id + JOIN candidate_issues ci ON ci.id = d.issue_id + WHERE n.is_system = 0 + GROUP BY d.issue_id + ), + note_ts_mr AS MATERIALIZED ( + SELECT d.merge_request_id, + MAX(CASE WHEN n.author_username = ?1 THEN n.created_at END) AS my_ts, + MAX(CASE WHEN n.author_username != ?1 THEN n.created_at END) AS others_ts, + MAX(n.created_at) AS any_ts + FROM notes n + JOIN discussions d ON n.discussion_id = d.id + JOIN candidate_mrs cm ON cm.id = d.merge_request_id + WHERE n.is_system = 0 + GROUP BY d.merge_request_id + ) + -- Issue mentions (scoped to candidate entities only) + SELECT 'issue', ci.iid, ci.title, ci.path_with_namespace, ci.state, + ci.updated_at, ci.web_url, + nt.my_ts, nt.others_ts, nt.any_ts, + n.body + FROM notes n + JOIN discussions d ON n.discussion_id = d.id + JOIN candidate_issues ci ON ci.id = d.issue_id + LEFT JOIN note_ts_issue nt ON nt.issue_id = ci.id + WHERE n.is_system = 0 + AND n.author_username != ?1 + AND LOWER(n.body) LIKE '%@' || LOWER(?1) || '%' + UNION ALL + -- MR mentions (scoped to candidate entities only) + SELECT 'mr', cm.iid, cm.title, cm.path_with_namespace, cm.state, + cm.updated_at, cm.web_url, + nt.my_ts, nt.others_ts, nt.any_ts, + n.body + FROM notes n + JOIN discussions d ON n.discussion_id = d.id + JOIN candidate_mrs cm ON cm.id = d.merge_request_id + LEFT JOIN note_ts_mr nt ON nt.merge_request_id = cm.id + WHERE n.is_system = 0 + AND n.author_username != ?1 + AND LOWER(n.body) LIKE '%@' || LOWER(?1) || '%' + ORDER BY 6 DESC + LIMIT 500", + ) +} + /// Query issues and MRs where the user is @mentioned but not assigned/authored/reviewing. /// /// Includes open items unconditionally, plus recently-closed/merged items @@ -802,77 +883,8 @@ pub fn query_mentioned_in( recency_cutoff_ms: i64, ) -> Result> { let project_clause = build_project_clause_at("p.id", project_ids, 3); - - // CTE: note timestamps per issue (for attention state computation) - // CTE: note timestamps per MR - // Then UNION ALL of issue mentions + MR mentions - let sql = format!( - "WITH note_ts_issue AS ( - SELECT d.issue_id, - MAX(CASE WHEN n.author_username = ?1 THEN n.created_at END) AS my_ts, - MAX(CASE WHEN n.author_username != ?1 THEN n.created_at END) AS others_ts, - MAX(n.created_at) AS any_ts - FROM notes n - JOIN discussions d ON n.discussion_id = d.id - WHERE n.is_system = 0 AND d.issue_id IS NOT NULL - GROUP BY d.issue_id - ), - note_ts_mr AS ( - SELECT d.merge_request_id, - MAX(CASE WHEN n.author_username = ?1 THEN n.created_at END) AS my_ts, - MAX(CASE WHEN n.author_username != ?1 THEN n.created_at END) AS others_ts, - MAX(n.created_at) AS any_ts - FROM notes n - JOIN discussions d ON n.discussion_id = d.id - WHERE n.is_system = 0 AND d.merge_request_id IS NOT NULL - GROUP BY d.merge_request_id - ) - -- Issue mentions - SELECT 'issue', i.iid, i.title, p.path_with_namespace, i.state, - i.updated_at, i.web_url, - nt.my_ts, nt.others_ts, nt.any_ts, - n.body - FROM notes n - JOIN discussions d ON n.discussion_id = d.id - JOIN issues i ON d.issue_id = i.id - JOIN projects p ON i.project_id = p.id - LEFT JOIN note_ts_issue nt ON nt.issue_id = i.id - WHERE n.is_system = 0 - AND n.author_username != ?1 - AND d.issue_id IS NOT NULL - AND LOWER(n.body) LIKE '%@' || LOWER(?1) || '%' - AND NOT EXISTS ( - SELECT 1 FROM issue_assignees ia - WHERE ia.issue_id = d.issue_id AND ia.username = ?1 - ) - AND (i.state = 'opened' OR (i.state = 'closed' AND i.updated_at > ?2)) - {project_clause} - UNION ALL - -- MR mentions - SELECT 'mr', m.iid, m.title, p.path_with_namespace, m.state, - m.updated_at, m.web_url, - nt.my_ts, nt.others_ts, nt.any_ts, - n.body - FROM notes n - JOIN discussions d ON n.discussion_id = d.id - JOIN merge_requests m ON d.merge_request_id = m.id - JOIN projects p ON m.project_id = p.id - LEFT JOIN note_ts_mr nt ON nt.merge_request_id = m.id - WHERE n.is_system = 0 - AND n.author_username != ?1 - AND d.merge_request_id IS NOT NULL - AND LOWER(n.body) LIKE '%@' || LOWER(?1) || '%' - AND m.author_username != ?1 - AND NOT EXISTS ( - SELECT 1 FROM mr_reviewers rv - WHERE rv.merge_request_id = d.merge_request_id AND rv.username = ?1 - ) - AND (m.state = 'opened' - OR (m.state IN ('merged', 'closed') AND m.updated_at > ?2)) - {project_clause} - ORDER BY 6 DESC - LIMIT 500", - ); + // Materialized CTEs avoid pathological query plans for project-scoped mentions. + let sql = build_mentioned_in_sql(&project_clause); let mut params: Vec> = Vec::new(); params.push(Box::new(username.to_string()));