btctax_cli/
bulk_estimated.rs1use crate::CliError;
13use btctax_core::EventId;
14use rusqlite::Connection;
15use std::collections::BTreeMap;
16
17pub fn init_table(conn: &Connection) -> Result<(), CliError> {
24 conn.execute_batch(
25 "CREATE TABLE IF NOT EXISTS bulk_estimated_proceeds \
26 (out_event TEXT PRIMARY KEY, date_marked TEXT NOT NULL);",
27 )?;
28 Ok(())
29}
30
31pub fn mark(conn: &Connection, out_event: &EventId, date_marked: &str) -> Result<(), CliError> {
34 init_table(conn)?;
35 conn.execute(
36 "INSERT INTO bulk_estimated_proceeds(out_event,date_marked) VALUES(?1,?2) \
37 ON CONFLICT(out_event) DO UPDATE SET date_marked=excluded.date_marked",
38 rusqlite::params![out_event.canonical(), date_marked],
39 )?;
40 Ok(())
41}
42
43pub fn clear(conn: &Connection, out_event: &EventId) -> Result<(), CliError> {
49 init_table(conn)?;
50 conn.execute(
51 "DELETE FROM bulk_estimated_proceeds WHERE out_event=?1",
52 [out_event.canonical()],
53 )?;
54 Ok(())
55}
56
57pub fn all(conn: &Connection) -> Result<BTreeMap<EventId, String>, CliError> {
60 init_table(conn)?;
61 let mut stmt = conn
62 .prepare("SELECT out_event, date_marked FROM bulk_estimated_proceeds ORDER BY out_event")?;
63 let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?;
64 let mut out = BTreeMap::new();
65 for row in rows {
66 let (ev_str, date_marked) = row?;
67 out.insert(crate::eventref::parse_event_id(&ev_str)?, date_marked);
68 }
69 Ok(out)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use btctax_core::{EventId, Source, SourceRef};
76
77 fn mem() -> rusqlite::Connection {
78 let c = rusqlite::Connection::open_in_memory().unwrap();
79 init_table(&c).unwrap();
80 c
81 }
82
83 fn eid(label: &str) -> EventId {
84 EventId::import(Source::Coinbase, SourceRef::new(label.to_string()))
85 }
86
87 #[test]
88 fn mark_then_all_contains_the_key() {
89 let c = mem();
90 let e = eid("out|bulk-est-1");
91 mark(&c, &e, "2026-07-03").unwrap();
92 let m = all(&c).unwrap();
93 assert_eq!(m.get(&e).map(String::as_str), Some("2026-07-03"));
94 }
95
96 #[test]
97 fn clear_removes_the_flag_and_is_idempotent() {
98 let c = mem();
99 let e = eid("out|bulk-est-clear");
100 mark(&c, &e, "2026-07-03").unwrap();
101 assert!(all(&c).unwrap().contains_key(&e));
102 clear(&c, &e).unwrap();
103 assert!(!all(&c).unwrap().contains_key(&e));
104 clear(&c, &e).unwrap();
106 assert!(!all(&c).unwrap().contains_key(&e));
107 }
108
109 #[test]
110 fn clear_absent_key_is_ok() {
111 let c = mem();
112 clear(&c, &eid("out|never-marked")).unwrap();
114 assert!(all(&c).unwrap().is_empty());
115 }
116
117 #[test]
118 fn mark_upserts_last_write_wins() {
119 let c = mem();
120 let e = eid("out|bulk-est-upsert");
121 mark(&c, &e, "2026-01-01").unwrap();
122 mark(&c, &e, "2026-07-03").unwrap();
123 assert_eq!(
124 all(&c).unwrap().get(&e).map(String::as_str),
125 Some("2026-07-03")
126 );
127 }
128
129 #[test]
130 fn all_on_tableless_vault_returns_empty() {
131 let c = rusqlite::Connection::open_in_memory().unwrap(); assert!(all(&c).unwrap().is_empty());
133 }
134
135 #[test]
136 fn defensive_guard_in_mark_creates_table() {
137 let c = rusqlite::Connection::open_in_memory().unwrap(); let e = eid("out|guard");
139 mark(&c, &e, "2026-07-03").unwrap();
140 assert!(all(&c).unwrap().contains_key(&e));
141 }
142
143 #[test]
144 fn all_returns_btreemap_in_deterministic_order() {
145 let c = mem();
146 let e1 = eid("out|alpha");
147 let e2 = eid("out|beta");
148 mark(&c, &e2, "2026-07-03").unwrap();
149 mark(&c, &e1, "2026-07-02").unwrap();
150 let m = all(&c).unwrap();
151 assert_eq!(m.len(), 2);
152 assert!(m.contains_key(&e1) && m.contains_key(&e2));
153 }
154}