use std::fmt;
use std::num::{NonZeroU64, NonZeroUsize};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::{Mutex, RwLock};
use crate::expression::CompiledFilter;
use crate::instrumentation;
use crate::mvcc::arena::RowArena;
use crate::mvcc::registry::TransactionRegistry;
use crate::timestamp::get_fast_timestamp;
use crate::Index;
use ahash::AHashMap;
use radixdb_core::{
CompactArc, CowBTree, DataType, Error, I64Map, I64Set, IndexType, Row, RowVec, Schema,
SmartString, Value,
};
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::{smallvec, SmallVec};
type CowBTreeMap<V> = RwLock<CowBTree<V>>;
#[inline]
fn new_i64_map<V>() -> I64Map<V> {
I64Map::new()
}
#[inline]
fn new_i64_map_with_capacity<V>(capacity: usize) -> I64Map<V> {
I64Map::with_capacity(capacity)
}
#[inline]
fn new_cow_btree_map<V: Clone>() -> CowBTreeMap<V> {
RwLock::new(CowBTree::new())
}
#[derive(Debug, Clone)]
pub struct IndexDefinition {
pub name: String,
pub table_name: String,
pub column_names: Vec<String>,
pub column_ids: Vec<i32>,
pub data_types: Vec<DataType>,
pub is_unique: bool,
pub index_type: IndexType,
pub hnsw_m: Option<u16>,
pub hnsw_ef_construction: Option<u16>,
pub hnsw_ef_search: Option<u16>,
pub hnsw_distance_metric: Option<u8>,
pub partial_predicate: Option<crate::index::PartialIndexPredicateMetadata>,
pub key_encoder: Option<crate::index::PreparedIndexKeyEncoder>,
}
#[cfg(not(test))]
const ROW_CLAIM_WAIT_TIMEOUT: Duration = Duration::from_secs(10);
#[cfg(test)]
const ROW_CLAIM_WAIT_TIMEOUT: Duration = Duration::from_millis(100);
struct ClaimOwnerWaitBudget<T> {
owner: Option<T>,
deadline: Instant,
}
impl<T: Copy + Eq> ClaimOwnerWaitBudget<T> {
fn new() -> Self {
Self {
owner: None,
deadline: Instant::now() + ROW_CLAIM_WAIT_TIMEOUT,
}
}
fn remaining(&mut self, owner: T) -> Option<Duration> {
let now = Instant::now();
if self.owner != Some(owner) {
self.owner = Some(owner);
self.deadline = now + ROW_CLAIM_WAIT_TIMEOUT;
}
(now < self.deadline).then(|| self.deadline - now)
}
}
type VersionList = SmallVec<[RowVersion; 1]>;
type RecoveryIndexTransition = (Arc<dyn Index>, Option<Vec<Value>>, Option<Vec<Value>>);
struct ExternalIndexRemoval {
index: Arc<dyn Index>,
values: Vec<Value>,
row_id: i64,
}
pub use crate::traits::index_values_for_row;
fn index_affecting_columns_changed(index: &dyn Index, old_row: &Row, new_row: &Row) -> bool {
if index.column_ids().iter().any(|&col_id| {
let col_idx = col_id as usize;
old_row.get(col_idx) != new_row.get(col_idx)
}) {
return true;
}
index.partial_predicate().is_some_and(|predicate| {
predicate.referenced_column_ids().iter().any(|&col_id| {
let col_idx = col_id as usize;
old_row.get(col_idx) != new_row.get(col_idx)
})
})
}
pub use crate::traits::GroupKey;
type GroupKeyMap<V> = AHashMap<GroupKey, V>;
pub use crate::traits::GroupedAggregateResult;
#[derive(Clone)]
pub struct RowVersion {
pub txn_id: i64,
pub deleted_at_txn_id: i64,
pub data: Row,
pub create_time: i64,
}
impl RowVersion {
pub fn new(txn_id: i64, data: Row) -> Self {
Self {
txn_id,
deleted_at_txn_id: 0,
data,
create_time: get_fast_timestamp(),
}
}
#[inline]
pub fn new_with_timestamp(txn_id: i64, data: Row, create_time: i64) -> Self {
Self {
txn_id,
deleted_at_txn_id: 0,
data,
create_time,
}
}
pub fn new_deleted(txn_id: i64, data: Row) -> Self {
Self {
txn_id,
deleted_at_txn_id: txn_id,
data,
create_time: get_fast_timestamp(),
}
}
#[inline]
pub fn new_deleted_with_timestamp(txn_id: i64, data: Row, create_time: i64) -> Self {
Self {
txn_id,
deleted_at_txn_id: txn_id,
data,
create_time,
}
}
pub fn is_deleted(&self) -> bool {
self.deleted_at_txn_id != 0
}
}
impl fmt::Debug for RowVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RowVersion")
.field("txn_id", &self.txn_id)
.field("deleted_at_txn_id", &self.deleted_at_txn_id)
.field("create_time", &self.create_time)
.finish()
}
}
impl fmt::Display for RowVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"RowVersion{{TxnID: {}, DeletedAtTxnID: {}, CreateTime: {}}}",
self.txn_id, self.deleted_at_txn_id, self.create_time
)
}
}
struct VersionChainEntry {
version: RowVersion,
prev: Option<Arc<VersionChainEntry>>,
arena_idx: Option<NonZeroU64>,
}
#[inline]
fn count_chain_depth(entry: &VersionChainEntry) -> usize {
let mut depth = 1;
let mut current = &entry.prev;
while let Some(prev) = current {
depth += 1;
current = &prev.prev;
}
depth
}
#[inline]
fn retention_cutoff(now: i64, retention: std::time::Duration) -> i64 {
let nanos = i64::try_from(retention.as_nanos()).unwrap_or(i64::MAX);
now.saturating_sub(nanos)
}
fn remove_column_from_version_chain(entry: &mut VersionChainEntry, column_index: usize) {
let mut current = entry;
loop {
current.version.data.remove_column(column_index);
let Some(previous) = &mut current.prev else {
break;
};
current = Arc::make_mut(previous);
}
}
#[inline]
fn estimate_value_hot_bytes(value: &Value) -> usize {
let base = std::mem::size_of::<Value>();
match value {
Value::Text(s) if s.is_heap() => base.saturating_add(s.len()),
Value::Extension(bytes) => base.saturating_add(bytes.len()),
_ => base,
}
}
#[inline]
pub fn estimate_row_hot_bytes(row: &Row) -> usize {
row.as_slice()
.iter()
.fold(std::mem::size_of::<Row>(), |total, value| {
total.saturating_add(estimate_value_hot_bytes(value))
})
}
#[inline]
fn accumulate_hot_bytes_delta(
old_bytes: usize,
new_bytes: usize,
add: &mut usize,
sub: &mut usize,
) {
if new_bytes > old_bytes {
*add = add.saturating_add(new_bytes - old_bytes);
} else if old_bytes > new_bytes {
*sub = sub.saturating_add(old_bytes - new_bytes);
}
}
#[inline(always)]
fn pack_arena_idx(idx: usize) -> Option<NonZeroU64> {
NonZeroU64::new((idx as u64).saturating_add(1))
}
#[inline(always)]
fn unpack_arena_idx(packed: Option<NonZeroU64>) -> Option<usize> {
packed.map(|nz| (nz.get() - 1) as usize)
}
#[inline(always)]
fn pack_row_arena_idx(idx: Option<usize>) -> Option<NonZeroUsize> {
idx.and_then(|i| NonZeroUsize::new(i.wrapping_add(1)))
}
#[inline(always)]
fn unpack_row_arena_idx(packed: Option<NonZeroUsize>) -> Option<usize> {
packed.map(|nz| nz.get().wrapping_sub(1))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteObservation {
Absent,
HotVersion,
ClaimedExisting,
}
#[derive(Clone)]
pub struct WriteSetEntry {
pub observation: WriteObservation,
pub read_version: Option<RowVersion>,
}
impl WriteSetEntry {
#[inline]
fn from_read(read_version: Option<RowVersion>) -> Self {
let observation = if read_version.is_some() {
WriteObservation::HotVersion
} else {
WriteObservation::Absent
};
Self {
observation,
read_version,
}
}
#[inline]
fn claimed() -> Self {
Self {
observation: WriteObservation::ClaimedExisting,
read_version: None,
}
}
}
const MAP_POOL_MAX_SIZE: usize = 64;
const MAP_POOL_MAX_RETAINED_CAPACITY: usize = 4096;
static VERSION_LIST_MAP_POOL: Mutex<Vec<I64Map<VersionList>>> = Mutex::new(Vec::new());
static WRITE_SET_MAP_POOL: Mutex<Vec<I64Map<WriteSetEntry>>> = Mutex::new(Vec::new());
#[inline]
fn get_version_list_map() -> I64Map<VersionList> {
if let Some(map) = VERSION_LIST_MAP_POOL.lock().pop() {
map
} else {
new_i64_map_with_capacity(TX_VERSION_MAP_INITIAL_CAPACITY)
}
}
#[inline]
fn get_write_set_map() -> I64Map<WriteSetEntry> {
if let Some(map) = WRITE_SET_MAP_POOL.lock().pop() {
map
} else {
new_i64_map_with_capacity(TX_VERSION_MAP_INITIAL_CAPACITY)
}
}
#[inline]
fn return_version_list_map(mut map: I64Map<VersionList>) {
if map.capacity() > MAP_POOL_MAX_RETAINED_CAPACITY {
return;
}
map.clear();
let mut pool = VERSION_LIST_MAP_POOL.lock();
if pool.len() < MAP_POOL_MAX_SIZE {
pool.push(map);
}
}
#[inline]
fn return_write_set_map(mut map: I64Map<WriteSetEntry>) {
if map.capacity() > MAP_POOL_MAX_RETAINED_CAPACITY {
return;
}
map.clear();
let mut pool = WRITE_SET_MAP_POOL.lock();
if pool.len() < MAP_POOL_MAX_SIZE {
pool.push(map);
}
}
pub fn clear_version_map_pools() {
VERSION_LIST_MAP_POOL.lock().clear();
WRITE_SET_MAP_POOL.lock().clear();
}
const TX_VERSION_MAP_INITIAL_CAPACITY: usize = 16;
#[derive(Clone, Copy, Debug)]
pub struct RowIndex {
pub row_id: i64,
arena_idx: Option<NonZeroUsize>,
}
impl RowIndex {
#[inline(always)]
pub fn new(row_id: i64, arena_idx: Option<usize>) -> Self {
Self {
row_id,
arena_idx: pack_row_arena_idx(arena_idx),
}
}
#[inline(always)]
pub fn arena_idx(&self) -> Option<usize> {
unpack_row_arena_idx(self.arena_idx)
}
}
pub use crate::traits::AggregateOp;
#[derive(Clone, Debug)]
pub enum AggregateResult {
Count(usize),
Sum(f64, usize),
Min(Option<Value>),
Max(Option<Value>),
Avg(f64, usize),
}
#[derive(Clone, Debug, Default)]
pub struct SealVisibilitySnapshot {
max_visible_txn_id: Option<i64>,
excluded_txn_ids: Arc<[i64]>,
}
impl SealVisibilitySnapshot {
pub fn new(max_visible_txn_id: Option<i64>, mut excluded_txn_ids: Vec<i64>) -> Self {
excluded_txn_ids.sort_unstable();
excluded_txn_ids.dedup();
Self {
max_visible_txn_id,
excluded_txn_ids: excluded_txn_ids.into(),
}
}
#[inline]
fn is_visible(&self, txn_id: i64) -> bool {
txn_id < 0
|| (txn_id > 0
&& self
.max_visible_txn_id
.is_none_or(|max_txn_id| txn_id <= max_txn_id)
&& self.excluded_txn_ids.binary_search(&txn_id).is_err())
}
}
enum AggregateAccumulator {
Count(usize),
Sum(i128, f64, usize),
Min(Option<Value>),
Max(Option<Value>),
Avg(i128, f64, usize),
}
pub trait VisibilityChecker: Send + Sync {
fn is_visible(&self, version_txn_id: i64, viewing_txn_id: i64) -> bool;
fn get_current_sequence(&self) -> i64;
fn get_active_transaction_ids(&self) -> Vec<i64>;
fn is_committed_before(&self, _txn_id: i64, _cutoff_commit_seq: i64) -> bool {
true }
fn capture_seal_visibility(&self) -> SealVisibilitySnapshot {
SealVisibilitySnapshot::default()
}
fn needs_snapshot_isolation(&self, _txn_id: i64) -> bool {
false }
fn register_row_wait(&self, _waiter_txn_id: i64, _owner_txn_id: i64) -> bool {
true
}
fn clear_row_wait(&self, _waiter_txn_id: i64) {}
}
enum VisibilityOwner {
Registry(Arc<TransactionRegistry>),
Custom(Arc<dyn VisibilityChecker>),
}
impl VisibilityChecker for VisibilityOwner {
#[inline]
fn is_visible(&self, version_txn_id: i64, viewing_txn_id: i64) -> bool {
match self {
Self::Registry(checker) => checker.is_visible(version_txn_id, viewing_txn_id),
Self::Custom(checker) => checker.is_visible(version_txn_id, viewing_txn_id),
}
}
#[inline]
fn get_current_sequence(&self) -> i64 {
match self {
Self::Registry(checker) => checker.get_current_sequence(),
Self::Custom(checker) => checker.get_current_sequence(),
}
}
fn get_active_transaction_ids(&self) -> Vec<i64> {
match self {
Self::Registry(checker) => checker.get_active_transaction_ids(),
Self::Custom(checker) => checker.get_active_transaction_ids(),
}
}
fn is_committed_before(&self, txn_id: i64, cutoff_commit_seq: i64) -> bool {
match self {
Self::Registry(checker) => checker.is_committed_before(txn_id, cutoff_commit_seq),
Self::Custom(checker) => checker.is_committed_before(txn_id, cutoff_commit_seq),
}
}
fn capture_seal_visibility(&self) -> SealVisibilitySnapshot {
match self {
Self::Registry(checker) => checker.capture_seal_visibility(),
Self::Custom(checker) => checker.capture_seal_visibility(),
}
}
#[inline]
fn needs_snapshot_isolation(&self, txn_id: i64) -> bool {
match self {
Self::Registry(checker) => checker.needs_snapshot_isolation(txn_id),
Self::Custom(checker) => checker.needs_snapshot_isolation(txn_id),
}
}
fn register_row_wait(&self, waiter_txn_id: i64, owner_txn_id: i64) -> bool {
match self {
Self::Registry(checker) => checker.register_row_wait(waiter_txn_id, owner_txn_id),
Self::Custom(checker) => checker.register_row_wait(waiter_txn_id, owner_txn_id),
}
}
fn clear_row_wait(&self, waiter_txn_id: i64) {
match self {
Self::Registry(checker) => checker.clear_row_wait(waiter_txn_id),
Self::Custom(checker) => checker.clear_row_wait(waiter_txn_id),
}
}
}
pub struct ExtractionSnapshot {
inner: radixdb_core::CowBTree<VersionChainEntry>,
}
#[derive(Default)]
pub struct SealedIndexCleanup {
pub removed_ids: Vec<i64>,
snapshot: Option<radixdb_core::CowBTree<VersionChainEntry>>,
}
pub struct VersionStore {
versions: CowBTreeMap<VersionChainEntry>,
table_name: SmartString,
schema: RwLock<CompactArc<Schema>>,
requires_row_normalization: AtomicBool,
indexes: RwLock<FxHashMap<String, Arc<dyn Index>>>,
closed: AtomicBool,
mutation_gate: RwLock<()>,
auto_increment_counter: AtomicI64,
uncommitted_writes: RwLock<I64Map<i64>>,
unique_key_claims: Mutex<unique_claims::SharedUniqueClaimMap>,
claim_wait_mutex: Mutex<()>,
claim_changed: parking_lot::Condvar,
visibility_checker: Option<VisibilityOwner>,
arena: RowArena,
zone_maps: RwLock<Option<Arc<crate::volume::zonemap::TableZoneMap>>>,
zone_map_generation: AtomicU64,
max_version_history: usize,
committed_row_count: AtomicUsize,
committed_hot_bytes: AtomicUsize,
membership_fence: Arc<RwLock<()>>,
}
mod aggregate;
mod committed;
mod indexes;
mod lifecycle;
mod private;
mod read;
mod unique_claims;
pub use private::TransactionVersionStore;
impl Clone for VersionChainEntry {
fn clone(&self) -> Self {
Self {
version: self.version.clone(),
prev: self.prev.clone(), arena_idx: self.arena_idx,
}
}
}
impl Drop for VersionChainEntry {
fn drop(&mut self) {
let mut previous = self.prev.take();
while let Some(shared) = previous {
match Arc::try_unwrap(shared) {
Ok(mut unique) => previous = unique.prev.take(),
Err(shared) => {
drop(shared);
break;
}
}
}
}
}
impl fmt::Debug for VersionStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VersionStore")
.field("table_name", &self.table_name)
.field("row_count", &self.row_count())
.field("committed_hot_bytes", &self.committed_hot_bytes())
.field("closed", &self.closed.load(Ordering::Acquire))
.finish()
}
}
#[cfg(test)]
mod tests;