Files
gitlore/src/core/events_db.rs
Taylor Eernisse 53ef21d653 fix: propagate DB errors instead of silently swallowing them
Replace .unwrap_or(), .ok(), and .filter_map(|r| r.ok()) patterns with
proper error propagation using ? and rusqlite::OptionalExtension where
the query may legitimately return no rows.

Affected areas:
- events_db::count_events: three count queries now propagate errors
  instead of defaulting to (0, 0) on failure
- note_parser::extract_refs_from_system_notes: row iteration errors
  are now propagated instead of silently dropped via filter_map
- note_parser::noteable_type_to_entity_type: unknown types now log a
  debug warning before defaulting to "issue"
- payloads::store_payload/read_payload: use .optional()? instead of
  .ok() to distinguish "no row" from "query failed"
- backoff::compute_next_attempt_at: use .clamp(0, 30) to guard against
  negative attempt_count, not just .min(30)
- search::vector::max_chunks_per_document: returns Result<i64> with
  proper error propagation through .optional()?.flatten()
- embedding::chunk_ids::decode_rowid: promote debug_assert to assert
  since negative rowids indicate data corruption worth failing fast on
- ingestion::dirty_tracker::record_dirty_error: use .optional()? to
  handle missing dirty_sources row gracefully instead of hard error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 10:15:36 -05:00

200 lines
6.0 KiB
Rust

use rusqlite::Connection;
use super::error::{LoreError, Result};
use super::time::iso_to_ms_strict;
use crate::gitlab::types::{GitLabLabelEvent, GitLabMilestoneEvent, GitLabStateEvent};
pub fn upsert_state_events(
conn: &Connection,
project_id: i64,
entity_type: &str,
entity_local_id: i64,
events: &[GitLabStateEvent],
) -> Result<usize> {
let (issue_id, merge_request_id) = resolve_entity_ids(entity_type, entity_local_id)?;
let mut stmt = conn.prepare_cached(
"INSERT OR REPLACE INTO resource_state_events
(gitlab_id, project_id, issue_id, merge_request_id, state,
actor_gitlab_id, actor_username, created_at,
source_commit, source_merge_request_iid)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
)?;
let mut count = 0;
for event in events {
let created_at = iso_to_ms_strict(&event.created_at).map_err(LoreError::Other)?;
let actor_id = event.user.as_ref().map(|u| u.id);
let actor_username = event.user.as_ref().map(|u| u.username.as_str());
let source_mr_iid = event.source_merge_request.as_ref().map(|mr| mr.iid);
stmt.execute(rusqlite::params![
event.id,
project_id,
issue_id,
merge_request_id,
event.state,
actor_id,
actor_username,
created_at,
event.source_commit,
source_mr_iid,
])?;
count += 1;
}
Ok(count)
}
pub fn upsert_label_events(
conn: &Connection,
project_id: i64,
entity_type: &str,
entity_local_id: i64,
events: &[GitLabLabelEvent],
) -> Result<usize> {
let (issue_id, merge_request_id) = resolve_entity_ids(entity_type, entity_local_id)?;
let mut stmt = conn.prepare_cached(
"INSERT OR REPLACE INTO resource_label_events
(gitlab_id, project_id, issue_id, merge_request_id, action,
label_name, actor_gitlab_id, actor_username, created_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
)?;
let mut count = 0;
for event in events {
let created_at = iso_to_ms_strict(&event.created_at).map_err(LoreError::Other)?;
let actor_id = event.user.as_ref().map(|u| u.id);
let actor_username = event.user.as_ref().map(|u| u.username.as_str());
stmt.execute(rusqlite::params![
event.id,
project_id,
issue_id,
merge_request_id,
event.action,
event.label.as_ref().map(|l| l.name.as_str()),
actor_id,
actor_username,
created_at,
])?;
count += 1;
}
Ok(count)
}
pub fn upsert_milestone_events(
conn: &Connection,
project_id: i64,
entity_type: &str,
entity_local_id: i64,
events: &[GitLabMilestoneEvent],
) -> Result<usize> {
let (issue_id, merge_request_id) = resolve_entity_ids(entity_type, entity_local_id)?;
let mut stmt = conn.prepare_cached(
"INSERT OR REPLACE INTO resource_milestone_events
(gitlab_id, project_id, issue_id, merge_request_id, action,
milestone_title, milestone_id, actor_gitlab_id, actor_username, created_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
)?;
let mut count = 0;
for event in events {
let created_at = iso_to_ms_strict(&event.created_at).map_err(LoreError::Other)?;
let actor_id = event.user.as_ref().map(|u| u.id);
let actor_username = event.user.as_ref().map(|u| u.username.as_str());
stmt.execute(rusqlite::params![
event.id,
project_id,
issue_id,
merge_request_id,
event.action,
event.milestone.as_ref().map(|m| m.title.as_str()),
event.milestone.as_ref().map(|m| m.id),
actor_id,
actor_username,
created_at,
])?;
count += 1;
}
Ok(count)
}
fn resolve_entity_ids(
entity_type: &str,
entity_local_id: i64,
) -> Result<(Option<i64>, Option<i64>)> {
match entity_type {
"issue" => Ok((Some(entity_local_id), None)),
"merge_request" => Ok((None, Some(entity_local_id))),
_ => Err(LoreError::Other(format!(
"Invalid entity type for resource events: {entity_type}"
))),
}
}
pub fn count_events(conn: &Connection) -> Result<EventCounts> {
let mut counts = EventCounts::default();
let row: (i64, i64) = conn.query_row(
"SELECT
COUNT(CASE WHEN issue_id IS NOT NULL THEN 1 END),
COUNT(CASE WHEN merge_request_id IS NOT NULL THEN 1 END)
FROM resource_state_events",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
counts.state_issue = row.0 as usize;
counts.state_mr = row.1 as usize;
let row: (i64, i64) = conn.query_row(
"SELECT
COUNT(CASE WHEN issue_id IS NOT NULL THEN 1 END),
COUNT(CASE WHEN merge_request_id IS NOT NULL THEN 1 END)
FROM resource_label_events",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
counts.label_issue = row.0 as usize;
counts.label_mr = row.1 as usize;
let row: (i64, i64) = conn.query_row(
"SELECT
COUNT(CASE WHEN issue_id IS NOT NULL THEN 1 END),
COUNT(CASE WHEN merge_request_id IS NOT NULL THEN 1 END)
FROM resource_milestone_events",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
counts.milestone_issue = row.0 as usize;
counts.milestone_mr = row.1 as usize;
Ok(counts)
}
#[derive(Debug, Default)]
pub struct EventCounts {
pub state_issue: usize,
pub state_mr: usize,
pub label_issue: usize,
pub label_mr: usize,
pub milestone_issue: usize,
pub milestone_mr: usize,
}
impl EventCounts {
pub fn total(&self) -> usize {
self.state_issue
+ self.state_mr
+ self.label_issue
+ self.label_mr
+ self.milestone_issue
+ self.milestone_mr
}
}