1use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
2use std::sync::Arc;
3
4use arrow_array::{Array, ArrayRef, RecordBatch};
5use arrow_schema::{Field, Schema, SchemaRef};
6use rustc_hash::FxHashMap;
7
8use crate::dictionary::interleave_column;
9use crate::group::{GroupState, Groups};
10use crate::key::{key_reader, KeyFn, KeyIndex, KeyRef};
11use crate::view::{Rows, ViewState};
12use crate::{DataError, GroupSpec, RowId, Scalar, ViewSpec};
13
14const REBUILD_RATIO: usize = 8;
16const MIN_DEAD_ROWS: usize = 4_096;
19const MAX_BATCHES: usize = 4_096;
21const COMPACT_ROWS: usize = 65_536;
23const MIN_SPLIT_ROWS: usize = 128;
26const MAX_ROWS: usize = 1 << 32;
28
29static NEXT_SERIAL: AtomicU64 = AtomicU64::new(1);
31
32fn next_serial() -> u64 {
33 NEXT_SERIAL.fetch_add(1, AtomicOrdering::Relaxed)
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub struct RowRef {
42 pub batch: usize,
44 pub row: usize,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub(crate) struct Slot {
51 batch: u32,
52 row: u32,
53}
54
55impl Slot {
56 pub(crate) fn row_ref(self) -> RowRef {
57 RowRef {
58 batch: self.batch as usize,
59 row: self.row as usize,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
66#[non_exhaustive]
67pub struct Changes {
68 pub inserted: usize,
70 pub updated: usize,
72 pub deleted: usize,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78#[non_exhaustive]
79pub struct TableOptions {
80 pub auto_compact: bool,
86}
87
88impl Default for TableOptions {
89 fn default() -> Self {
90 Self { auto_compact: true }
91 }
92}
93
94impl TableOptions {
95 #[must_use]
97 pub fn with_auto_compact(mut self, auto_compact: bool) -> Self {
98 self.auto_compact = auto_compact;
99 self
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub struct ViewId {
107 serial: u64,
109 index: usize,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
115pub struct GroupsId {
116 serial: u64,
117 index: usize,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129pub struct RecordId(u64);
130
131impl RecordId {
132 fn new(id: RowId, reuse: u32) -> Self {
133 Self(u64::from(reuse) << 32 | u64::from(id))
134 }
135
136 #[must_use]
138 pub const fn to_bits(self) -> u64 {
139 self.0
140 }
141
142 #[must_use]
145 pub const fn from_bits(bits: u64) -> Self {
146 Self(bits)
147 }
148
149 fn parts(self) -> (RowId, u32) {
151 (self.0 as u32, (self.0 >> 32) as u32)
153 }
154}
155
156type Change<'a> = (RowId, Option<Slot>, Slot, Option<KeyRef<'a>>);
159
160#[derive(Debug)]
178pub struct LiveTable {
179 instance: u64,
181 options: TableOptions,
182 schema: SchemaRef,
183 key_column: usize,
184 key_read: KeyFn,
185 keys: KeyIndex,
186 batches: Vec<RecordBatch>,
187 slots: Vec<Option<Slot>>,
189 free: Vec<RowId>,
190 reuses: Vec<u32>,
193 dead: usize,
195 views: Vec<Option<(u64, ViewState)>>,
197 groups: Vec<Option<(u64, GroupState)>>,
198 generation: u64,
199 compaction_error: Option<DataError>,
201 retry_at_dead: usize,
203 compacted_batches: usize,
205}
206
207impl Clone for LiveTable {
208 fn clone(&self) -> Self {
209 Self {
210 instance: next_serial(),
211 options: self.options,
212 schema: Arc::clone(&self.schema),
213 key_column: self.key_column,
214 key_read: self.key_read,
215 keys: self.keys.clone(),
216 batches: self.batches.clone(),
217 slots: self.slots.clone(),
218 free: self.free.clone(),
219 reuses: self.reuses.clone(),
220 dead: self.dead,
221 views: self.views.clone(),
222 groups: self.groups.clone(),
223 generation: self.generation,
224 compaction_error: self.compaction_error.clone(),
225 retry_at_dead: self.retry_at_dead,
226 compacted_batches: self.compacted_batches,
227 }
228 }
229}
230
231impl LiveTable {
232 pub fn new(schema: SchemaRef, key: &str) -> Result<Self, DataError> {
238 Self::with_options(schema, key, TableOptions::default())
239 }
240
241 pub fn with_options(
246 schema: SchemaRef,
247 key: &str,
248 options: TableOptions,
249 ) -> Result<Self, DataError> {
250 let (key_column, field) = schema
251 .column_with_name(key)
252 .ok_or_else(|| DataError::UnknownColumn(key.to_owned()))?;
253 let unsupported = || DataError::UnsupportedKey(key.to_owned());
254 let key_read = key_reader(field.data_type()).ok_or_else(unsupported)?;
255 let keys = KeyIndex::for_type(field.data_type()).ok_or_else(unsupported)?;
256 Ok(Self {
257 instance: next_serial(),
258 options,
259 schema: nullable(&schema),
260 key_column,
261 key_read,
262 keys,
263 batches: Vec::new(),
264 slots: Vec::new(),
265 free: Vec::new(),
266 reuses: Vec::new(),
267 dead: 0,
268 views: Vec::new(),
269 groups: Vec::new(),
270 generation: 0,
271 compaction_error: None,
272 retry_at_dead: MIN_DEAD_ROWS,
273 compacted_batches: 0,
274 })
275 }
276
277 pub fn schema(&self) -> &SchemaRef {
279 &self.schema
280 }
281
282 pub fn instance(&self) -> u64 {
286 self.instance
287 }
288
289 pub fn len(&self) -> usize {
291 self.keys.len()
292 }
293
294 pub fn is_empty(&self) -> bool {
296 self.len() == 0
297 }
298
299 pub fn batches(&self) -> &[RecordBatch] {
303 &self.batches
304 }
305
306 pub(crate) fn live_rows(&self) -> impl Iterator<Item = RowRef> + '_ {
308 self.slots.iter().flatten().map(|slot| slot.row_ref())
309 }
310
311 pub fn generation(&self) -> u64 {
314 self.generation
315 }
316
317 pub fn compaction_error(&self) -> Option<&DataError> {
321 self.compaction_error.as_ref()
322 }
323
324 pub fn find(&self, key: &Scalar) -> Option<RowRef> {
326 let id = self.key_id(key)?;
327 self.slots[id as usize].map(Slot::row_ref)
328 }
329
330 pub fn record_of_key(&self, key: &Scalar) -> Option<RecordId> {
332 let id = self.key_id(key)?;
333 Some(RecordId::new(id, reuse(&self.reuses, id)))
334 }
335
336 pub fn row_of(&self, record: RecordId) -> Option<RowRef> {
338 live_slot(&self.slots, &self.reuses, record).map(|(_, slot)| slot.row_ref())
339 }
340
341 fn key_id(&self, key: &Scalar) -> Option<RowId> {
343 let key = match (key, &self.keys) {
344 (Scalar::Int(k), KeyIndex::Int(_)) => KeyRef::Int(*k),
345 (Scalar::Int(k), KeyIndex::UInt(_)) => KeyRef::UInt(u64::try_from(*k).ok()?),
346 (Scalar::UInt(k), KeyIndex::UInt(_)) => KeyRef::UInt(*k),
347 (Scalar::UInt(k), KeyIndex::Int(_)) => KeyRef::Int(i64::try_from(*k).ok()?),
348 (Scalar::Str(k), KeyIndex::Str(_)) => KeyRef::Str(k),
349 _ => return None,
350 };
351 self.keys.get(key)
352 }
353
354 pub fn upsert(&mut self, batch: RecordBatch) -> Result<Changes, DataError> {
362 self.check(&batch)?;
363 let latest = self.latest_rows(&batch)?;
364 if latest.is_empty() {
365 return Ok(Changes::default());
366 }
367 let plan = self.plan(&batch, &latest)?;
368 let updated = plan.iter().filter(|c| c.1.is_some()).count();
369 let changes = Changes {
370 inserted: plan.len() - updated,
371 updated,
372 deleted: 0,
373 };
374 let rebuild = plan.len().saturating_mul(REBUILD_RATIO) > self.len() + changes.inserted;
375
376 self.batches.push(batch.clone());
377 if !rebuild {
378 self.drop_from_views(plan.iter().filter_map(|c| Some((c.0, c.1?))));
379 }
380 for &(id, old, at, key) in &plan {
381 self.slots[id as usize] = Some(at);
382 self.dead += usize::from(old.is_some());
383 if let Some(key) = key {
384 self.keys.insert(key, id);
385 }
386 }
387 self.dead += batch.num_rows() - plan.len();
389 self.add_to_views(rebuild, plan.iter().map(|c| (c.0, c.2)));
390 self.compact_if_due();
391 Ok(changes)
392 }
393
394 pub fn delete(&mut self, keys: &dyn Array) -> Result<Changes, DataError> {
400 let read = key_reader(keys.data_type())
401 .filter(|_| self.keys.same_kind(keys.data_type()))
402 .ok_or(DataError::KeyType)?;
403 let mut deleted = 0;
404 for row in 0..keys.len() {
405 let Some(id) = read(keys, row).and_then(|key| self.keys.remove(key)) else {
406 continue;
407 };
408 if let Some(slot) = self.slots[id as usize] {
409 self.drop_from_views(std::iter::once((id, slot)));
410 }
411 self.slots[id as usize] = None;
412 self.free.push(id);
413 if self.reuses.len() <= id as usize {
414 self.reuses.resize(id as usize + 1, 0);
415 }
416 self.reuses[id as usize] = self.reuses[id as usize].wrapping_add(1);
418 self.dead += 1;
419 deleted += 1;
420 }
421 self.compact_if_due();
422 Ok(Changes {
423 deleted,
424 ..Changes::default()
425 })
426 }
427
428 pub fn compact(&mut self) -> Result<(), DataError> {
438 let live: Vec<(RowId, Slot)> = self
439 .slots
440 .iter()
441 .enumerate()
442 .filter_map(|(id, slot)| Some((RowId::try_from(id).ok()?, (*slot)?)))
443 .collect();
444 let mut compacted = Vec::new();
445 for chunk in live.chunks(COMPACT_ROWS) {
446 self.gather_into(chunk, &mut compacted)?;
447 }
448 let places = compacted.iter().enumerate().flat_map(|(batch, rows)| {
450 (0..rows.num_rows()).map(move |row| Slot {
452 batch: batch as u32,
453 row: row as u32,
454 })
455 });
456 for (&(id, _), slot) in live.iter().zip(places) {
457 self.slots[id as usize] = Some(slot);
458 }
459 self.compacted_batches = compacted.len();
460 self.batches = compacted;
461 self.dead = 0;
462 self.generation += 1;
463 self.compaction_error = None;
464 self.retry_at_dead = MIN_DEAD_ROWS;
465 Ok(())
466 }
467
468 pub fn add_view(&mut self, spec: ViewSpec) -> Result<ViewId, DataError> {
474 let mut view = ViewState::compile(&spec, &self.schema)?;
475 view.rebuild(Rows {
476 batches: &self.batches,
477 slots: &self.slots,
478 });
479 let (serial, index) = store(&mut self.views, view);
480 Ok(ViewId { serial, index })
481 }
482
483 pub fn remove_view(&mut self, id: ViewId) -> bool {
486 take(&mut self.views, id.index, id.serial)
487 }
488
489 pub fn view(&self, id: ViewId) -> Option<View<'_>> {
492 let state = find(&self.views, id.index, id.serial)?;
493 Some(View {
494 state,
495 batches: &self.batches,
496 slots: &self.slots,
497 reuses: &self.reuses,
498 })
499 }
500
501 pub fn add_groups(&mut self, spec: GroupSpec) -> Result<GroupsId, DataError> {
509 let mut groups = GroupState::compile(&spec, &self.schema)?;
510 groups.rebuild(Rows {
511 batches: &self.batches,
512 slots: &self.slots,
513 });
514 let (serial, index) = store(&mut self.groups, groups);
515 Ok(GroupsId { serial, index })
516 }
517
518 pub fn remove_groups(&mut self, id: GroupsId) -> bool {
521 take(&mut self.groups, id.index, id.serial)
522 }
523
524 pub fn groups(&self, id: GroupsId) -> Option<Groups<'_>> {
526 let state = find(&self.groups, id.index, id.serial)?;
527 Some(Groups {
528 state,
529 rows: Rows {
530 batches: &self.batches,
531 slots: &self.slots,
532 },
533 })
534 }
535
536 fn check(&self, batch: &RecordBatch) -> Result<(), DataError> {
537 let ours = self.schema.fields();
538 let theirs = batch.schema_ref().fields();
539 if ours.len() != theirs.len() {
540 return Err(DataError::ColumnCount {
541 expected: ours.len(),
542 found: theirs.len(),
543 });
544 }
545 match ours
546 .iter()
547 .zip(theirs.iter())
548 .position(|(a, b)| a.name() != b.name() || a.data_type() != b.data_type())
549 {
550 Some(column) => Err(DataError::ColumnMismatch { column }),
551 None => Ok(()),
552 }
553 }
554
555 fn latest_rows<'b>(
557 &self,
558 batch: &'b RecordBatch,
559 ) -> Result<Vec<(usize, KeyRef<'b>)>, DataError> {
560 let keys = batch.column(self.key_column).as_ref();
561 let mut last: FxHashMap<KeyRef<'b>, usize> =
562 FxHashMap::with_capacity_and_hasher(batch.num_rows(), Default::default());
563 for row in 0..batch.num_rows() {
564 let key = (self.key_read)(keys, row).ok_or(DataError::NullKey { row })?;
565 last.insert(key, row);
566 }
567 let mut rows: Vec<(usize, KeyRef<'b>)> = last.into_iter().map(|(k, r)| (r, k)).collect();
568 rows.sort_unstable_by_key(|&(row, _)| row);
569 Ok(rows)
570 }
571
572 fn plan<'b>(
575 &mut self,
576 batch: &RecordBatch,
577 latest: &[(usize, KeyRef<'b>)],
578 ) -> Result<Vec<Change<'b>>, DataError> {
579 let segment = u32::try_from(self.batches.len()).map_err(|_| DataError::TooManyRows)?;
580 u32::try_from(batch.num_rows()).map_err(|_| DataError::TooManyRows)?;
581 let new_keys = latest
582 .iter()
583 .filter(|&&(_, key)| self.keys.get(key).is_none())
584 .count();
585 if new_keys > self.free.len() + (MAX_ROWS - self.slots.len()) {
586 return Err(DataError::TooManyRows);
587 }
588 let mut plan = Vec::with_capacity(latest.len());
589 for &(row, key) in latest {
590 let at = Slot {
592 batch: segment,
593 row: row as u32,
594 };
595 plan.push(match self.keys.get(key) {
596 Some(id) => (id, self.slots[id as usize], at, None),
597 None => (self.allocate_id(), None, at, Some(key)),
598 });
599 }
600 Ok(plan)
601 }
602
603 fn allocate_id(&mut self) -> RowId {
605 if let Some(id) = self.free.pop() {
606 return id;
607 }
608 self.slots.push(None);
609 (self.slots.len() - 1) as RowId
611 }
612
613 fn gather_into(
617 &self,
618 chunk: &[(RowId, Slot)],
619 out: &mut Vec<RecordBatch>,
620 ) -> Result<(), DataError> {
621 match self.gather(chunk) {
622 Ok(batch) => {
623 out.push(batch);
624 Ok(())
625 }
626 Err(_) if chunk.len() > MIN_SPLIT_ROWS => {
627 let (first, second) = chunk.split_at(chunk.len() / 2);
628 self.gather_into(first, out)?;
629 self.gather_into(second, out)
630 }
631 Err(error) => Err(error),
632 }
633 }
634
635 fn gather(&self, chunk: &[(RowId, Slot)]) -> Result<RecordBatch, DataError> {
637 let mut position: FxHashMap<usize, usize> = FxHashMap::default();
640 let mut sources = Vec::new();
641 let indices: Vec<(usize, usize)> = chunk
642 .iter()
643 .map(|(_, slot)| {
644 let batch = slot.batch as usize;
645 let at = *position.entry(batch).or_insert_with(|| {
646 sources.push(batch);
647 sources.len() - 1
648 });
649 (at, slot.row as usize)
650 })
651 .collect();
652 let columns = (0..self.schema.fields().len())
653 .map(|column| {
654 let arrays: Vec<&dyn Array> = sources
655 .iter()
656 .map(|&batch| self.batches[batch].column(column).as_ref())
657 .collect();
658 interleave_column(&arrays, &indices)
659 })
660 .collect::<Result<Vec<ArrayRef>, _>>()
661 .map_err(DataError::arrow)?;
662 RecordBatch::try_new(Arc::clone(&self.schema), columns).map_err(DataError::arrow)
663 }
664
665 fn drop_from_views(&mut self, rows: impl Iterator<Item = (RowId, Slot)> + Clone) {
668 let source = Rows {
669 batches: &self.batches,
670 slots: &self.slots,
671 };
672 for (_, view) in self.views.iter_mut().flatten() {
673 for (id, slot) in rows.clone() {
674 view.remove(source, id, slot.row_ref());
675 }
676 }
677 for (_, groups) in self.groups.iter_mut().flatten() {
678 for (id, slot) in rows.clone() {
679 groups.remove(source, id, slot.row_ref());
680 }
681 }
682 }
683
684 fn add_to_views(&mut self, rebuild: bool, rows: impl Iterator<Item = (RowId, Slot)> + Clone) {
686 let source = Rows {
687 batches: &self.batches,
688 slots: &self.slots,
689 };
690 for (_, view) in self.views.iter_mut().flatten() {
691 if rebuild {
692 view.rebuild(source);
693 } else {
694 for (id, slot) in rows.clone() {
695 view.insert(source, id, slot.row_ref());
696 }
697 }
698 }
699 for (_, groups) in self.groups.iter_mut().flatten() {
700 if rebuild {
701 groups.rebuild(source);
702 } else {
703 for (id, slot) in rows.clone() {
704 groups.insert(source, id, slot.row_ref());
705 }
706 }
707 }
708 }
709
710 fn compact_if_due(&mut self) {
713 if !self.options.auto_compact {
714 return;
715 }
716 let dead_majority = self.dead >= self.retry_at_dead && self.dead > self.len();
717 let added = self.batches.len().saturating_sub(self.compacted_batches);
720 let too_many_batches = added > MAX_BATCHES && self.compaction_error.is_none();
721 if !(dead_majority || too_many_batches) {
722 return;
723 }
724 if let Err(error) = self.compact() {
725 self.compaction_error = Some(error);
726 self.retry_at_dead = self.dead.saturating_mul(2);
727 }
728 }
729}
730
731fn find<T>(entries: &[Option<(u64, T)>], index: usize, serial: u64) -> Option<&T> {
733 match entries.get(index)? {
734 Some((s, entry)) if *s == serial => Some(entry),
735 _ => None,
736 }
737}
738
739fn store<T>(entries: &mut Vec<Option<(u64, T)>>, entry: T) -> (u64, usize) {
742 let serial = next_serial();
743 match entries.iter().position(Option::is_none) {
744 Some(index) => {
745 entries[index] = Some((serial, entry));
746 (serial, index)
747 }
748 None => {
749 entries.push(Some((serial, entry)));
750 (serial, entries.len() - 1)
751 }
752 }
753}
754
755fn take<T>(entries: &mut [Option<(u64, T)>], index: usize, serial: u64) -> bool {
757 entries
758 .get_mut(index)
759 .and_then(|entry| entry.take_if(|(s, _)| *s == serial))
760 .is_some()
761}
762
763fn nullable(schema: &Schema) -> SchemaRef {
766 let fields: Vec<Field> = schema
767 .fields()
768 .iter()
769 .map(|f| f.as_ref().clone().with_nullable(true))
770 .collect();
771 Arc::new(Schema::new_with_metadata(fields, schema.metadata().clone()))
772}
773
774#[derive(Debug, Clone, Copy)]
776pub struct View<'a> {
777 state: &'a ViewState,
778 batches: &'a [RecordBatch],
779 slots: &'a [Option<Slot>],
780 reuses: &'a [u32],
781}
782
783impl View<'_> {
784 pub fn len(&self) -> usize {
786 self.state.len()
787 }
788
789 pub fn is_empty(&self) -> bool {
791 self.len() == 0
792 }
793
794 pub fn row(&self, index: usize) -> Option<RowRef> {
796 let id = self.state.get(index)?;
797 self.slots
798 .get(id as usize)
799 .copied()
800 .flatten()
801 .map(Slot::row_ref)
802 }
803
804 pub fn record(&self, index: usize) -> Option<RecordId> {
806 let id = self.state.get(index)?;
807 Some(RecordId::new(id, reuse(self.reuses, id)))
808 }
809
810 pub fn position(&self, record: RecordId) -> Option<usize> {
813 let (id, _) = live_slot(self.slots, self.reuses, record)?;
814 let rows = Rows {
815 batches: self.batches,
816 slots: self.slots,
817 };
818 self.state.position(rows, id)
819 }
820}
821
822fn reuse(reuses: &[u32], id: RowId) -> u32 {
824 reuses.get(id as usize).copied().unwrap_or(0)
825}
826
827fn live_slot(slots: &[Option<Slot>], reuses: &[u32], record: RecordId) -> Option<(RowId, Slot)> {
829 let (id, reused) = record.parts();
830 let slot = (*slots.get(id as usize)?)?;
831 (reuse(reuses, id) == reused).then_some((id, slot))
832}
833
834#[cfg(test)]
835mod tests {
836 use std::sync::Arc;
837
838 use arrow_array::{ArrayRef, Int64Array};
839
840 use super::*;
841
842 fn ids(keys: Vec<i64>) -> RecordBatch {
843 RecordBatch::try_from_iter([("id", Arc::new(Int64Array::from(keys)) as ArrayRef)]).unwrap()
844 }
845
846 #[test]
847 fn replacing_a_view_again_and_again_keeps_one_place() {
848 let mut table = LiveTable::new(ids(vec![]).schema(), "id").unwrap();
849 let mut view = table.add_view(ViewSpec::new()).unwrap();
850 for _ in 0..100 {
851 let next = table.add_view(ViewSpec::new()).unwrap();
852 table.remove_view(view);
853 view = next;
854 }
855 assert!(table.views.len() <= 2, "{} places", table.views.len());
856 }
857
858 #[test]
859 fn ids_of_deleted_keys_are_reused() {
860 let mut table = LiveTable::new(ids(vec![]).schema(), "id").unwrap();
861 for round in 0..100 {
862 table
863 .upsert(ids((0..10).map(|k| round * 10 + k).collect()))
864 .unwrap();
865 table
866 .delete(&Int64Array::from(
867 (0..10).map(|k| round * 10 + k).collect::<Vec<_>>(),
868 ))
869 .unwrap();
870 }
871 assert!(
872 table.slots.len() <= 10,
873 "{} ids for 10 live keys",
874 table.slots.len()
875 );
876 }
877
878 #[test]
879 fn a_failed_automatic_compaction_is_recorded_and_backs_off() {
880 let mut table = LiveTable::new(ids(vec![]).schema(), "id").unwrap();
881 table.dead = MIN_DEAD_ROWS;
882 let other = RecordBatch::try_from_iter([(
884 "other",
885 Arc::new(arrow_array::StringArray::from(vec!["x"])) as ArrayRef,
886 )])
887 .unwrap();
888 table.batches.push(other);
889 table.slots.push(Some(Slot { batch: 0, row: 0 }));
890 table.compact_if_due();
891 assert!(table.compaction_error().is_some());
892 assert_eq!(table.retry_at_dead, 2 * MIN_DEAD_ROWS);
893 }
894
895 #[test]
896 fn automatic_compaction_counts_the_batches_stored_since_the_last_one() {
897 let mut table = LiveTable::new(ids(vec![]).schema(), "id").unwrap();
898 table.upsert(ids(vec![1])).unwrap();
899 table.compacted_batches = 10;
901 while table.batches.len() < 10 + MAX_BATCHES {
902 table.batches.push(ids(vec![]));
903 }
904 table.compact_if_due();
905 assert_eq!(table.generation(), 0, "4,096 batches added");
906 table.batches.push(ids(vec![]));
907 table.compact_if_due();
908 assert_eq!(table.generation(), 1, "4,097 batches added");
909 assert_eq!(table.compacted_batches, 1);
910 }
911}