use std::borrow::Cow;
use fsqlite_error::Result;
use fsqlite_types::cx::Cx;
pub(crate) mod sealed {
pub trait Sealed {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeekResult {
Found,
NotFound,
}
impl SeekResult {
#[must_use]
pub fn is_found(self) -> bool {
self == Self::Found
}
}
pub trait BtreeCursorOps: sealed::Sealed {
fn index_move_to(&mut self, cx: &Cx, key: &[u8]) -> Result<SeekResult>;
fn table_move_to(&mut self, cx: &Cx, rowid: i64) -> Result<SeekResult>;
fn first(&mut self, cx: &Cx) -> Result<bool>;
fn last(&mut self, cx: &Cx) -> Result<bool>;
fn next(&mut self, cx: &Cx) -> Result<bool>;
fn prev(&mut self, cx: &Cx) -> Result<bool>;
fn index_insert(&mut self, cx: &Cx, key: &[u8]) -> Result<()>;
fn index_insert_unique(
&mut self,
cx: &Cx,
key: &[u8],
n_unique_cols: usize,
columns_label: &str,
) -> Result<()> {
let _ = (n_unique_cols, columns_label);
self.index_insert(cx, key)
}
fn table_insert(&mut self, cx: &Cx, rowid: i64, data: &[u8]) -> Result<()>;
fn delete(&mut self, cx: &Cx) -> Result<()>;
fn payload(&self, cx: &Cx) -> Result<Vec<u8>>;
fn payload_into(&self, cx: &Cx, buf: &mut Vec<u8>) -> Result<()>;
fn rowid_and_payload_into(&self, cx: &Cx, buf: &mut Vec<u8>) -> Result<i64> {
let rowid = self.rowid(cx)?;
self.payload_into(cx, buf)?;
Ok(rowid)
}
fn rowid_and_payload_cow<'a>(&'a self, cx: &Cx) -> Result<(i64, Cow<'a, [u8]>)> {
let rowid = self.rowid(cx)?;
Ok((rowid, Cow::Owned(self.payload(cx)?)))
}
fn payload_prefix_into(
&self,
cx: &Cx,
max_prefix_bytes: usize,
buf: &mut Vec<u8>,
) -> Result<()>;
fn rowid(&self, cx: &Cx) -> Result<i64>;
fn eof(&self) -> bool;
}
#[derive(Debug, Default)]
pub struct MockBtreeCursor {
at_eof: bool,
current_rowid: i64,
entries: Vec<(i64, Vec<u8>)>,
pos: usize,
}
impl MockBtreeCursor {
#[must_use]
pub fn new(entries: Vec<(i64, Vec<u8>)>) -> Self {
Self {
at_eof: entries.is_empty(),
current_rowid: entries.first().map_or(0, |e| e.0),
entries,
pos: 0,
}
}
}
impl sealed::Sealed for MockBtreeCursor {}
#[allow(clippy::missing_errors_doc)]
impl BtreeCursorOps for MockBtreeCursor {
fn index_move_to(&mut self, _cx: &Cx, key: &[u8]) -> Result<SeekResult> {
for (i, (_, data)) in self.entries.iter().enumerate() {
if data.as_slice() == key {
self.pos = i;
self.at_eof = false;
self.current_rowid = self.entries[i].0;
return Ok(SeekResult::Found);
}
}
let successor_pos = self
.entries
.iter()
.position(|(_, data)| data.as_slice() > key);
if let Some(pos) = successor_pos {
self.pos = pos;
self.at_eof = false;
self.current_rowid = self.entries[pos].0;
} else {
self.pos = self.entries.len();
self.at_eof = true;
}
Ok(SeekResult::NotFound)
}
fn table_move_to(&mut self, _cx: &Cx, rowid: i64) -> Result<SeekResult> {
for (i, (rid, _)) in self.entries.iter().enumerate() {
if *rid == rowid {
self.pos = i;
self.at_eof = false;
self.current_rowid = rowid;
return Ok(SeekResult::Found);
}
}
let successor_pos = self.entries.iter().position(|(rid, _)| *rid > rowid);
if let Some(pos) = successor_pos {
self.pos = pos;
self.at_eof = false;
self.current_rowid = self.entries[pos].0;
} else {
self.pos = self.entries.len();
self.at_eof = true;
}
Ok(SeekResult::NotFound)
}
fn first(&mut self, _cx: &Cx) -> Result<bool> {
if self.entries.is_empty() {
self.at_eof = true;
return Ok(false);
}
self.pos = 0;
self.at_eof = false;
self.current_rowid = self.entries[0].0;
Ok(true)
}
fn last(&mut self, _cx: &Cx) -> Result<bool> {
if self.entries.is_empty() {
self.at_eof = true;
return Ok(false);
}
self.pos = self.entries.len() - 1;
self.at_eof = false;
self.current_rowid = self.entries[self.pos].0;
Ok(true)
}
fn next(&mut self, _cx: &Cx) -> Result<bool> {
if self.pos + 1 >= self.entries.len() {
self.at_eof = true;
return Ok(false);
}
self.pos += 1;
self.current_rowid = self.entries[self.pos].0;
Ok(true)
}
fn prev(&mut self, _cx: &Cx) -> Result<bool> {
if self.entries.is_empty() {
return Ok(false);
}
if self.at_eof {
self.pos = self.entries.len() - 1;
self.at_eof = false;
self.current_rowid = self.entries[self.pos].0;
return Ok(true);
}
if self.pos == 0 {
return Ok(false);
}
self.pos -= 1;
self.current_rowid = self.entries[self.pos].0;
Ok(true)
}
fn index_insert(&mut self, _cx: &Cx, key: &[u8]) -> Result<()> {
let next_rowid = self.entries.iter().map(|e| e.0).max().unwrap_or(0) + 1;
let pos = self
.entries
.binary_search_by(|e| e.1.as_slice().cmp(key))
.unwrap_or_else(|e| e);
if pos < self.entries.len() && self.entries[pos].1 == key {
self.entries[pos] = (next_rowid, key.to_vec());
} else {
self.entries.insert(pos, (next_rowid, key.to_vec()));
}
self.current_rowid = next_rowid;
self.pos = pos;
self.at_eof = false;
Ok(())
}
fn table_insert(&mut self, _cx: &Cx, rowid: i64, data: &[u8]) -> Result<()> {
let pos = self
.entries
.binary_search_by_key(&rowid, |e| e.0)
.unwrap_or_else(|e| e);
if pos < self.entries.len() && self.entries[pos].0 == rowid {
self.entries[pos] = (rowid, data.to_vec());
} else {
self.entries.insert(pos, (rowid, data.to_vec()));
}
self.current_rowid = rowid;
self.pos = pos;
self.at_eof = false;
Ok(())
}
fn delete(&mut self, _cx: &Cx) -> Result<()> {
if !self.at_eof && self.pos < self.entries.len() {
self.entries.remove(self.pos);
if self.pos >= self.entries.len() {
self.at_eof = true;
} else {
self.current_rowid = self.entries[self.pos].0;
}
}
Ok(())
}
fn payload(&self, _cx: &Cx) -> Result<Vec<u8>> {
if self.at_eof {
return Err(fsqlite_error::FrankenError::internal("cursor at EOF"));
}
Ok(self.entries[self.pos].1.clone())
}
fn payload_into(&self, _cx: &Cx, buf: &mut Vec<u8>) -> Result<()> {
if self.at_eof {
return Err(fsqlite_error::FrankenError::internal("cursor at EOF"));
}
buf.clear();
buf.extend_from_slice(&self.entries[self.pos].1);
Ok(())
}
fn payload_prefix_into(
&self,
_cx: &Cx,
max_prefix_bytes: usize,
buf: &mut Vec<u8>,
) -> Result<()> {
if self.at_eof {
return Err(fsqlite_error::FrankenError::internal("cursor at EOF"));
}
buf.clear();
let bytes = &self.entries[self.pos].1;
buf.extend_from_slice(&bytes[..bytes.len().min(max_prefix_bytes)]);
Ok(())
}
fn rowid(&self, _cx: &Cx) -> Result<i64> {
if self.at_eof {
return Err(fsqlite_error::FrankenError::internal("cursor at EOF"));
}
Ok(self.current_rowid)
}
fn eof(&self) -> bool {
self.at_eof
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_btree_cursor_ops_sealed_mock() -> Result<()> {
let entries = vec![
(1, b"alice".to_vec()),
(2, b"bob".to_vec()),
(3, b"charlie".to_vec()),
];
let mut cursor = MockBtreeCursor::new(entries);
let cx = Cx::new();
assert!(cursor.first(&cx)?);
assert_eq!(cursor.rowid(&cx)?, 1);
assert_eq!(cursor.payload(&cx)?, b"alice");
let mut payload = Vec::new();
assert_eq!(cursor.rowid_and_payload_into(&cx, &mut payload)?, 1);
assert_eq!(payload, b"alice");
assert!(cursor.next(&cx)?);
assert_eq!(cursor.rowid(&cx)?, 2);
assert!(cursor.next(&cx)?);
assert_eq!(cursor.rowid(&cx)?, 3);
assert!(!cursor.next(&cx)?);
assert!(cursor.eof());
Ok(())
}
#[test]
fn test_btree_cursor_seek() {
let entries = vec![
(10, b"ten".to_vec()),
(20, b"twenty".to_vec()),
(30, b"thirty".to_vec()),
];
let mut cursor = MockBtreeCursor::new(entries);
let cx = Cx::new();
assert!(cursor.table_move_to(&cx, 20).unwrap().is_found());
assert_eq!(cursor.rowid(&cx).unwrap(), 20);
assert_eq!(cursor.payload(&cx).unwrap(), b"twenty");
assert!(!cursor.table_move_to(&cx, 99).unwrap().is_found());
}
#[test]
fn test_btree_cursor_insert_delete() {
let mut cursor = MockBtreeCursor::new(vec![]);
let cx = Cx::new();
assert!(!cursor.first(&cx).unwrap());
assert!(cursor.eof());
cursor.table_insert(&cx, 1, b"hello").unwrap();
cursor.table_insert(&cx, 2, b"world").unwrap();
assert!(cursor.first(&cx).unwrap());
assert_eq!(cursor.payload(&cx).unwrap(), b"hello");
cursor.delete(&cx).unwrap();
assert!(!cursor.eof());
assert_eq!(cursor.rowid(&cx).unwrap(), 2);
}
#[test]
fn test_btree_cursor_navigate_backward() {
let entries = vec![(1, b"a".to_vec()), (2, b"b".to_vec()), (3, b"c".to_vec())];
let mut cursor = MockBtreeCursor::new(entries);
let cx = Cx::new();
assert!(cursor.last(&cx).unwrap());
assert_eq!(cursor.rowid(&cx).unwrap(), 3);
assert!(cursor.prev(&cx).unwrap());
assert_eq!(cursor.rowid(&cx).unwrap(), 2);
assert!(cursor.prev(&cx).unwrap());
assert_eq!(cursor.rowid(&cx).unwrap(), 1);
assert!(!cursor.prev(&cx).unwrap());
}
#[test]
fn test_btree_cursor_index_seek() {
let entries = vec![
(1, b"alpha".to_vec()),
(2, b"beta".to_vec()),
(3, b"gamma".to_vec()),
];
let mut cursor = MockBtreeCursor::new(entries);
let cx = Cx::new();
assert!(cursor.index_move_to(&cx, b"beta").unwrap().is_found());
assert_eq!(cursor.rowid(&cx).unwrap(), 2);
assert!(!cursor.index_move_to(&cx, b"delta").unwrap().is_found());
}
#[test]
fn test_seek_result_is_found() {
assert!(SeekResult::Found.is_found());
assert!(!SeekResult::NotFound.is_found());
}
#[test]
fn test_mock_cursor_prev_from_eof_revives() {
let entries = vec![(1, b"one".to_vec()), (2, b"two".to_vec())];
let mut cursor = MockBtreeCursor::new(entries);
let cx = Cx::new();
assert!(cursor.first(&cx).unwrap());
assert!(cursor.next(&cx).unwrap());
assert!(!cursor.next(&cx).unwrap());
assert!(cursor.eof());
assert!(cursor.prev(&cx).unwrap());
assert!(!cursor.eof());
assert_eq!(cursor.rowid(&cx).unwrap(), 2);
}
#[test]
fn test_mock_cursor_seek_positions_at_successor() {
let entries = vec![
(10, b"ten".to_vec()),
(20, b"twenty".to_vec()),
(30, b"thirty".to_vec()),
];
let mut cursor = MockBtreeCursor::new(entries);
let cx = Cx::new();
let result = cursor.table_move_to(&cx, 15).unwrap();
assert!(!result.is_found());
assert!(
!cursor.eof(),
"cursor should not be at EOF when successor exists"
);
assert_eq!(
cursor.rowid(&cx).unwrap(),
20,
"cursor should be at successor"
);
let result = cursor.table_move_to(&cx, 5).unwrap();
assert!(!result.is_found());
assert!(!cursor.eof());
assert_eq!(cursor.rowid(&cx).unwrap(), 10);
let result = cursor.table_move_to(&cx, 35).unwrap();
assert!(!result.is_found());
assert!(
cursor.eof(),
"cursor should be at EOF when no successor exists"
);
}
#[test]
fn test_mock_cursor_index_seek_positions_at_successor() {
let entries = vec![
(1, b"alpha".to_vec()),
(2, b"beta".to_vec()),
(3, b"gamma".to_vec()),
];
let mut cursor = MockBtreeCursor::new(entries);
let cx = Cx::new();
let result = cursor.index_move_to(&cx, b"aaa").unwrap();
assert!(!result.is_found());
assert!(!cursor.eof());
assert_eq!(cursor.rowid(&cx).unwrap(), 1);
let result = cursor.index_move_to(&cx, b"cat").unwrap();
assert!(!result.is_found());
assert!(!cursor.eof());
assert_eq!(cursor.rowid(&cx).unwrap(), 3);
let result = cursor.index_move_to(&cx, b"zzz").unwrap();
assert!(!result.is_found());
assert!(cursor.eof());
}
}