1use fallible_streaming_iterator::FallibleStreamingIterator;
8use rusqlite::hooks::Action;
9use rusqlite::session::ChangesetIter;
10use rusqlite::types::ValueRef;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ChangeOp {
15 Insert,
16 Update,
17 Delete,
18}
19
20#[derive(Debug, Clone)]
29pub struct RowChange {
30 pub table: String,
31 pub op: ChangeOp,
32 pub columns: Vec<Option<String>>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub(crate) enum UpdateValue {
37 New,
38 Old,
39}
40
41impl RowChange {
42 pub fn pk(&self) -> Option<&str> {
44 self.col(0)
45 }
46
47 pub fn col(&self, i: usize) -> Option<&str> {
49 self.columns.get(i).and_then(|c| c.as_deref())
50 }
51}
52
53enum ColumnCell {
54 Absent,
55 Present(Option<String>),
56}
57
58pub fn walk(changeset_bytes: &[u8]) -> Result<Vec<RowChange>, String> {
62 walk_with_update_values(changeset_bytes, UpdateValue::New)
63}
64
65pub(crate) fn walk_old(changeset_bytes: &[u8]) -> Result<Vec<RowChange>, String> {
66 walk_with_update_values(changeset_bytes, UpdateValue::Old)
67}
68
69fn walk_with_update_values(
70 changeset_bytes: &[u8],
71 update_value: UpdateValue,
72) -> Result<Vec<RowChange>, String> {
73 if changeset_bytes.is_empty() {
74 return Ok(Vec::new());
75 }
76
77 let input: &mut dyn std::io::Read = &mut &changeset_bytes[..];
78 let mut iter =
79 ChangesetIter::start_strm(&input).map_err(|e| format!("changeset start failed: {e}"))?;
80
81 let mut changes = Vec::new();
82 while let Some(item) = iter
83 .next()
84 .map_err(|e| format!("changeset next failed: {e}"))?
85 {
86 let op = item.op().map_err(|e| format!("changeset op failed: {e}"))?;
87 let change_op = match op.code() {
88 Action::SQLITE_INSERT => ChangeOp::Insert,
89 Action::SQLITE_UPDATE => ChangeOp::Update,
90 Action::SQLITE_DELETE => ChangeOp::Delete,
91 _ => continue,
92 };
93 let ncol = op.number_of_columns();
94
95 let cells = (0..ncol)
96 .map(|c| extract_col(item, c as usize, change_op, update_value))
97 .collect::<Result<Vec<_>, _>>()?;
98 let columns = cells
99 .iter()
100 .map(|cell| match cell {
101 ColumnCell::Absent => None,
102 ColumnCell::Present(value) => value.clone(),
103 })
104 .collect();
105 changes.push(RowChange {
106 table: op.table_name().to_string(),
107 op: change_op,
108 columns,
109 });
110 }
111
112 Ok(changes)
113}
114
115fn extract_col(
119 item: &rusqlite::session::ChangesetItem,
120 col: usize,
121 op: ChangeOp,
122 update_value: UpdateValue,
123) -> Result<ColumnCell, String> {
124 match op {
125 ChangeOp::Insert => changeset_value(item, col, UpdateValue::New),
126 ChangeOp::Delete => changeset_value(item, col, UpdateValue::Old),
127 ChangeOp::Update => {
128 let fallback = match update_value {
129 UpdateValue::New => UpdateValue::Old,
130 UpdateValue::Old => UpdateValue::New,
131 };
132 match changeset_value(item, col, update_value)? {
133 ColumnCell::Absent => changeset_value(item, col, fallback),
134 present => Ok(present),
135 }
136 }
137 }
138}
139
140fn changeset_value(
141 item: &rusqlite::session::ChangesetItem,
142 col: usize,
143 side: UpdateValue,
144) -> Result<ColumnCell, String> {
145 let value = match side {
146 UpdateValue::New => item.new_value(col),
147 UpdateValue::Old => item.old_value(col),
148 };
149 match value {
150 Ok(value) => Ok(ColumnCell::Present(value_ref_to_string(value))),
151 Err(rusqlite::Error::InvalidColumnIndex(_)) => Ok(ColumnCell::Absent),
152 Err(e) => Err(format!(
153 "changeset {side:?} value read failed for column {col}: {e}"
154 )),
155 }
156}
157
158pub(crate) fn value_ref_to_string(v: ValueRef<'_>) -> Option<String> {
174 match v {
175 ValueRef::Null => None,
176 ValueRef::Integer(i) => Some(i.to_string()),
177 ValueRef::Real(f) => Some(real_to_sqlite_text(f)),
178 ValueRef::Text(t) | ValueRef::Blob(t) => Some(String::from_utf8_lossy(t).into_owned()),
179 }
180}
181
182fn real_to_sqlite_text(f: f64) -> String {
189 if f.is_nan() {
190 return String::new();
191 }
192 if f.is_infinite() {
193 return if f < 0.0 {
194 "-Inf".to_string()
195 } else {
196 "Inf".to_string()
197 };
198 }
199 let s = f.to_string();
200 if s.contains('.') || s.contains('e') || s.contains('E') {
204 s
205 } else {
206 format!("{s}.0")
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
220 fn real_renders_as_a_faithful_float() {
221 let conn = rusqlite::Connection::open_in_memory().expect("open");
222 for &f in &[0.0_f64, 1.0, 1.5, -2.25, 0.1, 123456.789, 1.0e6, 100.0, 0.5] {
224 let sqlite_text: String = conn
225 .query_row("SELECT CAST(? AS TEXT)", [f], |r| r.get(0))
226 .expect("cast");
227 let ours = real_to_sqlite_text(f);
228 assert_eq!(
229 ours, sqlite_text,
230 "REAL {f} rendered {ours:?}, SQLite renders {sqlite_text:?}",
231 );
232 }
233
234 for &f in &[1.234567890123457_f64, 1.0e-7, 9_999_999_999_999.0, -42.0] {
237 let ours = real_to_sqlite_text(f);
238 assert!(
239 ours.contains('.') || ours.contains('e') || ours.contains('E'),
240 "REAL {f} rendered {ours:?} which doesn't read as a float",
241 );
242 assert_eq!(
243 ours.parse::<f64>().expect("parses back"),
244 f,
245 "REAL {f} rendered {ours:?} which doesn't round-trip",
246 );
247 }
248 }
249}