vibesql-storage 0.2.0

Storage engine for vibesql SQL database
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
//! Query operations for B+ Tree
//!
//! This module handles key lookups, range scans, and multi-key queries
//! with full support for non-unique indexes (duplicate keys).

use super::{
    super::structure::{Key, LeafNode, RowId},
    BTreeIndex,
};
use crate::StorageError;

impl BTreeIndex {
    /// Look up all row IDs for a given key
    ///
    /// For non-unique indexes, this returns all row IDs associated with the key.
    /// For unique indexes, the Vec will contain at most one element.
    ///
    /// # Arguments
    /// * `key` - The key to search for
    ///
    /// # Returns
    /// * Vector of row_ids associated with this key (empty Vec if key not found)
    ///
    /// # Performance
    /// - O(log n) to find the key (where n = number of unique keys)
    /// - O(k) to clone row IDs (where k = number of duplicates)
    /// - Total: O(log n + k)
    ///
    /// # Example
    /// ```text
    /// use vibesql_types::SqlValue;
    ///
    /// // Insert duplicate keys
    /// index.insert(vec![SqlValue::Integer(42)], 1)?;
    /// index.insert(vec![SqlValue::Integer(42)], 2)?;
    /// index.insert(vec![SqlValue::Integer(42)], 3)?;
    ///
    /// // Lookup returns all row IDs
    /// let rows = index.lookup(&vec![SqlValue::Integer(42)])?;
    /// assert_eq!(rows, vec![1, 2, 3]);
    ///
    /// // Nonexistent key returns empty Vec
    /// let rows = index.lookup(&vec![SqlValue::Integer(99)])?;
    /// assert!(rows.is_empty());
    /// ```
    ///
    /// # Algorithm
    /// 1. Navigate to the appropriate leaf node using find_leaf_path
    /// 2. Search for the key in the leaf node
    /// 3. Return all row_ids associated with the key (or empty Vec if not found)
    pub fn lookup(&self, key: &Key) -> Result<Vec<RowId>, StorageError> {
        // Find the leaf that would contain this key
        let (mut current_leaf, _) = self.find_leaf_path(key)?;

        let mut result = Vec::new();

        // Collect all row_ids from entries matching this key
        // This handles the case where large row_id lists are split across multiple entries
        loop {
            // Use binary search to find the first entry with matching key
            let start_idx =
                current_leaf.entries.binary_search_by(|(k, _)| k.cmp(key)).unwrap_or_else(|i| i);

            // Collect all consecutive entries with matching key
            for idx in start_idx..current_leaf.entries.len() {
                let (entry_key, row_ids) = &current_leaf.entries[idx];
                if entry_key == key {
                    result.extend(row_ids.iter().copied());
                } else {
                    // Past the key, we're done with this leaf
                    // If entry_key > key, we've gone past and can return
                    return Ok(result);
                }
            }

            // Check if we need to continue to the next leaf
            // This is needed when entries with the same key span multiple leaves
            if current_leaf.next_leaf == super::super::super::NULL_PAGE_ID {
                break;
            }

            // Check if next leaf might have more entries with this key
            // by looking at the first entry of the next leaf
            let next_leaf = self.read_leaf_node(current_leaf.next_leaf)?;
            if let Some((first_key, _)) = next_leaf.entries.first() {
                if first_key == key {
                    current_leaf = next_leaf;
                    continue;
                }
            }
            // Next leaf doesn't start with our key, we're done
            break;
        }

        Ok(result)
    }

    /// Perform a range scan on the B+ tree
    ///
    /// Returns all row_ids for keys in the specified range [start_key, end_key].
    /// The range can be inclusive or exclusive on either end based on the parameters.
    ///
    /// For non-unique indexes with duplicate keys, all row IDs for each key in the
    /// range are included in the result. The row IDs are returned in key order, with
    /// all duplicates for each key grouped together.
    ///
    /// # Arguments
    /// * `start_key` - Optional lower bound key (None = start from beginning)
    /// * `end_key` - Optional upper bound key (None = scan to end)
    /// * `inclusive_start` - Whether start_key is inclusive (default: true)
    /// * `inclusive_end` - Whether end_key is inclusive (default: true)
    ///
    /// # Returns
    /// Vector of row_ids in sorted key order (includes all duplicates)
    ///
    /// # Performance
    /// - O(log n) to find starting leaf (where n = number of unique keys)
    /// - O(m + k) to scan through results (where m = keys in range, k = total duplicates)
    /// - Total: O(log n + m + k)
    ///
    /// # Example
    /// ```text
    /// use vibesql_types::SqlValue;
    ///
    /// // Insert some data with duplicates
    /// index.insert(vec![SqlValue::Integer(10)], 1)?;
    /// index.insert(vec![SqlValue::Integer(20)], 2)?;
    /// index.insert(vec![SqlValue::Integer(20)], 3)?; // duplicate
    /// index.insert(vec![SqlValue::Integer(30)], 4)?;
    ///
    /// // Range scan [15, 25] returns both row IDs for key 20
    /// let rows = index.range_scan(
    ///     Some(&vec![SqlValue::Integer(15)]),
    ///     Some(&vec![SqlValue::Integer(25)]),
    ///     true,
    ///     true
    /// )?;
    /// assert_eq!(rows, vec![2, 3]); // Both duplicates included
    /// ```
    ///
    /// # Algorithm
    /// 1. Find the starting leaf node
    /// 2. Iterate through leaf nodes using next_leaf pointers
    /// 3. Collect all row_ids (including duplicates) within the range
    /// 4. Stop when reaching the end key or end of tree
    pub fn range_scan(
        &self,
        start_key: Option<&Key>,
        end_key: Option<&Key>,
        inclusive_start: bool,
        inclusive_end: bool,
    ) -> Result<Vec<RowId>, StorageError> {
        let mut result = Vec::new();

        // Find starting point
        let mut current_leaf = if let Some(start) = start_key {
            // Find leaf containing start_key
            let (leaf, _) = self.find_leaf_path(start)?;
            leaf
        } else {
            // Start from leftmost leaf
            self.find_leftmost_leaf()?
        };

        // Track whether we've started collecting results (passed start_key check)
        // Once true, we can skip redundant start_key comparisons
        let mut started = start_key.is_none();

        // Scan through leaves
        loop {
            // Process entries in current leaf
            for (key, row_ids) in &current_leaf.entries {
                // Check if we're past the end key
                if let Some(end) = end_key {
                    let cmp = key.cmp(end);
                    if cmp == std::cmp::Ordering::Greater {
                        // Past end key, stop
                        return Ok(result);
                    }
                    if cmp == std::cmp::Ordering::Equal && !inclusive_end {
                        // At end key but not inclusive, stop
                        return Ok(result);
                    }
                }

                // Check if we're before the start key (only if we haven't started yet)
                if !started {
                    if let Some(start) = start_key {
                        let cmp = key.cmp(start);
                        if cmp == std::cmp::Ordering::Less {
                            // Before start key, skip
                            continue;
                        }
                        if cmp == std::cmp::Ordering::Equal && !inclusive_start {
                            // At start key but not inclusive, skip
                            continue;
                        }
                        // We've now passed the start_key check
                        started = true;
                    }
                }

                // Key is in range, add all row_ids
                result.extend(row_ids.iter().copied());
            }

            // Move to next leaf
            if current_leaf.next_leaf == super::super::super::NULL_PAGE_ID {
                // No more leaves
                break;
            }
            current_leaf = self.read_leaf_node(current_leaf.next_leaf)?;
        }

        Ok(result)
    }

    /// Perform a range scan returning only the first matching row
    ///
    /// This is an optimized version of range_scan for queries with LIMIT 1.
    /// It returns immediately after finding the first matching row instead of
    /// collecting all rows in the range.
    ///
    /// # Arguments
    /// * `start_key` - Optional lower bound key (None = start from beginning)
    /// * `end_key` - Optional upper bound key (None = scan to end)
    /// * `inclusive_start` - Whether start_key is inclusive
    /// * `inclusive_end` - Whether end_key is inclusive
    ///
    /// # Returns
    /// The first row_id in the range, or None if no matching rows
    ///
    /// # Performance
    /// O(log n) - only finds the first leaf and returns immediately
    ///
    /// # Example
    /// ```text
    /// // TPC-C Delivery: find oldest new_order for a district
    /// // Only returns the first (minimum) order ID
    /// let first = index.range_scan_first(
    ///     Some(&vec![w_id, d_id]),  // prefix start
    ///     Some(&vec![w_id, d_id + 1]),  // prefix end
    ///     true, false
    /// )?;
    /// ```
    pub fn range_scan_first(
        &self,
        start_key: Option<&Key>,
        end_key: Option<&Key>,
        inclusive_start: bool,
        inclusive_end: bool,
    ) -> Result<Option<RowId>, StorageError> {
        // Find starting point
        let mut current_leaf = if let Some(start) = start_key {
            let (leaf, _) = self.find_leaf_path(start)?;
            leaf
        } else {
            self.find_leftmost_leaf()?
        };

        let mut started = start_key.is_none();

        // Scan through leaves looking for first match
        loop {
            for (key, row_ids) in &current_leaf.entries {
                // Check if we're past the end key
                if let Some(end) = end_key {
                    let cmp = key.cmp(end);
                    if cmp == std::cmp::Ordering::Greater {
                        return Ok(None);
                    }
                    if cmp == std::cmp::Ordering::Equal && !inclusive_end {
                        return Ok(None);
                    }
                }

                // Check if we're before the start key
                if !started {
                    if let Some(start) = start_key {
                        let cmp = key.cmp(start);
                        if cmp == std::cmp::Ordering::Less {
                            continue;
                        }
                        if cmp == std::cmp::Ordering::Equal && !inclusive_start {
                            continue;
                        }
                        started = true;
                    }
                }

                // Found a key in range - return immediately
                if let Some(&first_row) = row_ids.first() {
                    return Ok(Some(first_row));
                }
            }

            // Move to next leaf
            if current_leaf.next_leaf == super::super::super::NULL_PAGE_ID {
                break;
            }
            current_leaf = self.read_leaf_node(current_leaf.next_leaf)?;
        }

        Ok(None)
    }

    /// Lookup multiple keys in the B+ tree (for IN predicates)
    ///
    /// # Arguments
    /// * `keys` - List of keys to look up
    ///
    /// # Returns
    /// Vector of row_ids for all keys that were found
    ///
    /// # Algorithm
    /// For each key, perform a lookup and collect all row_ids.
    /// This is a simple implementation that performs individual lookups.
    /// A more optimized version could sort keys and batch lookups by leaf node.
    pub fn multi_lookup(&self, keys: &[Key]) -> Result<Vec<RowId>, StorageError> {
        let mut result = Vec::new();

        for key in keys {
            let row_ids = self.lookup(key)?;
            result.extend(row_ids);
        }

        Ok(result)
    }

    /// Find the leftmost (first) leaf node in the tree
    ///
    /// # Returns
    /// The leftmost leaf node
    pub(crate) fn find_leftmost_leaf(&self) -> Result<LeafNode, StorageError> {
        let mut current_page_id = self.root_page_id;

        // Navigate down the tree always taking the leftmost child
        for _ in 0..self.height - 1 {
            let internal = self.read_internal_node(current_page_id)?;
            if internal.children.is_empty() {
                return Err(StorageError::IoError("Internal node has no children".to_string()));
            }
            // Always take first child (leftmost)
            current_page_id = internal.children[0];
        }

        // Read the leftmost leaf
        self.read_leaf_node(current_page_id)
    }

    /// Find the rightmost (last) leaf node in the tree
    ///
    /// # Returns
    /// The rightmost leaf node
    pub(crate) fn find_rightmost_leaf(&self) -> Result<LeafNode, StorageError> {
        let mut current_page_id = self.root_page_id;

        // Navigate down the tree always taking the rightmost child
        for _ in 0..self.height - 1 {
            let internal = self.read_internal_node(current_page_id)?;
            if internal.children.is_empty() {
                return Err(StorageError::IoError("Internal node has no children".to_string()));
            }
            // Always take last child (rightmost)
            current_page_id = *internal.children.last().unwrap();
        }

        // Read the rightmost leaf
        self.read_leaf_node(current_page_id)
    }

    /// Perform a reverse range scan on the B+ tree
    ///
    /// Returns all row_ids for keys in the specified range [start_key, end_key]
    /// in descending key order. This uses the prev_leaf pointers for efficient
    /// backward traversal.
    ///
    /// # Arguments
    /// * `start_key` - Optional lower bound key (None = start from beginning)
    /// * `end_key` - Optional upper bound key (None = scan to end)
    /// * `inclusive_start` - Whether start_key is inclusive
    /// * `inclusive_end` - Whether end_key is inclusive
    ///
    /// # Returns
    /// Vector of row_ids in descending key order
    ///
    /// # Performance
    /// - O(log n) to find the ending leaf
    /// - O(m + k) to scan backwards through results
    /// - Total: O(log n + m + k)
    pub fn range_scan_reverse(
        &self,
        start_key: Option<&Key>,
        end_key: Option<&Key>,
        inclusive_start: bool,
        inclusive_end: bool,
    ) -> Result<Vec<RowId>, StorageError> {
        let mut result = Vec::new();

        // Find ending point (we start from the highest key and work backwards)
        let mut current_leaf = if let Some(end) = end_key {
            // Find leaf containing end_key
            let (leaf, _) = self.find_leaf_path(end)?;
            leaf
        } else {
            // Start from rightmost leaf
            self.find_rightmost_leaf()?
        };

        // Track whether we've started collecting results (passed end_key check)
        let mut started = end_key.is_none();

        // Scan through leaves in reverse order
        loop {
            // Process entries in current leaf in reverse order
            for (key, row_ids) in current_leaf.entries.iter().rev() {
                // Check if we're before the start key (in reverse, this means we've gone too far
                // back)
                if let Some(start) = start_key {
                    let cmp = key.cmp(start);
                    if cmp == std::cmp::Ordering::Less {
                        // Before start key, stop
                        return Ok(result);
                    }
                    if cmp == std::cmp::Ordering::Equal && !inclusive_start {
                        // At start key but not inclusive, stop
                        return Ok(result);
                    }
                }

                // Check if we're past the end key (only if we haven't started yet)
                if !started {
                    if let Some(end) = end_key {
                        let cmp = key.cmp(end);
                        if cmp == std::cmp::Ordering::Greater {
                            // Past end key, skip
                            continue;
                        }
                        if cmp == std::cmp::Ordering::Equal && !inclusive_end {
                            // At end key but not inclusive, skip
                            continue;
                        }
                        // We've now passed the end_key check
                        started = true;
                    }
                }

                // Key is in range, add all row_ids in reverse order
                result.extend(row_ids.iter().rev());
            }

            // Move to previous leaf
            if current_leaf.prev_leaf == super::super::super::NULL_PAGE_ID {
                // No more leaves
                break;
            }
            current_leaf = self.read_leaf_node(current_leaf.prev_leaf)?;
        }

        Ok(result)
    }

    /// Perform a reverse range scan returning only the first matching row
    ///
    /// This is an optimized version of range_scan_reverse for queries with LIMIT 1.
    /// It returns immediately after finding the first matching row (highest key).
    ///
    /// # Arguments
    /// * `start_key` - Optional lower bound key (None = start from beginning)
    /// * `end_key` - Optional upper bound key (None = scan to end)
    /// * `inclusive_start` - Whether start_key is inclusive
    /// * `inclusive_end` - Whether end_key is inclusive
    ///
    /// # Returns
    /// The first row_id in descending key order, or None if no matching rows
    ///
    /// # Performance
    /// O(log n) - only finds the ending leaf and returns immediately
    pub fn range_scan_reverse_first(
        &self,
        start_key: Option<&Key>,
        end_key: Option<&Key>,
        inclusive_start: bool,
        inclusive_end: bool,
    ) -> Result<Option<RowId>, StorageError> {
        // Find ending point
        let mut current_leaf = if let Some(end) = end_key {
            let (leaf, _) = self.find_leaf_path(end)?;
            leaf
        } else {
            self.find_rightmost_leaf()?
        };

        let mut started = end_key.is_none();

        // Scan through leaves in reverse looking for first match
        loop {
            for (key, row_ids) in current_leaf.entries.iter().rev() {
                // Check if we're before the start key
                if let Some(start) = start_key {
                    let cmp = key.cmp(start);
                    if cmp == std::cmp::Ordering::Less {
                        return Ok(None);
                    }
                    if cmp == std::cmp::Ordering::Equal && !inclusive_start {
                        return Ok(None);
                    }
                }

                // Check if we're past the end key
                if !started {
                    if let Some(end) = end_key {
                        let cmp = key.cmp(end);
                        if cmp == std::cmp::Ordering::Greater {
                            continue;
                        }
                        if cmp == std::cmp::Ordering::Equal && !inclusive_end {
                            continue;
                        }
                        started = true;
                    }
                }

                // Found a key in range - return immediately (last element for DESC order)
                if let Some(&last_row) = row_ids.last() {
                    return Ok(Some(last_row));
                }
            }

            // Move to previous leaf
            if current_leaf.prev_leaf == super::super::super::NULL_PAGE_ID {
                break;
            }
            current_leaf = self.read_leaf_node(current_leaf.prev_leaf)?;
        }

        Ok(None)
    }

    /// Get distinct N-column prefixes from a composite index
    ///
    /// This generalizes single-column skip-scan to multi-column skip-scan.
    /// It returns all distinct N-column prefixes, which are then used for
    /// targeted lookups when filtering on columns beyond the Nth position.
    ///
    /// # Arguments
    /// * `num_columns` - Number of prefix columns to extract (1 = first column only)
    ///
    /// # Returns
    /// Vector of distinct N-column prefixes in sorted order
    ///
    /// # Performance
    /// O(n) where n is the number of unique keys - must scan all leaves
    pub fn get_distinct_prefix_values(
        &self,
        num_columns: usize,
    ) -> Result<Vec<Vec<vibesql_types::SqlValue>>, StorageError> {
        if num_columns == 0 {
            return Ok(vec![vec![]]); // Empty prefix matches everything
        }

        let mut prefixes = Vec::new();
        let mut last_prefix: Option<Vec<vibesql_types::SqlValue>> = None;

        // Start from leftmost leaf
        let mut current_leaf = self.find_leftmost_leaf()?;

        loop {
            for (key, _) in &current_leaf.entries {
                if key.len() < num_columns {
                    continue; // Key doesn't have enough columns
                }

                let prefix: Vec<vibesql_types::SqlValue> = key[..num_columns].to_vec();
                let is_new = match &last_prefix {
                    None => true,
                    Some(last) => prefix != *last,
                };
                if is_new {
                    prefixes.push(prefix.clone());
                    last_prefix = Some(prefix);
                }
            }

            // Move to next leaf
            if current_leaf.next_leaf == super::super::super::NULL_PAGE_ID {
                break;
            }
            current_leaf = self.read_leaf_node(current_leaf.next_leaf)?;
        }

        Ok(prefixes)
    }

    /// Get distinct values of the first column in a composite index
    ///
    /// This is a convenience method that calls `get_distinct_prefix_values(1)`
    /// and flattens the result to single values.
    ///
    /// # Returns
    /// Vector of distinct first-column values in sorted order
    ///
    /// # Performance
    /// O(n) where n is the number of unique keys - must scan all leaves
    pub fn get_distinct_first_column_values(
        &self,
    ) -> Result<Vec<vibesql_types::SqlValue>, StorageError> {
        let prefixes = self.get_distinct_prefix_values(1)?;
        Ok(prefixes.into_iter().filter_map(|mut p| p.pop()).collect())
    }

    /// Skip-scan equality lookup on a non-prefix column
    ///
    /// Scans all entries and returns rows where the specified column matches.
    ///
    /// # Arguments
    /// * `filter_column_idx` - Index of the column to filter on (0-based)
    /// * `filter_value` - Value to match
    ///
    /// # Returns
    /// Vector of row indices matching the filter
    ///
    /// # Performance
    /// O(n) where n is total entries - scans all leaves
    pub fn skip_scan_equality(
        &self,
        filter_column_idx: usize,
        filter_value: &vibesql_types::SqlValue,
    ) -> Result<Vec<RowId>, StorageError> {
        let mut result = Vec::new();

        // Start from leftmost leaf
        let mut current_leaf = self.find_leftmost_leaf()?;

        loop {
            for (key, row_ids) in &current_leaf.entries {
                if key.len() > filter_column_idx && &key[filter_column_idx] == filter_value {
                    result.extend(row_ids.iter().copied());
                }
            }

            // Move to next leaf
            if current_leaf.next_leaf == super::super::super::NULL_PAGE_ID {
                break;
            }
            current_leaf = self.read_leaf_node(current_leaf.next_leaf)?;
        }

        Ok(result)
    }

    /// Skip-scan range lookup on a non-prefix column
    ///
    /// Scans all entries and returns rows where the specified column is in range.
    ///
    /// # Arguments
    /// * `filter_column_idx` - Index of the column to filter on (0-based)
    /// * `lower_bound` - Optional lower bound
    /// * `inclusive_lower` - Whether lower bound is inclusive
    /// * `upper_bound` - Optional upper bound
    /// * `inclusive_upper` - Whether upper bound is inclusive
    ///
    /// # Returns
    /// Vector of row indices matching the range filter
    pub fn skip_scan_range(
        &self,
        filter_column_idx: usize,
        lower_bound: Option<&vibesql_types::SqlValue>,
        inclusive_lower: bool,
        upper_bound: Option<&vibesql_types::SqlValue>,
        inclusive_upper: bool,
    ) -> Result<Vec<RowId>, StorageError> {
        let mut result = Vec::new();

        // Start from leftmost leaf
        let mut current_leaf = self.find_leftmost_leaf()?;

        loop {
            for (key, row_ids) in &current_leaf.entries {
                if key.len() > filter_column_idx {
                    let key_val = &key[filter_column_idx];

                    // Check bounds
                    let passes_lower = match lower_bound {
                        None => true,
                        Some(lb) => {
                            if inclusive_lower {
                                key_val >= lb
                            } else {
                                key_val > lb
                            }
                        }
                    };

                    let passes_upper = match upper_bound {
                        None => true,
                        Some(ub) => {
                            if inclusive_upper {
                                key_val <= ub
                            } else {
                                key_val < ub
                            }
                        }
                    };

                    if passes_lower && passes_upper {
                        result.extend(row_ids.iter().copied());
                    }
                }
            }

            // Move to next leaf
            if current_leaf.next_leaf == super::super::super::NULL_PAGE_ID {
                break;
            }
            current_leaf = self.read_leaf_node(current_leaf.next_leaf)?;
        }

        Ok(result)
    }
}