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:
@@ -880,6 +880,21 @@ fn mentioned_in_excludes_assigned_issue() {
|
|||||||
assert!(results.is_empty(), "should exclude assigned issues");
|
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]
|
#[test]
|
||||||
fn mentioned_in_finds_mention_on_non_authored_mr() {
|
fn mentioned_in_finds_mention_on_non_authored_mr() {
|
||||||
let conn = setup_test_db();
|
let conn = setup_test_db();
|
||||||
@@ -1093,6 +1108,27 @@ fn mentioned_in_rejects_false_positive_email() {
|
|||||||
|
|
||||||
// ─── Helper Tests ──────────────────────────────────────────────────────────
|
// ─── 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]
|
#[test]
|
||||||
fn parse_attention_state_all_variants() {
|
fn parse_attention_state_all_variants() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -789,6 +789,87 @@ struct RawMentionRow {
|
|||||||
mention_body: String,
|
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.
|
/// Query issues and MRs where the user is @mentioned but not assigned/authored/reviewing.
|
||||||
///
|
///
|
||||||
/// Includes open items unconditionally, plus recently-closed/merged items
|
/// Includes open items unconditionally, plus recently-closed/merged items
|
||||||
@@ -802,77 +883,8 @@ pub fn query_mentioned_in(
|
|||||||
recency_cutoff_ms: i64,
|
recency_cutoff_ms: i64,
|
||||||
) -> Result<Vec<MeMention>> {
|
) -> Result<Vec<MeMention>> {
|
||||||
let project_clause = build_project_clause_at("p.id", project_ids, 3);
|
let project_clause = build_project_clause_at("p.id", project_ids, 3);
|
||||||
|
// Materialized CTEs avoid pathological query plans for project-scoped mentions.
|
||||||
// CTE: note timestamps per issue (for attention state computation)
|
let sql = build_mentioned_in_sql(&project_clause);
|
||||||
// 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",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
|
let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
|
||||||
params.push(Box::new(username.to_string()));
|
params.push(Box::new(username.to_string()));
|
||||||
|
|||||||
Reference in New Issue
Block a user