use super::*;
const TASK_AUDIT: &str = "gtd_lifecycle_audit";
const NAMESPACE_SCOPED_PACK_TABLES: &[&str] = &[
"knowledge_eval_runs",
"exec_runs",
"exec_events",
"git_receipts",
];
const POLICY: &str = "tool_policy";
const GRANTS: &str = "tool_grants";
const SNAPSHOTS: &str = "retrieval_snapshots";
fn present(census: &NamespaceCensus, table: &str) -> bool {
census
.tables
.iter()
.any(|candidate| candidate.name == table)
}
pub(super) fn move_task_audit(
conn: &Connection,
census: &NamespaceCensus,
request: &MoveRequest,
rows: &mut BTreeMap<String, u64>,
) -> rusqlite::Result<()> {
if !present(census, TASK_AUDIT) {
return Ok(());
}
let source = request.source.as_str();
for route in &request.routes {
let SubjectClass::Note(kind) = &route.class else {
continue;
};
let moved = conn.execute(
"UPDATE gtd_lifecycle_audit SET namespace = ?2 \
WHERE namespace = ?1 AND note_id IN (\
SELECT id FROM notes WHERE namespace = ?1 AND kind = ?3)",
rusqlite::params![source, route.target.as_str(), kind.as_str()],
)? as u64;
*rows.entry(TASK_AUDIT.to_string()).or_default() += moved;
}
Ok(())
}
pub(super) fn settle_pack_tables(
conn: &Connection,
census: &NamespaceCensus,
request: &MoveRequest,
counts: &mut MoveCounts,
) -> rusqlite::Result<()> {
let source = request.source.as_str();
if present(census, TASK_AUDIT) {
let left = count_in_namespace(conn, TASK_AUDIT, source)?;
if left > 0 {
counts.left_behind.insert(TASK_AUDIT.to_string(), left);
}
}
for table in NAMESPACE_SCOPED_PACK_TABLES {
if !present(census, table) {
continue;
}
match request.single_target() {
Some(target) => {
move_whole_table(conn, table, source, target, &mut counts.rows)?;
}
None => {
let left = count_in_namespace(conn, table, source)?;
if left > 0 {
counts.left_behind.insert((*table).to_string(), left);
}
}
}
}
leave_authorization_behind(conn, census, request, counts)?;
if present(census, SNAPSHOTS) {
drop_source_snapshots(conn, source)?;
}
Ok(())
}
fn leave_authorization_behind(
conn: &Connection,
census: &NamespaceCensus,
request: &MoveRequest,
counts: &mut MoveCounts,
) -> rusqlite::Result<()> {
let source = request.source.as_str();
let now_micros = request
.now_micros
.unwrap_or_else(|| chrono::Utc::now().timestamp_micros());
if present(census, POLICY) {
let left = count_in_namespace(conn, POLICY, source)?;
if left > 0 {
counts.left_behind.insert(POLICY.to_string(), left);
let live = conn.query_row(
"SELECT COUNT(*) FROM tool_policy WHERE namespace = ?1 AND deleted_at IS NULL",
[source],
|row| row.get::<_, i64>(0),
)? as u64;
counts.live_policies_left_behind = live;
}
}
if present(census, GRANTS) {
let left = count_in_namespace(conn, GRANTS, source)?;
if left > 0 {
counts.left_behind.insert(GRANTS.to_string(), left);
let in_force = conn.query_row(
"SELECT COUNT(*) FROM tool_grants \
WHERE namespace = ?1 AND status = 'granted' \
AND (expires_at IS NULL OR expires_at > ?2) \
AND invalidated_by_registry_id IS NULL AND invalidated_at IS NULL",
rusqlite::params![source, now_micros],
|row| row.get::<_, i64>(0),
)? as u64;
counts.grants_in_force_left_behind = in_force;
}
}
if counts.live_policies_left_behind > 0 || counts.grants_in_force_left_behind > 0 {
tracing::warn!(
namespace = source,
live_policies = counts.live_policies_left_behind,
grants_in_force = counts.grants_in_force_left_behind,
"a namespace move left authorization rows in the source namespace"
);
}
Ok(())
}
fn drop_source_snapshots(conn: &Connection, source: &str) -> rusqlite::Result<()> {
let prefix = format!("{}::%", khive_types::escape_like_literal(source));
conn.execute(
"DELETE FROM retrieval_snapshots WHERE namespace = ?1 \
OR (namespace LIKE ?2 ESCAPE '\\' \
AND substr(namespace, 1, length(?1) + 2) = ?1 || '::')",
rusqlite::params![source, prefix],
)?;
Ok(())
}