use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Default)]
pub struct ValidityBitmap {
bits: Vec<u64>,
null_count: usize,
len: usize,
}
impl ValidityBitmap {
pub fn new_all_valid(len: usize) -> Self {
let num_words = len.div_ceil(64);
Self {
bits: vec![u64::MAX; num_words],
null_count: 0,
len,
}
}
pub fn new_all_null(len: usize) -> Self {
let num_words = len.div_ceil(64);
Self {
bits: vec![0; num_words],
null_count: len,
len,
}
}
#[inline]
pub fn is_valid(&self, idx: usize) -> bool {
if idx >= self.len {
return false;
}
let word = idx / 64;
let bit = idx % 64;
(self.bits[word] >> bit) & 1 == 1
}
#[inline]
pub fn set_valid(&mut self, idx: usize) {
if idx >= self.len {
return;
}
let word = idx / 64;
let bit = idx % 64;
if !self.is_valid(idx) {
self.bits[word] |= 1 << bit;
self.null_count = self.null_count.saturating_sub(1);
}
}
#[inline]
pub fn set_null(&mut self, idx: usize) {
if idx >= self.len {
return;
}
let word = idx / 64;
let bit = idx % 64;
if self.is_valid(idx) {
self.bits[word] &= !(1 << bit);
self.null_count = self.null_count.saturating_add(1);
}
}
pub fn push(&mut self, valid: bool) {
let idx = self.len;
self.len += 1;
let num_words = self.len.div_ceil(64);
while self.bits.len() < num_words {
self.bits.push(0);
}
if valid {
self.set_valid(idx);
} else {
self.null_count += 1;
}
}
pub fn null_count(&self) -> usize {
self.null_count
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
}
#[derive(Debug, Clone, Default)]
pub struct ColumnStats {
pub min_i64: Option<i64>,
pub max_i64: Option<i64>,
pub min_f64: Option<f64>,
pub max_f64: Option<f64>,
pub distinct_count: u64,
pub null_count: u64,
pub row_count: u64,
}
impl ColumnStats {
pub fn update_i64(&mut self, value: i64) {
self.min_i64 = Some(self.min_i64.map_or(value, |m| m.min(value)));
self.max_i64 = Some(self.max_i64.map_or(value, |m| m.max(value)));
self.row_count += 1;
}
pub fn update_f64(&mut self, value: f64) {
self.min_f64 = Some(self.min_f64.map_or(value, |m| m.min(value)));
self.max_f64 = Some(self.max_f64.map_or(value, |m| m.max(value)));
self.row_count += 1;
}
pub fn update_null(&mut self) {
self.null_count += 1;
self.row_count += 1;
}
}
#[derive(Debug, Clone)]
pub enum TypedColumn {
Int64 {
values: Vec<i64>,
validity: ValidityBitmap,
stats: ColumnStats,
},
UInt64 {
values: Vec<u64>,
validity: ValidityBitmap,
stats: ColumnStats,
},
Float64 {
values: Vec<f64>,
validity: ValidityBitmap,
stats: ColumnStats,
},
Text {
offsets: Vec<u32>,
data: Vec<u8>,
validity: ValidityBitmap,
stats: ColumnStats,
},
Binary {
offsets: Vec<u32>,
data: Vec<u8>,
validity: ValidityBitmap,
stats: ColumnStats,
},
Bool {
values: Vec<u64>,
validity: ValidityBitmap,
stats: ColumnStats,
len: usize,
},
}
impl TypedColumn {
pub fn new_int64() -> Self {
TypedColumn::Int64 {
values: Vec::new(),
validity: ValidityBitmap::default(),
stats: ColumnStats::default(),
}
}
pub fn new_uint64() -> Self {
TypedColumn::UInt64 {
values: Vec::new(),
validity: ValidityBitmap::default(),
stats: ColumnStats::default(),
}
}
pub fn new_float64() -> Self {
TypedColumn::Float64 {
values: Vec::new(),
validity: ValidityBitmap::default(),
stats: ColumnStats::default(),
}
}
pub fn new_text() -> Self {
TypedColumn::Text {
offsets: vec![0], data: Vec::new(),
validity: ValidityBitmap::default(),
stats: ColumnStats::default(),
}
}
pub fn new_binary() -> Self {
TypedColumn::Binary {
offsets: vec![0],
data: Vec::new(),
validity: ValidityBitmap::default(),
stats: ColumnStats::default(),
}
}
pub fn new_bool() -> Self {
TypedColumn::Bool {
values: Vec::new(),
validity: ValidityBitmap::default(),
stats: ColumnStats::default(),
len: 0,
}
}
pub fn len(&self) -> usize {
match self {
TypedColumn::Int64 { values, .. } => values.len(),
TypedColumn::UInt64 { values, .. } => values.len(),
TypedColumn::Float64 { values, .. } => values.len(),
TypedColumn::Text { offsets, .. } => offsets.len().saturating_sub(1),
TypedColumn::Binary { offsets, .. } => offsets.len().saturating_sub(1),
TypedColumn::Bool { len, .. } => *len,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn push_i64(&mut self, value: Option<i64>) {
if let TypedColumn::Int64 {
values,
validity,
stats,
} = self
{
match value {
Some(v) => {
values.push(v);
validity.push(true);
stats.update_i64(v);
}
None => {
values.push(0); validity.push(false);
stats.update_null();
}
}
}
}
pub fn push_u64(&mut self, value: Option<u64>) {
if let TypedColumn::UInt64 {
values,
validity,
stats,
} = self
{
match value {
Some(v) => {
values.push(v);
validity.push(true);
stats.update_i64(v as i64);
}
None => {
values.push(0);
validity.push(false);
stats.update_null();
}
}
}
}
pub fn push_f64(&mut self, value: Option<f64>) {
if let TypedColumn::Float64 {
values,
validity,
stats,
} = self
{
match value {
Some(v) => {
values.push(v);
validity.push(true);
stats.update_f64(v);
}
None => {
values.push(0.0);
validity.push(false);
stats.update_null();
}
}
}
}
pub fn push_text(&mut self, value: Option<&str>) {
if let TypedColumn::Text {
offsets,
data,
validity,
stats,
} = self
{
match value {
Some(s) => {
data.extend_from_slice(s.as_bytes());
offsets.push(data.len() as u32);
validity.push(true);
stats.row_count += 1;
}
None => {
offsets.push(data.len() as u32);
validity.push(false);
stats.update_null();
}
}
}
}
pub fn push_binary(&mut self, value: Option<&[u8]>) {
if let TypedColumn::Binary {
offsets,
data,
validity,
stats,
} = self
{
match value {
Some(b) => {
data.extend_from_slice(b);
offsets.push(data.len() as u32);
validity.push(true);
stats.row_count += 1;
}
None => {
offsets.push(data.len() as u32);
validity.push(false);
stats.update_null();
}
}
}
}
pub fn push_bool(&mut self, value: Option<bool>) {
if let TypedColumn::Bool {
values,
validity,
stats,
len,
} = self
{
let idx = *len;
*len += 1;
let num_words = (*len).div_ceil(64);
while values.len() < num_words {
values.push(0);
}
match value {
Some(v) => {
if v {
let word = idx / 64;
let bit = idx % 64;
values[word] |= 1 << bit;
}
validity.push(true);
stats.row_count += 1;
}
None => {
validity.push(false);
stats.update_null();
}
}
}
}
pub fn get_i64(&self, idx: usize) -> Option<i64> {
if let TypedColumn::Int64 {
values, validity, ..
} = self
&& idx < values.len()
&& validity.is_valid(idx)
{
return Some(values[idx]);
}
None
}
pub fn get_u64(&self, idx: usize) -> Option<u64> {
if let TypedColumn::UInt64 {
values, validity, ..
} = self
&& idx < values.len()
&& validity.is_valid(idx)
{
return Some(values[idx]);
}
None
}
pub fn get_f64(&self, idx: usize) -> Option<f64> {
if let TypedColumn::Float64 {
values, validity, ..
} = self
&& idx < values.len()
&& validity.is_valid(idx)
{
return Some(values[idx]);
}
None
}
pub fn get_text(&self, idx: usize) -> Option<&str> {
if let TypedColumn::Text {
offsets,
data,
validity,
..
} = self
&& idx + 1 < offsets.len()
&& validity.is_valid(idx)
{
let start = offsets[idx] as usize;
let end = offsets[idx + 1] as usize;
return std::str::from_utf8(&data[start..end]).ok();
}
None
}
pub fn get_binary(&self, idx: usize) -> Option<&[u8]> {
if let TypedColumn::Binary {
offsets,
data,
validity,
..
} = self
&& idx + 1 < offsets.len()
&& validity.is_valid(idx)
{
let start = offsets[idx] as usize;
let end = offsets[idx + 1] as usize;
return Some(&data[start..end]);
}
None
}
pub fn get_bool(&self, idx: usize) -> Option<bool> {
if let TypedColumn::Bool {
values,
validity,
len,
..
} = self
&& idx < *len
&& validity.is_valid(idx)
{
let word = idx / 64;
let bit = idx % 64;
return Some((values[word] >> bit) & 1 == 1);
}
None
}
pub fn is_null(&self, idx: usize) -> bool {
match self {
TypedColumn::Int64 { validity, .. } => !validity.is_valid(idx),
TypedColumn::UInt64 { validity, .. } => !validity.is_valid(idx),
TypedColumn::Float64 { validity, .. } => !validity.is_valid(idx),
TypedColumn::Text { validity, .. } => !validity.is_valid(idx),
TypedColumn::Binary { validity, .. } => !validity.is_valid(idx),
TypedColumn::Bool { validity, .. } => !validity.is_valid(idx),
}
}
pub fn stats(&self) -> &ColumnStats {
match self {
TypedColumn::Int64 { stats, .. } => stats,
TypedColumn::UInt64 { stats, .. } => stats,
TypedColumn::Float64 { stats, .. } => stats,
TypedColumn::Text { stats, .. } => stats,
TypedColumn::Binary { stats, .. } => stats,
TypedColumn::Bool { stats, .. } => stats,
}
}
#[inline]
pub fn sum_i64(&self) -> i64 {
if let TypedColumn::Int64 {
values, validity, ..
} = self
{
if validity.null_count() == 0 {
values.iter().sum()
} else {
values
.iter()
.enumerate()
.filter(|(i, _)| validity.is_valid(*i))
.map(|(_, v)| *v)
.sum()
}
} else {
0
}
}
#[inline]
pub fn sum_f64(&self) -> f64 {
if let TypedColumn::Float64 {
values, validity, ..
} = self
{
if validity.null_count() == 0 {
values.iter().sum()
} else {
values
.iter()
.enumerate()
.filter(|(i, _)| validity.is_valid(*i))
.map(|(_, v)| *v)
.sum()
}
} else {
0.0
}
}
pub fn memory_size(&self) -> usize {
match self {
TypedColumn::Int64 {
values, validity, ..
} => values.len() * 8 + validity.bits.len() * 8,
TypedColumn::UInt64 {
values, validity, ..
} => values.len() * 8 + validity.bits.len() * 8,
TypedColumn::Float64 {
values, validity, ..
} => values.len() * 8 + validity.bits.len() * 8,
TypedColumn::Text {
offsets,
data,
validity,
..
} => offsets.len() * 4 + data.len() + validity.bits.len() * 8,
TypedColumn::Binary {
offsets,
data,
validity,
..
} => offsets.len() * 4 + data.len() + validity.bits.len() * 8,
TypedColumn::Bool {
values, validity, ..
} => values.len() * 8 + validity.bits.len() * 8,
}
}
pub fn value_at(&self, idx: usize) -> crate::SochValue {
use crate::SochValue;
match self {
TypedColumn::Int64 {
values, validity, ..
} => {
if idx < values.len() && validity.is_valid(idx) {
SochValue::Int(values[idx])
} else {
SochValue::Null
}
}
TypedColumn::UInt64 {
values, validity, ..
} => {
if idx < values.len() && validity.is_valid(idx) {
SochValue::UInt(values[idx])
} else {
SochValue::Null
}
}
TypedColumn::Float64 {
values, validity, ..
} => {
if idx < values.len() && validity.is_valid(idx) {
SochValue::Float(values[idx])
} else {
SochValue::Null
}
}
TypedColumn::Text {
offsets,
data,
validity,
..
} => {
if idx + 1 < offsets.len() && validity.is_valid(idx) {
let start = offsets[idx] as usize;
let end = offsets[idx + 1] as usize;
std::str::from_utf8(&data[start..end])
.map(|s| SochValue::Text(s.to_owned()))
.unwrap_or(SochValue::Null)
} else {
SochValue::Null
}
}
TypedColumn::Binary {
offsets,
data,
validity,
..
} => {
if idx + 1 < offsets.len() && validity.is_valid(idx) {
let start = offsets[idx] as usize;
let end = offsets[idx + 1] as usize;
SochValue::Binary(data[start..end].to_vec())
} else {
SochValue::Null
}
}
TypedColumn::Bool {
values,
validity,
len,
..
} => {
if idx < *len && validity.is_valid(idx) {
let word = idx / 64;
let bit = idx % 64;
SochValue::Bool((values[word] >> bit) & 1 == 1)
} else {
SochValue::Null
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ColumnType {
Int64,
UInt64,
Float64,
Text,
Binary,
Bool,
}
impl ColumnType {
pub fn create_column(&self) -> TypedColumn {
match self {
ColumnType::Int64 => TypedColumn::new_int64(),
ColumnType::UInt64 => TypedColumn::new_uint64(),
ColumnType::Float64 => TypedColumn::new_float64(),
ColumnType::Text => TypedColumn::new_text(),
ColumnType::Binary => TypedColumn::new_binary(),
ColumnType::Bool => TypedColumn::new_bool(),
}
}
}
#[derive(Debug, Clone)]
pub struct ColumnChunk {
pub name: String,
pub column_type: ColumnType,
pub data: TypedColumn,
}
impl ColumnChunk {
pub fn new(name: impl Into<String>, column_type: ColumnType) -> Self {
Self {
name: name.into(),
column_type,
data: column_type.create_column(),
}
}
pub fn stats(&self) -> &ColumnStats {
self.data.stats()
}
}
#[derive(Debug)]
pub struct ColumnarTable {
pub name: String,
columns: HashMap<String, ColumnChunk>,
column_order: Vec<String>,
primary_key: Option<String>,
pk_index: std::collections::BTreeMap<i64, u32>,
row_count: AtomicU64,
}
impl Clone for ColumnarTable {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
columns: self.columns.clone(),
column_order: self.column_order.clone(),
primary_key: self.primary_key.clone(),
pk_index: self.pk_index.clone(),
row_count: AtomicU64::new(self.row_count.load(std::sync::atomic::Ordering::Relaxed)),
}
}
}
impl ColumnarTable {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
columns: HashMap::new(),
column_order: Vec::new(),
primary_key: None,
pk_index: std::collections::BTreeMap::new(),
row_count: AtomicU64::new(0),
}
}
pub fn add_column(&mut self, name: impl Into<String>, column_type: ColumnType) -> &mut Self {
let name = name.into();
self.column_order.push(name.clone());
self.columns
.insert(name.clone(), ColumnChunk::new(name, column_type));
self
}
pub fn set_primary_key(&mut self, column: impl Into<String>) -> &mut Self {
self.primary_key = Some(column.into());
self
}
pub fn row_count(&self) -> u64 {
self.row_count.load(Ordering::Relaxed)
}
pub fn get_column(&self, name: &str) -> Option<&ColumnChunk> {
self.columns.get(name)
}
pub fn get_column_mut(&mut self, name: &str) -> Option<&mut ColumnChunk> {
self.columns.get_mut(name)
}
pub fn get_by_pk(&self, pk: i64) -> Option<u32> {
self.pk_index.get(&pk).copied()
}
pub fn insert_row(&mut self, values: &HashMap<String, ColumnValue>) -> u32 {
let row_idx = self.row_count.fetch_add(1, Ordering::Relaxed) as u32;
for col_name in &self.column_order {
let chunk = self.columns.get_mut(col_name).unwrap();
let value = values.get(col_name);
match &mut chunk.data {
TypedColumn::Int64 {
values,
validity,
stats,
} => {
match value {
Some(ColumnValue::Int64(v)) => {
values.push(*v);
validity.push(true);
stats.update_i64(*v);
if self.primary_key.as_ref() == Some(col_name) {
self.pk_index.insert(*v, row_idx);
}
}
_ => {
values.push(0);
validity.push(false);
stats.update_null();
}
}
}
TypedColumn::UInt64 {
values,
validity,
stats,
} => match value {
Some(ColumnValue::UInt64(v)) => {
values.push(*v);
validity.push(true);
stats.update_i64(*v as i64);
}
_ => {
values.push(0);
validity.push(false);
stats.update_null();
}
},
TypedColumn::Float64 {
values,
validity,
stats,
} => match value {
Some(ColumnValue::Float64(v)) => {
values.push(*v);
validity.push(true);
stats.update_f64(*v);
}
_ => {
values.push(0.0);
validity.push(false);
stats.update_null();
}
},
TypedColumn::Text {
offsets,
data,
validity,
stats,
} => match value {
Some(ColumnValue::Text(s)) => {
data.extend_from_slice(s.as_bytes());
offsets.push(data.len() as u32);
validity.push(true);
stats.row_count += 1;
}
_ => {
offsets.push(data.len() as u32);
validity.push(false);
stats.update_null();
}
},
TypedColumn::Binary {
offsets,
data,
validity,
stats,
} => match value {
Some(ColumnValue::Binary(b)) => {
data.extend_from_slice(b);
offsets.push(data.len() as u32);
validity.push(true);
stats.row_count += 1;
}
_ => {
offsets.push(data.len() as u32);
validity.push(false);
stats.update_null();
}
},
TypedColumn::Bool {
values,
validity,
stats,
len,
} => {
let idx = *len;
*len += 1;
let num_words = (*len).div_ceil(64);
while values.len() < num_words {
values.push(0);
}
match value {
Some(ColumnValue::Bool(v)) => {
if *v {
let word = idx / 64;
let bit = idx % 64;
values[word] |= 1 << bit;
}
validity.push(true);
stats.row_count += 1;
}
_ => {
validity.push(false);
stats.update_null();
}
}
}
}
}
row_idx
}
pub fn memory_size(&self) -> usize {
self.columns.values().map(|c| c.data.memory_size()).sum()
}
pub fn memory_comparison(&self) -> MemoryComparison {
let typed_size = self.memory_size();
let row_count = self.row_count() as usize;
let column_count = self.columns.len();
let enum_size = row_count * column_count * 32;
MemoryComparison {
typed_bytes: typed_size,
enum_bytes: enum_size,
savings_ratio: if typed_size > 0 {
enum_size as f64 / typed_size as f64
} else {
1.0
},
}
}
}
#[derive(Debug, Clone)]
pub struct MemoryComparison {
pub typed_bytes: usize,
pub enum_bytes: usize,
pub savings_ratio: f64,
}
#[derive(Debug, Clone)]
pub enum ColumnValue {
Null,
Int64(i64),
UInt64(u64),
Float64(f64),
Text(String),
Binary(Vec<u8>),
Bool(bool),
}
#[derive(Debug, Default)]
pub struct ColumnarStore {
tables: HashMap<String, ColumnarTable>,
}
impl ColumnarStore {
pub fn new() -> Self {
Self {
tables: HashMap::new(),
}
}
pub fn create_table(&mut self, name: impl Into<String>) -> &mut ColumnarTable {
let name = name.into();
self.tables
.entry(name.clone())
.or_insert_with(|| ColumnarTable::new(name))
}
pub fn get_table(&self, name: &str) -> Option<&ColumnarTable> {
self.tables.get(name)
}
pub fn get_table_mut(&mut self, name: &str) -> Option<&mut ColumnarTable> {
self.tables.get_mut(name)
}
pub fn drop_table(&mut self, name: &str) -> bool {
self.tables.remove(name).is_some()
}
pub fn memory_size(&self) -> usize {
self.tables.values().map(|t| t.memory_size()).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validity_bitmap() {
let mut bitmap = ValidityBitmap::new_all_valid(10);
assert_eq!(bitmap.len(), 10);
assert_eq!(bitmap.null_count(), 0);
assert!(bitmap.is_valid(0));
assert!(bitmap.is_valid(9));
bitmap.set_null(5);
assert_eq!(bitmap.null_count(), 1);
assert!(!bitmap.is_valid(5));
bitmap.set_valid(5);
assert_eq!(bitmap.null_count(), 0);
assert!(bitmap.is_valid(5));
}
#[test]
fn test_int64_column() {
let mut col = TypedColumn::new_int64();
col.push_i64(Some(100));
col.push_i64(Some(200));
col.push_i64(None);
col.push_i64(Some(300));
assert_eq!(col.len(), 4);
assert_eq!(col.get_i64(0), Some(100));
assert_eq!(col.get_i64(1), Some(200));
assert_eq!(col.get_i64(2), None);
assert_eq!(col.get_i64(3), Some(300));
assert!(col.is_null(2));
assert_eq!(col.sum_i64(), 600);
}
#[test]
fn test_text_column() {
let mut col = TypedColumn::new_text();
col.push_text(Some("hello"));
col.push_text(Some("world"));
col.push_text(None);
col.push_text(Some("test"));
assert_eq!(col.len(), 4);
assert_eq!(col.get_text(0), Some("hello"));
assert_eq!(col.get_text(1), Some("world"));
assert_eq!(col.get_text(2), None);
assert_eq!(col.get_text(3), Some("test"));
}
#[test]
fn test_bool_column() {
let mut col = TypedColumn::new_bool();
col.push_bool(Some(true));
col.push_bool(Some(false));
col.push_bool(None);
col.push_bool(Some(true));
assert_eq!(col.len(), 4);
assert_eq!(col.get_bool(0), Some(true));
assert_eq!(col.get_bool(1), Some(false));
assert_eq!(col.get_bool(2), None);
assert_eq!(col.get_bool(3), Some(true));
assert!(col.memory_size() < 32);
}
#[test]
fn test_columnar_table() {
let mut table = ColumnarTable::new("users");
table.add_column("id", ColumnType::Int64);
table.add_column("name", ColumnType::Text);
table.add_column("active", ColumnType::Bool);
table.set_primary_key("id");
let mut row1 = HashMap::new();
row1.insert("id".to_string(), ColumnValue::Int64(1));
row1.insert("name".to_string(), ColumnValue::Text("Alice".to_string()));
row1.insert("active".to_string(), ColumnValue::Bool(true));
table.insert_row(&row1);
let mut row2 = HashMap::new();
row2.insert("id".to_string(), ColumnValue::Int64(2));
row2.insert("name".to_string(), ColumnValue::Text("Bob".to_string()));
row2.insert("active".to_string(), ColumnValue::Bool(false));
table.insert_row(&row2);
assert_eq!(table.row_count(), 2);
assert_eq!(table.get_by_pk(1), Some(0));
assert_eq!(table.get_by_pk(2), Some(1));
assert_eq!(table.get_by_pk(3), None);
let id_col = table.get_column("id").unwrap();
assert_eq!(id_col.data.get_i64(0), Some(1));
assert_eq!(id_col.data.get_i64(1), Some(2));
}
#[test]
fn test_memory_savings() {
let mut table = ColumnarTable::new("test");
table.add_column("id", ColumnType::Int64);
table.add_column("value", ColumnType::Float64);
table.add_column("flag", ColumnType::Bool);
for i in 0..1000 {
let mut row = HashMap::new();
row.insert("id".to_string(), ColumnValue::Int64(i));
row.insert("value".to_string(), ColumnValue::Float64(i as f64 * 1.5));
row.insert("flag".to_string(), ColumnValue::Bool(i % 2 == 0));
table.insert_row(&row);
}
let comparison = table.memory_comparison();
assert!(
comparison.savings_ratio > 3.0,
"Expected 3x+ savings, got {:.2}x",
comparison.savings_ratio
);
}
#[test]
fn test_simd_sum() {
let mut col = TypedColumn::new_int64();
for i in 0..10000 {
col.push_i64(Some(i));
}
let sum = col.sum_i64();
let expected: i64 = (0..10000).sum();
assert_eq!(sum, expected);
}
#[test]
fn test_columnar_store() {
let mut store = ColumnarStore::new();
{
let table = store.create_table("users");
table.add_column("id", ColumnType::Int64);
table.add_column("name", ColumnType::Text);
}
assert!(store.get_table("users").is_some());
assert!(store.get_table("orders").is_none());
store.drop_table("users");
assert!(store.get_table("users").is_none());
}
#[test]
fn test_column_stats() {
let mut col = TypedColumn::new_int64();
col.push_i64(Some(10));
col.push_i64(Some(50));
col.push_i64(None);
col.push_i64(Some(30));
col.push_i64(Some(20));
let stats = col.stats();
assert_eq!(stats.min_i64, Some(10));
assert_eq!(stats.max_i64, Some(50));
assert_eq!(stats.null_count, 1);
assert_eq!(stats.row_count, 5);
}
#[test]
fn test_typed_column_value_at() {
use crate::SochValue;
let mut col = TypedColumn::new_int64();
col.push_i64(Some(42));
col.push_i64(None);
col.push_i64(Some(-7));
assert_eq!(col.value_at(0), SochValue::Int(42));
assert_eq!(col.value_at(1), SochValue::Null);
assert_eq!(col.value_at(2), SochValue::Int(-7));
assert_eq!(col.value_at(99), SochValue::Null);
let mut fcol = TypedColumn::new_float64();
fcol.push_f64(Some(3.15));
fcol.push_f64(None);
assert_eq!(fcol.value_at(0), SochValue::Float(3.15));
assert_eq!(fcol.value_at(1), SochValue::Null);
let mut tcol = TypedColumn::new_text();
tcol.push_text(Some("hello"));
tcol.push_text(None);
tcol.push_text(Some("world"));
assert_eq!(tcol.value_at(0), SochValue::Text("hello".to_string()));
assert_eq!(tcol.value_at(1), SochValue::Null);
assert_eq!(tcol.value_at(2), SochValue::Text("world".to_string()));
let mut bcol = TypedColumn::new_bool();
bcol.push_bool(Some(true));
bcol.push_bool(Some(false));
bcol.push_bool(None);
assert_eq!(bcol.value_at(0), SochValue::Bool(true));
assert_eq!(bcol.value_at(1), SochValue::Bool(false));
assert_eq!(bcol.value_at(2), SochValue::Null);
}
}