From ca007a618d50c3f58794280d02f56d05054e0ce3 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Sat, 13 Jun 2026 15:19:41 -0400 Subject: [PATCH] Reels pre-gen: record true media count + real upsert for user_ai_prefs - pregen_one recorded media_count as planned.len() (beat count); record the actual media item total (media.len(), photos + clips) in both the cache-hit and freshly-rendered ledger paths. Drops the redundant photo_count binding. - Replace upsert_prefs's insert-then-catch-error-then-update dance with a single atomic INSERT ... ON CONFLICT(id) DO UPDATE. Explicit id=1 makes the conflict target deterministic; explicit column .set((...)) keeps None -> NULL overwrite semantics so the row mirrors the latest request exactly, and genuine insert errors surface instead of being swallowed. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/database/user_ai_prefs_dao.rs | 46 ++++++++++++++----------------- src/reels/mod.rs | 7 +++-- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/database/user_ai_prefs_dao.rs b/src/database/user_ai_prefs_dao.rs index d58a56c..129ef0c 100644 --- a/src/database/user_ai_prefs_dao.rs +++ b/src/database/user_ai_prefs_dao.rs @@ -84,32 +84,26 @@ impl UserAiPrefsDao for SqliteUserAiPrefsDao { .lock() .expect("Unable to lock UserAiPrefsDao"); - // SQLite: INSERT on first call, UPDATE on subsequent calls. - // The first INSERT creates the row with id=1 (auto-increment). - // Subsequent calls UPDATE the existing row. - let result = diesel::insert_into(dsl::user_ai_prefs) - .values(prefs) - .execute(connection.deref_mut()); - - match result { - Ok(_) => { - // First insert succeeded. - Ok(()) - } - Err(_e) => { - // Insert failed (likely due to duplicate key). Update instead. - diesel::update(dsl::user_ai_prefs.filter(dsl::id.eq(1))) - .set(( - dsl::voice.eq(&prefs.voice), - dsl::tz_offset_minutes.eq(&prefs.tz_offset_minutes), - dsl::library.eq(&prefs.library), - dsl::updated_at.eq(&prefs.updated_at), - )) - .execute(connection.deref_mut()) - .map_err(|e| anyhow::anyhow!("Failed to upsert prefs: {}", e))?; - Ok(()) - } - } + // Single-row table (id=1): one atomic upsert. The explicit id=1 + // makes the conflict target deterministic so the second call + // updates in place rather than tripping the CHECK(id=1) constraint, + // and real insert errors surface instead of being swallowed into a + // separate update branch. The columns are set explicitly (rather + // than via AsChangeset) so a None field overwrites to NULL — the + // row mirrors the latest request exactly, not a merge of past ones. + diesel::insert_into(dsl::user_ai_prefs) + .values((dsl::id.eq(1), prefs)) + .on_conflict(dsl::id) + .do_update() + .set(( + dsl::voice.eq(&prefs.voice), + dsl::tz_offset_minutes.eq(&prefs.tz_offset_minutes), + dsl::library.eq(&prefs.library), + dsl::updated_at.eq(&prefs.updated_at), + )) + .execute(connection.deref_mut()) + .map_err(|e| anyhow::anyhow!("Failed to upsert prefs: {}", e))?; + Ok(()) }) .map_err(|e| DbError::log(DbErrorKind::InsertError, e)) } diff --git a/src/reels/mod.rs b/src/reels/mod.rs index 7fc71b0..059ce43 100644 --- a/src/reels/mod.rs +++ b/src/reels/mod.rs @@ -1075,6 +1075,8 @@ async fn pregen_one( // Flatten every media item across beats (in order) into the cache key. let media: Vec = planned.iter().flat_map(|b| b.media.clone()).collect(); let key = cache_key(&selector, &media, voice.as_deref()); + // Total media items shown (photos + clips), not beat count. + let media_count = media.len() as i32; // Dedup: check if fresh ledger row exists let now = std::time::SystemTime::now() @@ -1127,7 +1129,7 @@ async fn pregen_one( cache_key: key.clone(), output_path: mp4_path.to_string_lossy().to_string(), title, - media_count: planned.len() as i32, + media_count, render_version: RENDER_VERSION as i32, tz_offset_minutes: tz, voice: voice.clone(), @@ -1139,7 +1141,6 @@ async fn pregen_one( // Generate the reel log::info!("Generating precomputed reel for span={}, key={}", span, key); - let photo_count = planned.len() as i32; let (title, mp4) = produce_reel( app_state, insight_dao, @@ -1163,7 +1164,7 @@ async fn pregen_one( cache_key: key.clone(), output_path: mp4.to_string_lossy().to_string(), title, - media_count: photo_count, + media_count, render_version: RENDER_VERSION as i32, tz_offset_minutes: tz, voice: voice.clone(),