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::{ChangesetPerTable, 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 self.estimated_column_byte_size() +
39 16
41 }
42
43 pub fn estimated_column_byte_size(&self) -> usize {
44 self.cid.len() + self.val.estimated_byte_size() +
45 8 +
47 8 +
49 8
51 }
52}
53
54pub fn row_to_change(row: &Row) -> Result<Change, rusqlite::Error> {
55 Ok(Change {
56 table: row.get(0)?,
57 pk: row.get(1)?,
58 cid: row.get(2)?,
59 val: row.get(3)?,
60 col_version: row.get(4)?,
61 db_version: row.get(5)?,
62 seq: row.get(6)?,
63 site_id: row.get(7)?,
64 cl: row.get(8)?,
65 })
66}
67
68pub struct ChunkedChanges<I: Iterator> {
69 iter: Peekable<I>,
70 changes: ChangesetPerTable,
71 last_pushed_seq: CrsqlSeq,
72 last_start_seq: CrsqlSeq,
73 last_seq: CrsqlSeq,
74 max_buf_size: usize,
75 buffered_size: usize,
76 done: bool,
77}
78
79impl<I> ChunkedChanges<I>
80where
81 I: Iterator,
82{
83 pub fn new(iter: I, start_seq: CrsqlSeq, last_seq: CrsqlSeq, max_buf_size: usize) -> Self {
84 Self {
85 iter: iter.peekable(),
86 changes: Default::default(),
87 last_pushed_seq: CrsqlSeq(0),
88 last_start_seq: start_seq,
89 last_seq,
90 max_buf_size,
91 buffered_size: 0,
92 done: false,
93 }
94 }
95
96 pub fn max_buf_size(&self) -> usize {
97 self.max_buf_size
98 }
99
100 pub fn set_max_buf_size(&mut self, size: usize) {
101 self.max_buf_size = size;
102 }
103}
104
105impl<I> Iterator for ChunkedChanges<I>
106where
107 I: Iterator<Item = rusqlite::Result<Change>>,
108{
109 type Item = Result<(ChangesetPerTable, CrsqlSeqRange), rusqlite::Error>;
110
111 fn next(&mut self) -> Option<Self::Item> {
112 if self.done {
114 return None;
115 }
116
117 let details = json!({});
118 assert_always!(
119 self.changes.is_empty(),
120 "iterator for ChunkedChanges still has changes when next() is called",
121 &details
122 );
123
124 self.buffered_size = 0;
126
127 loop {
128 trace!("chunking through the rows iterator");
129 match self.iter.next() {
130 Some(Ok(change)) => {
131 trace!("got change: {change:?}");
132
133 self.last_pushed_seq = change.seq;
134
135 let size = self.changes.insert(change);
136 self.buffered_size += size;
137
138 if self.last_pushed_seq == self.last_seq {
139 break;
141 }
142
143 if self.buffered_size >= self.max_buf_size {
144 let start_seq = self.last_start_seq;
146
147 if self.iter.peek().is_none() {
148 break;
150 }
151
152 self.last_start_seq = self.last_pushed_seq + 1;
154
155 return Some(Ok((
156 self.changes.drain(),
157 CrsqlSeqRange::new(start_seq, self.last_pushed_seq),
158 )));
159 }
160 }
161 None => {
162 trace!("no more changes to iterate on");
165 break;
166 }
167 Some(Err(e)) => return Some(Err(e)),
168 }
169 }
170
171 self.done = true;
172
173 Some(Ok((
175 self.changes.clone(), CrsqlSeqRange::new(self.last_start_seq, self.last_seq), )))
178 }
179}
180
181pub const MAX_CHANGES_BYTE_SIZE: usize = 8 * 1024;
182
183pub struct InsertChangesInfo {
184 pub db_version: CrsqlDbVersion,
185 pub last_seq: CrsqlSeq,
186 pub ts: Timestamp,
187 pub snap: VersionsSnapshot,
188}
189
190pub fn insert_local_changes(
191 agent: &Agent,
192 tx: &Connection,
193 book_writer: &mut tokio::sync::RwLockWriteGuard<'_, BookedVersions>,
194) -> Result<Option<InsertChangesInfo>, ChangeError> {
195 let actor_id = agent.actor_id();
196
197 let db_version: CrsqlDbVersion = tx
198 .prepare_cached("SELECT crsql_peek_next_db_version()")
199 .map_err(|source| ChangeError::Rusqlite {
200 source,
201 actor_id: Some(actor_id),
202 version: None,
203 })?
204 .query_row((), |row| row.get(0))
205 .map_err(|source| ChangeError::Rusqlite {
206 source,
207 actor_id: Some(actor_id),
208 version: None,
209 })?;
210
211 let version_info: (Option<CrsqlSeq>, Option<Timestamp>) = tx
212 .prepare_cached(
213 "SELECT MAX(seq), MAX(ts) FROM crsql_changes WHERE site_id = ? AND db_version = ?;",
214 )
215 .map_err(|source| ChangeError::Rusqlite {
216 source,
217 actor_id: Some(actor_id),
218 version: None,
219 })?
220 .query_row((agent.actor_id(), db_version), |row| {
221 Ok((row.get(0)?, row.get(1)?))
222 })
223 .map_err(|source| ChangeError::Rusqlite {
224 source,
225 actor_id: Some(actor_id),
226 version: None,
227 })?;
228
229 match version_info {
230 (None, None) => Ok(None),
231 (None, Some(ts)) => {
232 warn!("found db_version {db_version} without seq, last ts: {ts:?})");
233 Ok(None)
234 }
235 (Some(last_seq), ts) => {
236 let ts = ts.unwrap_or_else(|| {
237 warn!("found db_version {db_version} without seq, last ts: {ts:?}");
238 Timestamp::from(agent.clock().new_timestamp())
239 });
240
241 debug!("found db_version {db_version} (last seq: {last_seq}, last ts: {ts})");
242
243 let db_versions = db_version..=db_version;
244
245 let mut snap = book_writer.snapshot();
246 snap.insert_db(tx, [db_versions].into())
247 .map_err(|source| ChangeError::Rusqlite {
248 source,
249 actor_id: Some(actor_id),
250 version: Some(db_version),
251 })?;
252
253 Ok(Some(InsertChangesInfo {
254 db_version,
255 last_seq,
256 ts,
257 snap,
258 }))
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::dbsr;
267
268 #[test]
269 fn test_change_chunker() {
270 let mut chunker = ChunkedChanges::new(vec![].into_iter(), CrsqlSeq(0), CrsqlSeq(100), 50);
272
273 assert_eq!(
274 chunker.next(),
275 Some(Ok((ChangesetPerTable::default(), dbsr!(0, 100))))
276 );
277 assert_eq!(chunker.next(), None);
278
279 let changes: Vec<Change> = (0..100)
280 .map(|seq| Change {
281 seq: CrsqlSeq(seq),
282 ..Default::default()
283 })
284 .collect();
285
286 let (changeset, size) =
287 mapped_changeset_from_changes(vec![changes[0].clone(), changes[1].clone()]);
288 let mut chunker = ChunkedChanges::new(
290 vec![
291 Ok(changes[0].clone()),
292 Ok(changes[1].clone()),
293 Ok(changes[2].clone()),
294 ]
295 .into_iter(),
296 CrsqlSeq(0),
297 CrsqlSeq(100),
298 size,
299 );
300
301 assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(0, 1)))));
302
303 let (changeset, _) = mapped_changeset_from_changes(vec![changes[2].clone()]);
304 assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(2, 100)))));
305 assert_eq!(chunker.next(), None);
306
307 let (changeset, size) = mapped_changeset_from_changes(vec![changes[0].clone()]);
308 let mut chunker = ChunkedChanges::new(
309 vec![Ok(changes[0].clone()), Ok(changes[1].clone())].into_iter(),
310 CrsqlSeq(0),
311 CrsqlSeq(0),
312 size,
313 );
314
315 assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(0, 0)))));
316 assert_eq!(chunker.next(), None);
317
318 let (changeset, size) =
319 mapped_changeset_from_changes(vec![changes[0].clone(), changes[2].clone()]);
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 size,
326 );
327
328 assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(0, 100)))));
329
330 assert_eq!(chunker.next(), None);
331
332 let (changeset, _) = mapped_changeset_from_changes(vec![
334 changes[2].clone(),
335 changes[4].clone(),
336 changes[7].clone(),
337 changes[8].clone(),
338 ]);
339 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!(chunker.next(), Some(Ok((changeset, dbsr!(0, 100)))));
353
354 assert_eq!(chunker.next(), None);
355
356 let (changeset, size) =
358 mapped_changeset_from_changes(vec![changes[2].clone(), changes[4].clone()]);
359 let mut chunker = ChunkedChanges::new(
360 vec![
361 Ok(changes[2].clone()),
362 Ok(changes[4].clone()),
363 Ok(changes[7].clone()),
364 Ok(changes[8].clone()),
365 ]
366 .into_iter(),
367 CrsqlSeq(0),
368 CrsqlSeq(10),
369 size,
370 );
371
372 assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(0, 4)))));
373
374 let (changeset, _) =
375 mapped_changeset_from_changes(vec![changes[7].clone(), changes[8].clone()]);
376 assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(5, 10)))));
377
378 assert_eq!(chunker.next(), None);
379 }
380
381 fn mapped_changeset_from_changes(changes: Vec<Change>) -> (ChangesetPerTable, usize) {
382 let mut changeset = ChangesetPerTable::default();
383 let mut size = 0;
384 for change in changes {
385 size += changeset.insert(change);
386 }
387 (changeset, size)
388 }
389}