use std::collections::{BTreeSet, HashMap};
use std::sync::atomic::{AtomicU64, Ordering};
use bitvec::order::Lsb0;
use bitvec::vec::BitVec;
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::encoding::{ChunkEncodingMeta, ColumnEncoding, EncodingType};
use crate::vertex::column::chunk_encoding::{
overlay_capacity_for, UpdateOverlay, OVERLAY_RECODE_MEMORY_FLOOR,
OVERLAY_RECODE_MEMORY_RATIO_DEN, OVERLAY_RECODE_MEMORY_RATIO_NUM,
};
use crate::vertex::column::chunk_residency::{next_tick, ChunkResidency};
use crate::vertex::column::column::ColumnInner;
use crate::vertex::column::mvcc::{RowVisibility, VersionEntry};
use crate::vertex::column::overflow::OverflowHandle;
use crate::vertex::latch_order::{Guard, RANK_SEGMENT};
use linkrs_core::{DataType, Value};
pub const DEFAULT_CHUNK_ROWS: usize = 4_096;
pub struct ChunkState {
pub raw: ColumnInner,
pub encoding: ColumnEncoding,
pub overlay: UpdateOverlay,
pub encoding_meta: ChunkEncodingMeta,
pub updates_since_encode: u64,
pub version_chains: Option<HashMap<usize, Vec<VersionEntry>>>,
pub visibility: RowVisibility,
pub dirty_pages: BTreeSet<u32>,
pub overflow_rows: HashMap<u32, OverflowHandle>,
pub residency: ChunkResidency,
}
pub struct ColumnChunk {
pub row_offset: usize,
pub row_count: usize,
pub element_size: usize,
pub state: RwLock<ChunkState>,
pub last_access: AtomicU64,
}
impl ColumnChunk {
pub fn new(row_offset: usize, row_count: usize, data_type: &DataType, nullable: bool) -> Self {
use super::column::ColumnInner;
use super::fixed_width::FixedWidthColumn;
use super::variable_width::VariableWidthColumn;
let raw = if super::is_variable_length_type(data_type) {
ColumnInner::Variable(VariableWidthColumn::new(data_type.clone(), nullable))
} else {
let mut fixed = FixedWidthColumn::new(data_type.clone(), nullable);
fixed.data = vec![0u8; row_count * fixed.element_size];
fixed.row_count = row_count;
ColumnInner::Fixed(fixed)
};
Self {
row_offset,
row_count,
element_size: super::fixed_width::element_size(data_type),
state: RwLock::new(ChunkState {
raw,
encoding: ColumnEncoding::None,
overlay: UpdateOverlay::new(overlay_capacity_for(row_count)),
encoding_meta: ChunkEncodingMeta::default(),
updates_since_encode: 0,
version_chains: None,
visibility: RowVisibility::new(),
dirty_pages: BTreeSet::new(),
overflow_rows: HashMap::new(),
residency: ChunkResidency::Resident,
}),
last_access: AtomicU64::new(0),
}
}
#[inline]
pub fn read_state(&self) -> Guard<RwLockReadGuard<'_, ChunkState>> {
Guard::claim(
self.state.read(),
RANK_SEGMENT,
"column segment latch (read)",
)
}
#[inline]
pub fn write_state(&self) -> Guard<RwLockWriteGuard<'_, ChunkState>> {
Guard::claim(
self.state.write(),
RANK_SEGMENT,
"column segment latch (write)",
)
}
pub fn touch(&self) {
self.last_access.store(next_tick(), Ordering::Relaxed);
}
pub fn evicted_encoding(&self) -> EncodingType {
let state = self.read_state();
state
.residency
.effective_encoding(state.encoding.encoding_type())
}
pub fn is_evictable(&self) -> bool {
let state = self.read_state();
matches!(state.residency, ChunkResidency::Resident)
&& state.encoding.is_encoded()
&& state.overlay.len() == 0
&& state.overflow_rows.is_empty()
&& state
.version_chains
.as_ref()
.is_none_or(|chains| chains.values().all(|chain| chain.is_empty()))
}
pub fn set_value(
state: &mut ChunkState,
local_row: u32,
value: Option<Value>,
data_type: &linkrs_core::DataType,
) -> crate::vertex::column::chunk_encoding::UpdateDecision {
use crate::vertex::column::chunk_encoding::{can_update_in_place, UpdateDecision};
if !state.encoding.is_encoded() {
return UpdateDecision::InPlace;
}
let decision = can_update_in_place(
&state.encoding,
data_type,
&state.encoding_meta,
local_row,
value.as_ref(),
state.overlay.is_full(),
);
match decision {
UpdateDecision::InPlace => {
if state
.encoding
.set(local_row as usize, value.as_ref())
.is_ok()
{
return UpdateDecision::InPlace;
}
state.overlay.put(local_row, value);
state.updates_since_encode += 1;
UpdateDecision::Overlay
}
UpdateDecision::Overlay | UpdateDecision::OverlayAndRecode => {
state.overlay.put(local_row, value);
state.updates_since_encode += 1;
decision
}
}
}
pub fn mark_hot(&self) {
let mut state = self.write_state();
state.updates_since_encode = state
.updates_since_encode
.max(state.overlay.capacity() as u64);
}
pub fn needs_recode(&self) -> bool {
let state = self.read_state();
if state.overlay.is_full() {
return true;
}
if state.updates_since_encode < state.overlay.capacity() as u64 {
return false;
}
if !state.encoding.is_encoded() {
return true;
}
let overlay_bytes = state.overlay.memory_usage();
if overlay_bytes < OVERLAY_RECODE_MEMORY_FLOOR {
return false;
}
let encoded_bytes = state.encoding.memory_usage().max(1);
overlay_bytes.saturating_mul(OVERLAY_RECODE_MEMORY_RATIO_DEN)
>= encoded_bytes.saturating_mul(OVERLAY_RECODE_MEMORY_RATIO_NUM)
}
pub fn refresh_encoding_meta(
&self,
num_values: u32,
all_null: bool,
min: Option<Value>,
max: Option<Value>,
compressed_size: u64,
raw_size: u64,
) {
let mut state = self.write_state();
state.encoding_meta.scheme = state.encoding.encoding_type();
state.encoding_meta.num_values = num_values;
state.encoding_meta.all_null = all_null;
state.encoding_meta.min = min;
state.encoding_meta.max = max;
state.encoding_meta.compressed_size = compressed_size;
state.encoding_meta.raw_size = raw_size;
}
}
impl Clone for ColumnChunk {
fn clone(&self) -> Self {
let state = self.read_state();
Self {
row_offset: self.row_offset,
row_count: self.row_count,
element_size: self.element_size,
state: RwLock::new(ChunkState {
raw: state.raw.clone(),
encoding: state.encoding.clone(),
overlay: state.overlay.clone(),
encoding_meta: state.encoding_meta.clone(),
updates_since_encode: state.updates_since_encode,
version_chains: state.version_chains.clone(),
visibility: state.visibility.clone(),
dirty_pages: state.dirty_pages.clone(),
overflow_rows: state.overflow_rows.clone(),
residency: state.residency.clone(),
}),
last_access: AtomicU64::new(self.last_access.load(Ordering::Relaxed)),
}
}
}
impl std::fmt::Debug for ColumnChunk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state = self.read_state();
f.debug_struct("ColumnChunk")
.field("row_offset", &self.row_offset)
.field("row_count", &self.row_count)
.field("raw_len", &state.raw.as_storage().len())
.field("encoding", &state.encoding.encoding_type())
.field("resident", &state.residency.is_resident())
.finish()
}
}
#[derive(Debug, Clone)]
pub struct ChunkFlushView {
pub row_offset: usize,
pub row_count: usize,
pub encoding_meta: ChunkEncodingMeta,
pub encoding: ColumnEncoding,
pub overlay: Vec<(u32, Option<Value>)>,
pub raw_form: bool,
pub raw_data: Vec<u8>,
pub raw_offsets: Vec<u32>,
pub raw_bitmap: Option<BitVec<u8, Lsb0>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chunk_fixed_width_create() {
let chunk = ColumnChunk::new(0, 100, &DataType::Int, true);
assert_eq!(chunk.row_offset, 0);
assert_eq!(chunk.row_count, 100);
assert_eq!(chunk.read_state().raw.as_storage().len(), 100);
assert_eq!(
chunk.read_state().raw.as_storage().get(0),
Some(Value::Int(0))
);
}
#[test]
fn test_chunk_variable_width_create() {
let chunk = ColumnChunk::new(0, 50, &DataType::String, false);
assert_eq!(chunk.row_offset, 0);
assert_eq!(chunk.row_count, 50);
assert_eq!(chunk.read_state().raw.as_storage().len(), 0);
}
#[test]
fn test_recode_combines_count_with_memory() {
use crate::encoding::ColumnEncoding;
use crate::encoding::RleBoolColumn;
let chunk = ColumnChunk::new(0, 4096, &DataType::Bool, true);
let capacity = chunk.read_state().overlay.capacity();
assert!(capacity >= 2);
let mut runs = RleBoolColumn::new();
for i in 0..4096 {
runs.append(Some(&Value::Bool(i % 2 == 0))).unwrap();
}
{
let mut state = chunk.write_state();
state.encoding = ColumnEncoding::RleBool(runs);
state.overlay.put(7, Some(Value::Bool(true)));
state.updates_since_encode = capacity as u64;
}
assert!(
!chunk.needs_recode(),
"repeated tiny overwrites must stay buffered while the payload is small"
);
{
let mut state = chunk.write_state();
for row in 0..capacity as u32 {
state.overlay.put(100 + row, Some(Value::Bool(false)));
}
}
assert!(
chunk.needs_recode(),
"a full overlay must still trigger regardless of payload size"
);
}
#[test]
fn test_recode_raw_keeps_count_signal() {
let chunk = ColumnChunk::new(0, 4096, &DataType::Int, true);
let capacity = chunk.read_state().overlay.capacity();
{
let mut state = chunk.write_state();
state.updates_since_encode = capacity as u64;
}
assert!(chunk.needs_recode());
}
}