Skip to content

Commit 48aa86d

Browse files
enriquephlclaude
andcommitted
feat(store): cleanup migration deleting legacy non-tip gift_user rows (0027)
Resolves the codex P2 on #76 at the data layer: removes role='gift_user' rows that lack tips_amount_usd metadata (legacy event_gift rows), so gift_user is tip-only in data as well as code. Tips and user rows are preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d5cd4af commit 48aa86d

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- SPDX-License-Identifier: AGPL-3.0-only
2+
-- One-time cleanup: remove legacy in-app Gift Event rows from chat_messages.
3+
-- The event_gift endpoint (removed in #76) wrote role='gift_user' rows with a
4+
-- bare label (e.g. "rose") and NO tips_amount_usd metadata. With gift_user now
5+
-- tip-only, those rows would be miscounted as user turns by assemble_chat_request
6+
-- and compute_signals_for_session. This deletes exactly the non-tip gift_user
7+
-- rows; tips (gift_user rows carrying metadata.tips_amount_usd) are preserved.
8+
-- Idempotent; a no-op for fresh/OSS deployments (eros-app, the only event_gift
9+
-- caller, is the sole source of such rows). companion_affinity_events
10+
-- event_type='gift' audit rows are intentionally left (append-only, already
11+
-- EMA-applied, still listed by the affinity BFF).
12+
--
13+
-- Spec: docs/superpowers/specs/2026-06-03-cleanup-legacy-gift-user-rows-design.md
14+
15+
DELETE FROM engine.chat_messages
16+
WHERE role = 'gift_user'
17+
AND (metadata IS NULL OR NOT (metadata ? 'tips_amount_usd'));

crates/eros-engine-store/src/chat.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,4 +2043,75 @@ mod tests {
20432043
assert_eq!(meta["vision_generation_id"], "gen-1");
20442044
assert_eq!(rows[0].content, ""); // untouched
20452045
}
2046+
2047+
/// 0027 cleanup: legacy non-tip gift_user rows (no tips_amount_usd metadata)
2048+
/// are deleted; tips (gift_user with tips_amount_usd) and user rows survive.
2049+
/// Mirrors the 0018 backfill-test pattern: #[sqlx::test] runs 0027 on the
2050+
/// empty DB (a no-op), then we seed rows and re-run the embedded DELETE
2051+
/// (valid because DELETE is idempotent).
2052+
const CLEANUP_SQL_0027: &str =
2053+
include_str!("../migrations/0027_drop_legacy_gift_user_rows.sql");
2054+
2055+
#[sqlx::test(migrations = "./migrations")]
2056+
async fn migration_0027_drops_legacy_non_tip_gift_user_rows(pool: PgPool) {
2057+
let user_id = Uuid::new_v4();
2058+
let instance_id = Uuid::new_v4();
2059+
let session_id: Uuid = sqlx::query_scalar(
2060+
"INSERT INTO engine.chat_sessions (user_id, instance_id) VALUES ($1, $2) RETURNING id",
2061+
)
2062+
.bind(user_id)
2063+
.bind(instance_id)
2064+
.fetch_one(&pool)
2065+
.await
2066+
.unwrap();
2067+
2068+
sqlx::query(
2069+
"INSERT INTO engine.chat_messages (session_id, role, content, metadata) VALUES \
2070+
($1, 'user', 'hi', NULL), \
2071+
($1, 'gift_user', '(打赏 $20)', '{\"tips_amount_usd\": 20.0}'::jsonb), \
2072+
($1, 'gift_user', 'rose', NULL), \
2073+
($1, 'gift_user', 'rose', '{\"label\": \"rose\"}'::jsonb)",
2074+
)
2075+
.bind(session_id)
2076+
.execute(&pool)
2077+
.await
2078+
.unwrap();
2079+
2080+
// Run the embedded cleanup migration.
2081+
sqlx::query(CLEANUP_SQL_0027).execute(&pool).await.unwrap();
2082+
2083+
// Only the tip gift_user row survives.
2084+
let gift_user_count: i64 = sqlx::query_scalar(
2085+
"SELECT COUNT(*) FROM engine.chat_messages \
2086+
WHERE session_id = $1 AND role = 'gift_user'",
2087+
)
2088+
.bind(session_id)
2089+
.fetch_one(&pool)
2090+
.await
2091+
.unwrap();
2092+
assert_eq!(gift_user_count, 1, "only the tip gift_user row survives");
2093+
2094+
// The survivor is the tip (carries tips_amount_usd).
2095+
let surviving_tip: i64 = sqlx::query_scalar(
2096+
"SELECT COUNT(*) FROM engine.chat_messages \
2097+
WHERE session_id = $1 AND role = 'gift_user' \
2098+
AND metadata ? 'tips_amount_usd'",
2099+
)
2100+
.bind(session_id)
2101+
.fetch_one(&pool)
2102+
.await
2103+
.unwrap();
2104+
assert_eq!(surviving_tip, 1, "the surviving gift_user row is the tip");
2105+
2106+
// user rows are untouched.
2107+
let user_count: i64 = sqlx::query_scalar(
2108+
"SELECT COUNT(*) FROM engine.chat_messages \
2109+
WHERE session_id = $1 AND role = 'user'",
2110+
)
2111+
.bind(session_id)
2112+
.fetch_one(&pool)
2113+
.await
2114+
.unwrap();
2115+
assert_eq!(user_count, 1, "user rows are untouched");
2116+
}
20462117
}

0 commit comments

Comments
 (0)