use crate::error::{Error, Result};
const BASE64_ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
pub const MAX_ROWID_LENGTH: usize = 18;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RowId {
pub rba: u32,
pub partition_id: u16,
pub block_num: u32,
pub slot_num: u16,
}
impl RowId {
pub fn new(rba: u32, partition_id: u16, block_num: u32, slot_num: u16) -> Self {
Self {
rba,
partition_id,
block_num,
slot_num,
}
}
pub fn is_valid(&self) -> bool {
self.rba != 0 || self.partition_id != 0 || self.block_num != 0 || self.slot_num != 0
}
pub fn to_string(&self) -> Option<String> {
if !self.is_valid() {
return None;
}
let mut buf = [0u8; MAX_ROWID_LENGTH];
let mut offset = 0;
offset = convert_base64(&mut buf, self.rba as u64, 6, offset);
offset = convert_base64(&mut buf, self.partition_id as u64, 3, offset);
offset = convert_base64(&mut buf, self.block_num as u64, 6, offset);
convert_base64(&mut buf, self.slot_num as u64, 3, offset);
Some(String::from_utf8_lossy(&buf).to_string())
}
}
impl Default for RowId {
fn default() -> Self {
Self::new(0, 0, 0, 0)
}
}
impl std::fmt::Display for RowId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.to_string() {
Some(s) => write!(f, "{}", s),
None => write!(f, "NULL"),
}
}
}
fn convert_base64(buf: &mut [u8], value: u64, num_chars: usize, mut offset: usize) -> usize {
let mut val = value;
for i in (0..num_chars).rev() {
let idx = (val & 0x3f) as usize;
buf[offset + i] = BASE64_ALPHABET[idx];
val >>= 6;
}
offset += num_chars;
offset
}
pub fn decode_rowid(data: &[u8]) -> Result<RowId> {
if data.is_empty() {
return Ok(RowId::default());
}
if data[0] == 1 && data.len() >= 13 {
let rba = u32::from_be_bytes([data[1], data[2], data[3], data[4]]);
let partition_id = u16::from_be_bytes([data[5], data[6]]);
let block_num = u32::from_be_bytes([data[7], data[8], data[9], data[10]]);
let slot_num = u16::from_be_bytes([data[11], data[12]]);
return Ok(RowId {
rba,
partition_id,
block_num,
slot_num,
});
}
if data.len() >= 10 {
let rba = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let partition_id = u16::from_be_bytes([data[4], data[5]]);
let block_num = u32::from_be_bytes([data[6], data[7], data[8], data[9]]);
let slot_num = if data.len() >= 12 {
u16::from_be_bytes([data[10], data[11]])
} else {
0
};
return Ok(RowId {
rba,
partition_id,
block_num,
slot_num,
});
}
Err(Error::DataConversionError(format!(
"Invalid ROWID data length: {}",
data.len()
)))
}
pub fn parse_rowid_string(s: &str) -> Result<RowId> {
if s.len() != MAX_ROWID_LENGTH {
return Err(Error::DataConversionError(format!(
"Invalid ROWID string length: {}, expected {}",
s.len(),
MAX_ROWID_LENGTH
)));
}
let bytes = s.as_bytes();
let rba = decode_base64(&bytes[0..6])? as u32;
let partition_id = decode_base64(&bytes[6..9])? as u16;
let block_num = decode_base64(&bytes[9..15])? as u32;
let slot_num = decode_base64(&bytes[15..18])? as u16;
Ok(RowId {
rba,
partition_id,
block_num,
slot_num,
})
}
fn decode_base64(chars: &[u8]) -> Result<u64> {
let mut value: u64 = 0;
for &c in chars {
let idx = match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
b'/' => 63,
_ => {
return Err(Error::DataConversionError(format!(
"Invalid base64 character in ROWID: {}",
char::from(c)
)))
}
};
value = (value << 6) | (idx as u64);
}
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rowid_to_string() {
let rowid = RowId::new(0x000001, 0x0001, 0x000001, 0x0000);
let s = rowid.to_string().unwrap();
assert_eq!(s.len(), 18);
}
#[test]
fn test_rowid_invalid() {
let rowid = RowId::default();
assert!(!rowid.is_valid());
assert!(rowid.to_string().is_none());
}
#[test]
fn test_rowid_roundtrip() {
let rowid = RowId::new(12345, 67, 89012, 345);
let s = rowid.to_string().unwrap();
let parsed = parse_rowid_string(&s).unwrap();
assert_eq!(rowid, parsed);
}
#[test]
fn test_decode_physical_rowid() {
let data = vec![
1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 10, 0, 5, ];
let rowid = decode_rowid(&data).unwrap();
assert_eq!(rowid.rba, 1);
assert_eq!(rowid.partition_id, 1);
assert_eq!(rowid.block_num, 10);
assert_eq!(rowid.slot_num, 5);
}
#[test]
fn test_rowid_display() {
let rowid = RowId::new(1, 1, 1, 1);
let display = format!("{}", rowid);
assert_eq!(display.len(), 18);
let null_rowid = RowId::default();
let null_display = format!("{}", null_rowid);
assert_eq!(null_display, "NULL");
}
#[test]
fn test_parse_invalid_rowid_string() {
assert!(parse_rowid_string("short").is_err());
assert!(parse_rowid_string("this_is_too_long_for_a_rowid").is_err());
}
#[test]
fn test_base64_encoding() {
let rowid = RowId::new(
0, 4, 0x1A2, 0, );
let s = rowid.to_string().unwrap();
let parsed = parse_rowid_string(&s).unwrap();
assert_eq!(parsed.rba, 0);
assert_eq!(parsed.partition_id, 4);
assert_eq!(parsed.block_num, 0x1A2);
assert_eq!(parsed.slot_num, 0);
}
#[test]
fn test_rowid_various_values() {
let test_cases = [
(1, 1, 1, 1),
(0xFFFFFF, 0xFFFF, 0xFFFFFF, 0xFFFF),
(0, 0, 0, 1),
(1234567, 890, 1234567, 890),
];
for (rba, partition_id, block_num, slot_num) in test_cases {
let rowid = RowId::new(rba, partition_id, block_num, slot_num);
let s = rowid.to_string().unwrap();
assert_eq!(s.len(), 18, "ROWID string should be 18 characters");
let parsed = parse_rowid_string(&s).unwrap();
assert_eq!(parsed.rba, rba);
assert_eq!(parsed.partition_id, partition_id);
assert_eq!(parsed.block_num, block_num);
assert_eq!(parsed.slot_num, slot_num);
}
}
}