refactor(timeline): extract shared resolve_entity_ref and make target_iid optional
The seed, expand, and collect stages each had their own near-identical resolve_entity_ref helper that converted internal DB IDs to full EntityRef structs. This duplication made it easy for bug fixes to land in one copy but not the others. Extract a single public resolve_entity_ref into timeline.rs with an optional project_id parameter: - Some(project_id): scopes the lookup (used by seed, which knows the project from the FTS result) - None: unscoped lookup (used by expand, which traverses cross-project references) Also changes UnresolvedRef.target_iid from i64 to Option<i64>. Cross- project references parsed from descriptions may not always carry an IID (e.g. when the reference is malformed or the target was deleted). The previous sentinel value of 0 was semantically incorrect since GitLab IIDs start at 1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use rusqlite::Connection;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use super::error::Result;
|
||||||
|
|
||||||
/// The core timeline event. All pipeline stages produce or consume these.
|
/// The core timeline event. All pipeline stages produce or consume these.
|
||||||
/// Spec ref: Section 3.3 "Event Model"
|
/// Spec ref: Section 3.3 "Event Model"
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -121,7 +124,7 @@ pub struct UnresolvedRef {
|
|||||||
pub source: EntityRef,
|
pub source: EntityRef,
|
||||||
pub target_project: Option<String>,
|
pub target_project: Option<String>,
|
||||||
pub target_type: String,
|
pub target_type: String,
|
||||||
pub target_iid: i64,
|
pub target_iid: Option<i64>,
|
||||||
pub reference_type: String,
|
pub reference_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +138,45 @@ pub struct TimelineResult {
|
|||||||
pub unresolved_references: Vec<UnresolvedRef>,
|
pub unresolved_references: Vec<UnresolvedRef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve an entity's internal DB id to a full [`EntityRef`] with iid and project path.
|
||||||
|
///
|
||||||
|
/// When `project_id` is `Some`, the query is scoped to that project.
|
||||||
|
/// Returns `Ok(None)` for unknown entity types or when no matching row exists.
|
||||||
|
pub fn resolve_entity_ref(
|
||||||
|
conn: &Connection,
|
||||||
|
entity_type: &str,
|
||||||
|
entity_id: i64,
|
||||||
|
project_id: Option<i64>,
|
||||||
|
) -> Result<Option<EntityRef>> {
|
||||||
|
let table = match entity_type {
|
||||||
|
"issue" => "issues",
|
||||||
|
"merge_request" => "merge_requests",
|
||||||
|
_ => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sql = format!(
|
||||||
|
"SELECT e.iid, p.path_with_namespace
|
||||||
|
FROM {table} e
|
||||||
|
JOIN projects p ON p.id = e.project_id
|
||||||
|
WHERE e.id = ?1 AND (?2 IS NULL OR e.project_id = ?2)"
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = conn.query_row(&sql, rusqlite::params![entity_id, project_id], |row| {
|
||||||
|
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok((iid, project_path)) => Ok(Some(EntityRef {
|
||||||
|
entity_type: entity_type.to_owned(),
|
||||||
|
entity_id,
|
||||||
|
entity_iid: iid,
|
||||||
|
project_path,
|
||||||
|
})),
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user