use crate::error::{Result, SQLRiteError};
use crate::sql::db::table::Value;
use crate::sql::pager::cell::{KIND_INDEX, decode_value, encode_value};
use crate::sql::pager::varint;
#[derive(Debug, Clone, PartialEq)]
pub struct IndexCell {
pub rowid: i64,
pub value: Value,
}
impl IndexCell {
pub fn new(rowid: i64, value: Value) -> Self {
Self { rowid, value }
}
pub fn encode(&self) -> Result<Vec<u8>> {
if matches!(self.value, Value::Null) {
return Err(SQLRiteError::Internal(
"refusing to encode a NULL index cell — NULLs aren't indexed".to_string(),
));
}
let mut body = Vec::with_capacity(1 + varint::MAX_VARINT_BYTES + 16);
body.push(KIND_INDEX);
varint::write_i64(&mut body, self.rowid);
encode_value(&mut body, &self.value)?;
let mut out = Vec::with_capacity(body.len() + varint::MAX_VARINT_BYTES);
varint::write_u64(&mut out, body.len() as u64);
out.extend_from_slice(&body);
Ok(out)
}
pub fn decode(buf: &[u8], pos: usize) -> Result<(IndexCell, usize)> {
let (body_len, len_bytes) = varint::read_u64(buf, pos)?;
let body_start = pos + len_bytes;
let body_end = body_start
.checked_add(body_len as usize)
.ok_or_else(|| SQLRiteError::Internal("index cell length overflow".to_string()))?;
if body_end > buf.len() {
return Err(SQLRiteError::Internal(format!(
"index cell extends past buffer: needs {body_start}..{body_end}, have {}",
buf.len()
)));
}
let body = &buf[body_start..body_end];
if body.first().copied() != Some(KIND_INDEX) {
return Err(SQLRiteError::Internal(format!(
"IndexCell::decode called on non-index entry (kind_tag = {:#x})",
body.first().copied().unwrap_or(0)
)));
}
let mut cur = 1usize;
let (rowid, n) = varint::read_i64(body, cur)?;
cur += n;
let (value, n) = decode_value(body, cur)?;
cur += n;
if cur != body.len() {
return Err(SQLRiteError::Internal(format!(
"index cell had {} trailing bytes",
body.len() - cur
)));
}
Ok((IndexCell { rowid, value }, body_end - pos))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sql::pager::cell::Cell;
#[test]
fn round_trip_integer_index_cell() {
let c = IndexCell::new(42, Value::Integer(-7));
let bytes = c.encode().unwrap();
let (back, n) = IndexCell::decode(&bytes, 0).unwrap();
assert_eq!(back, c);
assert_eq!(n, bytes.len());
}
#[test]
fn round_trip_text_index_cell() {
let c = IndexCell::new(99, Value::Text("alice".to_string()));
let bytes = c.encode().unwrap();
let (back, _) = IndexCell::decode(&bytes, 0).unwrap();
assert_eq!(back, c);
}
#[test]
fn peek_rowid_works_on_index_cells() {
let c = IndexCell::new(12345, Value::Integer(0));
let bytes = c.encode().unwrap();
assert_eq!(Cell::peek_rowid(&bytes, 0).unwrap(), 12345);
}
#[test]
fn null_value_is_rejected() {
let c = IndexCell::new(1, Value::Null);
let err = c.encode().unwrap_err();
assert!(format!("{err}").contains("NULLs aren't indexed"));
}
#[test]
fn decode_rejects_wrong_kind_tag() {
use crate::sql::pager::cell::KIND_LOCAL;
let mut buf = Vec::new();
buf.push(1);
buf.push(KIND_LOCAL);
let err = IndexCell::decode(&buf, 0).unwrap_err();
assert!(format!("{err}").contains("non-index"));
}
}