1use std::{iter::Peekable, ops::RangeInclusive};
2
3pub use crate::api::SqliteValue;
4use crate::api::{ColumnName, TableName};
5use antithesis_sdk::assert_always;
6use rusqlite::{Connection, Row};
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9use speedy::{Readable, Writable};
10use tracing::{debug, trace, warn};
11
12use crate::{
13 agent::{Agent, BookedVersions, ChangeError, VersionsSnapshot},
14 base::{CrsqlDbVersion, CrsqlSeq},
15 broadcast::Timestamp,
16};
17
18#[derive(Debug, Default, Clone, Serialize, Deserialize, Readable, Writable, PartialEq)]
19pub struct Change {
20 pub table: TableName,
21 pub pk: Vec<u8>,
22 pub cid: ColumnName,
23 pub val: SqliteValue,
24 pub col_version: i64,
25 pub db_version: CrsqlDbVersion,
26 pub seq: CrsqlSeq,
27 pub site_id: [u8; 16],
28 pub cl: i64,
29}
30
31impl Change {
32 pub fn estimated_byte_size(&self) -> usize {
35 self.table.len() + self.pk.len() + self.cid.len() + self.val.estimated_byte_size() +
36 8 +
38 8 +
40 8 +
42 16 +
44 8 +
46 8
48 }
49}
50
51pub fn row_to_change(row: &Row) -> Result<Change, rusqlite::Error> {
52 Ok(Change {
53 table: row.get(0)?,
54 pk: row.get(1)?,
55 cid: row.get(2)?,
56 val: row.get(3)?,
57 col_version: row.get(4)?,
58 db_version: row.get(5)?,
59 seq: row.get(6)?,
60 site_id: row.get(7)?,
61 cl: row.get(8)?,
62 })
63}
64
65pub struct ChunkedChanges<I: Iterator> {
66 iter: Peekable<I>,
67 changes: Vec<Change>,
68 last_pushed_seq: CrsqlSeq,
69 last_start_seq: CrsqlSeq,
70 last_seq: CrsqlSeq,
71 max_buf_size: usize,
72 buffered_size: usize,
73 done: bool,
74}
75
76impl<I> ChunkedChanges<I>
77where
78 I: Iterator,
79{
80 pub fn new(iter: I, start_seq: CrsqlSeq, last_seq: CrsqlSeq, max_buf_size: usize) -> Self {
81 Self {
82 iter: iter.peekable(),
83 changes: vec![],
84 last_pushed_seq: CrsqlSeq(0),
85 last_start_seq: start_seq,
86 last_seq,
87 max_buf_size,
88 buffered_size: 0,
89 done: false,
90 }
91 }
92
93 pub fn max_buf_size(&self) -> usize {
94 self.max_buf_size
95 }
96
97 pub fn set_max_buf_size(&mut self, size: usize) {
98 self.max_buf_size = size;
99 }
100}
101
102impl<I> Iterator for ChunkedChanges<I>
103where
104 I: Iterator<Item = rusqlite::Result<Change>>,
105{
106 type Item = Result<(Vec<Change>, RangeInclusive<CrsqlSeq>), rusqlite::Error>;
107
108 fn next(&mut self) -> Option<Self::Item> {
109 if self.done {
111 return None;
112 }
113
114 let details = json!({});
115 assert_always!(
116 self.changes.is_empty(),
117 "iterator for ChunkedChanges still has changes when next() is called",
118 &details
119 );
120
121 self.buffered_size = 0;
123
124 loop {
125 trace!("chunking through the rows iterator");
126 match self.iter.next() {
127 Some(Ok(change)) => {
128 trace!("got change: {change:?}");
129
130 self.last_pushed_seq = change.seq;
131
132 self.buffered_size += change.estimated_byte_size();
133
134 self.changes.push(change);
135
136 if self.last_pushed_seq == self.last_seq {
137 break;
139 }
140
141 if self.buffered_size >= self.max_buf_size {
142 let start_seq = self.last_start_seq;
144
145 if self.iter.peek().is_none() {
146 break;
148 }
149
150 self.last_start_seq = self.last_pushed_seq + 1;
152
153 return Some(Ok((
154 self.changes.drain(..).collect(),
155 start_seq..=self.last_pushed_seq,
156 )));
157 }
158 }
159 None => {
160 trace!("no more changes to iterate on");
163 break;
164 }
165 Some(Err(e)) => return Some(Err(e)),
166 }
167 }
168
169 self.done = true;
170
171 Some(Ok((
173 self.changes.clone(), self.last_start_seq..=self.last_seq, )))
176 }
177}
178
179pub const MAX_CHANGES_BYTE_SIZE: usize = 8 * 1024;
180
181pub struct InsertChangesInfo {
182 pub db_version: CrsqlDbVersion,
183 pub last_seq: CrsqlSeq,
184 pub ts: Timestamp,
185 pub snap: VersionsSnapshot,
186}
187
188pub fn insert_local_changes(
189 agent: &Agent,
190 tx: &Connection,
191 book_writer: &mut tokio::sync::RwLockWriteGuard<'_, BookedVersions>,
192) -> Result<Option<InsertChangesInfo>, ChangeError> {
193 let actor_id = agent.actor_id();
194
195 let db_version: CrsqlDbVersion = tx
196 .prepare_cached("SELECT crsql_peek_next_db_version()")
197 .map_err(|source| ChangeError::Rusqlite {
198 source,
199 actor_id: Some(actor_id),
200 version: None,
201 })?
202 .query_row((), |row| row.get(0))
203 .map_err(|source| ChangeError::Rusqlite {
204 source,
205 actor_id: Some(actor_id),
206 version: None,
207 })?;
208
209 let version_info: (Option<CrsqlSeq>, Option<Timestamp>) = tx
210 .prepare_cached(
211 "SELECT MAX(seq), MAX(ts) FROM crsql_changes WHERE site_id = ? AND db_version = ?;",
212 )
213 .map_err(|source| ChangeError::Rusqlite {
214 source,
215 actor_id: Some(actor_id),
216 version: None,
217 })?
218 .query_row((agent.actor_id(), db_version), |row| {
219 Ok((row.get(0)?, row.get(1)?))
220 })
221 .map_err(|source| ChangeError::Rusqlite {
222 source,
223 actor_id: Some(actor_id),
224 version: None,
225 })?;
226
227 match version_info {
228 (None, None) => Ok(None),
229 (None, Some(ts)) => {
230 warn!("found db_version {db_version} without seq, last ts: {ts:?})");
231 Ok(None)
232 }
233 (Some(last_seq), ts) => {
234 let ts = ts.unwrap_or_else(|| {
235 warn!("found db_version {db_version} without seq, last ts: {ts:?}");
236 Timestamp::from(agent.clock().new_timestamp())
237 });
238
239 debug!("found db_version {db_version} (last seq: {last_seq}, last ts: {ts})");
240
241 let db_versions = db_version..=db_version;
242
243 let mut snap = book_writer.snapshot();
244 snap.insert_db(tx, [db_versions].into())
245 .map_err(|source| ChangeError::Rusqlite {
246 source,
247 actor_id: Some(actor_id),
248 version: Some(db_version),
249 })?;
250
251 Ok(Some(InsertChangesInfo {
252 db_version,
253 last_seq,
254 ts,
255 snap,
256 }))
257 }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_change_chunker() {
267 let mut chunker = ChunkedChanges::new(vec![].into_iter(), CrsqlSeq(0), CrsqlSeq(100), 50);
269
270 assert_eq!(
271 chunker.next(),
272 Some(Ok((vec![], CrsqlSeq(0)..=CrsqlSeq(100))))
273 );
274 assert_eq!(chunker.next(), None);
275
276 let changes: Vec<Change> = (CrsqlSeq(0)..CrsqlSeq(100))
277 .map(|seq| Change {
278 seq,
279 ..Default::default()
280 })
281 .collect();
282
283 let mut chunker = ChunkedChanges::new(
285 vec![
286 Ok(changes[0].clone()),
287 Ok(changes[1].clone()),
288 Ok(changes[2].clone()),
289 ]
290 .into_iter(),
291 CrsqlSeq(0),
292 CrsqlSeq(100),
293 changes[0].estimated_byte_size() + changes[1].estimated_byte_size(),
294 );
295
296 assert_eq!(
297 chunker.next(),
298 Some(Ok((
299 vec![changes[0].clone(), changes[1].clone()],
300 CrsqlSeq(0)..=CrsqlSeq(1)
301 )))
302 );
303 assert_eq!(
304 chunker.next(),
305 Some(Ok((vec![changes[2].clone()], CrsqlSeq(2)..=CrsqlSeq(100))))
306 );
307 assert_eq!(chunker.next(), None);
308
309 let mut chunker = ChunkedChanges::new(
310 vec![Ok(changes[0].clone()), Ok(changes[1].clone())].into_iter(),
311 CrsqlSeq(0),
312 CrsqlSeq(0),
313 changes[0].estimated_byte_size(),
314 );
315
316 assert_eq!(
317 chunker.next(),
318 Some(Ok((vec![changes[0].clone()], CrsqlSeq(0)..=CrsqlSeq(0))))
319 );
320 assert_eq!(chunker.next(), None);
321
322 let mut chunker = ChunkedChanges::new(
324 vec![Ok(changes[0].clone()), Ok(changes[2].clone())].into_iter(),
325 CrsqlSeq(0),
326 CrsqlSeq(100),
327 changes[0].estimated_byte_size() + changes[2].estimated_byte_size(),
328 );
329
330 assert_eq!(
331 chunker.next(),
332 Some(Ok((
333 vec![changes[0].clone(), changes[2].clone()],
334 CrsqlSeq(0)..=CrsqlSeq(100)
335 )))
336 );
337
338 assert_eq!(chunker.next(), None);
339
340 let mut chunker = ChunkedChanges::new(
342 vec![
343 Ok(changes[2].clone()),
344 Ok(changes[4].clone()),
345 Ok(changes[7].clone()),
346 Ok(changes[8].clone()),
347 ]
348 .into_iter(),
349 CrsqlSeq(0),
350 CrsqlSeq(100),
351 100000, );
353
354 assert_eq!(
355 chunker.next(),
356 Some(Ok((
357 vec![
358 changes[2].clone(),
359 changes[4].clone(),
360 changes[7].clone(),
361 changes[8].clone()
362 ],
363 CrsqlSeq(0)..=CrsqlSeq(100)
364 )))
365 );
366
367 assert_eq!(chunker.next(), None);
368
369 let mut chunker = ChunkedChanges::new(
371 vec![
372 Ok(changes[2].clone()),
373 Ok(changes[4].clone()),
374 Ok(changes[7].clone()),
375 Ok(changes[8].clone()),
376 ]
377 .into_iter(),
378 CrsqlSeq(0),
379 CrsqlSeq(10),
380 changes[2].estimated_byte_size() + changes[4].estimated_byte_size(),
381 );
382
383 assert_eq!(
384 chunker.next(),
385 Some(Ok((
386 vec![changes[2].clone(), changes[4].clone(),],
387 CrsqlSeq(0)..=CrsqlSeq(4)
388 )))
389 );
390
391 assert_eq!(
392 chunker.next(),
393 Some(Ok((
394 vec![changes[7].clone(), changes[8].clone(),],
395 CrsqlSeq(5)..=CrsqlSeq(10)
396 )))
397 );
398
399 assert_eq!(chunker.next(), None);
400 }
401}