Skip to main content

bsql_arena/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(clippy::all)]
3
4//! Bump allocator for row data — one allocation per query result.
5//!
6//! All row data (strings, byte arrays) from a single query is allocated into a
7//! contiguous arena. When the result is dropped, one deallocation frees everything.
8//!
9//! # Thread-local recycling
10//!
11//! Arenas are recycled from a thread-local pool (LIFO, up to 4 per thread).
12//! The arena object itself is never heap-allocated fresh on the hot path.
13//!
14//! # Chunk growth
15//!
16//! Initial chunk: 8KB. Growth: double the previous chunk size (capped at 1MB).
17//! On `reset()`, chunks larger than 64KB are discarded to prevent long-term bloat.
18
19use std::cell::RefCell;
20
21/// Initial chunk size: 8KB.
22///
23/// Arena is used only for streaming queries (portal-based chunked fetch).
24/// Regular queries store data in QueryResult.data_buf (a Vec<u8>), not arena.
25/// 8KB is sufficient for streaming chunks and keeps thread-local pool light
26/// (4 arenas × 8KB = 32KB per thread).
27const INITIAL_CHUNK_SIZE: usize = 8 * 1024;
28
29/// Maximum chunk size: 1MB cap to prevent runaway growth.
30const MAX_CHUNK_SIZE: usize = 1024 * 1024;
31
32/// Maximum number of arenas in the thread-local pool.
33const MAX_POOL_SIZE: usize = 4;
34
35/// Shrink threshold: chunks larger than this are discarded on reset.
36const SHRINK_THRESHOLD: usize = 64 * 1024;
37
38/// A bump allocator for row data.
39///
40/// Memory is allocated in contiguous chunks. Each `alloc` call bumps a pointer
41/// forward. There is no per-allocation deallocation — the entire arena is freed
42/// at once via `reset()` or `Drop`.
43///
44/// # Example
45///
46/// ```
47/// use bsql_arena::Arena;
48///
49/// let mut arena = Arena::new();
50/// let offset = arena.alloc_copy(b"hello");
51/// assert_eq!(arena.get(offset, 5), b"hello");
52/// arena.reset();
53/// ```
54pub struct Arena {
55    chunks: Vec<Vec<u8>>,
56    /// Cached cumulative chunk capacities for O(1) offset resolution.
57    /// `prefix_sums[i]` = sum of capacities of chunks 0..i.
58    prefix_sums: Vec<usize>,
59    current: usize,
60    offset: usize,
61}
62
63impl Arena {
64    /// Create a new arena with an 8KB initial chunk.
65    pub fn new() -> Self {
66        let chunk = Vec::with_capacity(INITIAL_CHUNK_SIZE);
67        Self {
68            chunks: vec![chunk],
69            prefix_sums: vec![0],
70            current: 0,
71            offset: 0,
72        }
73    }
74
75    /// Create an empty arena with zero allocation.
76    ///
77    /// No memory is allocated until `alloc` or `alloc_copy` is called.
78    /// Used by query paths that store data in `QueryResult.data_buf` instead.
79    pub fn empty() -> Self {
80        Self {
81            chunks: Vec::new(),
82            prefix_sums: Vec::new(),
83            current: 0,
84            offset: 0,
85        }
86    }
87
88    /// Allocate `len` bytes, returning a mutable slice into the arena.
89    ///
90    /// The returned slice is zeroed. For copying data in, prefer `alloc_copy`.
91    pub fn alloc(&mut self, len: usize) -> &mut [u8] {
92        if len == 0 {
93            return &mut [];
94        }
95
96        self.ensure_capacity(len);
97
98        let chunk = &mut self.chunks[self.current];
99        let start = self.offset;
100        let new_len = start + len;
101
102        // Extend the chunk's length (capacity is guaranteed by ensure_capacity)
103        // vec![0u8; N].resize(N, 0) zeroes new bytes. This is the cost of safe
104        // Rust — the kernel already zeroes mmap'd pages, so the real overhead is
105        // only on reused capacity. Cannot avoid without unsafe.
106        if new_len > chunk.len() {
107            chunk.resize(new_len, 0);
108        }
109
110        self.offset = new_len;
111        &mut chunk[start..new_len]
112    }
113
114    /// Copy `data` into the arena and return the global offset.
115    ///
116    /// The offset can be used with `get()` to retrieve the data later.
117    #[inline(always)]
118    pub fn alloc_copy(&mut self, data: &[u8]) -> usize {
119        let len = data.len();
120        if len == 0 {
121            return self.global_offset();
122        }
123
124        // Fast path: data fits in current chunk's remaining capacity.
125        // No function calls, no branches — just memcpy and bump.
126        let chunk = &mut self.chunks[self.current];
127        let remaining = chunk.capacity() - self.offset;
128        if remaining >= len {
129            let start = self.offset;
130            // Append directly — extend_from_slice is the fastest way to
131            // copy data into a Vec when we know capacity is sufficient.
132            // It compiles to a single memcpy + length update.
133            if start == chunk.len() {
134                chunk.extend_from_slice(data);
135            } else {
136                let new_len = start + len;
137                if new_len > chunk.len() {
138                    chunk.resize(new_len, 0);
139                }
140                chunk[start..start + len].copy_from_slice(data);
141            }
142            let global = self.prefix_sums[self.current] + start;
143            self.offset = start + len;
144            return global;
145        }
146
147        // Slow path: need a new chunk.
148        self.alloc_copy_slow(data)
149    }
150
151    /// Slow path for alloc_copy — allocates a new chunk.
152    #[cold]
153    #[inline(never)]
154    fn alloc_copy_slow(&mut self, data: &[u8]) -> usize {
155        self.ensure_capacity(data.len());
156
157        let chunk = &mut self.chunks[self.current];
158        let start = self.offset;
159        chunk.extend_from_slice(data);
160
161        let global = self.prefix_sums[self.current] + start;
162        self.offset = start + data.len();
163        global
164    }
165
166    /// Retrieve a slice from the arena by global offset and length.
167    ///
168    /// # Panics
169    ///
170    /// Panics if the offset + length exceeds the arena's allocated range.
171    pub fn get(&self, global_offset: usize, len: usize) -> &[u8] {
172        if len == 0 {
173            return &[];
174        }
175
176        let (chunk_idx, local_offset) = self.resolve_offset(global_offset);
177        &self.chunks[chunk_idx][local_offset..local_offset + len]
178    }
179
180    /// Retrieve a str slice from the arena. Returns `None` if not valid UTF-8.
181    ///
182    /// Uses SIMD-accelerated UTF-8 validation via `simdutf8`.
183    pub fn get_str(&self, global_offset: usize, len: usize) -> Option<&str> {
184        if len == 0 {
185            return Some("");
186        }
187        simdutf8::basic::from_utf8(self.get(global_offset, len)).ok()
188    }
189
190    /// Reset the arena for reuse. Keeps allocated memory but resets the bump pointer.
191    ///
192    /// Chunks larger than 64KB are discarded to prevent long-term bloat.
193    pub fn reset(&mut self) {
194        // Discard oversized chunks, keep small ones
195        self.chunks.retain(|c| c.capacity() <= SHRINK_THRESHOLD);
196
197        if self.chunks.is_empty() {
198            self.chunks.push(Vec::with_capacity(INITIAL_CHUNK_SIZE));
199        }
200
201        // Clear all chunks (set len to 0, keep capacity)
202        for chunk in &mut self.chunks {
203            chunk.clear();
204        }
205
206        // Rebuild prefix_sums
207        self.rebuild_prefix_sums();
208
209        self.current = 0;
210        self.offset = 0;
211    }
212
213    /// Total bytes allocated in this arena (across all chunks).
214    pub fn allocated(&self) -> usize {
215        if self.chunks.is_empty() {
216            return 0;
217        }
218        // Fast path: single chunk (common case — most queries fit in 8KB).
219        if self.current == 0 {
220            return self.offset;
221        }
222        // Multi-chunk: sum filled chunks + current offset.
223        let mut total = self.offset;
224        for chunk in &self.chunks[..self.current] {
225            total += chunk.len();
226        }
227        total
228    }
229
230    /// Total capacity of all chunks (for diagnostics).
231    pub fn capacity(&self) -> usize {
232        self.chunks.iter().map(|c| c.capacity()).sum()
233    }
234
235    // --- Internal ---
236
237    /// Ensure the current chunk has room for `len` bytes. If not, allocate a new chunk.
238    fn ensure_capacity(&mut self, len: usize) {
239        let chunk = &self.chunks[self.current];
240        let remaining = chunk.capacity().saturating_sub(self.offset);
241
242        if remaining >= len {
243            return;
244        }
245
246        // Need a new chunk. Size = max(double previous capacity, len, INITIAL_CHUNK_SIZE)
247        let prev_cap = chunk.capacity();
248        let new_cap = prev_cap
249            .saturating_mul(2)
250            .max(len)
251            .max(INITIAL_CHUNK_SIZE)
252            .min(MAX_CHUNK_SIZE.max(len)); // allow exceeding MAX for single large allocs
253
254        // Check if the next chunk already exists and has enough capacity
255        let next_idx = self.current + 1;
256        if next_idx < self.chunks.len() && self.chunks[next_idx].capacity() >= len {
257            self.current = next_idx;
258            self.offset = 0;
259            return;
260        }
261
262        // Allocate a new chunk and update prefix_sums
263        let new_chunk = Vec::with_capacity(new_cap);
264        let prefix = self.prefix_sums[self.chunks.len() - 1]
265            + self.chunks.last().map_or(0, |c| c.capacity());
266        if next_idx < self.chunks.len() {
267            self.chunks[next_idx] = new_chunk;
268            // Rebuild prefix_sums since a chunk capacity changed
269            self.rebuild_prefix_sums();
270        } else {
271            self.chunks.push(new_chunk);
272            self.prefix_sums.push(prefix);
273        }
274        self.current = next_idx;
275        self.offset = 0;
276    }
277
278    /// Rebuild the prefix_sums cache from current chunk capacities.
279    fn rebuild_prefix_sums(&mut self) {
280        self.prefix_sums.clear();
281        let mut sum = 0;
282        for chunk in &self.chunks {
283            self.prefix_sums.push(sum);
284            sum += chunk.capacity();
285        }
286    }
287
288    /// Compute the global offset for the current position.
289    pub fn global_offset(&self) -> usize {
290        self.global_offset_at(self.current, self.offset)
291    }
292
293    /// Compute a global offset from chunk index and local offset.
294    /// O(1) using cached prefix_sums.
295    fn global_offset_at(&self, chunk_idx: usize, local_offset: usize) -> usize {
296        self.prefix_sums[chunk_idx] + local_offset
297    }
298
299    /// Resolve a global offset to (chunk_index, local_offset).
300    /// O(log n) using binary search on prefix_sums.
301    fn resolve_offset(&self, global_offset: usize) -> (usize, usize) {
302        // for the common case (most queries fit in one 8KB chunk).
303        if self.chunks.len() == 1 {
304            debug_assert!(
305                global_offset < self.chunks[0].capacity(),
306                "arena offset {global_offset} out of bounds in single chunk (cap={})",
307                self.chunks[0].capacity()
308            );
309            return (0, global_offset);
310        }
311
312        // Binary search: find the last chunk whose prefix_sum <= global_offset
313        let idx = match self.prefix_sums.binary_search(&global_offset) {
314            Ok(i) => i,
315            Err(0) => 0, // guard against underflow when global_offset < prefix_sums[0]
316            Err(i) => i - 1,
317        };
318        let local = global_offset - self.prefix_sums[idx];
319        debug_assert!(
320            local < self.chunks[idx].capacity(),
321            "arena offset {global_offset} out of bounds in chunk {idx} (cap={})",
322            self.chunks[idx].capacity()
323        );
324        (idx, local)
325    }
326}
327
328impl Default for Arena {
329    fn default() -> Self {
330        Self::new()
331    }
332}
333
334// --- Thread-local arena pool ---
335
336thread_local! {
337    static ARENA_POOL: RefCell<Vec<Arena>> = const { RefCell::new(Vec::new()) };
338}
339
340/// Acquire an arena from the thread-local pool, or create a new one.
341///
342/// LIFO ordering: returns the most recently released arena (warmest cache).
343///
344/// # Example
345///
346/// ```
347/// use bsql_arena::{acquire_arena, release_arena};
348///
349/// let mut arena = acquire_arena();
350/// let offset = arena.alloc_copy(b"data");
351/// // ... use arena ...
352/// release_arena(arena);
353/// ```
354pub fn acquire_arena() -> Arena {
355    ARENA_POOL
356        .with(|pool| pool.borrow_mut().pop())
357        .unwrap_or_default()
358}
359
360/// Return an arena to the thread-local pool for reuse.
361///
362/// The arena is reset (bump pointer zeroed, oversized chunks discarded).
363/// If the pool is full (4 arenas), the arena is dropped instead.
364pub fn release_arena(mut arena: Arena) {
365    arena.reset();
366    ARENA_POOL.with(|pool| {
367        let mut pool = pool.borrow_mut();
368        if pool.len() < MAX_POOL_SIZE {
369            pool.push(arena);
370        }
371        // else: drop the arena (too many in pool)
372    });
373}
374
375// ---------------------------------------------------------------------------
376// ArenaRows — arena-backed row storage with borrowed strings
377// ---------------------------------------------------------------------------
378
379/// A collection of decoded rows backed by an arena.
380///
381/// Text and blob columns in `T` are `&'static str` / `&'static [u8]` whose
382/// memory actually lives in the arena stored alongside them. The `'static`
383/// lifetime is a fiction — the data is valid for as long as this struct lives.
384///
385/// # Safety contract
386///
387/// The `Vec<T>` is dropped **before** the `Arena` (Rust drops fields in
388/// declaration order). The `&'static str` / `&'static [u8]` references
389/// inside `T` are never dereferenced after the arena is freed.
390///
391/// # Drop order guarantee
392///
393/// Rust guarantees fields are dropped in declaration order (RFC 1857).
394/// `rows` is declared before `arena`, so all `T` values (and their borrowed
395/// pointers) are dropped before the arena memory is freed.
396pub struct ArenaRows<T> {
397    rows: Vec<T>,
398    arena: Arena,
399}
400
401impl<T> ArenaRows<T> {
402    /// Build `ArenaRows` from an arena and a row vector.
403    ///
404    /// `T` should contain only Copy types (integers, floats, bools) and
405    /// byte-range indices into a separately validated text buffer. No
406    /// `&'static str` transmute is involved.
407    pub fn new(rows: Vec<T>, arena: Arena) -> Self {
408        Self { rows, arena }
409    }
410
411    /// Number of rows.
412    #[inline]
413    pub fn len(&self) -> usize {
414        self.rows.len()
415    }
416
417    /// Whether the result set is empty.
418    #[inline]
419    pub fn is_empty(&self) -> bool {
420        self.rows.is_empty()
421    }
422
423    /// Get a row by index.
424    #[inline]
425    pub fn get(&self, idx: usize) -> Option<&T> {
426        self.rows.get(idx)
427    }
428
429    /// Iterate over rows by reference.
430    #[inline]
431    pub fn iter(&self) -> std::slice::Iter<'_, T> {
432        self.rows.iter()
433    }
434
435    /// Consume into the inner `Vec<T>` and arena.
436    ///
437    /// Returns both so the caller can decide what to do with the arena.
438    pub fn into_parts(self) -> (Vec<T>, Arena) {
439        (self.rows, self.arena)
440    }
441
442    /// Total bytes allocated in the backing arena.
443    #[inline]
444    pub fn arena_allocated(&self) -> usize {
445        self.arena.allocated()
446    }
447}
448
449impl<T> std::ops::Deref for ArenaRows<T> {
450    type Target = [T];
451
452    #[inline]
453    fn deref(&self) -> &[T] {
454        &self.rows
455    }
456}
457
458impl<'a, T> IntoIterator for &'a ArenaRows<T> {
459    type Item = &'a T;
460    type IntoIter = std::slice::Iter<'a, T>;
461
462    #[inline]
463    fn into_iter(self) -> Self::IntoIter {
464        self.rows.iter()
465    }
466}
467
468impl<T: std::fmt::Debug> std::fmt::Debug for ArenaRows<T> {
469    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470        f.debug_struct("ArenaRows")
471            .field("len", &self.rows.len())
472            .field("arena_allocated", &self.arena.allocated())
473            .field("rows", &self.rows)
474            .finish()
475    }
476}
477
478// ---------------------------------------------------------------------------
479// ValidatedRows — batch-validated text, zero unsafe
480// ---------------------------------------------------------------------------
481
482/// A collection of decoded rows with batch-validated text data.
483///
484/// Text columns are stored as byte ranges `(u32, u32)` into a shared,
485/// batch-validated `String` buffer. Blob columns are stored as byte ranges
486/// into the `Arena`. Scalar columns (i64, f64, bool) are stored directly.
487///
488/// # Zero unsafe
489///
490/// The text buffer is validated once via `String::from_utf8` (SIMD-accelerated
491/// in std on modern CPUs). No `from_utf8_unchecked`, no `transmute`, no
492/// lifetime extension.
493///
494/// # Usage pattern
495///
496/// The codegen generates an "inner" struct with byte ranges and a "view" struct
497/// with `&str`. `ValidatedRows::iter()` maps inner -> view by slicing the
498/// validated text buffer.
499pub struct ValidatedRows<T> {
500    rows: Vec<T>,
501    text_buf: String,
502    blob_arena: Arena,
503}
504
505impl<T> ValidatedRows<T> {
506    /// Build `ValidatedRows` from a text buffer (already validated as UTF-8),
507    /// a blob arena, and the decoded inner rows.
508    pub fn new(rows: Vec<T>, text_buf: String, blob_arena: Arena) -> Self {
509        Self {
510            rows,
511            text_buf,
512            blob_arena,
513        }
514    }
515
516    /// Get the validated text buffer.
517    #[inline]
518    pub fn text(&self) -> &str {
519        &self.text_buf
520    }
521
522    /// Get a text slice by byte range. Panics if range is out of bounds
523    /// or not on a UTF-8 char boundary (impossible if ranges were recorded
524    /// correctly during the step loop).
525    #[inline]
526    pub fn text_slice(&self, start: u32, end: u32) -> &str {
527        &self.text_buf[start as usize..end as usize]
528    }
529
530    /// Get a blob slice from the arena by global offset and length.
531    #[inline]
532    pub fn blob_slice(&self, offset: u32, len: u32) -> &[u8] {
533        self.blob_arena.get(offset as usize, len as usize)
534    }
535
536    /// Number of rows.
537    #[inline]
538    pub fn len(&self) -> usize {
539        self.rows.len()
540    }
541
542    /// Whether the result set is empty.
543    #[inline]
544    pub fn is_empty(&self) -> bool {
545        self.rows.is_empty()
546    }
547
548    /// Get an inner row by index.
549    #[inline]
550    pub fn get_inner(&self, idx: usize) -> Option<&T> {
551        self.rows.get(idx)
552    }
553
554    /// Iterate over inner rows by reference.
555    #[inline]
556    pub fn iter_inner(&self) -> std::slice::Iter<'_, T> {
557        self.rows.iter()
558    }
559
560    /// Total bytes in the text buffer.
561    #[inline]
562    pub fn text_len(&self) -> usize {
563        self.text_buf.len()
564    }
565
566    /// Total bytes allocated in the blob arena.
567    #[inline]
568    pub fn blob_allocated(&self) -> usize {
569        self.blob_arena.allocated()
570    }
571
572    /// Total bytes allocated (text + blobs).
573    #[inline]
574    pub fn arena_allocated(&self) -> usize {
575        self.text_buf.len() + self.blob_arena.allocated()
576    }
577}
578
579impl<T> std::ops::Deref for ValidatedRows<T> {
580    type Target = [T];
581
582    #[inline]
583    fn deref(&self) -> &[T] {
584        &self.rows
585    }
586}
587
588impl<'a, T> IntoIterator for &'a ValidatedRows<T> {
589    type Item = &'a T;
590    type IntoIter = std::slice::Iter<'a, T>;
591
592    #[inline]
593    fn into_iter(self) -> Self::IntoIter {
594        self.rows.iter()
595    }
596}
597
598impl<T: std::fmt::Debug> std::fmt::Debug for ValidatedRows<T> {
599    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600        f.debug_struct("ValidatedRows")
601            .field("len", &self.rows.len())
602            .field("text_len", &self.text_buf.len())
603            .field("blob_allocated", &self.blob_arena.allocated())
604            .field("rows", &self.rows)
605            .finish()
606    }
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612
613    #[test]
614    fn basic_alloc_and_get() {
615        let mut arena = Arena::new();
616        let offset = arena.alloc_copy(b"hello");
617        assert_eq!(arena.get(offset, 5), b"hello");
618    }
619
620    #[test]
621    fn multiple_allocs() {
622        let mut arena = Arena::new();
623        let o1 = arena.alloc_copy(b"foo");
624        let o2 = arena.alloc_copy(b"bar");
625        let o3 = arena.alloc_copy(b"baz");
626
627        assert_eq!(arena.get(o1, 3), b"foo");
628        assert_eq!(arena.get(o2, 3), b"bar");
629        assert_eq!(arena.get(o3, 3), b"baz");
630    }
631
632    #[test]
633    fn alloc_str_retrieval() {
634        let mut arena = Arena::new();
635        let offset = arena.alloc_copy(b"hello world");
636        assert_eq!(arena.get_str(offset, 11), Some("hello world"));
637    }
638
639    #[test]
640    fn zero_length_alloc() {
641        let mut arena = Arena::new();
642        let offset = arena.alloc_copy(b"");
643        let data = arena.get(offset, 0);
644        assert!(data.is_empty());
645    }
646
647    #[test]
648    fn alloc_returns_zeroed_slice() {
649        let mut arena = Arena::new();
650        let slice = arena.alloc(16);
651        assert!(slice.iter().all(|&b| b == 0));
652    }
653
654    #[test]
655    fn reset_allows_reuse() {
656        let mut arena = Arena::new();
657        let _o1 = arena.alloc_copy(b"before reset");
658        assert_eq!(arena.allocated(), 12);
659
660        arena.reset();
661        assert_eq!(arena.allocated(), 0);
662
663        let o2 = arena.alloc_copy(b"after reset");
664        assert_eq!(arena.get(o2, 11), b"after reset");
665    }
666
667    #[test]
668    fn chunk_growth() {
669        let mut arena = Arena::new();
670
671        // Fill the initial 8KB chunk
672        let big = vec![0xAA; INITIAL_CHUNK_SIZE + 1];
673        let offset = arena.alloc_copy(&big);
674        assert_eq!(arena.get(offset, big.len())[0], 0xAA);
675        assert!(
676            arena.chunks.len() >= 2,
677            "should have grown to a second chunk"
678        );
679    }
680
681    #[test]
682    fn large_single_alloc() {
683        let mut arena = Arena::new();
684        let data = vec![0x42; 2 * MAX_CHUNK_SIZE];
685        let offset = arena.alloc_copy(&data);
686        let result = arena.get(offset, data.len());
687        assert!(result.iter().all(|&b| b == 0x42));
688    }
689
690    #[test]
691    fn one_hundred_rows_in_one_chunk() {
692        let mut arena = Arena::new();
693        let row_data = b"typical row data, about 50 bytes of text content.";
694
695        let mut offsets = Vec::new();
696        for _ in 0..100 {
697            offsets.push(arena.alloc_copy(row_data));
698        }
699
700        // 100 * 50 = 5000 bytes, fits in 8KB initial chunk
701        assert_eq!(arena.chunks.len(), 1);
702
703        for &offset in &offsets {
704            assert_eq!(arena.get(offset, row_data.len()), row_data);
705        }
706    }
707
708    #[test]
709    fn reset_discards_oversized_chunks() {
710        let mut arena = Arena::new();
711
712        // Allocate a chunk larger than SHRINK_THRESHOLD
713        let big = vec![0xFF; SHRINK_THRESHOLD + 1];
714        arena.alloc_copy(&big);
715
716        let _chunks_before = arena.chunks.len();
717        arena.reset();
718
719        // Oversized chunks should be discarded
720        for chunk in &arena.chunks {
721            assert!(
722                chunk.capacity() <= SHRINK_THRESHOLD,
723                "oversized chunk not discarded: capacity={}",
724                chunk.capacity()
725            );
726        }
727    }
728
729    #[test]
730    fn thread_local_pool_acquire_release() {
731        let mut arena = acquire_arena();
732        arena.alloc_copy(b"test data");
733        release_arena(arena);
734
735        // Second acquire should get the recycled arena
736        let arena2 = acquire_arena();
737        assert_eq!(arena2.allocated(), 0); // should be reset
738        release_arena(arena2);
739    }
740
741    #[test]
742    fn thread_local_pool_max_size() {
743        // Release MAX_POOL_SIZE + 1 arenas, only MAX_POOL_SIZE should be kept
744        for _ in 0..MAX_POOL_SIZE + 2 {
745            let arena = Arena::new();
746            release_arena(arena);
747        }
748
749        ARENA_POOL.with(|pool| {
750            assert!(pool.borrow().len() <= MAX_POOL_SIZE);
751        });
752    }
753
754    #[test]
755    fn capacity_reports_total() {
756        let arena = Arena::new();
757        assert!(arena.capacity() >= INITIAL_CHUNK_SIZE);
758    }
759
760    #[test]
761    fn allocated_tracks_usage() {
762        let mut arena = Arena::new();
763        assert_eq!(arena.allocated(), 0);
764        arena.alloc_copy(b"12345");
765        assert_eq!(arena.allocated(), 5);
766        arena.alloc_copy(b"67890");
767        assert_eq!(arena.allocated(), 10);
768    }
769
770    #[test]
771    fn alloc_at_exact_8kb_boundary() {
772        let mut arena = Arena::new();
773
774        // Fill exactly to the 8KB boundary
775        let filler = vec![0xAA; INITIAL_CHUNK_SIZE];
776        let o1 = arena.alloc_copy(&filler);
777        assert_eq!(arena.get(o1, INITIAL_CHUNK_SIZE)[0], 0xAA);
778        assert_eq!(arena.chunks.len(), 1);
779
780        // Next alloc (even 1 byte) must trigger a new chunk
781        let o2 = arena.alloc_copy(b"x");
782        assert_eq!(arena.get(o2, 1), b"x");
783        assert!(arena.chunks.len() >= 2, "should have grown past 8KB chunk");
784
785        // Data from both chunks must still be accessible
786        assert_eq!(arena.get(o1, INITIAL_CHUNK_SIZE)[0], 0xAA);
787        assert_eq!(
788            arena.get(o1, INITIAL_CHUNK_SIZE)[INITIAL_CHUNK_SIZE - 1],
789            0xAA
790        );
791    }
792
793    #[test]
794    fn prefix_sums_correct_after_multi_chunk() {
795        let mut arena = Arena::new();
796        let mut offsets = Vec::new();
797
798        // Force 4 chunks
799        for i in 0..4 {
800            let data = vec![i as u8; INITIAL_CHUNK_SIZE + 1];
801            offsets.push((arena.alloc_copy(&data), data.len()));
802        }
803
804        // Verify all data is retrievable (exercises prefix_sums-based resolve_offset)
805        for (idx, &(offset, len)) in offsets.iter().enumerate() {
806            let data = arena.get(offset, len);
807            assert!(data.iter().all(|&b| b == idx as u8));
808        }
809    }
810
811    #[test]
812    fn prefix_sums_correct_after_reset() {
813        let mut arena = Arena::new();
814
815        // Force a second chunk
816        let big = vec![0xBB; INITIAL_CHUNK_SIZE + 1];
817        arena.alloc_copy(&big);
818        assert!(arena.chunks.len() >= 2);
819
820        arena.reset();
821
822        // After reset, alloc should work correctly with rebuilt prefix_sums
823        let o = arena.alloc_copy(b"after reset");
824        assert_eq!(arena.get(o, 11), b"after reset");
825    }
826
827    /// T-01: resolve_offset with global_offset=0 must return (0, 0)
828    #[test]
829    fn resolve_offset_zero() {
830        let arena = Arena::new();
831        let (chunk_idx, local) = arena.resolve_offset(0);
832        assert_eq!(chunk_idx, 0);
833        assert_eq!(local, 0);
834    }
835
836    /// Single-chunk fast-path in resolve_offset.
837    #[test]
838    fn resolve_offset_single_chunk_fast_path() {
839        let mut arena = Arena::new();
840        // Stay within one chunk
841        let o1 = arena.alloc_copy(b"hello");
842        let o2 = arena.alloc_copy(b"world");
843        assert_eq!(arena.chunks.len(), 1, "should be single chunk");
844
845        // resolve_offset uses fast-path
846        assert_eq!(arena.get(o1, 5), b"hello");
847        assert_eq!(arena.get(o2, 5), b"world");
848    }
849
850    // --- Audit gap tests ---
851
852    // #56: get_str with invalid UTF-8
853    #[test]
854    fn get_str_invalid_utf8_returns_none() {
855        let mut arena = Arena::new();
856        let offset = arena.alloc_copy(&[0xFF, 0xFE, 0xFD]);
857        assert_eq!(arena.get_str(offset, 3), None);
858    }
859
860    // #56 extra: get_str with valid UTF-8
861    #[test]
862    fn get_str_valid_utf8() {
863        let mut arena = Arena::new();
864        let offset = arena.alloc_copy("hello".as_bytes());
865        assert_eq!(arena.get_str(offset, 5), Some("hello"));
866    }
867
868    // #56 extra: get_str with empty string
869    #[test]
870    fn get_str_empty_returns_some_empty() {
871        let arena = Arena::new();
872        assert_eq!(arena.get_str(0, 0), Some(""));
873    }
874
875    // #57: get() with offset beyond bounds panics
876    #[test]
877    #[should_panic]
878    fn get_out_of_bounds_panics() {
879        let arena = Arena::new();
880        // Try to read beyond the arena (capacity is 8KB but nothing allocated)
881        arena.get(INITIAL_CHUNK_SIZE + 100, 1);
882    }
883
884    // #58: ensure_capacity reusing existing next chunk
885    #[test]
886    fn ensure_capacity_reuses_next_chunk() {
887        let mut arena = Arena::new();
888
889        // Fill first chunk to force a second
890        let big = vec![0xAA; INITIAL_CHUNK_SIZE + 1];
891        arena.alloc_copy(&big);
892        assert!(arena.chunks.len() >= 2);
893
894        // Reset (keeps small chunks)
895        arena.reset();
896        assert_eq!(arena.current, 0);
897        assert_eq!(arena.offset, 0);
898
899        // Now fill first chunk again — second alloc should reuse existing chunk
900        let filler = vec![0xBB; INITIAL_CHUNK_SIZE];
901        arena.alloc_copy(&filler);
902        // Next alloc should reuse the existing second chunk if capacity is sufficient
903        let o = arena.alloc_copy(b"reuse check");
904        assert_eq!(arena.get(o, 11), b"reuse check");
905    }
906
907    // #59: Multi-thread safety: acquire on thread A, release on thread B
908    #[test]
909    fn arena_cross_thread_no_crash() {
910        // Thread-local pools are per-thread, so this just verifies
911        // Arena is Send (can move between threads) without crashing.
912        let mut arena = Arena::new();
913        arena.alloc_copy(b"test data");
914
915        let handle = std::thread::spawn(move || {
916            // Arena moved to another thread — should not crash
917            assert_eq!(arena.get(0, 9), b"test data");
918            arena.reset();
919            arena
920        });
921
922        let arena = handle.join().unwrap();
923        // Release on the original thread's pool
924        release_arena(arena);
925    }
926
927    // --- ArenaRows tests (safe) ---
928
929    #[test]
930    fn arena_rows_basic() {
931        let arena = Arena::new();
932        let ar: ArenaRows<i64> = ArenaRows::new(vec![42], arena);
933        assert_eq!(ar.len(), 1);
934        assert!(!ar.is_empty());
935        assert_eq!(ar[0], 42);
936        assert_eq!(ar.get(0), Some(&42));
937    }
938
939    #[test]
940    fn arena_rows_empty() {
941        let arena = Arena::new();
942        let ar: ArenaRows<i64> = ArenaRows::new(vec![], arena);
943        assert!(ar.is_empty());
944        assert_eq!(ar.len(), 0);
945        assert!(ar.get(0).is_none());
946    }
947
948    #[test]
949    fn arena_rows_iter() {
950        let arena = Arena::new();
951        let ar: ArenaRows<i64> = ArenaRows::new(vec![10, 20, 30], arena);
952        let vals: Vec<&i64> = ar.iter().collect();
953        assert_eq!(vals, vec![&10, &20, &30]);
954    }
955
956    #[test]
957    fn arena_rows_deref() {
958        let arena = Arena::new();
959        let ar: ArenaRows<i64> = ArenaRows::new(vec![1, 2, 3], arena);
960        let slice: &[i64] = &ar;
961        assert_eq!(slice, &[1, 2, 3]);
962    }
963
964    #[test]
965    fn arena_rows_for_loop() {
966        let arena = Arena::new();
967        let ar: ArenaRows<i64> = ArenaRows::new(vec![10, 20], arena);
968        let mut sum = 0;
969        for &val in &ar {
970            sum += val;
971        }
972        assert_eq!(sum, 30);
973    }
974
975    #[test]
976    fn arena_rows_debug() {
977        let arena = Arena::new();
978        let ar: ArenaRows<i64> = ArenaRows::new(vec![42], arena);
979        let dbg = format!("{ar:?}");
980        assert!(dbg.contains("ArenaRows"));
981        assert!(dbg.contains("42"));
982    }
983
984    #[test]
985    fn arena_rows_arena_allocated() {
986        let mut arena = Arena::new();
987        arena.alloc_copy(b"some data");
988        let allocated = arena.allocated();
989        let ar: ArenaRows<i64> = ArenaRows::new(vec![], arena);
990        assert_eq!(ar.arena_allocated(), allocated);
991    }
992
993    #[test]
994    fn arena_rows_into_parts() {
995        let arena = Arena::new();
996        let ar: ArenaRows<i64> = ArenaRows::new(vec![1, 2, 3], arena);
997        let (v, _arena) = ar.into_parts();
998        assert_eq!(v, vec![1, 2, 3]);
999    }
1000
1001    #[test]
1002    fn arena_rows_into_parts_empty() {
1003        let arena = Arena::new();
1004        let ar: ArenaRows<i64> = ArenaRows::new(vec![], arena);
1005        let (v, _arena) = ar.into_parts();
1006        assert!(v.is_empty());
1007    }
1008
1009    #[test]
1010    fn arena_rows_get_out_of_bounds() {
1011        let arena = Arena::new();
1012        let ar: ArenaRows<i64> = ArenaRows::new(vec![42], arena);
1013        assert_eq!(ar.get(0), Some(&42));
1014        assert_eq!(ar.get(1), None);
1015        assert_eq!(ar.get(999), None);
1016    }
1017
1018    // --- ValidatedRows tests ---
1019
1020    #[test]
1021    fn validated_rows_basic() {
1022        let text_buf = String::from("alicebob");
1023        let blob_arena = Arena::new();
1024
1025        #[derive(Debug)]
1026        #[allow(dead_code)]
1027        struct Inner {
1028            id: i64,
1029            name_start: u32,
1030            name_end: u32,
1031        }
1032
1033        let rows = vec![
1034            Inner {
1035                id: 1,
1036                name_start: 0,
1037                name_end: 5,
1038            },
1039            Inner {
1040                id: 2,
1041                name_start: 5,
1042                name_end: 8,
1043            },
1044        ];
1045        let vr = ValidatedRows::new(rows, text_buf, blob_arena);
1046
1047        assert_eq!(vr.len(), 2);
1048        assert!(!vr.is_empty());
1049        assert_eq!(vr.text_slice(vr[0].name_start, vr[0].name_end), "alice");
1050        assert_eq!(vr.text_slice(vr[1].name_start, vr[1].name_end), "bob");
1051    }
1052
1053    #[test]
1054    fn validated_rows_empty() {
1055        let vr: ValidatedRows<i64> = ValidatedRows::new(vec![], String::new(), Arena::new());
1056        assert!(vr.is_empty());
1057        assert_eq!(vr.len(), 0);
1058        assert_eq!(vr.text_len(), 0);
1059    }
1060
1061    #[test]
1062    fn validated_rows_blob() {
1063        let mut blob_arena = Arena::new();
1064        let off = blob_arena.alloc_copy(&[0xDE, 0xAD]);
1065
1066        #[derive(Debug)]
1067        struct Inner {
1068            blob_off: u32,
1069            blob_len: u32,
1070        }
1071
1072        let rows = vec![Inner {
1073            blob_off: off as u32,
1074            blob_len: 2,
1075        }];
1076        let vr = ValidatedRows::new(rows, String::new(), blob_arena);
1077
1078        assert_eq!(vr.blob_slice(vr[0].blob_off, vr[0].blob_len), &[0xDE, 0xAD]);
1079    }
1080
1081    #[test]
1082    fn validated_rows_arena_allocated() {
1083        let mut blob_arena = Arena::new();
1084        blob_arena.alloc_copy(&[1, 2, 3]);
1085        let text_buf = String::from("hello");
1086
1087        let vr: ValidatedRows<i64> = ValidatedRows::new(vec![], text_buf, blob_arena);
1088        assert_eq!(vr.arena_allocated(), 5 + 3); // text_len + blob_allocated
1089    }
1090
1091    #[test]
1092    fn validated_rows_debug() {
1093        let vr: ValidatedRows<i64> = ValidatedRows::new(vec![42], String::new(), Arena::new());
1094        let dbg = format!("{vr:?}");
1095        assert!(dbg.contains("ValidatedRows"));
1096        assert!(dbg.contains("42"));
1097    }
1098
1099    #[test]
1100    fn validated_rows_deref() {
1101        let vr: ValidatedRows<i64> = ValidatedRows::new(vec![1, 2, 3], String::new(), Arena::new());
1102        let slice: &[i64] = &vr;
1103        assert_eq!(slice, &[1, 2, 3]);
1104    }
1105
1106    #[test]
1107    fn validated_rows_iter() {
1108        let vr: ValidatedRows<i64> = ValidatedRows::new(vec![10, 20], String::new(), Arena::new());
1109        let mut sum = 0;
1110        for &val in &vr {
1111            sum += val;
1112        }
1113        assert_eq!(sum, 30);
1114    }
1115
1116    // --- alloc zero length slice ---
1117
1118    #[test]
1119    fn alloc_zero_returns_empty_slice() {
1120        let mut arena = Arena::new();
1121        let slice = arena.alloc(0);
1122        assert!(slice.is_empty());
1123    }
1124
1125    // --- get_str with zero length ---
1126
1127    #[test]
1128    fn get_str_zero_len_returns_empty() {
1129        let arena = Arena::new();
1130        assert_eq!(arena.get_str(0, 0), Some(""));
1131    }
1132
1133    // ===============================================================
1134    // ValidatedRows — comprehensive tests
1135    // ===============================================================
1136
1137    #[test]
1138    fn validated_rows_empty_text_buf() {
1139        let vr: ValidatedRows<i64> = ValidatedRows::new(vec![1, 2, 3], String::new(), Arena::new());
1140        assert_eq!(vr.text(), "");
1141        assert_eq!(vr.text_len(), 0);
1142        assert_eq!(vr.len(), 3);
1143    }
1144
1145    #[test]
1146    fn validated_rows_blob_only_no_text() {
1147        let mut blob_arena = Arena::new();
1148        let o1 = blob_arena.alloc_copy(&[0x01, 0x02, 0x03]);
1149        let o2 = blob_arena.alloc_copy(&[0xAA, 0xBB]);
1150
1151        #[derive(Debug)]
1152        struct Inner {
1153            off: u32,
1154            len: u32,
1155        }
1156
1157        let rows = vec![
1158            Inner {
1159                off: o1 as u32,
1160                len: 3,
1161            },
1162            Inner {
1163                off: o2 as u32,
1164                len: 2,
1165            },
1166        ];
1167        let vr = ValidatedRows::new(rows, String::new(), blob_arena);
1168        assert_eq!(vr.text_len(), 0);
1169        assert_eq!(vr.blob_slice(vr[0].off, vr[0].len), &[0x01, 0x02, 0x03]);
1170        assert_eq!(vr.blob_slice(vr[1].off, vr[1].len), &[0xAA, 0xBB]);
1171    }
1172
1173    #[test]
1174    #[should_panic]
1175    fn validated_rows_text_slice_out_of_bounds() {
1176        let vr: ValidatedRows<i64> = ValidatedRows::new(vec![], String::from("hi"), Arena::new());
1177        // end is beyond the text buffer
1178        vr.text_slice(0, 100);
1179    }
1180
1181    #[test]
1182    #[should_panic]
1183    fn validated_rows_blob_slice_out_of_bounds() {
1184        let blob_arena = Arena::new();
1185        let vr: ValidatedRows<i64> = ValidatedRows::new(vec![], String::new(), blob_arena);
1186        // nothing allocated in blob arena
1187        vr.blob_slice(0, 100);
1188    }
1189
1190    #[test]
1191    fn validated_rows_large_10k_rows() {
1192        let mut text_buf = String::new();
1193        let blob_arena = Arena::new();
1194
1195        #[derive(Debug)]
1196        struct Inner {
1197            start: u32,
1198            end: u32,
1199        }
1200
1201        let mut rows = Vec::with_capacity(10_000);
1202        for i in 0..10_000u32 {
1203            let start = text_buf.len() as u32;
1204            text_buf.push_str(&format!("row_{i}"));
1205            let end = text_buf.len() as u32;
1206            rows.push(Inner { start, end });
1207        }
1208
1209        let vr = ValidatedRows::new(rows, text_buf, blob_arena);
1210        assert_eq!(vr.len(), 10_000);
1211        assert_eq!(vr.text_slice(vr[0].start, vr[0].end), "row_0");
1212        assert_eq!(vr.text_slice(vr[9999].start, vr[9999].end), "row_9999");
1213    }
1214
1215    #[test]
1216    fn validated_rows_text_slice_empty_range() {
1217        let vr: ValidatedRows<i64> =
1218            ValidatedRows::new(vec![], String::from("hello"), Arena::new());
1219        assert_eq!(vr.text_slice(0, 0), "");
1220        assert_eq!(vr.text_slice(3, 3), "");
1221    }
1222
1223    #[test]
1224    fn validated_rows_get_inner() {
1225        let vr: ValidatedRows<i64> =
1226            ValidatedRows::new(vec![10, 20, 30], String::new(), Arena::new());
1227        assert_eq!(vr.get_inner(0), Some(&10));
1228        assert_eq!(vr.get_inner(1), Some(&20));
1229        assert_eq!(vr.get_inner(2), Some(&30));
1230        assert_eq!(vr.get_inner(3), None);
1231    }
1232
1233    #[test]
1234    fn validated_rows_iter_inner() {
1235        let vr: ValidatedRows<i64> = ValidatedRows::new(vec![5, 10], String::new(), Arena::new());
1236        let vals: Vec<&i64> = vr.iter_inner().collect();
1237        assert_eq!(vals, vec![&5, &10]);
1238    }
1239
1240    #[test]
1241    fn validated_rows_blob_allocated_zero() {
1242        let vr: ValidatedRows<i64> = ValidatedRows::new(vec![], String::new(), Arena::new());
1243        assert_eq!(vr.blob_allocated(), 0);
1244    }
1245
1246    // ===============================================================
1247    // Arena — additional edge cases
1248    // ===============================================================
1249
1250    #[test]
1251    fn arena_get_zero_len() {
1252        let arena = Arena::new();
1253        let data = arena.get(0, 0);
1254        assert!(data.is_empty());
1255    }
1256
1257    #[test]
1258    fn arena_alloc_copy_zero_len() {
1259        let mut arena = Arena::new();
1260        let offset = arena.alloc_copy(b"");
1261        assert_eq!(arena.get(offset, 0), &[]);
1262    }
1263
1264    #[test]
1265    fn arena_global_offset_initial() {
1266        let arena = Arena::new();
1267        assert_eq!(arena.global_offset(), 0);
1268    }
1269
1270    #[test]
1271    fn arena_global_offset_advances() {
1272        let mut arena = Arena::new();
1273        arena.alloc_copy(b"12345");
1274        assert_eq!(arena.global_offset(), 5);
1275        arena.alloc_copy(b"67890");
1276        assert_eq!(arena.global_offset(), 10);
1277    }
1278
1279    #[test]
1280    fn arena_multiple_resets() {
1281        let mut arena = Arena::new();
1282        for _ in 0..10 {
1283            arena.alloc_copy(b"data");
1284            assert_eq!(arena.allocated(), 4);
1285            arena.reset();
1286            assert_eq!(arena.allocated(), 0);
1287        }
1288    }
1289
1290    #[test]
1291    fn arena_get_str_unicode() {
1292        let texts = [
1293            "\u{1F600}\u{1F4A9}",         // emoji
1294            "\u{4e16}\u{754c}",           // CJK
1295            "caf\u{00e9}",                // accented
1296            "\u{1F468}\u{200D}\u{1F469}", // ZWJ
1297        ];
1298        for text in &texts {
1299            let mut arena = Arena::new();
1300            let offset = arena.alloc_copy(text.as_bytes());
1301            assert_eq!(
1302                arena.get_str(offset, text.len()),
1303                Some(*text),
1304                "failed for text: {text}"
1305            );
1306        }
1307    }
1308
1309    #[test]
1310    fn arena_get_str_partial_utf8_returns_none() {
1311        // 0xC3 is the start of a 2-byte UTF-8 sequence, incomplete without the second byte
1312        let mut arena = Arena::new();
1313        let offset = arena.alloc_copy(&[0xC3]);
1314        assert_eq!(arena.get_str(offset, 1), None);
1315    }
1316
1317    #[test]
1318    fn arena_default_is_new() {
1319        let a1 = Arena::new();
1320        let a2 = Arena::default();
1321        assert_eq!(a1.allocated(), a2.allocated());
1322        assert_eq!(a1.capacity(), a2.capacity());
1323    }
1324
1325    // ===============================================================
1326    // ArenaRows — additional edge cases
1327    // ===============================================================
1328
1329    #[test]
1330    fn arena_rows_large() {
1331        let arena = Arena::new();
1332        let rows: Vec<i64> = (0..1000).collect();
1333        let ar = ArenaRows::new(rows, arena);
1334        assert_eq!(ar.len(), 1000);
1335        assert_eq!(ar[0], 0);
1336        assert_eq!(ar[999], 999);
1337    }
1338
1339    #[test]
1340    fn arena_rows_with_arena_data() {
1341        let mut arena = Arena::new();
1342        let offset = arena.alloc_copy(b"stored data");
1343
1344        #[derive(Debug)]
1345        #[allow(dead_code)]
1346        struct Inner {
1347            off: usize,
1348            len: usize,
1349        }
1350
1351        let ar = ArenaRows::new(
1352            vec![Inner {
1353                off: offset,
1354                len: 11,
1355            }],
1356            arena,
1357        );
1358        assert_eq!(ar.len(), 1);
1359    }
1360
1361    // ===============================================================
1362    // Thread-local pool edge cases
1363    // ===============================================================
1364
1365    #[test]
1366    fn thread_local_pool_acquire_fresh() {
1367        // Drain the pool first
1368        ARENA_POOL.with(|pool| pool.borrow_mut().clear());
1369        let arena = acquire_arena();
1370        assert_eq!(arena.allocated(), 0);
1371        release_arena(arena);
1372    }
1373
1374    #[test]
1375    fn thread_local_pool_recycle_resets() {
1376        let mut arena = Arena::new();
1377        arena.alloc_copy(b"something");
1378        assert!(arena.allocated() > 0);
1379        release_arena(arena);
1380
1381        let arena2 = acquire_arena();
1382        assert_eq!(arena2.allocated(), 0, "recycled arena should be reset");
1383        release_arena(arena2);
1384    }
1385
1386    // --- Audit: arena cannot return stale data after reset ---
1387
1388    #[test]
1389    fn arena_reset_clears_data_positions() {
1390        let mut arena = Arena::new();
1391        let o1 = arena.alloc_copy(b"first query data");
1392        assert_eq!(arena.get(o1, 16), b"first query data");
1393
1394        arena.reset();
1395        assert_eq!(arena.allocated(), 0);
1396        assert_eq!(arena.current, 0);
1397        assert_eq!(arena.offset, 0);
1398
1399        // After reset, a new alloc should produce offset 0 (same as o1)
1400        // but the data is different. No stale data leaks.
1401        let o2 = arena.alloc_copy(b"second query dat");
1402        assert_eq!(o2, 0, "first alloc after reset should be at offset 0");
1403        assert_eq!(arena.get(o2, 16), b"second query dat");
1404    }
1405
1406    #[test]
1407    fn arena_reset_discards_oversized_chunks() {
1408        let mut arena = Arena::new();
1409        // Allocate a 128KB blob (> SHRINK_THRESHOLD of 64KB)
1410        let big = vec![0xAA; 128 * 1024];
1411        arena.alloc_copy(&big);
1412        let cap_before = arena.capacity();
1413        assert!(cap_before >= 128 * 1024);
1414
1415        arena.reset();
1416        let cap_after = arena.capacity();
1417        // Oversized chunks should be discarded — capacity should shrink
1418        assert!(
1419            cap_after < cap_before,
1420            "oversized chunks should be discarded on reset: before={cap_before}, after={cap_after}"
1421        );
1422    }
1423
1424    // --- Audit: alloc_copy zero-length returns stable offset ---
1425
1426    #[test]
1427    fn alloc_copy_zero_length_returns_valid_offset() {
1428        let mut arena = Arena::new();
1429        let o1 = arena.alloc_copy(b"");
1430        let o2 = arena.alloc_copy(b"hello");
1431        // Zero-length alloc should return a valid global offset
1432        // without advancing the bump pointer.
1433        assert_eq!(o1, o2, "zero-length alloc should not advance offset");
1434        assert_eq!(arena.get(o2, 5), b"hello");
1435    }
1436
1437    // --- Audit: get with zero length returns empty slice ---
1438
1439    #[test]
1440    fn get_zero_length_returns_empty() {
1441        let arena = Arena::new();
1442        assert_eq!(arena.get(0, 0), &[]);
1443        assert_eq!(arena.get(9999, 0), &[]);
1444    }
1445
1446    // --- Arena::empty() edge cases ---
1447
1448    // Arena::empty() is designed for query paths that use data_buf instead of arena.
1449    // Direct alloc on an empty arena (no chunks) panics because it indexes into
1450    // an empty chunks vec. Call reset() first to initialize a chunk.
1451
1452    #[test]
1453    #[should_panic]
1454    fn arena_empty_alloc_copy_panics_without_reset() {
1455        let mut arena = Arena::empty();
1456        assert_eq!(arena.chunks.len(), 0);
1457        // alloc_copy on empty arena panics -- must call reset() first
1458        let _ = arena.alloc_copy(b"boom");
1459    }
1460
1461    #[test]
1462    #[should_panic]
1463    fn arena_empty_alloc_panics_without_reset() {
1464        let mut arena = Arena::empty();
1465        // alloc on empty arena panics -- must call reset() first
1466        let _ = arena.alloc(8);
1467    }
1468
1469    #[test]
1470    fn arena_empty_reset_does_not_panic() {
1471        let mut arena = Arena::empty();
1472        // reset on empty arena should not crash
1473        arena.reset();
1474        // After reset, arena should be usable with a fresh chunk
1475        assert!(
1476            !arena.chunks.is_empty(),
1477            "reset should create initial chunk if empty"
1478        );
1479        assert_eq!(arena.allocated(), 0);
1480        assert_eq!(arena.current, 0);
1481        assert_eq!(arena.offset, 0);
1482    }
1483
1484    #[test]
1485    fn arena_empty_reset_then_alloc() {
1486        let mut arena = Arena::empty();
1487        arena.reset();
1488        let offset = arena.alloc_copy(b"after reset on empty");
1489        assert_eq!(arena.get(offset, 20), b"after reset on empty");
1490    }
1491
1492    #[test]
1493    fn arena_empty_capacity_is_zero() {
1494        let arena = Arena::empty();
1495        assert_eq!(arena.capacity(), 0);
1496        assert_eq!(arena.allocated(), 0);
1497    }
1498
1499    #[test]
1500    fn arena_empty_reset_then_multiple_allocs() {
1501        let mut arena = Arena::empty();
1502        arena.reset(); // initialize a chunk
1503        let o1 = arena.alloc_copy(b"first");
1504        let o2 = arena.alloc_copy(b"second");
1505        assert_eq!(arena.get(o1, 5), b"first");
1506        assert_eq!(arena.get(o2, 6), b"second");
1507    }
1508
1509    // --- alloc_copy exactly at chunk boundary ---
1510
1511    #[test]
1512    fn alloc_copy_exactly_fills_chunk() {
1513        let mut arena = Arena::new();
1514        // Fill exactly to the 8KB boundary
1515        let data = vec![0xCC; INITIAL_CHUNK_SIZE];
1516        let offset = arena.alloc_copy(&data);
1517        assert_eq!(arena.chunks.len(), 1, "should still be one chunk");
1518        assert_eq!(arena.get(offset, INITIAL_CHUNK_SIZE)[0], 0xCC);
1519        assert_eq!(arena.allocated(), INITIAL_CHUNK_SIZE);
1520
1521        // Allocating 0 bytes should not trigger a new chunk
1522        let o2 = arena.alloc_copy(b"");
1523        assert_eq!(arena.get(o2, 0), &[]);
1524    }
1525
1526    // --- get with offset=0, len=0 on empty Arena ---
1527
1528    #[test]
1529    fn arena_empty_get_zero_len() {
1530        // Even on Arena::empty(), get with len=0 should return empty slice
1531        // without panicking (no chunks to index into)
1532        let arena = Arena::empty();
1533        assert_eq!(arena.get(0, 0), &[]);
1534    }
1535
1536    // --- Gap: get_str with unicode multi-byte ---
1537
1538    #[test]
1539    fn get_str_unicode_multibyte() {
1540        let mut arena = Arena::new();
1541        let text = "\u{1F600}\u{00E9}";
1542        let offset = arena.alloc_copy(text.as_bytes());
1543        assert_eq!(arena.get_str(offset, text.len()), Some(text));
1544    }
1545
1546    // --- Gap: release_arena of Arena::empty() does not panic ---
1547
1548    #[test]
1549    fn release_arena_empty_does_not_panic() {
1550        let arena = Arena::empty();
1551        release_arena(arena); // Should not panic even though no chunks
1552    }
1553
1554    // --- Gap: ValidatedRows blob_slice ---
1555
1556    #[test]
1557    fn validated_rows_blob_slice_roundtrip() {
1558        let mut arena = Arena::new();
1559        let offset = arena.alloc_copy(&[0xDE, 0xAD]);
1560        let rows: ValidatedRows<u32> =
1561            ValidatedRows::new(vec![offset as u32], String::new(), arena);
1562        let blob = rows.blob_slice(offset as u32, 2);
1563        assert_eq!(blob, &[0xDE, 0xAD]);
1564    }
1565
1566    // --- Gap: ValidatedRows deref and into_iter ---
1567
1568    #[test]
1569    fn validated_rows_deref_and_for_loop() {
1570        let arena = Arena::new();
1571        let rows: ValidatedRows<i32> = ValidatedRows::new(vec![1, 2, 3], String::new(), arena);
1572        let slice: &[i32] = &rows;
1573        assert_eq!(slice, &[1, 2, 3]);
1574
1575        let mut sum = 0;
1576        for &v in &rows {
1577            sum += v;
1578        }
1579        assert_eq!(sum, 6);
1580    }
1581}