Skip to main content

cqlite_core/storage/write_engine/
memtable.rs

1//! In-memory write buffer (memtable)
2//!
3//! Stores mutations in memory using a BTreeMap for partition and clustering ordering.
4//! Flushes to L0 SSTable when size threshold is reached.
5//!
6//! The memtable maintains mutations in token-sorted order (via DecoratedKey) and tracks
7//! approximate memory usage to trigger flushes at a configurable threshold.
8
9use crate::error::Result;
10use crate::storage::write_engine::mutation::{DecoratedKey, Mutation};
11use std::collections::BTreeMap;
12
13/// In-memory write buffer
14///
15/// Stores mutations in memory with token-based ordering. Each partition can have
16/// multiple mutations (e.g., multiple rows with different clustering keys).
17#[derive(Debug)]
18pub struct Memtable {
19    /// Partition-level storage: token-ordered map of mutations
20    data: BTreeMap<DecoratedKey, Vec<Mutation>>,
21    /// Approximate size in bytes
22    size_bytes: usize,
23    /// Approximate row count (total mutations across all partitions)
24    row_count: usize,
25    /// Creation timestamp (Unix epoch microseconds)
26    created_at: i64,
27}
28
29impl Memtable {
30    /// Create a new memtable
31    pub fn new() -> Self {
32        Self {
33            data: BTreeMap::new(),
34            size_bytes: 0,
35            row_count: 0,
36            created_at: Self::current_timestamp_micros(),
37        }
38    }
39
40    /// Insert a mutation into the memtable
41    ///
42    /// Mutations are grouped by partition key (DecoratedKey). Multiple mutations
43    /// for the same partition are stored as a vector.
44    pub fn insert(&mut self, _mutation: Mutation) -> Result<()> {
45        // Calculate decorated key from partition key
46        // Note: This requires schema, but mutation doesn't store it.
47        // For now, we expect the mutation to be pre-validated and the key
48        // to be extractable. In practice, the caller will need to provide
49        // the decorated key or schema context.
50        //
51        // WORKAROUND: Since Mutation doesn't store DecoratedKey directly,
52        // and calculating it requires schema, we need to pass the key separately.
53        // For Issue #362, we'll implement a public API that accepts DecoratedKey.
54        //
55        // This is a design limitation that will be addressed in the full WriteEngine.
56        // For now, insert_with_key() is the primary API.
57
58        // This method is kept for API compatibility but requires rethinking.
59        // We'll implement the core logic in insert_with_key() below.
60        Err(crate::error::Error::InvalidInput(
61            "Use insert_with_key() - decorated key must be provided with mutation".to_string(),
62        ))
63    }
64
65    /// Insert a mutation with an explicit decorated key
66    ///
67    /// This is the primary insertion API. The caller is responsible for computing
68    /// the decorated key from the partition key using the table schema.
69    #[tracing::instrument(name = "memtable.insert", skip(self, key, mutation))]
70    pub fn insert_with_key(&mut self, key: DecoratedKey, mutation: Mutation) -> Result<()> {
71        // Calculate mutation size (conservative estimate). This is the SAME
72        // computation the admission gate consults via `estimate_mutation_size`,
73        // so accounting and admission can never drift (issue #1625).
74        let mutation_size = Self::mutation_size(&mutation);
75
76        // Get or create mutation list for this partition
77        let mutations = self.data.entry(key).or_default();
78
79        // Add mutation
80        mutations.push(mutation);
81        self.row_count = self.row_count.saturating_add(1);
82        // `saturating_add`: `mutation_size` can legitimately be `usize::MAX`
83        // (the estimator fails closed at the node cap, issue #1625). Admission
84        // rejects over-limit mutations, but a direct `Memtable` user (bypassing
85        // admission) or a `WriteEngine` with `memtable_hard_limit == usize::MAX`
86        // could otherwise panic in debug (overflow check) or wrap in release —
87        // the ledger update MUST be self-safe.
88        self.size_bytes = self.size_bytes.saturating_add(mutation_size);
89
90        Ok(())
91    }
92
93    /// Get all mutations for a given partition key
94    pub fn get(&self, key: &DecoratedKey) -> Option<&[Mutation]> {
95        self.data.get(key).map(|v| v.as_slice())
96    }
97
98    /// Check if memtable is empty
99    pub fn is_empty(&self) -> bool {
100        self.data.is_empty()
101    }
102
103    /// Get current size in bytes (approximate)
104    pub fn size_bytes(&self) -> usize {
105        self.size_bytes
106    }
107
108    /// Get approximate row count
109    pub fn row_count(&self) -> usize {
110        self.row_count
111    }
112
113    /// Check if memtable should be flushed
114    pub fn should_flush(&self, threshold_bytes: usize) -> bool {
115        self.size_bytes >= threshold_bytes
116    }
117
118    /// Get creation timestamp (microseconds since Unix epoch)
119    pub fn created_at(&self) -> i64 {
120        self.created_at
121    }
122
123    /// Iterate over all partitions and their mutations
124    ///
125    /// Returns an iterator over (DecoratedKey, mutations) pairs in token order.
126    pub fn iter(&self) -> impl Iterator<Item = (&DecoratedKey, &[Mutation])> {
127        self.data.iter().map(|(k, v)| (k, v.as_slice()))
128    }
129
130    /// Clear all data from the memtable
131    ///
132    /// Used after successful flush to SSTable.
133    pub fn clear(&mut self) {
134        self.data.clear();
135        self.size_bytes = 0;
136        self.row_count = 0;
137        // Keep created_at unchanged - represents original creation time
138    }
139
140    /// Upper bound on the number of value nodes the ITERATIVE size estimator
141    /// visits before it fails closed (issue #1625).
142    ///
143    /// The estimator walks a value with an explicit heap worklist (no recursion,
144    /// so no stack-overflow risk and no depth cap collapsing deep children to a
145    /// floor). To bound worst-case work on a pathologically huge/deep value, the
146    /// traversal stops after visiting this many nodes and returns a
147    /// CONSERVATIVE-LARGE estimate (`usize::MAX`) that is GUARANTEED to exceed any
148    /// hard limit, so admission REJECTS the value rather than under-counting it.
149    /// Failing closed on the pathological case is the correct behavior for a DoS
150    /// guard. `1_000_000` is far beyond any legitimate mutation's node count yet
151    /// keeps the traversal cheap.
152    const MAX_ESTIMATE_NODES: usize = 1_000_000;
153
154    /// Estimate the number of bytes a mutation would add to this memtable.
155    ///
156    /// Returns the SAME value that [`Memtable::insert_with_key`] adds to
157    /// `size_bytes`, so the write-engine admission gate (issue #1625) and the
158    /// running size accounting agree by construction — both funnel through the
159    /// private [`Memtable::mutation_size`] computation, so there is no drift.
160    pub(crate) fn estimate_mutation_size(&self, m: &Mutation) -> usize {
161        Self::mutation_size(m)
162    }
163
164    /// Estimate the size of a mutation in bytes
165    ///
166    /// Conservative estimate includes:
167    /// - Fixed overhead per mutation (48 bytes for struct fields)
168    /// - Partition key size (key bytes)
169    /// - Clustering key size (if present)
170    /// - Cell operation sizes (column names + values)
171    fn mutation_size(mutation: &Mutation) -> usize {
172        // Base struct overhead. All accumulation uses `saturating_add` because
173        // `estimate_value_size` may return `usize::MAX` (fail-closed) for a
174        // pathological value (issue #1625); a plain `+` would panic under debug
175        // overflow checks.
176        let mut size: usize = 48;
177
178        // Partition key size
179        for (col_name, value) in &mutation.partition_key.columns {
180            size = size.saturating_add(col_name.len());
181            size = size.saturating_add(Self::estimate_value_size(value));
182        }
183
184        // Clustering key size
185        if let Some(ref clustering_key) = mutation.clustering_key {
186            for (col_name, value) in &clustering_key.columns {
187                size = size.saturating_add(col_name.len());
188                size = size.saturating_add(Self::estimate_value_size(value));
189            }
190        }
191
192        // Cell operations
193        for op in &mutation.operations {
194            size = size.saturating_add(Self::estimate_operation_size(op));
195        }
196
197        size
198    }
199
200    /// Estimate the size in bytes a CQL value would add to the memtable.
201    ///
202    /// Implemented as a BOUNDED ITERATIVE traversal (issue #1625): an explicit
203    /// worklist of `&Value` replaces the previous recursive estimator. Because
204    /// traversal state is not on the call stack there is NO stack-overflow risk,
205    /// so there is NO depth cap and therefore NO collapsing of deeply nested
206    /// children to a conservative floor — a large scalar buried arbitrarily deep
207    /// (e.g. `List([List([List([Text(128KB)])])])`) is counted at its real heap
208    /// size, closing the hard-limit bypass.
209    ///
210    /// The worklist is a stack-backed [`SmallVec`] with 32 inline slots, so the
211    /// common case (shallow/normal mutations — what the #1660 write-path
212    /// allocation budget test exercises) performs the whole traversal with ZERO
213    /// heap allocation on the admission/accounting hot path. Only pathologically
214    /// deep/wide values spill the worklist to the heap.
215    ///
216    /// Scalars contribute their real heap size (`Text`/`Blob`/`Varint`/`Inet`/
217    /// `Json`/`Decimal` byte length; fixed widths otherwise). Collections, maps,
218    /// UDTs, tuples and frozen values contribute their per-container overhead and
219    /// push their children (maps push keys AND values; UDTs count field names and
220    /// push field values) onto the worklist.
221    ///
222    /// To bound worst-case work on a pathologically huge/deep value, traversal
223    /// stops after visiting [`MAX_ESTIMATE_NODES`](Self::MAX_ESTIMATE_NODES) and
224    /// FAILS CLOSED, returning `usize::MAX` so admission REJECTS the value rather
225    /// than under-counting it. All accumulation uses `saturating_add`, so the
226    /// estimate can never wrap around a small value.
227    fn estimate_value_size(value: &crate::types::Value) -> usize {
228        use crate::types::Value;
229        use smallvec::SmallVec;
230
231        let mut total: usize = 0;
232        let mut visited: usize = 0;
233        // Stack-backed worklist of borrowed values still to be measured. The 32
234        // inline slots cover normal nesting/width, so shallow/normal mutations
235        // (the #1660 write-path allocation budget case) traverse with ZERO heap
236        // allocation; only pathological deep/wide values spill to the heap.
237        let mut worklist: SmallVec<[&Value; 32]> = SmallVec::new();
238        worklist.push(value);
239
240        // Would scheduling `incoming` more children push the total node count
241        // past the cap? `visited` (popped so far) + `pending` (already queued) +
242        // `incoming` is the upper bound on nodes this traversal will touch. This
243        // is checked BEFORE enqueuing so a single flat collection with far more
244        // than `MAX_ESTIMATE_NODES` elements can never grow the worklist
245        // proportional to its element count — the DoS guard fails closed WITHOUT
246        // the huge allocation (issue #1625).
247        let would_exceed_cap = |visited: usize, pending: usize, incoming: usize| -> bool {
248            visited.saturating_add(pending).saturating_add(incoming) > Self::MAX_ESTIMATE_NODES
249        };
250
251        while let Some(v) = worklist.pop() {
252            visited += 1;
253            if visited > Self::MAX_ESTIMATE_NODES {
254                // Pathological value: fail closed so admission rejects it.
255                return usize::MAX;
256            }
257
258            match v {
259                Value::Null => {}
260                Value::Boolean(_) | Value::TinyInt(_) => total = total.saturating_add(1),
261                Value::SmallInt(_) => total = total.saturating_add(2),
262                Value::Integer(_) | Value::Float32(_) | Value::Date(_) => {
263                    total = total.saturating_add(4)
264                }
265                Value::BigInt(_)
266                | Value::Counter(_)
267                | Value::Timestamp(_)
268                | Value::Time(_)
269                | Value::Float(_) => total = total.saturating_add(8),
270                Value::Uuid(_) | Value::Duration { .. } => total = total.saturating_add(16),
271                Value::Text(s) => total = total.saturating_add(s.len()),
272                Value::Blob(bytes) | Value::Varint(bytes) | Value::Inet(bytes) => {
273                    total = total.saturating_add(bytes.len())
274                }
275                Value::Decimal { scale: _, unscaled } => {
276                    total = total.saturating_add(4).saturating_add(unscaled.len())
277                }
278                Value::Json(json) => total = total.saturating_add(json.to_string().len()),
279                Value::Tombstone(_) => total = total.saturating_add(24),
280                Value::List(items) | Value::Set(items) | Value::Tuple(items) => {
281                    total = total.saturating_add(16);
282                    if would_exceed_cap(visited, worklist.len(), items.len()) {
283                        return usize::MAX;
284                    }
285                    worklist.extend(items.iter());
286                }
287                Value::Map(entries) => {
288                    total = total.saturating_add(16);
289                    // Each entry enqueues both a key and a value.
290                    let incoming = entries.len().saturating_mul(2);
291                    if would_exceed_cap(visited, worklist.len(), incoming) {
292                        return usize::MAX;
293                    }
294                    for (k, val) in entries {
295                        worklist.push(k);
296                        worklist.push(val);
297                    }
298                }
299                Value::Udt(udt) => {
300                    total = total.saturating_add(16);
301                    // Upper bound: at most one child per field (fields with a
302                    // value); check before touching any field so a wide UDT
303                    // cannot balloon the worklist.
304                    if would_exceed_cap(visited, worklist.len(), udt.fields.len()) {
305                        return usize::MAX;
306                    }
307                    for field in &udt.fields {
308                        total = total.saturating_add(field.name.len());
309                        if let Some(fv) = field.value.as_ref() {
310                            worklist.push(fv);
311                        }
312                    }
313                }
314                Value::Frozen(inner) => {
315                    total = total.saturating_add(8);
316                    if would_exceed_cap(visited, worklist.len(), 1) {
317                        return usize::MAX;
318                    }
319                    worklist.push(inner);
320                }
321            }
322        }
323
324        total
325    }
326
327    /// Estimate the size of a cell operation
328    fn estimate_operation_size(
329        op: &crate::storage::write_engine::mutation::CellOperation,
330    ) -> usize {
331        use crate::storage::write_engine::mutation::CellOperation;
332
333        // `saturating_add` throughout: `estimate_value_size` may return
334        // `usize::MAX` (fail-closed) for a pathological value (issue #1625).
335        match op {
336            CellOperation::Write { column, value } => column
337                .len()
338                .saturating_add(Self::estimate_value_size(value))
339                .saturating_add(8), // +8 for overhead
340            CellOperation::WriteWithTtl {
341                column,
342                value,
343                ttl_seconds: _,
344            } => {
345                // TTL cells: same as Write + 4 bytes for TTL + 4 bytes for local_deletion_time
346                column
347                    .len()
348                    .saturating_add(Self::estimate_value_size(value))
349                    .saturating_add(16)
350            }
351            CellOperation::Delete { column, .. } => column.len().saturating_add(8),
352            CellOperation::DeleteRow => 8,
353            // Epic #899: per-element complex ops. Each carries a column name,
354            // the preserved cell path, an optional value, and temporal metadata.
355            CellOperation::WriteComplexElement {
356                column,
357                cell_path,
358                value,
359                ..
360            } => column
361                .len()
362                .saturating_add(cell_path.len())
363                .saturating_add(value.as_ref().map(Self::estimate_value_size).unwrap_or(0))
364                .saturating_add(16), // flags + ts/ldt/ttl deltas + length prefixes overhead
365            CellOperation::ComplexDeletion { column, .. } => column.len().saturating_add(16),
366        }
367    }
368
369    /// Test-only: force the tracked approximate size, used to exercise the
370    /// admission gate's `saturating_add` overflow guard near `usize::MAX`
371    /// (issue #1625) — a size unreachable through real inserts.
372    #[cfg(test)]
373    pub(crate) fn set_size_bytes_for_test(&mut self, size: usize) {
374        self.size_bytes = size;
375    }
376
377    /// Get current timestamp in microseconds since Unix epoch
378    fn current_timestamp_micros() -> i64 {
379        std::time::SystemTime::now()
380            .duration_since(std::time::UNIX_EPOCH)
381            .unwrap_or_default()
382            .as_micros() as i64
383    }
384}
385
386impl Default for Memtable {
387    fn default() -> Self {
388        Self::new()
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use crate::storage::write_engine::mutation::{
396        CellOperation, ClusteringKey, PartitionKey, TableId,
397    };
398    use crate::types::Value;
399
400    fn create_test_mutation(
401        id: i32,
402        name: &str,
403        clustering_val: Option<i64>,
404    ) -> (DecoratedKey, Mutation) {
405        let table_id = TableId::new("test_ks", "test_table");
406        let partition_key = PartitionKey::single("id", Value::Integer(id));
407
408        // Calculate decorated key from partition key bytes
409        let key_bytes = id.to_be_bytes().to_vec();
410        let decorated_key = DecoratedKey::from_key_bytes(key_bytes).unwrap();
411
412        let clustering_key =
413            clustering_val.map(|val| ClusteringKey::single("ts", Value::BigInt(val)));
414
415        let operations = vec![CellOperation::Write {
416            column: "name".to_string(),
417            value: Value::Text(name.to_string()),
418        }];
419
420        let mutation = Mutation::new(
421            table_id,
422            partition_key,
423            clustering_key,
424            operations,
425            1234567890,
426            None,
427        );
428
429        (decorated_key, mutation)
430    }
431
432    #[test]
433    fn test_memtable_new() {
434        let memtable = Memtable::new();
435        assert!(memtable.is_empty());
436        assert_eq!(memtable.size_bytes(), 0);
437        assert_eq!(memtable.row_count(), 0);
438        assert!(memtable.created_at() > 0);
439    }
440
441    #[test]
442    fn test_memtable_insert_and_get() {
443        let mut memtable = Memtable::new();
444
445        let (key, mutation) = create_test_mutation(1, "Alice", None);
446        memtable.insert_with_key(key.clone(), mutation).unwrap();
447
448        assert!(!memtable.is_empty());
449        assert_eq!(memtable.row_count(), 1);
450        assert!(memtable.size_bytes() > 0);
451
452        // Retrieve mutation
453        let mutations = memtable.get(&key).unwrap();
454        assert_eq!(mutations.len(), 1);
455        assert_eq!(mutations[0].table.table, "test_table");
456    }
457
458    #[test]
459    fn test_memtable_multiple_mutations_same_partition() {
460        let mut memtable = Memtable::new();
461
462        // Insert multiple mutations for same partition (different clustering keys)
463        let (key, mutation1) = create_test_mutation(1, "Alice", Some(1000));
464        let (_, mutation2) = create_test_mutation(1, "Alice Updated", Some(2000));
465
466        memtable.insert_with_key(key.clone(), mutation1).unwrap();
467        memtable.insert_with_key(key.clone(), mutation2).unwrap();
468
469        assert_eq!(memtable.row_count(), 2);
470
471        // Both mutations should be stored for this partition
472        let mutations = memtable.get(&key).unwrap();
473        assert_eq!(mutations.len(), 2);
474    }
475
476    #[test]
477    fn test_memtable_multiple_partitions() {
478        let mut memtable = Memtable::new();
479
480        let (key1, mutation1) = create_test_mutation(1, "Alice", None);
481        let (key2, mutation2) = create_test_mutation(2, "Bob", None);
482        let (key3, mutation3) = create_test_mutation(3, "Charlie", None);
483
484        memtable.insert_with_key(key1, mutation1).unwrap();
485        memtable.insert_with_key(key2, mutation2).unwrap();
486        memtable.insert_with_key(key3, mutation3).unwrap();
487
488        assert_eq!(memtable.row_count(), 3);
489        assert!(!memtable.is_empty());
490    }
491
492    #[test]
493    fn test_memtable_token_ordering() {
494        let mut memtable = Memtable::new();
495
496        // Insert in non-sorted order
497        let (key3, mutation3) = create_test_mutation(300, "Charlie", None);
498        let (key1, mutation1) = create_test_mutation(100, "Alice", None);
499        let (key2, mutation2) = create_test_mutation(200, "Bob", None);
500
501        memtable.insert_with_key(key3.clone(), mutation3).unwrap();
502        memtable.insert_with_key(key1.clone(), mutation1).unwrap();
503        memtable.insert_with_key(key2.clone(), mutation2).unwrap();
504
505        // Verify iteration returns partitions in token order
506        let keys: Vec<_> = memtable.iter().map(|(k, _)| k.token).collect();
507        assert_eq!(keys.len(), 3);
508
509        // Keys should be sorted by token
510        assert!(keys.windows(2).all(|w| w[0] <= w[1]));
511    }
512
513    #[test]
514    fn test_memtable_size_tracking() {
515        let mut memtable = Memtable::new();
516
517        let initial_size = memtable.size_bytes();
518        assert_eq!(initial_size, 0);
519
520        // Insert mutation
521        let (key, mutation) = create_test_mutation(1, "Alice", None);
522        memtable.insert_with_key(key, mutation).unwrap();
523
524        // Size should increase
525        assert!(memtable.size_bytes() > initial_size);
526        let size_after_insert = memtable.size_bytes();
527
528        // Insert another mutation - size should increase more
529        let (key2, mutation2) = create_test_mutation(2, "Bob with a longer name", None);
530        memtable.insert_with_key(key2, mutation2).unwrap();
531
532        assert!(memtable.size_bytes() > size_after_insert);
533    }
534
535    #[test]
536    fn test_memtable_should_flush() {
537        let mut memtable = Memtable::new();
538
539        // Should not flush when empty
540        assert!(!memtable.should_flush(1024));
541
542        // Insert mutations until threshold
543        for i in 0..100 {
544            let (key, mutation) = create_test_mutation(i, "Test data", None);
545            memtable.insert_with_key(key, mutation).unwrap();
546        }
547
548        // Should flush if size exceeds threshold
549        let current_size = memtable.size_bytes();
550        assert!(memtable.should_flush(current_size - 1));
551        assert!(!memtable.should_flush(current_size + 1000));
552    }
553
554    #[test]
555    fn test_memtable_clear() {
556        let mut memtable = Memtable::new();
557
558        let created_at = memtable.created_at();
559
560        // Insert some data
561        let (key, mutation) = create_test_mutation(1, "Alice", None);
562        memtable.insert_with_key(key, mutation).unwrap();
563
564        assert!(!memtable.is_empty());
565        assert!(memtable.size_bytes() > 0);
566        assert!(memtable.row_count() > 0);
567
568        // Clear
569        memtable.clear();
570
571        assert!(memtable.is_empty());
572        assert_eq!(memtable.size_bytes(), 0);
573        assert_eq!(memtable.row_count(), 0);
574        assert_eq!(memtable.created_at(), created_at); // Timestamp unchanged
575    }
576
577    #[test]
578    fn test_memtable_iterator() {
579        let mut memtable = Memtable::new();
580
581        // Insert multiple partitions
582        let (key1, mutation1) = create_test_mutation(1, "Alice", None);
583        let (key2, mutation2) = create_test_mutation(2, "Bob", None);
584
585        memtable.insert_with_key(key1.clone(), mutation1).unwrap();
586        memtable.insert_with_key(key2.clone(), mutation2).unwrap();
587
588        // Iterate and verify
589        let mut count = 0;
590        for (key, mutations) in memtable.iter() {
591            assert!(!mutations.is_empty());
592            assert!([key1.token, key2.token].contains(&key.token));
593            count += 1;
594        }
595
596        assert_eq!(count, 2);
597    }
598
599    #[test]
600    fn test_memtable_empty_check() {
601        let mut memtable = Memtable::new();
602        assert!(memtable.is_empty());
603
604        let (key, mutation) = create_test_mutation(1, "Alice", None);
605        memtable.insert_with_key(key, mutation).unwrap();
606        assert!(!memtable.is_empty());
607
608        memtable.clear();
609        assert!(memtable.is_empty());
610    }
611
612    #[test]
613    fn test_memtable_size_estimates() {
614        // Test size estimation for different value types
615        let small_text = Value::Text("hi".to_string());
616        let large_text = Value::Text("a".repeat(1000));
617        let integer = Value::Integer(42);
618        let uuid = Value::Uuid([0u8; 16]);
619
620        assert_eq!(Memtable::estimate_value_size(&small_text), 2);
621        assert_eq!(Memtable::estimate_value_size(&large_text), 1000);
622        assert_eq!(Memtable::estimate_value_size(&integer), 4);
623        assert_eq!(Memtable::estimate_value_size(&uuid), 16);
624    }
625
626    #[test]
627    fn test_memtable_collection_size_estimates() {
628        // List
629        let list = Value::List(vec![
630            Value::Integer(1),
631            Value::Integer(2),
632            Value::Integer(3),
633        ]);
634        let size = Memtable::estimate_value_size(&list);
635        assert!(size >= 12); // 3 * 4 bytes + overhead
636
637        // Set
638        let set = Value::Set(vec![
639            Value::Text("a".to_string()),
640            Value::Text("b".to_string()),
641        ]);
642        let size = Memtable::estimate_value_size(&set);
643        assert!(size >= 2); // 2 * 1 byte + overhead
644
645        // Map
646        let map = Value::Map(vec![
647            (Value::Integer(1), Value::Text("one".to_string())),
648            (Value::Integer(2), Value::Text("two".to_string())),
649        ]);
650        let size = Memtable::estimate_value_size(&map);
651        assert!(size >= 11); // 2 * (4 + 3) bytes + overhead
652    }
653
654    #[test]
655    fn test_memtable_realistic_flush_threshold() {
656        let mut memtable = Memtable::new();
657
658        // Target: ~10K mutations before 64MB flush (conservative estimate)
659        // Average mutation size should be < 6.4KB
660        let flush_threshold = 64 * 1024 * 1024; // 64MB
661
662        // Insert 10K typical mutations
663        for i in 0..10_000 {
664            let (key, mutation) = create_test_mutation(
665                i,
666                "Typical user data with moderate length name",
667                Some(i as i64),
668            );
669            memtable.insert_with_key(key, mutation).unwrap();
670        }
671
672        let final_size = memtable.size_bytes();
673        println!(
674            "10K mutations size: {} bytes ({} KB)",
675            final_size,
676            final_size / 1024
677        );
678
679        // Should be well under 64MB for 10K mutations
680        assert!(final_size < flush_threshold);
681
682        // Verify avg size per mutation is reasonable
683        let avg_size = final_size / 10_000;
684        println!("Average mutation size: {} bytes", avg_size);
685        assert!(avg_size > 0);
686        assert!(avg_size < 10_000); // Should be less than 10KB per mutation
687    }
688
689    #[test]
690    fn test_memtable_get_nonexistent_key() {
691        let memtable = Memtable::new();
692        let key = DecoratedKey::new(12345, vec![0, 0, 0, 99]);
693
694        assert!(memtable.get(&key).is_none());
695    }
696
697    #[test]
698    fn test_memtable_insert_deprecated_api() {
699        let mut memtable = Memtable::new();
700
701        let table_id = TableId::new("test_ks", "test_table");
702        let partition_key = PartitionKey::single("id", Value::Integer(1));
703        let operations = vec![CellOperation::Write {
704            column: "name".to_string(),
705            value: Value::Text("Alice".to_string()),
706        }];
707
708        let mutation = Mutation::new(table_id, partition_key, None, operations, 1234567890, None);
709
710        // Deprecated insert() should return error
711        let result = memtable.insert(mutation);
712        assert!(result.is_err());
713    }
714
715    #[test]
716    fn test_memtable_nested_collection_depth_limit() {
717        // Issue #1625: a deeply nested list wrapping a tiny scalar is handled by
718        // the ITERATIVE estimator without stack overflow and counted ACCURATELY
719        // (no depth cap, no floor). 40 lists (16 each) around Integer(42) (4).
720        let mut nested_value = Value::Integer(42);
721        for _ in 0..40 {
722            nested_value = Value::List(vec![nested_value]);
723        }
724
725        let size = Memtable::estimate_value_size(&nested_value);
726        assert_eq!(
727            size,
728            40 * 16 + 4,
729            "deep list of a tiny scalar must be counted accurately, not floored"
730        );
731    }
732
733    #[test]
734    fn test_memtable_nested_map_depth_limit() {
735        // Issue #1625: deep map nesting counted accurately. 35 maps, each +16
736        // overhead + Integer key (4), innermost Text("bottom") (6). No floor.
737        let mut nested_value = Value::Text("bottom".to_string());
738        for _ in 0..35 {
739            nested_value = Value::Map(vec![(Value::Integer(1), nested_value)]);
740        }
741
742        let size = Memtable::estimate_value_size(&nested_value);
743        assert_eq!(
744            size,
745            35 * (16 + 4) + 6,
746            "deep map of a tiny scalar must be counted accurately, not floored"
747        );
748    }
749
750    #[test]
751    fn test_memtable_nested_udt_depth_limit() {
752        use crate::types::{UdtField, UdtValue};
753
754        // Issue #1625: deep UDT nesting counted accurately. 35 UDTs, each +16
755        // overhead + field name "field" (5), innermost Integer(1) (4). No floor.
756        let mut nested_value = Value::Integer(1);
757        for i in 0..35 {
758            let udt = UdtValue {
759                type_name: format!("type_{}", i),
760                keyspace: "test_ks".to_string(),
761                fields: vec![UdtField {
762                    name: "field".to_string(),
763                    value: Some(nested_value),
764                }],
765            };
766            nested_value = Value::Udt(udt);
767        }
768
769        let size = Memtable::estimate_value_size(&nested_value);
770        assert_eq!(
771            size,
772            35 * (16 + 5) + 4,
773            "deep UDT of a tiny scalar must be counted accurately, not floored"
774        );
775    }
776
777    #[test]
778    fn test_memtable_frozen_nested_depth_limit() {
779        // Issue #1625: deep Frozen nesting must NOT stack-overflow (iterative)
780        // and is counted accurately. 40 frozen (8 each) around Integer(99) (4).
781        let mut nested_value = Value::Integer(99);
782        for _ in 0..40 {
783            nested_value = Value::Frozen(Box::new(nested_value));
784        }
785
786        let size = Memtable::estimate_value_size(&nested_value);
787        assert_eq!(
788            size,
789            40 * 8 + 4,
790            "deep frozen of a tiny scalar must be counted accurately, not floored"
791        );
792    }
793
794    #[test]
795    fn test_memtable_mixed_nested_collections() {
796        use crate::types::{UdtField, UdtValue};
797
798        // Create a complex nested structure mixing different types
799        let mut nested_value = Value::Text("base".to_string());
800
801        // Alternate between different collection types
802        for i in 0..50 {
803            nested_value = match i % 5 {
804                0 => Value::List(vec![nested_value]),
805                1 => Value::Set(vec![nested_value]),
806                2 => Value::Map(vec![(Value::Integer(i), nested_value)]),
807                3 => Value::Tuple(vec![nested_value]),
808                4 => Value::Udt(UdtValue {
809                    type_name: format!("type_{}", i),
810                    keyspace: "test_ks".to_string(),
811                    fields: vec![UdtField {
812                        name: "f".to_string(),
813                        value: Some(nested_value),
814                    }],
815                }),
816                _ => unreachable!(),
817            };
818        }
819
820        // Should handle mixed nesting without panic
821        let size = Memtable::estimate_value_size(&nested_value);
822        assert!(size > 0);
823    }
824
825    #[test]
826    fn test_memtable_depth_limit_exact_boundary() {
827        // Issue #1625: with the iterative estimator there is no depth cap, so
828        // adding a wrapper level increases the estimate by exactly one List's
829        // overhead (16) — no discontinuity/jump to a floor at any depth.
830        let mut nested_value = Value::Integer(1);
831        for _ in 0..32 {
832            nested_value = Value::List(vec![nested_value]);
833        }
834
835        let size = Memtable::estimate_value_size(&nested_value);
836        assert_eq!(size, 32 * 16 + 4);
837
838        // One more wrapper: exactly +16, not a jump to any conservative floor.
839        nested_value = Value::List(vec![nested_value]);
840        let size_over = Memtable::estimate_value_size(&nested_value);
841        assert_eq!(size_over, size + 16);
842    }
843
844    #[test]
845    fn test_estimate_mutation_size_matches_insert_accounting() {
846        // Issue #1625: the admission gate and the running size accounting must
847        // agree by construction — `estimate_mutation_size(&m)` must equal the
848        // delta `size_bytes()` gains from inserting the same mutation.
849        let mut memtable = Memtable::new();
850        let (key, mutation) = create_test_mutation(7, "some data here", Some(42));
851
852        let before = memtable.size_bytes();
853        let predicted = memtable.estimate_mutation_size(&mutation);
854        memtable.insert_with_key(key, mutation).unwrap();
855        let actual_delta = memtable.size_bytes() - before;
856
857        assert_eq!(
858            predicted, actual_delta,
859            "estimate_mutation_size must equal the size delta insert applies"
860        );
861        assert!(predicted > 0);
862    }
863
864    #[test]
865    fn test_deep_wide_collection_counted_accurately() {
866        // Issue #1625: a WIDE collection buried under many wrapper lists must be
867        // counted at its REAL byte size regardless of depth. The iterative
868        // estimator has no depth cap, so the 500 × 200-byte strings (~100KB) at
869        // the bottom are summed exactly — not collapsed to any floor.
870        let wide = Value::List((0..500).map(|_| Value::Text("y".repeat(200))).collect());
871        let mut nested = wide;
872        for _ in 0..32 {
873            nested = Value::List(vec![nested]);
874        }
875
876        let size = Memtable::estimate_value_size(&nested);
877        let real_payload = 500 * 200; // 100_000 bytes of text
878        assert!(
879            size >= real_payload,
880            "wide collection must count real bytes at any depth (got {size})"
881        );
882        // Accurate, not wildly inflated: real payload + bounded per-container
883        // overhead only.
884        assert!(
885            size < real_payload + 10 * 1024,
886            "estimate must be accurate, not floor-inflated (got {size})"
887        );
888    }
889
890    #[test]
891    fn test_deep_narrow_collection_large_scalar_not_undercounted() {
892        // Issue #1625 (roborev finding): a deep NARROW collection wrapping a
893        // large DIRECT scalar must not be systematically under-counted. Wrap a
894        // `List([Text(128KB)])` in 32 single-element lists.
895        //
896        // Pre-fix: any node past the depth cap collapsed to a ~1KB floor — small
897        // enough to slip a 64KB gate. Post-fix: the iterative estimator counts
898        // the 128KB scalar at its real heap size regardless of depth.
899        let big = 128 * 1024;
900        let mut nested = Value::List(vec![Value::Text("x".repeat(big))]);
901        for _ in 0..32 {
902            nested = Value::List(vec![nested]);
903        }
904
905        let size = Memtable::estimate_value_size(&nested);
906        assert!(
907            size >= big,
908            "deep narrow collection with a large scalar must count the scalar's \
909             real heap size, not the old ~1KB floor (got {size}, expected >= {big})"
910        );
911    }
912
913    #[test]
914    fn test_large_scalar_buried_below_old_cap_counted() {
915        // Issue #1625 (3rd iteration): a large scalar buried MANY levels below
916        // where the old depth cap (32) sat must be counted at real size. Wrap a
917        // 128KB text in 40 single-element lists — well past the old cap — and
918        // confirm the iterative estimator still sees the full 128KB.
919        let big = 128 * 1024;
920        let mut nested = Value::Text("z".repeat(big));
921        for _ in 0..40 {
922            nested = Value::List(vec![nested]);
923        }
924
925        let size = Memtable::estimate_value_size(&nested);
926        assert!(
927            size >= big,
928            "scalar buried below the old depth cap must be counted at real size \
929             (got {size}, expected >= {big})"
930        );
931    }
932
933    #[test]
934    fn test_pathological_node_cap_returns_usize_max() {
935        // Issue #1625: exceeding MAX_ESTIMATE_NODES must FAIL CLOSED (usize::MAX),
936        // never under-count — and never overflow or hang. A flat list of
937        // 1_000_001 integers is a single fast allocation that trips the cap.
938        let value = Value::List((0..1_000_001i32).map(Value::Integer).collect());
939        let size = Memtable::estimate_value_size(&value);
940        assert_eq!(
941            size,
942            usize::MAX,
943            "hitting the node cap must fail closed with usize::MAX (got {size})"
944        );
945    }
946
947    #[test]
948    fn test_insert_with_pathological_value_saturates_ledger_no_panic() {
949        // Issue #1625 (roborev finding 1): a direct `Memtable` user bypasses the
950        // WriteEngine admission gate, so `insert_with_key` MUST be self-safe when
951        // the estimator returns `usize::MAX` (node cap fail-closed). The ledger
952        // update must saturate — never panic (debug overflow) or wrap (release).
953        let mut memtable = Memtable::new();
954
955        // A flat list past the node cap makes `mutation_size` == usize::MAX.
956        let pathological = Value::List(
957            (0..(Memtable::MAX_ESTIMATE_NODES as i32 + 5))
958                .map(Value::Integer)
959                .collect(),
960        );
961        let table_id = TableId::new("test_ks", "test_table");
962        let partition_key = PartitionKey::single("id", Value::Integer(1));
963        let key = DecoratedKey::from_key_bytes(1i32.to_be_bytes().to_vec()).unwrap();
964        let operations = vec![CellOperation::Write {
965            column: "big".to_string(),
966            value: pathological,
967        }];
968        let mutation = Mutation::new(table_id, partition_key, None, operations, 1, None);
969
970        // Sanity: the estimate really is usize::MAX for this mutation.
971        assert_eq!(memtable.estimate_mutation_size(&mutation), usize::MAX);
972
973        // Must not panic; ledger saturates at usize::MAX (no wrap to a small value).
974        memtable.insert_with_key(key, mutation).unwrap();
975        assert_eq!(memtable.size_bytes(), usize::MAX);
976    }
977
978    #[test]
979    fn test_insert_saturates_when_ledger_already_near_max() {
980        // Issue #1625 (roborev finding 1): incrementing an already-huge ledger by
981        // a normal mutation size must saturate, not wrap around to a small value.
982        let mut memtable = Memtable::new();
983        memtable.set_size_bytes_for_test(usize::MAX - 3);
984
985        let (key, mutation) = create_test_mutation(1, "Alice", None);
986        memtable.insert_with_key(key, mutation).unwrap();
987
988        assert_eq!(
989            memtable.size_bytes(),
990            usize::MAX,
991            "ledger must saturate at usize::MAX, never wrap"
992        );
993    }
994
995    #[test]
996    fn test_wide_collection_fails_closed_before_enqueuing_children() {
997        // Issue #1625 (roborev finding 2): a single flat collection whose element
998        // count exceeds MAX_ESTIMATE_NODES must fail closed (usize::MAX) at the
999        // ENQUEUE check — the worklist is never grown proportional to the element
1000        // count. Verified for every enqueue site (list, map, UDT, frozen).
1001        use crate::types::{UdtField, UdtValue};
1002
1003        let over = Memtable::MAX_ESTIMATE_NODES + 5;
1004
1005        // List / Set / Tuple enqueue site.
1006        let list = Value::List((0..over as i32).map(Value::Integer).collect());
1007        assert_eq!(Memtable::estimate_value_size(&list), usize::MAX);
1008
1009        // Map enqueue site (keys + values).
1010        let map = Value::Map(
1011            (0..over as i32)
1012                .map(|i| (Value::Integer(i), Value::Integer(i)))
1013                .collect(),
1014        );
1015        assert_eq!(Memtable::estimate_value_size(&map), usize::MAX);
1016
1017        // UDT enqueue site (field values).
1018        let udt = Value::Udt(UdtValue {
1019            type_name: "t".to_string(),
1020            keyspace: "ks".to_string(),
1021            fields: (0..over)
1022                .map(|i| UdtField {
1023                    name: String::new(),
1024                    value: Some(Value::Integer(i as i32)),
1025                })
1026                .collect(),
1027        });
1028        assert_eq!(Memtable::estimate_value_size(&udt), usize::MAX);
1029    }
1030
1031    #[test]
1032    fn test_memtable_shallow_collections_unaffected() {
1033        // Verify shallow collections are not affected by depth limit
1034
1035        // Simple list
1036        let simple_list = Value::List(vec![
1037            Value::Integer(1),
1038            Value::Integer(2),
1039            Value::Integer(3),
1040        ]);
1041        let size = Memtable::estimate_value_size(&simple_list);
1042        assert_eq!(size, 12 + 16); // 3 * 4 bytes + overhead
1043
1044        // Nested but shallow (3 levels)
1045        let shallow_nested =
1046            Value::List(vec![Value::List(vec![Value::List(vec![Value::Integer(
1047                1,
1048            )])])]);
1049        let size = Memtable::estimate_value_size(&shallow_nested);
1050        assert!(size > 0);
1051        assert!(size < 1024); // Should not use conservative estimate
1052    }
1053}