use anyhow::{Context, Result};
use rusqlite::{OptionalExtension, params};
use crate::db::Pool;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LidMigrationReport {
pub(crate) renamed: usize,
pub(crate) merged: usize,
pub(crate) messages_moved: usize,
}
impl LidMigrationReport {
pub(crate) fn is_noop(&self) -> bool {
self.renamed == 0 && self.merged == 0 && self.messages_moved == 0
}
}
fn chat_key(wa_id: &str) -> String {
format!("[chat:wa-{wa_id}]")
}
fn generated_title(wa_id: &str) -> String {
format!("WhatsApp: {wa_id} {}", chat_key(wa_id))
}
pub(crate) async fn reconcile_lid_sessions(
pool: &Pool,
mappings: Vec<(String, String)>,
) -> Result<LidMigrationReport> {
if mappings.is_empty() {
return Ok(LidMigrationReport::default());
}
let conn = pool
.get()
.await
.context("Failed to get a connection for LID reconciliation")?;
conn.interact(move |conn| -> Result<LidMigrationReport> {
let tx = conn.transaction()?;
let mut report = LidMigrationReport::default();
let now = chrono::Utc::now().timestamp();
for (lid, pn) in mappings {
let (lid, pn) = (lid.trim(), pn.trim());
if lid.is_empty() || pn.is_empty() || lid == pn {
continue;
}
let lid_key = chat_key(lid);
let pn_key = chat_key(pn);
let lid_row: Option<(String, Option<String>, i64, f64)> = tx
.query_row(
"SELECT id, title, token_count, total_cost FROM sessions \
WHERE channel_chat_key = ?1 AND archived_at IS NULL \
ORDER BY updated_at DESC LIMIT 1",
params![lid_key],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
)
.optional()?;
let Some((lid_id, lid_title, lid_tokens, lid_cost)) = lid_row else {
continue;
};
let pn_id: Option<String> = tx
.query_row(
"SELECT id FROM sessions \
WHERE channel_chat_key = ?1 AND archived_at IS NULL \
ORDER BY updated_at DESC LIMIT 1",
params![pn_key],
|r| r.get(0),
)
.optional()?;
match pn_id {
None => {
let legacy = generated_title(lid);
let title = if lid_title.as_deref() == Some(legacy.as_str()) {
Some(generated_title(pn))
} else {
lid_title.clone()
};
tx.execute(
"UPDATE sessions SET channel_chat_key = ?1, title = ?2, updated_at = ?3 \
WHERE id = ?4",
params![pn_key, title, now, lid_id],
)?;
report.renamed += 1;
}
Some(pn_id) if pn_id == lid_id => {}
Some(pn_id) => {
let moved = tx.execute(
"UPDATE messages SET session_id = ?1 WHERE session_id = ?2",
params![pn_id, lid_id],
)?;
tx.execute(
"WITH ordered AS ( \
SELECT id, ROW_NUMBER() OVER (ORDER BY created_at, sequence, id) - 1 \
AS new_seq \
FROM messages WHERE session_id = ?1 \
) \
UPDATE messages \
SET sequence = (SELECT new_seq FROM ordered WHERE ordered.id = messages.id) \
WHERE session_id = ?1",
params![pn_id],
)?;
tx.execute(
"UPDATE sessions SET token_count = token_count + ?1, \
total_cost = total_cost + ?2, updated_at = ?3 \
WHERE id = ?4",
params![lid_tokens, lid_cost, now, pn_id],
)?;
tx.execute(
"UPDATE sessions SET archived_at = ?1 WHERE id = ?2",
params![now, lid_id],
)?;
report.merged += 1;
report.messages_moved += moved;
}
}
}
tx.commit()?;
Ok(report)
})
.await
.map_err(crate::db::interact_err)?
}