fix: critical data integrity — timeline dedup, discussion atomicity, index collision

Three correctness bugs found via peer code review:

1. TimelineEvent PartialEq/Ord omitted entity_type — issue #42 and MR #42
   with the same timestamp and event_type were treated as equal. In a
   BTreeSet or dedup, one would silently be dropped. Added entity_type to
   both PartialEq and Ord comparisons.

2. discussions.rs: store_payload() was called outside the transaction
   (on bare conn) while upsert_discussion/notes were inside. A crash
   between them left orphaned payload rows. Moved store_payload inside
   the unchecked_transaction block, matching mr_discussions.rs pattern.

3. Migration 017 created idx_issue_assignees_username(username, issue_id)
   but migration 005 already created the same index name with just
   (username). SQLite's IF NOT EXISTS silently skipped the composite
   version on every existing database. New migration 018 drops and
   recreates the index with correct composite columns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-08 07:54:59 -05:00
parent f267578aab
commit 121a634653
4 changed files with 27 additions and 10 deletions

View File

@@ -53,6 +53,10 @@ const MIGRATIONS: &[(&str, &str)] = &[
include_str!("../../migrations/016_mr_file_changes.sql"),
),
("017", include_str!("../../migrations/017_who_indexes.sql")),
(
"018",
include_str!("../../migrations/018_fix_assignees_composite_index.sql"),
),
];
pub fn create_connection(db_path: &Path) -> Result<Connection> {

View File

@@ -25,6 +25,7 @@ pub struct TimelineEvent {
impl PartialEq for TimelineEvent {
fn eq(&self, other: &Self) -> bool {
self.timestamp == other.timestamp
&& self.entity_type == other.entity_type
&& self.entity_id == other.entity_id
&& self.event_type == other.event_type
}
@@ -42,6 +43,7 @@ impl Ord for TimelineEvent {
fn cmp(&self, other: &Self) -> Ordering {
self.timestamp
.cmp(&other.timestamp)
.then_with(|| self.entity_type.cmp(&other.entity_type))
.then_with(|| self.entity_id.cmp(&other.entity_id))
.then_with(|| self.event_type.cmp(&other.event_type))
}