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) <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-06-13 15:19:41 -04:00
parent e4d8d374fb
commit ca007a618d
2 changed files with 24 additions and 29 deletions
+11 -17
View File
@@ -84,21 +84,17 @@ impl UserAiPrefsDao for SqliteUserAiPrefsDao {
.lock() .lock()
.expect("Unable to lock UserAiPrefsDao"); .expect("Unable to lock UserAiPrefsDao");
// SQLite: INSERT on first call, UPDATE on subsequent calls. // Single-row table (id=1): one atomic upsert. The explicit id=1
// The first INSERT creates the row with id=1 (auto-increment). // makes the conflict target deterministic so the second call
// Subsequent calls UPDATE the existing row. // updates in place rather than tripping the CHECK(id=1) constraint,
let result = diesel::insert_into(dsl::user_ai_prefs) // and real insert errors surface instead of being swallowed into a
.values(prefs) // separate update branch. The columns are set explicitly (rather
.execute(connection.deref_mut()); // than via AsChangeset) so a None field overwrites to NULL — the
// row mirrors the latest request exactly, not a merge of past ones.
match result { diesel::insert_into(dsl::user_ai_prefs)
Ok(_) => { .values((dsl::id.eq(1), prefs))
// First insert succeeded. .on_conflict(dsl::id)
Ok(()) .do_update()
}
Err(_e) => {
// Insert failed (likely due to duplicate key). Update instead.
diesel::update(dsl::user_ai_prefs.filter(dsl::id.eq(1)))
.set(( .set((
dsl::voice.eq(&prefs.voice), dsl::voice.eq(&prefs.voice),
dsl::tz_offset_minutes.eq(&prefs.tz_offset_minutes), dsl::tz_offset_minutes.eq(&prefs.tz_offset_minutes),
@@ -108,8 +104,6 @@ impl UserAiPrefsDao for SqliteUserAiPrefsDao {
.execute(connection.deref_mut()) .execute(connection.deref_mut())
.map_err(|e| anyhow::anyhow!("Failed to upsert prefs: {}", e))?; .map_err(|e| anyhow::anyhow!("Failed to upsert prefs: {}", e))?;
Ok(()) Ok(())
}
}
}) })
.map_err(|e| DbError::log(DbErrorKind::InsertError, e)) .map_err(|e| DbError::log(DbErrorKind::InsertError, e))
} }
+4 -3
View File
@@ -1075,6 +1075,8 @@ async fn pregen_one(
// Flatten every media item across beats (in order) into the cache key. // Flatten every media item across beats (in order) into the cache key.
let media: Vec<SegmentMedia> = planned.iter().flat_map(|b| b.media.clone()).collect(); let media: Vec<SegmentMedia> = planned.iter().flat_map(|b| b.media.clone()).collect();
let key = cache_key(&selector, &media, voice.as_deref()); 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 // Dedup: check if fresh ledger row exists
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
@@ -1127,7 +1129,7 @@ async fn pregen_one(
cache_key: key.clone(), cache_key: key.clone(),
output_path: mp4_path.to_string_lossy().to_string(), output_path: mp4_path.to_string_lossy().to_string(),
title, title,
media_count: planned.len() as i32, media_count,
render_version: RENDER_VERSION as i32, render_version: RENDER_VERSION as i32,
tz_offset_minutes: tz, tz_offset_minutes: tz,
voice: voice.clone(), voice: voice.clone(),
@@ -1139,7 +1141,6 @@ async fn pregen_one(
// Generate the reel // Generate the reel
log::info!("Generating precomputed reel for span={}, key={}", span, key); log::info!("Generating precomputed reel for span={}, key={}", span, key);
let photo_count = planned.len() as i32;
let (title, mp4) = produce_reel( let (title, mp4) = produce_reel(
app_state, app_state,
insight_dao, insight_dao,
@@ -1163,7 +1164,7 @@ async fn pregen_one(
cache_key: key.clone(), cache_key: key.clone(),
output_path: mp4.to_string_lossy().to_string(), output_path: mp4.to_string_lossy().to_string(),
title, title,
media_count: photo_count, media_count,
render_version: RENDER_VERSION as i32, render_version: RENDER_VERSION as i32,
tz_offset_minutes: tz, tz_offset_minutes: tz,
voice: voice.clone(), voice: voice.clone(),