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 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-03-06 13:36:26 -05:00
parent 9107a78b57
commit d3f8020cf8
2 changed files with 119 additions and 71 deletions

View File

@@ -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!(

View File

@@ -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<Vec<MeMention>> {
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<Box<dyn rusqlite::types::ToSql>> = Vec::new();
params.push(Box::new(username.to_string()));