1use std::iter::Peekable;
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, CrsqlSeqRange},
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>, CrsqlSeqRange), 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 CrsqlSeqRange::new(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(), CrsqlSeqRange::new(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 use crate::dbsr;
265
266 #[test]
267 fn test_change_chunker() {
268 let mut chunker = ChunkedChanges::new(vec![].into_iter(), CrsqlSeq(0), CrsqlSeq(100), 50);
270
271 assert_eq!(chunker.next(), Some(Ok((vec![], dbsr!(0, 100)))));
272 assert_eq!(chunker.next(), None);
273
274 let changes: Vec<Change> = (0..100)
275 .map(|seq| Change {
276 seq: CrsqlSeq(seq),
277 ..Default::default()
278 })
279 .collect();
280
281 let mut chunker = ChunkedChanges::new(
283 vec![
284 Ok(changes[0].clone()),
285 Ok(changes[1].clone()),
286 Ok(changes[2].clone()),
287 ]
288 .into_iter(),
289 CrsqlSeq(0),
290 CrsqlSeq(100),
291 changes[0].estimated_byte_size() + changes[1].estimated_byte_size(),
292 );
293
294 assert_eq!(
295 chunker.next(),
296 Some(Ok((
297 vec![changes[0].clone(), changes[1].clone()],
298 dbsr!(0, 1)
299 )))
300 );
301 assert_eq!(
302 chunker.next(),
303 Some(Ok((vec![changes[2].clone()], dbsr!(2, 100))))
304 );
305 assert_eq!(chunker.next(), None);
306
307 let mut chunker = ChunkedChanges::new(
308 vec![Ok(changes[0].clone()), Ok(changes[1].clone())].into_iter(),
309 CrsqlSeq(0),
310 CrsqlSeq(0),
311 changes[0].estimated_byte_size(),
312 );
313
314 assert_eq!(
315 chunker.next(),
316 Some(Ok((vec![changes[0].clone()], dbsr!(0, 0))))
317 );
318 assert_eq!(chunker.next(), None);
319
320 let mut chunker = ChunkedChanges::new(
322 vec![Ok(changes[0].clone()), Ok(changes[2].clone())].into_iter(),
323 CrsqlSeq(0),
324 CrsqlSeq(100),
325 changes[0].estimated_byte_size() + changes[2].estimated_byte_size(),
326 );
327
328 assert_eq!(
329 chunker.next(),
330 Some(Ok((
331 vec![changes[0].clone(), changes[2].clone()],
332 dbsr!(0, 100)
333 )))
334 );
335
336 assert_eq!(chunker.next(), None);
337
338 let mut chunker = ChunkedChanges::new(
340 vec![
341 Ok(changes[2].clone()),
342 Ok(changes[4].clone()),
343 Ok(changes[7].clone()),
344 Ok(changes[8].clone()),
345 ]
346 .into_iter(),
347 CrsqlSeq(0),
348 CrsqlSeq(100),
349 100000, );
351
352 assert_eq!(
353 chunker.next(),
354 Some(Ok((
355 vec![
356 changes[2].clone(),
357 changes[4].clone(),
358 changes[7].clone(),
359 changes[8].clone()
360 ],
361 dbsr!(0, 100)
362 )))
363 );
364
365 assert_eq!(chunker.next(), None);
366
367 let mut chunker = ChunkedChanges::new(
369 vec![
370 Ok(changes[2].clone()),
371 Ok(changes[4].clone()),
372 Ok(changes[7].clone()),
373 Ok(changes[8].clone()),
374 ]
375 .into_iter(),
376 CrsqlSeq(0),
377 CrsqlSeq(10),
378 changes[2].estimated_byte_size() + changes[4].estimated_byte_size(),
379 );
380
381 assert_eq!(
382 chunker.next(),
383 Some(Ok((
384 vec![changes[2].clone(), changes[4].clone(),],
385 dbsr!(0, 4)
386 )))
387 );
388
389 assert_eq!(
390 chunker.next(),
391 Some(Ok((
392 vec![changes[7].clone(), changes[8].clone(),],
393 dbsr!(5, 10)
394 )))
395 );
396
397 assert_eq!(chunker.next(), None);
398 }
399}