use smallvec::SmallVec;
use std::collections::HashMap;
use vibesql_types::{Date, SqlValue};
pub type TxnId = u64;
pub const PRE_MVCC_TXN_ID: TxnId = 0;
pub const ROW_INLINE_CAPACITY: usize = 8;
pub type RowValues = SmallVec<[SqlValue; ROW_INLINE_CAPACITY]>;
#[derive(Debug, Clone, PartialEq)]
pub struct Row {
pub values: RowValues,
pub row_id: Option<u64>,
pub row_ids: Option<HashMap<String, u64>>,
pub xmin: TxnId,
pub xmax: Option<TxnId>,
}
impl Row {
pub fn new(values: impl Into<RowValues>) -> Self {
Row {
values: values.into(),
row_id: None,
row_ids: None,
xmin: PRE_MVCC_TXN_ID,
xmax: None,
}
}
pub fn from_vec(values: Vec<SqlValue>) -> Self {
Row {
values: SmallVec::from_vec(values),
row_id: None,
row_ids: None,
xmin: PRE_MVCC_TXN_ID,
xmax: None,
}
}
pub fn with_row_id(values: impl Into<RowValues>, row_id: u64) -> Self {
Row {
values: values.into(),
row_id: Some(row_id),
row_ids: None,
xmin: PRE_MVCC_TXN_ID,
xmax: None,
}
}
pub fn with_row_ids(values: impl Into<RowValues>, row_ids: HashMap<String, u64>) -> Self {
Row {
values: values.into(),
row_id: None,
row_ids: if row_ids.is_empty() { None } else { Some(row_ids) },
xmin: PRE_MVCC_TXN_ID,
xmax: None,
}
}
pub fn get_row_id_for_table(&self, table_name: Option<&str>) -> Option<u64> {
if let Some(ref row_ids) = self.row_ids {
if let Some(name) = table_name {
let name_lower = name.to_lowercase();
return row_ids.get(&name_lower).copied();
}
if row_ids.len() == 1 {
return row_ids.values().next().copied();
}
return None;
}
self.row_id
}
pub fn set_row_id(&mut self, row_id: u64) {
self.row_id = Some(row_id);
}
pub fn combine_for_join(
left: &Row,
right: &Row,
left_table_names: &[String],
right_table_names: &[String],
) -> Row {
let mut combined_values = Vec::with_capacity(left.values.len() + right.values.len());
combined_values.extend_from_slice(&left.values);
combined_values.extend_from_slice(&right.values);
let mut combined_row_ids = HashMap::new();
if let Some(ref row_ids) = left.row_ids {
combined_row_ids.extend(row_ids.iter().map(|(k, v)| (k.clone(), *v)));
} else if let Some(row_id) = left.row_id {
for name in left_table_names {
combined_row_ids.insert(name.to_lowercase(), row_id);
}
}
if let Some(ref row_ids) = right.row_ids {
combined_row_ids.extend(row_ids.iter().map(|(k, v)| (k.clone(), *v)));
} else if let Some(row_id) = right.row_id {
for name in right_table_names {
combined_row_ids.insert(name.to_lowercase(), row_id);
}
}
if combined_row_ids.is_empty() {
Row::new(combined_values)
} else {
Row::with_row_ids(combined_values, combined_row_ids)
}
}
pub fn get_row_id(&self) -> Option<u64> {
self.row_id
}
pub fn get(&self, index: usize) -> Option<&SqlValue> {
self.values.get(index)
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn estimated_size_bytes(&self) -> usize {
use std::mem::size_of;
let base_overhead = size_of::<Row>();
let heap_overhead =
if self.values.spilled() { self.values.capacity() * size_of::<SqlValue>() } else { 0 };
let values_heap_size: usize = self.values.iter().map(|v| v.estimated_size_bytes()).sum();
base_overhead + heap_overhead + values_heap_size
}
pub fn set(&mut self, index: usize, value: SqlValue) -> Result<(), crate::StorageError> {
if index >= self.values.len() {
return Err(crate::StorageError::ColumnIndexOutOfBounds { index });
}
self.values[index] = value;
Ok(())
}
pub fn add_value(&mut self, value: SqlValue) {
self.values.push(value);
}
pub fn remove_value(&mut self, index: usize) -> Result<SqlValue, crate::StorageError> {
if index >= self.values.len() {
return Err(crate::StorageError::ColumnIndexOutOfBounds { index });
}
Ok(self.values.remove(index))
}
#[inline(always)]
pub unsafe fn get_f64_unchecked(&self, idx: usize) -> f64 {
debug_assert!(
matches!(self.values[idx], SqlValue::Double(_) | SqlValue::Float(_)),
"get_f64_unchecked called on non-float value: {:?}",
self.values[idx]
);
match &self.values[idx] {
SqlValue::Double(d) => *d,
SqlValue::Float(f) => *f as f64,
_ => std::hint::unreachable_unchecked(),
}
}
#[inline(always)]
pub unsafe fn get_i64_unchecked(&self, idx: usize) -> i64 {
debug_assert!(
matches!(
self.values[idx],
SqlValue::Integer(_) | SqlValue::Bigint(_) | SqlValue::Smallint(_)
),
"get_i64_unchecked called on non-integer value: {:?}",
self.values[idx]
);
match &self.values[idx] {
SqlValue::Integer(i) | SqlValue::Bigint(i) => *i,
SqlValue::Smallint(s) => *s as i64,
_ => std::hint::unreachable_unchecked(),
}
}
#[inline(always)]
pub unsafe fn get_numeric_as_f64_unchecked(&self, idx: usize) -> f64 {
debug_assert!(
matches!(
self.values[idx],
SqlValue::Integer(_)
| SqlValue::Bigint(_)
| SqlValue::Smallint(_)
| SqlValue::Unsigned(_)
| SqlValue::Numeric(_)
| SqlValue::Double(_)
| SqlValue::Float(_)
| SqlValue::Real(_)
),
"get_numeric_as_f64_unchecked called on non-numeric value: {:?}",
self.values[idx]
);
match &self.values[idx] {
SqlValue::Integer(i) | SqlValue::Bigint(i) => *i as f64,
SqlValue::Smallint(s) => *s as f64,
SqlValue::Unsigned(u) => *u as f64,
SqlValue::Numeric(n) | SqlValue::Double(n) | SqlValue::Real(n) => *n, SqlValue::Float(f) => *f as f64,
_ => std::hint::unreachable_unchecked(),
}
}
#[inline(always)]
pub unsafe fn get_date_unchecked(&self, idx: usize) -> Date {
debug_assert!(
matches!(self.values[idx], SqlValue::Date(_)),
"get_date_unchecked called on non-date value: {:?}",
self.values[idx]
);
match &self.values[idx] {
SqlValue::Date(d) => *d,
_ => std::hint::unreachable_unchecked(),
}
}
#[inline(always)]
pub unsafe fn get_bool_unchecked(&self, idx: usize) -> bool {
debug_assert!(
matches!(self.values[idx], SqlValue::Boolean(_)),
"get_bool_unchecked called on non-boolean value: {:?}",
self.values[idx]
);
match &self.values[idx] {
SqlValue::Boolean(b) => *b,
_ => std::hint::unreachable_unchecked(),
}
}
#[inline(always)]
pub unsafe fn get_string_unchecked(&self, idx: usize) -> &str {
debug_assert!(
matches!(self.values[idx], SqlValue::Varchar(_) | SqlValue::Character(_)),
"get_string_unchecked called on non-string value: {:?}",
self.values[idx]
);
match &self.values[idx] {
SqlValue::Varchar(s) | SqlValue::Character(s) => s,
_ => std::hint::unreachable_unchecked(),
}
}
#[inline]
pub fn visible_to(&self, snapshot: &crate::mvcc::TxnSnapshot) -> bool {
if !snapshot.is_committed(self.xmin) {
return false;
}
match self.xmax {
None => true,
Some(deleter) => {
if deleter > snapshot.xmin_active {
return true;
}
if snapshot.is_in_progress(deleter) {
return true;
}
false
}
}
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
#[test]
fn test_unchecked_accessors_correct_types() {
let row = Row::from_vec(vec![
SqlValue::Double(3.14),
SqlValue::Integer(42),
SqlValue::Date(Date::from_str("2024-01-01").unwrap()),
SqlValue::Boolean(true),
SqlValue::Varchar(arcstr::ArcStr::from("hello")),
]);
unsafe {
assert_eq!(row.get_f64_unchecked(0), 3.14);
assert_eq!(row.get_i64_unchecked(1), 42);
assert_eq!(row.get_date_unchecked(2), Date::from_str("2024-01-01").unwrap());
assert!(row.get_bool_unchecked(3));
assert_eq!(row.get_string_unchecked(4), "hello");
}
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "get_f64_unchecked called on non-float value")]
fn test_get_f64_unchecked_wrong_type() {
let row = Row::from_vec(vec![SqlValue::Integer(42)]);
unsafe {
row.get_f64_unchecked(0); }
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "get_i64_unchecked called on non-integer value")]
fn test_get_i64_unchecked_wrong_type() {
let row = Row::from_vec(vec![SqlValue::Double(3.14)]);
unsafe {
row.get_i64_unchecked(0); }
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "get_date_unchecked called on non-date value")]
fn test_get_date_unchecked_wrong_type() {
let row = Row::from_vec(vec![SqlValue::Integer(42)]);
unsafe {
row.get_date_unchecked(0); }
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "get_bool_unchecked called on non-boolean value")]
fn test_get_bool_unchecked_wrong_type() {
let row = Row::from_vec(vec![SqlValue::Integer(42)]);
unsafe {
row.get_bool_unchecked(0); }
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "get_string_unchecked called on non-string value")]
fn test_get_string_unchecked_wrong_type() {
let row = Row::from_vec(vec![SqlValue::Integer(42)]);
unsafe {
row.get_string_unchecked(0); }
}
mod visibility {
use std::collections::HashSet;
use super::super::*;
use crate::mvcc::TxnSnapshot;
fn row_with(xmin: TxnId, xmax: Option<TxnId>) -> Row {
let mut r = Row::new(vec![SqlValue::Integer(0)]);
r.xmin = xmin;
r.xmax = xmax;
r
}
fn snap(xmin_active: TxnId, xmax_committed: TxnId, in_progress: &[TxnId]) -> TxnSnapshot {
TxnSnapshot::new(xmin_active, xmax_committed, in_progress.iter().copied().collect())
}
#[test]
fn pre_mvcc_row_visible_under_empty_snapshot() {
let r = Row::new(vec![SqlValue::Integer(42)]);
assert!(r.visible_to(&TxnSnapshot::empty()));
}
#[test]
fn pre_mvcc_row_visible_under_active_snapshot() {
let r = row_with(PRE_MVCC_TXN_ID, None);
let s = snap(5, 10, &[5, 7]);
assert!(r.visible_to(&s));
}
#[test]
fn row_visible_when_creator_committed_pre_snapshot() {
let r = row_with(3, None);
let s = snap(5, 10, &[5, 7]);
assert!(r.visible_to(&s));
}
#[test]
fn row_invisible_when_creator_in_progress() {
let r = row_with(7, None);
let s = snap(5, 10, &[5, 7]);
assert!(!r.visible_to(&s));
}
#[test]
fn row_invisible_when_creator_after_snapshot_high_watermark() {
let r = row_with(20, None);
let s = snap(5, 10, &[5, 7]);
assert!(!r.visible_to(&s));
}
#[test]
fn row_visible_when_creator_equals_high_watermark() {
let r = row_with(10, None);
let s = snap(5, 10, &[5, 7]);
assert!(r.visible_to(&s));
}
#[test]
fn row_invisible_when_deleter_committed_pre_snapshot() {
let r = row_with(3, Some(4));
let s = snap(5, 10, &[5, 7]);
assert!(!r.visible_to(&s));
}
#[test]
fn row_visible_when_deleter_after_snapshot_oldest_active() {
let r = row_with(3, Some(9));
let s = snap(5, 10, &[5, 7]);
assert!(r.visible_to(&s));
}
#[test]
fn row_visible_when_deleter_still_in_progress() {
let r = row_with(3, Some(5));
let s = snap(5, 10, &[5, 7]);
assert!(r.visible_to(&s));
}
#[test]
fn row_visible_when_deleter_eq_xmin_active_but_in_progress() {
let r = row_with(3, Some(5));
let s = snap(5, 10, &[5]);
assert!(r.visible_to(&s));
}
#[test]
fn row_invisible_when_deleter_eq_xmin_active_not_in_progress() {
let r = row_with(3, Some(5));
let s = snap(5, 10, &[7]);
assert!(!r.visible_to(&s));
}
#[test]
fn updated_row_chain_only_new_version_visible() {
let old = row_with(3, Some(6));
let new = row_with(6, None);
let s = snap(10, 10, &[]); assert!(!old.visible_to(&s));
assert!(new.visible_to(&s));
}
#[test]
fn updated_row_chain_concurrent_snapshot_sees_old_version() {
let old = row_with(3, Some(6));
let new = row_with(6, None);
let mut in_progress = HashSet::new();
in_progress.insert(6);
let s = TxnSnapshot::new(6, 5, in_progress);
assert!(old.visible_to(&s));
assert!(!new.visible_to(&s));
}
#[test]
fn empty_snapshot_hides_all_mvcc_rows() {
assert!(!row_with(1, None).visible_to(&TxnSnapshot::empty()));
assert!(!row_with(42, None).visible_to(&TxnSnapshot::empty()));
assert!(row_with(0, None).visible_to(&TxnSnapshot::empty()));
}
#[test]
fn deleter_with_pre_mvcc_sentinel_treated_as_committed() {
let r = row_with(1, Some(0));
let s = snap(5, 10, &[]);
assert!(!r.visible_to(&s));
}
#[test]
fn writer_aborted_modeling_note() {
let r = row_with(3, None);
let s = snap(5, 10, &[]);
assert!(r.visible_to(&s));
}
}
#[test]
fn test_unchecked_accessor_with_type_coercion() {
let row = Row::from_vec(vec![SqlValue::Float(3.14)]);
unsafe {
assert_eq!(row.get_f64_unchecked(0), 3.14f32 as f64);
}
let row = Row::from_vec(vec![SqlValue::Smallint(42)]);
unsafe {
assert_eq!(row.get_i64_unchecked(0), 42i64);
}
let row = Row::from_vec(vec![SqlValue::Bigint(1000000)]);
unsafe {
assert_eq!(row.get_i64_unchecked(0), 1000000i64);
}
let row = Row::from_vec(vec![SqlValue::Character(arcstr::ArcStr::from("test"))]);
unsafe {
assert_eq!(row.get_string_unchecked(0), "test");
}
}
}