Skip to content

Commit 79961a4

Browse files
asiniscalchiclaude
andauthored
Merge pull request #102 from asiniscalchi/fix/daily-review-embedding-vec-orphans
fix(journal): delete daily review embedding vectors on review invalidation The vec0 virtual table backing daily review embeddings does not participate in FK cascades, so invalidating a daily review (new entry or /undo for that date) deleted the review and its embedding metadata but orphaned the vector row in daily_review_embedding_vec forever. Delete the vector rows explicitly before deleting the review, the same way delete_last_for_conversation already does for journal entry embeddings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 parents d83daa3 + 89ab816 commit 79961a4

1 file changed

Lines changed: 90 additions & 1 deletion

File tree

src/journal/store.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,24 @@ async fn delete_daily_review(
132132
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
133133
date: NaiveDate,
134134
) -> Result<(), sqlx::Error> {
135+
// The vec0 virtual table does not participate in FK cascades, so its rows
136+
// must be deleted explicitly before the review row cascades the metadata
137+
// away.
138+
sqlx::query(
139+
r#"
140+
DELETE FROM daily_review_embedding_vec
141+
WHERE rowid IN (
142+
SELECT metadata.id
143+
FROM daily_review_embedding_metadata metadata
144+
JOIN daily_reviews ON daily_reviews.id = metadata.daily_review_id
145+
WHERE daily_reviews.review_date = ?
146+
)
147+
"#,
148+
)
149+
.bind(date.to_string())
150+
.execute(&mut **tx)
151+
.await?;
152+
135153
sqlx::query(
136154
r#"
137155
DELETE FROM daily_reviews
@@ -154,8 +172,12 @@ mod tests {
154172
use crate::{
155173
database,
156174
journal::{
157-
embedding::{Embedding, SqliteEmbeddingRepository},
175+
embedding::{Embedding, EmbeddingIndex, SqliteEmbeddingRepository},
158176
repository::JournalRepository,
177+
review::{
178+
embedding_repository::SqliteDailyReviewEmbeddingRepository,
179+
repository::DailyReviewRepository,
180+
},
159181
},
160182
};
161183

@@ -245,6 +267,73 @@ mod tests {
245267
assert_eq!(review_count, 0);
246268
}
247269

270+
/// Seed a completed daily review for 2026-04-28 with a stored embedding
271+
/// vector, returning the review's embedding metadata id.
272+
async fn seed_embedded_daily_review(pool: &SqlitePool) -> i64 {
273+
let review = DailyReviewRepository::new(pool.clone())
274+
.upsert_completed(
275+
chrono::NaiveDate::from_ymd_opt(2026, 4, 28).unwrap(),
276+
"persisted review",
277+
"model",
278+
"v1",
279+
)
280+
.await
281+
.unwrap();
282+
let embedding = Embedding::new(vec![0.1; 1536], 1536).unwrap();
283+
EmbeddingIndex::store_embedding(
284+
&SqliteDailyReviewEmbeddingRepository::new(pool.clone()),
285+
review.id,
286+
"test-model",
287+
1536,
288+
&embedding,
289+
)
290+
.await
291+
.unwrap();
292+
review.id
293+
}
294+
295+
async fn daily_review_embedding_counts(pool: &SqlitePool) -> (i64, i64) {
296+
let metadata = sqlx::query_scalar("SELECT COUNT(*) FROM daily_review_embedding_metadata")
297+
.fetch_one(pool)
298+
.await
299+
.unwrap();
300+
let vectors = sqlx::query_scalar("SELECT COUNT(*) FROM daily_review_embedding_vec")
301+
.fetch_one(pool)
302+
.await
303+
.unwrap();
304+
(metadata, vectors)
305+
}
306+
307+
#[tokio::test]
308+
async fn store_invalidating_daily_review_removes_its_embedding_vectors() {
309+
let (store, _repo, pool) = setup().await;
310+
seed_embedded_daily_review(&pool).await;
311+
312+
store
313+
.store(&incoming("1", "new entry", at(10, 0)))
314+
.await
315+
.unwrap();
316+
317+
assert_eq!(daily_review_embedding_counts(&pool).await, (0, 0));
318+
}
319+
320+
#[tokio::test]
321+
async fn delete_last_for_conversation_removes_daily_review_embedding_vectors() {
322+
let (store, _repo, pool) = setup().await;
323+
store
324+
.store(&incoming("1", "reviewed entry", at(10, 0)))
325+
.await
326+
.unwrap();
327+
seed_embedded_daily_review(&pool).await;
328+
329+
store
330+
.delete_last_for_conversation(&MessageSource::Telegram, "42")
331+
.await
332+
.unwrap();
333+
334+
assert_eq!(daily_review_embedding_counts(&pool).await, (0, 0));
335+
}
336+
248337
#[tokio::test]
249338
async fn delete_last_for_conversation_removes_embedding_rows() {
250339
let (store, _repo, pool) = setup().await;

0 commit comments

Comments
 (0)