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