From dfa44e5bcd9962ae99a2ffed8101a36c1b211975 Mon Sep 17 00:00:00 2001 From: Taylor Eernisse Date: Mon, 9 Feb 2026 10:15:53 -0500 Subject: [PATCH] fix(ingestion): label upsert reliability, init idempotency, and sync health Label upsert (issues + merge_requests): Replace INSERT ... ON CONFLICT DO UPDATE RETURNING with INSERT OR IGNORE + SELECT. The prior RETURNING-based approach relied on last_insert_rowid() matching the returned id, which is not guaranteed when ON CONFLICT triggers an update (SQLite may return 0). The new two-step approach is unambiguous and correctly tracks created_count. Init: Add ON CONFLICT(gitlab_project_id) DO UPDATE to the project insert so re-running `lore init` updates path/branch/url instead of failing with a unique constraint violation. MR discussions sync: Reset discussions_sync_attempts to 0 when clearing a sync health error, so previously-failed MRs get a fresh retry budget after successful sync. Count: format_number now handles negative numbers correctly by extracting the sign before inserting thousand-separators. Co-Authored-By: Claude Opus 4.6 --- src/cli/commands/count.rs | 10 ++++++++-- src/cli/commands/init.rs | 6 +++++- src/ingestion/issues.rs | 17 ++++++++++------- src/ingestion/merge_requests.rs | 17 ++++++++++------- src/ingestion/mr_discussions.rs | 1 + 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/cli/commands/count.rs b/src/cli/commands/count.rs index 0b3056e..1ad4fdd 100644 --- a/src/cli/commands/count.rs +++ b/src/cli/commands/count.rs @@ -179,9 +179,15 @@ fn count_notes(conn: &Connection, type_filter: Option<&str>) -> Result String { - let s = n.to_string(); + let (prefix, abs) = if n < 0 { + ("-", n.unsigned_abs()) + } else { + ("", n.unsigned_abs()) + }; + + let s = abs.to_string(); let chars: Vec = s.chars().collect(); - let mut result = String::new(); + let mut result = String::from(prefix); for (i, c) in chars.iter().enumerate() { if i > 0 && (chars.len() - i).is_multiple_of(3) { diff --git a/src/cli/commands/init.rs b/src/cli/commands/init.rs index 5c5a2d0..fa53806 100644 --- a/src/cli/commands/init.rs +++ b/src/cli/commands/init.rs @@ -133,7 +133,11 @@ pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result Result { + tx.execute( + "INSERT OR IGNORE INTO labels (project_id, name) VALUES (?1, ?2)", + (project_id, name), + )?; + + if tx.changes() > 0 { + *created_count += 1; + } + let id: i64 = tx.query_row( - "INSERT INTO labels (project_id, name) VALUES (?1, ?2) - ON CONFLICT(project_id, name) DO UPDATE SET name = excluded.name - RETURNING id", + "SELECT id FROM labels WHERE project_id = ?1 AND name = ?2", (project_id, name), |row| row.get(0), )?; - if tx.last_insert_rowid() == id { - *created_count += 1; - } - Ok(id) } diff --git a/src/ingestion/merge_requests.rs b/src/ingestion/merge_requests.rs index 88c7ee5..384d606 100644 --- a/src/ingestion/merge_requests.rs +++ b/src/ingestion/merge_requests.rs @@ -295,18 +295,21 @@ fn upsert_label_tx( name: &str, created_count: &mut usize, ) -> Result { + tx.execute( + "INSERT OR IGNORE INTO labels (project_id, name) VALUES (?1, ?2)", + (project_id, name), + )?; + + if tx.changes() > 0 { + *created_count += 1; + } + let id: i64 = tx.query_row( - "INSERT INTO labels (project_id, name) VALUES (?1, ?2) - ON CONFLICT(project_id, name) DO UPDATE SET name = excluded.name - RETURNING id", + "SELECT id FROM labels WHERE project_id = ?1 AND name = ?2", (project_id, name), |row| row.get(0), )?; - if tx.last_insert_rowid() == id { - *created_count += 1; - } - Ok(id) } diff --git a/src/ingestion/mr_discussions.rs b/src/ingestion/mr_discussions.rs index 00b0434..6f4db0f 100644 --- a/src/ingestion/mr_discussions.rs +++ b/src/ingestion/mr_discussions.rs @@ -593,6 +593,7 @@ fn clear_sync_health_error(conn: &Connection, local_mr_id: i64) -> Result<()> { conn.execute( "UPDATE merge_requests SET discussions_sync_last_attempt_at = ?, + discussions_sync_attempts = 0, discussions_sync_last_error = NULL WHERE id = ?", params![now_ms(), local_mr_id],