mod append_mode;
mod indexes;
mod normalization;
use append_mode::AppendModeTracker;
use indexes::IndexManager;
use normalization::RowNormalizer;
use vibesql_types::SqlValue;
use crate::{Row, StorageError};
#[inline]
fn columnar_index_in(deleted: &[bool], physical_index: usize) -> usize {
let deleted_before = deleted[..physical_index].iter().filter(|&&d| d).count();
physical_index - deleted_before
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeleteResult {
pub deleted_count: usize,
pub compacted: bool,
}
impl DeleteResult {
pub fn new(deleted_count: usize, compacted: bool) -> Self {
Self { deleted_count, compacted }
}
}
#[derive(Debug)]
pub struct Table {
pub schema: vibesql_catalog::TableSchema,
rows: Vec<Row>,
deleted: Vec<bool>,
deleted_count: usize,
native_columnar: Option<crate::ColumnarTable>,
indexes: IndexManager,
append_tracker: AppendModeTracker,
statistics: Option<crate::statistics::TableStatistics>,
modifications_since_stats: usize,
}
impl Clone for Table {
fn clone(&self) -> Self {
Table {
schema: self.schema.clone(),
rows: self.rows.clone(),
deleted: self.deleted.clone(),
deleted_count: self.deleted_count,
native_columnar: self.native_columnar.clone(),
indexes: self.indexes.clone(),
append_tracker: self.append_tracker.clone(),
statistics: self.statistics.clone(),
modifications_since_stats: self.modifications_since_stats,
}
}
}
impl Table {
pub fn new(schema: vibesql_catalog::TableSchema) -> Self {
let indexes = IndexManager::new(&schema);
let is_columnar = schema.is_columnar();
let native_columnar = if is_columnar {
let column_names: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
Some(
crate::ColumnarTable::from_rows(&[], &column_names)
.expect("Creating empty columnar table should never fail"),
)
} else {
None
};
Table {
schema,
rows: Vec::new(),
deleted: Vec::new(),
deleted_count: 0,
native_columnar,
indexes,
append_tracker: AppendModeTracker::new(),
statistics: None,
modifications_since_stats: 0,
}
}
pub fn is_native_columnar(&self) -> bool {
self.native_columnar.is_some()
}
pub fn insert(&mut self, row: Row) -> Result<(), StorageError> {
let normalizer = RowNormalizer::new(&self.schema);
let normalized_row = normalizer.normalize_and_validate(row)?;
if let Some(pk_indices) = self.schema.get_primary_key_indices() {
let pk_values: Vec<SqlValue> =
pk_indices.iter().map(|&idx| normalized_row.values[idx].clone()).collect();
self.append_tracker.update(&pk_values);
}
let row_index = self.rows.len();
self.rows.push(normalized_row.clone());
self.deleted.push(false);
self.indexes.update_for_insert(&self.schema, &normalized_row, row_index);
self.modifications_since_stats += 1;
if let Some(stats) = &mut self.statistics {
if self.modifications_since_stats > stats.row_count / 10 {
stats.mark_stale();
}
}
if let Some(ref mut columnar) = self.native_columnar {
columnar.append_row(&normalized_row).map_err(|e| {
StorageError::Other(format!("Columnar append failed: {}", e))
})?;
}
Ok(())
}
fn rebuild_native_columnar(&mut self) -> Result<(), StorageError> {
let column_names: Vec<String> =
self.schema.columns.iter().map(|c| c.name.clone()).collect();
let live_rows: Vec<&Row> = self
.rows
.iter()
.enumerate()
.filter(|(idx, _)| !self.deleted[*idx])
.map(|(_, row)| row)
.collect();
let columnar = crate::ColumnarTable::from_row_refs(&live_rows, &column_names)
.map_err(|e| StorageError::Other(format!("Columnar rebuild failed: {}", e)))?;
self.native_columnar = Some(columnar);
Ok(())
}
pub fn insert_batch(&mut self, rows: Vec<Row>) -> Result<usize, StorageError> {
if rows.is_empty() {
return Ok(0);
}
let row_count = rows.len();
let normalizer = RowNormalizer::new(&self.schema);
let mut normalized_rows = Vec::with_capacity(row_count);
for row in rows {
let normalized = normalizer.normalize_and_validate(row)?;
normalized_rows.push(normalized);
}
self.rows.reserve(row_count);
self.deleted.reserve(row_count);
let start_index = self.rows.len();
for row in normalized_rows {
self.rows.push(row);
self.deleted.push(false);
}
for (i, row) in self.rows[start_index..].iter().enumerate() {
self.indexes.update_for_insert(&self.schema, row, start_index + i);
}
if let Some(pk_indices) = self.schema.get_primary_key_indices() {
if let Some(last_row) = self.rows.last() {
let pk_values: Vec<SqlValue> =
pk_indices.iter().map(|&idx| last_row.values[idx].clone()).collect();
self.append_tracker.reset();
self.append_tracker.update(&pk_values);
}
}
self.modifications_since_stats += row_count;
if let Some(stats) = &mut self.statistics {
if self.modifications_since_stats > stats.row_count / 10 {
stats.mark_stale();
}
}
if let Some(ref mut columnar) = self.native_columnar {
for row in &self.rows[start_index..] {
columnar.append_row(row).map_err(|e| {
StorageError::Other(format!("Columnar append failed: {}", e))
})?;
}
}
Ok(row_count)
}
pub fn insert_from_iter<I>(&mut self, rows: I, batch_size: usize) -> Result<usize, StorageError>
where
I: Iterator<Item = Row>,
{
let batch_size = if batch_size == 0 { 1000 } else { batch_size };
let mut total_inserted = 0;
let mut batch = Vec::with_capacity(batch_size);
for row in rows {
batch.push(row);
if batch.len() >= batch_size {
let count = self.insert_batch(std::mem::take(&mut batch))?;
total_inserted += count;
batch = Vec::with_capacity(batch_size);
}
}
if !batch.is_empty() {
let count = self.insert_batch(batch)?;
total_inserted += count;
}
Ok(total_inserted)
}
pub fn scan(&self) -> &[Row] {
&self.rows
}
#[inline]
pub fn is_row_deleted(&self, idx: usize) -> bool {
idx < self.deleted.len() && self.deleted[idx]
}
#[inline]
pub fn scan_live(&self) -> impl Iterator<Item = (usize, &Row)> {
self.rows.iter().enumerate().filter(|(idx, _)| !self.deleted[*idx])
}
#[inline]
pub fn scan_live_vec(&self) -> Vec<Row> {
let mut result = Vec::with_capacity(self.row_count());
for (idx, row) in self.rows.iter().enumerate() {
if !self.deleted[idx] {
let mut cloned = row.clone();
if cloned.row_id.is_none() {
cloned.row_id = Some((idx + 1) as u64);
}
result.push(cloned);
}
}
result
}
#[inline]
pub fn scan_visible_vec(&self, snapshot: &crate::mvcc::TxnSnapshot) -> Vec<Row> {
#[cfg(not(feature = "mvcc_enabled"))]
{
let _ = snapshot;
return self.scan_live_vec();
}
#[cfg(feature = "mvcc_enabled")]
{
let mut result = Vec::with_capacity(self.row_count());
for (idx, row) in self.rows.iter().enumerate() {
if self.deleted[idx] {
continue;
}
if !row.visible_to(snapshot) {
continue;
}
let mut cloned = row.clone();
if cloned.row_id.is_none() {
cloned.row_id = Some((idx + 1) as u64);
}
result.push(cloned);
}
result
}
}
#[inline]
pub fn scan_visible<'a>(
&'a self,
snapshot: &'a crate::mvcc::TxnSnapshot,
) -> impl Iterator<Item = (usize, &'a Row)> + 'a {
self.rows.iter().enumerate().filter(move |(idx, row)| {
if self.deleted[*idx] {
return false;
}
#[cfg(feature = "mvcc_enabled")]
{
row.visible_to(snapshot)
}
#[cfg(not(feature = "mvcc_enabled"))]
{
let _ = (row, snapshot);
true
}
})
}
#[inline]
pub fn is_row_visible(&self, idx: usize, snapshot: &crate::mvcc::TxnSnapshot) -> bool {
if idx >= self.deleted.len() || self.deleted[idx] {
return false;
}
#[cfg(feature = "mvcc_enabled")]
{
match self.rows.get(idx) {
Some(row) => row.visible_to(snapshot),
None => false,
}
}
#[cfg(not(feature = "mvcc_enabled"))]
{
let _ = snapshot;
idx < self.rows.len()
}
}
#[inline]
pub fn get_row(&self, idx: usize) -> Option<&Row> {
if idx < self.deleted.len() && self.deleted[idx] {
return None;
}
self.rows.get(idx)
}
pub fn scan_columnar(&self) -> Result<crate::ColumnarTable, StorageError> {
if let Some(ref native) = self.native_columnar {
return Ok(native.clone());
}
let column_names: Vec<String> =
self.schema.columns.iter().map(|c| c.name.clone()).collect();
let live_rows: Vec<&Row> = self
.rows
.iter()
.enumerate()
.filter(|(idx, _)| !self.deleted[*idx])
.map(|(_, row)| row)
.collect();
crate::ColumnarTable::from_row_refs(&live_rows, &column_names)
.map_err(|e| StorageError::Other(format!("Columnar conversion failed: {}", e)))
}
pub fn row_count(&self) -> usize {
self.rows.len() - self.deleted_count
}
#[inline]
pub fn physical_row_count(&self) -> usize {
self.rows.len()
}
#[inline]
pub fn deleted_count(&self) -> usize {
self.deleted_count
}
pub fn statistics(&mut self) -> &crate::statistics::TableStatistics {
if self.statistics.is_none() || self.statistics.as_ref().unwrap().needs_refresh() {
self.statistics =
Some(crate::statistics::TableStatistics::compute(&self.rows, &self.schema));
self.modifications_since_stats = 0;
}
self.statistics.as_ref().unwrap()
}
pub fn get_statistics(&self) -> Option<&crate::statistics::TableStatistics> {
self.statistics.as_ref()
}
pub fn analyze(&mut self) {
self.statistics =
Some(crate::statistics::TableStatistics::compute(&self.rows, &self.schema));
self.modifications_since_stats = 0;
}
pub fn is_in_append_mode(&self) -> bool {
self.append_tracker.is_active()
}
pub fn clear(&mut self) {
self.rows.clear();
self.deleted.clear();
self.deleted_count = 0;
self.indexes.clear();
self.append_tracker.reset();
if self.native_columnar.is_some() {
let column_names: Vec<String> =
self.schema.columns.iter().map(|c| c.name.clone()).collect();
self.native_columnar = Some(
crate::ColumnarTable::from_rows(&[], &column_names)
.expect("Creating empty columnar table should never fail"),
);
}
}
pub fn update_row(&mut self, index: usize, row: Row) -> Result<(), StorageError> {
if index >= self.rows.len() {
return Err(StorageError::ColumnIndexOutOfBounds { index });
}
if self.deleted[index] {
return Err(StorageError::RowNotFound);
}
let normalizer = RowNormalizer::new(&self.schema);
let normalized_row = normalizer.normalize_and_validate(row)?;
let old_row = self.rows[index].clone();
self.rows[index] = normalized_row.clone();
self.indexes.update_for_update(&self.schema, &old_row, &normalized_row, index);
if let Some(ref mut columnar) = self.native_columnar {
let columnar_idx = columnar_index_in(&self.deleted, index);
columnar.update_row_at(columnar_idx, &normalized_row).map_err(|e| {
StorageError::Other(format!("Columnar update failed: {}", e))
})?;
}
Ok(())
}
pub fn update_row_selective(
&mut self,
index: usize,
row: Row,
changed_columns: &std::collections::HashSet<usize>,
) -> Result<(), StorageError> {
if index >= self.rows.len() {
return Err(StorageError::ColumnIndexOutOfBounds { index });
}
if self.deleted[index] {
return Err(StorageError::RowNotFound);
}
let normalizer = RowNormalizer::new(&self.schema);
let normalized_row = normalizer.normalize_and_validate(row)?;
let old_row = self.rows[index].clone();
let affected_indexes = self.indexes.get_affected_indexes(&self.schema, changed_columns);
self.indexes.update_selective(
&self.schema,
&old_row,
&normalized_row,
index,
&affected_indexes,
);
self.rows[index] = normalized_row;
if let Some(ref mut columnar) = self.native_columnar {
let columnar_idx = columnar_index_in(&self.deleted, index);
columnar.update_row_at(columnar_idx, &self.rows[index]).map_err(|e| {
StorageError::Other(format!("Columnar update failed: {}", e))
})?;
}
Ok(())
}
#[inline]
pub fn update_row_unchecked(
&mut self,
index: usize,
new_row: Row,
old_row: &Row,
changed_columns: &std::collections::HashSet<usize>,
) {
let affected_indexes = self.indexes.get_affected_indexes(&self.schema, changed_columns);
self.indexes.update_selective(&self.schema, old_row, &new_row, index, &affected_indexes);
self.rows[index] = new_row;
if let Some(ref mut columnar) = self.native_columnar {
let columnar_idx = columnar_index_in(&self.deleted, index);
let _ = columnar.update_row_at(columnar_idx, &self.rows[index]);
}
}
#[inline]
pub fn update_column_inplace(
&mut self,
row_index: usize,
col_index: usize,
new_value: vibesql_types::SqlValue,
) {
self.rows[row_index].values[col_index] = new_value;
if let Some(ref mut columnar) = self.native_columnar {
let columnar_idx = columnar_index_in(&self.deleted, row_index);
let col_name = &self.schema.columns[col_index].name;
if let Some(column) = columnar.get_column_mut(col_name) {
let _ = column.set_value(columnar_idx, &self.rows[row_index].values[col_index]);
}
}
}
#[inline]
pub fn stamp_row_xmin_inplace(&mut self, row_index: usize, txn_id: crate::row::TxnId) {
if let Some(row) = self.rows.get_mut(row_index) {
row.xmin = txn_id;
}
}
#[inline]
pub fn stamp_row_xmax_inplace(&mut self, row_index: usize, txn_id: crate::row::TxnId) {
if let Some(row) = self.rows.get_mut(row_index) {
row.xmax = Some(txn_id);
}
}
pub fn mark_deleted_inplace(&mut self, idx: usize) -> bool {
if idx >= self.rows.len() || self.deleted[idx] {
return false;
}
let row = &self.rows[idx];
self.indexes.update_for_delete(&self.schema, row);
self.deleted[idx] = true;
self.deleted_count += 1;
if self.native_columnar.is_some() {
let _ = self.rebuild_native_columnar();
}
true
}
pub fn compact_if_needed(&mut self) -> bool {
if self.should_compact() {
self.compact();
if self.native_columnar.is_some() {
let _ = self.rebuild_native_columnar();
}
true
} else {
false
}
}
pub fn delete_where<F>(&mut self, mut predicate: F) -> DeleteResult
where
F: FnMut(&Row) -> bool,
{
let mut indices_to_delete: Vec<usize> = Vec::new();
for (index, row) in self.rows.iter().enumerate() {
if !self.deleted[index] && predicate(row) {
indices_to_delete.push(index);
}
}
if indices_to_delete.is_empty() {
return DeleteResult::new(0, false);
}
self.delete_by_indices(&indices_to_delete)
}
pub fn remove_row(&mut self, target_row: &Row) -> Result<(), StorageError> {
for (idx, row) in self.rows.iter().enumerate() {
if !self.deleted[idx] && row == target_row {
let _ = self.delete_by_indices(&[idx]);
return Ok(());
}
}
Err(StorageError::RowNotFound)
}
pub fn delete_by_indices(&mut self, indices: &[usize]) -> DeleteResult {
if indices.is_empty() {
return DeleteResult::new(0, false);
}
let mut deleted = 0;
for &idx in indices {
if idx >= self.rows.len() || self.deleted[idx] {
continue;
}
let row = &self.rows[idx];
self.indexes.update_for_delete(&self.schema, row);
self.deleted[idx] = true;
self.deleted_count += 1;
deleted += 1;
}
if deleted == 0 {
return DeleteResult::new(0, false);
}
let compacted = if self.should_compact() {
self.compact();
true
} else {
false
};
if self.native_columnar.is_some() {
let _ = self.rebuild_native_columnar();
}
DeleteResult::new(deleted, compacted)
}
pub fn delete_by_indices_batch(&mut self, indices: &[usize]) -> DeleteResult {
if indices.is_empty() {
return DeleteResult::new(0, false);
}
if indices.len() == 1 {
return self.delete_by_indices(indices);
}
let mut valid_indices: Vec<usize> = Vec::with_capacity(indices.len());
let mut rows_to_delete: Vec<&Row> = Vec::with_capacity(indices.len());
for &idx in indices {
if idx < self.rows.len() && !self.deleted[idx] {
valid_indices.push(idx);
rows_to_delete.push(&self.rows[idx]);
}
}
if valid_indices.is_empty() {
return DeleteResult::new(0, false);
}
self.indexes.batch_update_for_delete(&self.schema, &rows_to_delete);
let deleted = valid_indices.len();
for idx in valid_indices {
self.deleted[idx] = true;
self.deleted_count += 1;
}
let compacted = if self.should_compact() {
self.compact();
true
} else {
false
};
if self.native_columnar.is_some() {
let _ = self.rebuild_native_columnar();
}
DeleteResult::new(deleted, compacted)
}
#[inline]
fn should_compact(&self) -> bool {
!self.rows.is_empty() && self.deleted_count > self.rows.len() / 2
}
fn compact(&mut self) {
if self.deleted_count == 0 {
return;
}
let mut new_rows = Vec::with_capacity(self.rows.len() - self.deleted_count);
for (idx, row) in self.rows.iter().enumerate() {
if !self.deleted[idx] {
new_rows.push(row.clone());
}
}
self.rows = new_rows;
self.deleted = vec![false; self.rows.len()];
self.deleted_count = 0;
self.indexes.rebuild(&self.schema, &self.rows);
}
pub fn gc_old_versions(&mut self, horizon: crate::row::TxnId) -> usize {
if !cfg!(feature = "mvcc_enabled") {
let _ = horizon;
return 0;
}
let mut indices_to_gc: Vec<usize> = Vec::new();
for (idx, row) in self.rows.iter().enumerate() {
if self.deleted[idx] {
continue;
}
if let Some(xmax) = row.xmax {
if xmax != crate::row::PRE_MVCC_TXN_ID && xmax < horizon {
indices_to_gc.push(idx);
}
}
}
if indices_to_gc.is_empty() {
return 0;
}
let result = self.delete_by_indices(&indices_to_gc);
result.deleted_count
}
#[inline]
pub fn is_deleted(&self, idx: usize) -> bool {
idx < self.deleted.len() && self.deleted[idx]
}
pub fn rows_mut(&mut self) -> &mut Vec<Row> {
&mut self.rows
}
pub fn schema_mut(&mut self) -> &mut vibesql_catalog::TableSchema {
&mut self.schema
}
pub fn primary_key_index(&self) -> Option<&std::collections::HashMap<Vec<SqlValue>, usize>> {
self.indexes.primary_key_index()
}
pub fn unique_indexes(&self) -> &[std::collections::HashMap<Vec<SqlValue>, usize>] {
self.indexes.unique_indexes()
}
pub fn rebuild_indexes(&mut self) {
self.indexes = IndexManager::new(&self.schema);
self.indexes.rebuild(&self.schema, &self.rows);
}
}
#[cfg(test)]
mod tests {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use super::*;
fn create_test_table() -> Table {
let columns = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("name".to_string(), DataType::Varchar { max_length: Some(50) }, true),
];
let schema = TableSchema::with_primary_key(
"test_table".to_string(),
columns,
vec!["id".to_string()],
);
Table::new(schema)
}
fn create_row(id: i64, name: &str) -> Row {
Row::from_vec(vec![SqlValue::Integer(id), SqlValue::Varchar(arcstr::ArcStr::from(name))])
}
#[test]
fn test_append_mode_integration() {
let mut table = create_test_table();
assert!(!table.is_in_append_mode());
table.insert(create_row(1, "Alice")).unwrap();
table.insert(create_row(2, "Bob")).unwrap();
table.insert(create_row(3, "Charlie")).unwrap();
table.insert(create_row(4, "David")).unwrap();
assert!(table.is_in_append_mode());
table.clear();
assert!(!table.is_in_append_mode());
}
#[test]
fn test_scan_columnar() {
let mut table = create_test_table();
table.insert(create_row(1, "Alice")).unwrap();
table.insert(create_row(2, "Bob")).unwrap();
table.insert(create_row(3, "Charlie")).unwrap();
let columnar = table.scan_columnar().unwrap();
assert_eq!(columnar.row_count(), 3);
assert_eq!(columnar.column_count(), 2);
let id_col = columnar.get_column("id").expect("id column should exist");
assert_eq!(id_col.len(), 3);
assert!(!id_col.is_null(0));
assert!(!id_col.is_null(1));
assert!(!id_col.is_null(2));
let name_col = columnar.get_column("name").expect("name column should exist");
assert_eq!(name_col.len(), 3);
}
#[test]
fn test_scan_columnar_empty_table() {
let table = create_test_table();
let columnar = table.scan_columnar().unwrap();
assert_eq!(columnar.row_count(), 0);
assert_eq!(columnar.column_count(), 2); }
#[test]
fn test_scan_columnar_with_nulls() {
let columns = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("value".to_string(), DataType::Integer, true), ];
let schema = TableSchema::new("test_nulls".to_string(), columns);
let mut table = Table::new(schema);
table.insert(Row::from_vec(vec![SqlValue::Integer(1), SqlValue::Integer(100)])).unwrap();
table.insert(Row::from_vec(vec![SqlValue::Integer(2), SqlValue::Null])).unwrap();
table.insert(Row::from_vec(vec![SqlValue::Integer(3), SqlValue::Integer(300)])).unwrap();
let columnar = table.scan_columnar().unwrap();
let value_col = columnar.get_column("value").expect("value column should exist");
assert!(!value_col.is_null(0)); assert!(value_col.is_null(1)); assert!(!value_col.is_null(2)); }
#[test]
fn test_insert_batch_basic() {
let mut table = create_test_table();
let rows = vec![create_row(1, "Alice"), create_row(2, "Bob"), create_row(3, "Charlie")];
let count = table.insert_batch(rows).unwrap();
assert_eq!(count, 3);
assert_eq!(table.row_count(), 3);
let scanned: Vec<_> = table.scan().to_vec();
assert_eq!(scanned[0].values[0], SqlValue::Integer(1));
assert_eq!(scanned[1].values[0], SqlValue::Integer(2));
assert_eq!(scanned[2].values[0], SqlValue::Integer(3));
}
#[test]
fn test_insert_batch_empty() {
let mut table = create_test_table();
let count = table.insert_batch(Vec::new()).unwrap();
assert_eq!(count, 0);
assert_eq!(table.row_count(), 0);
}
#[test]
fn test_insert_batch_preserves_indexes() {
let mut table = create_test_table();
let rows = vec![create_row(1, "Alice"), create_row(2, "Bob"), create_row(3, "Charlie")];
table.insert_batch(rows).unwrap();
assert!(table.primary_key_index().is_some());
let pk_index = table.primary_key_index().unwrap();
assert_eq!(pk_index.len(), 3);
assert_eq!(pk_index.get(&vec![SqlValue::Integer(1)]), Some(&0));
assert_eq!(pk_index.get(&vec![SqlValue::Integer(2)]), Some(&1));
assert_eq!(pk_index.get(&vec![SqlValue::Integer(3)]), Some(&2));
}
#[test]
fn test_insert_batch_columnar_scan_includes_new_rows() {
let mut table = create_test_table();
table.insert(create_row(1, "Alice")).unwrap();
let _ = table.scan_columnar().unwrap();
let rows = vec![create_row(2, "Bob"), create_row(3, "Charlie")];
table.insert_batch(rows).unwrap();
let columnar = table.scan_columnar().unwrap();
assert_eq!(columnar.row_count(), 3);
}
#[test]
fn test_insert_batch_validation_failure_is_atomic() {
let mut table = create_test_table();
table.insert(create_row(1, "Alice")).unwrap();
let rows = vec![
Row::new(vec![SqlValue::Integer(2), SqlValue::Varchar(arcstr::ArcStr::from("Bob"))]),
Row::new(vec![SqlValue::Integer(3)]), ];
let result = table.insert_batch(rows);
assert!(result.is_err());
assert_eq!(table.row_count(), 1);
}
#[test]
fn test_insert_batch_large() {
let mut table = create_test_table();
let rows: Vec<Row> = (0..10_000).map(|i| create_row(i, &format!("User{}", i))).collect();
let count = table.insert_batch(rows).unwrap();
assert_eq!(count, 10_000);
assert_eq!(table.row_count(), 10_000);
let scanned = table.scan();
assert_eq!(scanned[0].values[0], SqlValue::Integer(0));
assert_eq!(scanned[9999].values[0], SqlValue::Integer(9999));
}
#[test]
fn test_insert_from_iter_basic() {
let mut table = create_test_table();
let rows = (0..100).map(|i| create_row(i, &format!("User{}", i)));
let count = table.insert_from_iter(rows, 10).unwrap();
assert_eq!(count, 100);
assert_eq!(table.row_count(), 100);
}
#[test]
fn test_insert_from_iter_default_batch_size() {
let mut table = create_test_table();
let rows = (0..50).map(|i| create_row(i, &format!("User{}", i)));
let count = table.insert_from_iter(rows, 0).unwrap();
assert_eq!(count, 50);
assert_eq!(table.row_count(), 50);
}
#[test]
fn test_insert_from_iter_partial_final_batch() {
let mut table = create_test_table();
let rows = (0..25).map(|i| create_row(i, &format!("User{}", i)));
let count = table.insert_from_iter(rows, 10).unwrap();
assert_eq!(count, 25);
assert_eq!(table.row_count(), 25);
}
#[test]
fn test_insert_batch_after_single_inserts() {
let mut table = create_test_table();
table.insert(create_row(1, "Alice")).unwrap();
table.insert(create_row(2, "Bob")).unwrap();
let rows = vec![create_row(3, "Charlie"), create_row(4, "David")];
table.insert_batch(rows).unwrap();
assert_eq!(table.row_count(), 4);
let pk_index = table.primary_key_index().unwrap();
assert_eq!(pk_index.get(&vec![SqlValue::Integer(1)]), Some(&0));
assert_eq!(pk_index.get(&vec![SqlValue::Integer(2)]), Some(&1));
assert_eq!(pk_index.get(&vec![SqlValue::Integer(3)]), Some(&2));
assert_eq!(pk_index.get(&vec![SqlValue::Integer(4)]), Some(&3));
}
#[test]
fn gc_off_state_returns_zero() {
let mut table = create_test_table();
table.insert(create_row(1, "Alice")).unwrap();
table.insert(create_row(2, "Bob")).unwrap();
let reclaimed = table.gc_old_versions(100);
assert_eq!(reclaimed, 0);
assert_eq!(table.row_count(), 2);
}
#[cfg(feature = "mvcc_enabled")]
#[test]
fn gc_reclaims_only_rows_with_xmax_below_horizon() {
let mut table = create_test_table();
table.insert(create_row(1, "Alice")).unwrap();
table.insert(create_row(2, "Bob")).unwrap();
table.insert(create_row(3, "Charlie")).unwrap();
table.stamp_row_xmax_inplace(0, 5);
table.stamp_row_xmax_inplace(1, 10);
let reclaimed = table.gc_old_versions(8);
assert_eq!(reclaimed, 1);
assert_eq!(table.row_count(), 2);
}
#[cfg(feature = "mvcc_enabled")]
#[test]
fn gc_reclaims_nothing_when_horizon_is_zero() {
let mut table = create_test_table();
table.insert(create_row(1, "Alice")).unwrap();
table.stamp_row_xmax_inplace(0, 5);
let reclaimed = table.gc_old_versions(0);
assert_eq!(reclaimed, 0);
assert_eq!(table.row_count(), 1);
}
#[cfg(feature = "mvcc_enabled")]
#[test]
fn gc_skips_rows_already_in_deletion_bitmap() {
let mut table = create_test_table();
table.insert(create_row(1, "Alice")).unwrap();
table.insert(create_row(2, "Bob")).unwrap();
let _ = table.delete_by_indices(&[0]);
table.stamp_row_xmax_inplace(1, 4);
let reclaimed = table.gc_old_versions(10);
assert_eq!(reclaimed, 1);
assert_eq!(table.row_count(), 0);
}
#[cfg(feature = "mvcc_enabled")]
#[test]
fn gc_respects_horizon_boundary() {
let mut table = create_test_table();
table.insert(create_row(1, "Alice")).unwrap();
table.stamp_row_xmax_inplace(0, 7);
let reclaimed = table.gc_old_versions(7);
assert_eq!(reclaimed, 0);
let reclaimed = table.gc_old_versions(8);
assert_eq!(reclaimed, 1);
}
}