use arc_swap::{ArcSwap, ArcSwapOption};
use radixdb_catalog::{CatalogPublisher, ObjectId};
use radixdb_core::{CompactArc, I64Map, SmartString};
use rustc_hash::{FxHashMap, FxHashSet};
use serde::Serialize;
use smallvec::SmallVec;
use std::borrow::Cow;
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex, OnceLock, RwLock};
use std::time::Duration;
use radixdb_core::time_compat::Instant;
#[inline]
fn to_lowercase_cow(s: &str) -> Cow<'_, str> {
if s.bytes().all(|b| !b.is_ascii_uppercase()) {
Cow::Borrowed(s)
} else {
Cow::Owned(s.to_lowercase())
}
}
use super::file_lock::FileLock;
use crate::config::Config;
use crate::expression::Expression;
use crate::index::BTreeIndex;
use crate::instrumentation;
use crate::mvcc::persistence::PendingDmlWalOperation;
use crate::mvcc::wal_manager::WALOperationType;
use crate::mvcc::{
get_fast_timestamp, CatalogWriteFenceGuard, DdlFenceGuard, MVCCTable, MvccTransaction,
PersistenceManager, PkIndex, RowVersion, SealFenceGuard, TransactionEngineOperations,
TransactionRegistry, TransactionVersionStore, TransactionalDdlPreparation,
TransactionalDdlPublication, VersionStore, VisibilityFenceGuard, INVALID_TRANSACTION_ID,
};
#[cfg(test)]
use crate::traits::PendingIndexDefinition;
use crate::traits::{
Engine, Index, PendingIndexDrop, PendingSchemaChange, Scanner, SchemaPhysicalTransition, Table,
Transaction,
};
use crate::volume::column::ROW_GROUP_SIZE;
use crate::volume::manifest::SegmentLevel;
use radixdb_core::{
DataType, Error, ForeignKeyConstraint, IsolationLevel, Result, Row, Schema,
SchemaConstraintKind, Value, ValueSet,
};
type StringMap<V> = ahash::AHashMap<String, V>;
mod catalog;
mod catalog_runtime;
mod checkpoint;
mod cleanup;
mod compaction;
mod engine_trait;
mod lifecycle;
mod open;
mod operations;
mod physical_publication;
mod physical_snapshot;
mod recovery;
mod runtime;
mod seal;
mod views;
pub use catalog_runtime::{
CatalogRuntime, CatalogRuntimeBinder, CatalogRuntimeTable, IntoCatalogRuntimeBinder,
};
pub use cleanup::CleanupHandle;
use operations::EngineOperations;
use runtime::{BackgroundWorkerRuntimeGuard, EngineMaintenanceState, PendingTable};
pub use runtime::{
EngineCompactionCostSnapshot, EngineLifecycleState, EngineMaintenanceSnapshot,
EngineRuntimeOperationDetail, EngineRuntimeOperationSnapshot, EngineRuntimeStatsV2,
EngineRuntimeVisitLimits,
};
#[cfg(test)]
use runtime::{EngineCompactionCostState, RuntimeOperationState};
type TxnTableEntry = (SmartString, Arc<RwLock<TransactionVersionStore>>);
type TxnVersionStoreMap = I64Map<SmallVec<[TxnTableEntry; 2]>>;
type CheckpointCandidate = (
String,
CompactArc<Schema>,
Arc<VersionStore>,
bool,
usize,
u64,
);
fn frozen_volume_physical_bytes(volume: &crate::volume::writer::FrozenVolume) -> u64 {
let data = volume
.artifact_source()
.map_or(0, |source| source.layout().reference().byte_length());
let index = volume
.artifact_index_source()
.map_or(0, |source| source.layout().reference().byte_length());
data.saturating_add(index)
}
const FORCED_CHECKPOINT_HOT_ROWS_UNSEALED: &str =
"forced checkpoint left committed hot rows unsealed";
const SEAL_ROW_THRESHOLD: usize = 100_000;
const SEAL_INCREMENTAL_THRESHOLD: usize = 10_000;
const TOTAL_HOT_SOFT_MULTIPLIER: usize = 4;
const TOTAL_HOT_HARD_MULTIPLIER: usize = 8;
const fn total_hot_soft_threshold(first_seal_bytes: usize) -> usize {
first_seal_bytes.saturating_mul(TOTAL_HOT_SOFT_MULTIPLIER)
}
const fn total_hot_hard_threshold(first_seal_bytes: usize) -> usize {
first_seal_bytes.saturating_mul(TOTAL_HOT_HARD_MULTIPLIER)
}
const RUNTIME_STATS_MAX_TABLES: usize = 4_096;
const RUNTIME_STATS_MAX_SEGMENTS: usize = 16_384;
const RUNTIME_STATS_MAX_TRANSACTIONS: usize = 4_096;
const RUNTIME_STATS_MAX_STAGING_TABLES: usize = 16_384;
const RUNTIME_STATS_MAX_ACTIVE_SEGMENT_IDS: usize = 64;
fn runtime_unix_millis() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.min(u128::from(u64::MAX)) as u64
}
fn runtime_owner_id(name: &str) -> u64 {
let mut hash = 0xcbf29ce484222325_u64;
for byte in name.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
struct PressureSealControl {
requested: AtomicBool,
worker_active: AtomicBool,
completed_cycles: AtomicU64,
wait_lock: Mutex<()>,
wait_condvar: Condvar,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PressureSealCycleOutcome {
Idle,
Completed,
Superseded,
}
impl PressureSealCycleOutcome {
const fn keeps_request_armed(self, pressure_remains: bool) -> bool {
matches!(self, Self::Completed) && pressure_remains
}
}
impl PressureSealControl {
fn new() -> Self {
Self {
requested: AtomicBool::new(false),
worker_active: AtomicBool::new(false),
completed_cycles: AtomicU64::new(0),
wait_lock: Mutex::new(()),
wait_condvar: Condvar::new(),
}
}
fn request(&self) {
self.requested.store(true, Ordering::Release);
}
fn wait_before_commit(&self) {
if !self.requested.load(Ordering::Acquire) || !self.worker_active.load(Ordering::Acquire) {
return;
}
let observed_cycle = self.completed_cycles.load(Ordering::Acquire);
let mut guard = self.wait_lock.lock().unwrap_or_else(|e| e.into_inner());
while self.requested.load(Ordering::Acquire)
&& self.worker_active.load(Ordering::Acquire)
&& self.completed_cycles.load(Ordering::Acquire) == observed_cycle
{
guard = self
.wait_condvar
.wait(guard)
.unwrap_or_else(|e| e.into_inner());
}
}
fn finish_cycle(&self, still_requested: bool) {
let _guard = self.wait_lock.lock().unwrap_or_else(|e| e.into_inner());
self.requested.store(still_requested, Ordering::Release);
self.completed_cycles.fetch_add(1, Ordering::Release);
self.wait_condvar.notify_all();
}
fn worker_started(&self) {
self.worker_active.store(true, Ordering::Release);
}
fn worker_stopped(&self) {
let _guard = self.wait_lock.lock().unwrap_or_else(|e| e.into_inner());
self.worker_active.store(false, Ordering::Release);
self.requested.store(false, Ordering::Release);
self.wait_condvar.notify_all();
}
}
struct PressureSealWorkerGuard(Arc<PressureSealControl>);
impl Drop for PressureSealWorkerGuard {
fn drop(&mut self) {
self.0.worker_stopped();
}
}
#[inline]
fn compaction_segment_is_mergeable(row_count: usize, target_volume_rows: usize) -> bool {
row_count < target_volume_rows
}
#[inline]
fn size_tier_target_rows(target_volume_rows: usize, compact_threshold: usize) -> usize {
target_volume_rows.saturating_mul(compact_threshold.max(2))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CompactionCandidate {
manifest_index: usize,
physical_bytes: u64,
must_rewrite: bool,
}
fn select_bounded_compaction_run(
candidates: &[CompactionCandidate],
max_input_segments: usize,
max_input_bytes: u64,
) -> Vec<usize> {
let max_input_segments = max_input_segments.max(1);
let max_input_bytes = max_input_bytes.max(1);
for start in 0..candidates.len() {
let mut selected = Vec::new();
let mut selected_bytes = 0_u64;
let mut previous_index = None;
let mut contains_rewrite = false;
for candidate in &candidates[start..] {
if previous_index.is_some_and(|previous| candidate.manifest_index != previous + 1) {
break;
}
if selected.len() >= max_input_segments {
break;
}
let next_bytes = selected_bytes.saturating_add(candidate.physical_bytes);
if next_bytes > max_input_bytes {
break;
}
selected.push(candidate.manifest_index);
selected_bytes = next_bytes;
contains_rewrite |= candidate.must_rewrite;
previous_index = Some(candidate.manifest_index);
}
if contains_rewrite || selected.len() >= 2 {
return selected;
}
}
Vec::new()
}
fn effective_compaction_input_budget(
max_input_bytes: u64,
max_output_bytes: u64,
time_budget_ms: u64,
io_bytes_per_sec: u64,
) -> u64 {
let time_limited_input = if time_budget_ms == 0 || io_bytes_per_sec == 0 {
u64::MAX
} else {
io_bytes_per_sec
.saturating_mul(time_budget_ms)
.checked_div(2_000)
.unwrap_or(u64::MAX)
.max(1)
};
max_input_bytes
.max(1)
.min(max_output_bytes.max(1))
.min(time_limited_input)
}
fn compaction_io_rate_per_job(total_bytes_per_sec: u64, concurrent_jobs: usize) -> u64 {
if total_bytes_per_sec == 0 {
return 0;
}
total_bytes_per_sec
.checked_div(concurrent_jobs.max(1) as u64)
.unwrap_or(total_bytes_per_sec)
.max(1)
}
#[cfg(unix)]
fn compaction_filesystem_available_bytes(path: &Path) -> Result<Option<u64>> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let path = CString::new(path.as_os_str().as_bytes()).map_err(|_| {
Error::internal(format!(
"cannot inspect compaction filesystem for path containing NUL: {}",
path.display()
))
})?;
let mut stats = std::mem::MaybeUninit::<libc::statvfs>::uninit();
if unsafe { libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) } != 0 {
return Err(Error::internal(format!(
"cannot inspect compaction filesystem free space: {}",
std::io::Error::last_os_error()
)));
}
let stats = unsafe { stats.assume_init() };
Ok(Some(stats.f_bavail.saturating_mul(stats.f_frsize)))
}
#[cfg(not(unix))]
fn compaction_filesystem_available_bytes(_path: &Path) -> Result<Option<u64>> {
Ok(None)
}
#[derive(Default)]
struct CompactionDiskReservationPool {
remaining_output_bytes: Mutex<u64>,
}
impl CompactionDiskReservationPool {
fn reserve(
self: &Arc<Self>,
output_dir: &Path,
max_output_bytes: u64,
disk_reserve_bytes: u64,
) -> Result<CompactionDiskReservation> {
let requested = max_output_bytes.max(1);
let mut remaining = self
.remaining_output_bytes
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let new_remaining = remaining.checked_add(requested).ok_or_else(|| {
compaction_abort(
"disk_reserve_exhausted",
"aggregate compaction output reservation overflow",
)
})?;
let required = disk_reserve_bytes
.checked_add(new_remaining)
.ok_or_else(|| {
compaction_abort(
"disk_reserve_exhausted",
"aggregate compaction disk reserve overflow",
)
})?;
if let Some(available) = compaction_filesystem_available_bytes(output_dir)? {
if available < required {
return Err(compaction_abort(
"disk_reserve_exhausted",
format_args!("available {available} bytes, required {required} bytes"),
));
}
}
*remaining = new_remaining;
Ok(CompactionDiskReservation {
pool: Arc::clone(self),
remaining: requested,
})
}
}
struct CompactionDiskReservation {
pool: Arc<CompactionDiskReservationPool>,
remaining: u64,
}
impl CompactionDiskReservation {
fn ensure_headroom(&self, output_dir: &Path, disk_reserve_bytes: u64) -> Result<()> {
let remaining = self
.pool
.remaining_output_bytes
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let required = disk_reserve_bytes.checked_add(*remaining).ok_or_else(|| {
compaction_abort(
"disk_reserve_exhausted",
"aggregate compaction disk reserve overflow",
)
})?;
if let Some(available) = compaction_filesystem_available_bytes(output_dir)? {
if available < required {
return Err(compaction_abort(
"disk_reserve_exhausted",
format_args!("available {available} bytes, required {required} bytes"),
));
}
}
Ok(())
}
fn account_output(&mut self, bytes: u64) {
let consumed = bytes.min(self.remaining);
self.remaining = self.remaining.saturating_sub(consumed);
let mut aggregate = self
.pool
.remaining_output_bytes
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*aggregate = aggregate.saturating_sub(consumed);
}
}
impl Drop for CompactionDiskReservation {
fn drop(&mut self) {
let mut aggregate = self
.pool
.remaining_output_bytes
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*aggregate = aggregate.saturating_sub(self.remaining);
}
}
struct CompactionExecutionBudget {
started: Instant,
time_budget: Option<Duration>,
io_bytes_per_sec: u64,
max_output_bytes: u64,
disk_reserve_bytes: u64,
read_bytes: u64,
output_bytes: u64,
disk_reservation: Option<CompactionDiskReservation>,
}
impl CompactionExecutionBudget {
fn new(
time_budget_ms: u64,
io_bytes_per_sec: u64,
max_output_bytes: u64,
disk_reserve_bytes: u64,
) -> Self {
Self {
started: Instant::now(),
time_budget: (time_budget_ms > 0).then(|| Duration::from_millis(time_budget_ms)),
io_bytes_per_sec,
max_output_bytes: max_output_bytes.max(1),
disk_reserve_bytes,
read_bytes: 0,
output_bytes: 0,
disk_reservation: None,
}
}
fn attach_disk_reservation(&mut self, reservation: CompactionDiskReservation) {
self.disk_reservation = Some(reservation);
}
fn ensure_time(&self) -> Result<()> {
if self
.time_budget
.is_some_and(|budget| self.started.elapsed() >= budget)
{
return Err(compaction_abort(
"time_budget_exceeded",
format_args!("elapsed {} ms", self.started.elapsed().as_millis()),
));
}
Ok(())
}
fn throttle(&self, ensure_live: &(dyn Fn() -> Result<()> + Send + Sync)) -> Result<()> {
self.ensure_time()?;
ensure_live()?;
if self.io_bytes_per_sec == 0 {
return Ok(());
}
let accounted_bytes = self.read_bytes.saturating_add(self.output_bytes);
let desired_nanos = (u128::from(accounted_bytes))
.saturating_mul(1_000_000_000)
.checked_div(u128::from(self.io_bytes_per_sec))
.unwrap_or(u128::MAX)
.min(u128::from(u64::MAX)) as u64;
let desired = Duration::from_nanos(desired_nanos);
while self.started.elapsed() < desired {
self.ensure_time()?;
ensure_live()?;
let remaining = desired.saturating_sub(self.started.elapsed());
std::thread::sleep(remaining.min(Duration::from_millis(50)));
}
self.ensure_time()
}
fn account_read_bytes(
&mut self,
bytes: u64,
ensure_live: &(dyn Fn() -> Result<()> + Send + Sync),
) -> Result<()> {
self.read_bytes = self.read_bytes.max(bytes);
self.throttle(ensure_live)
}
fn ensure_output_headroom(&self, output_dir: &Path) -> Result<()> {
self.ensure_time()?;
if let Some(reservation) = &self.disk_reservation {
return reservation.ensure_headroom(output_dir, self.disk_reserve_bytes);
}
let remaining_output = self.max_output_bytes.saturating_sub(self.output_bytes);
let required = self.disk_reserve_bytes.saturating_add(remaining_output);
if let Some(available) = compaction_filesystem_available_bytes(output_dir)? {
if available < required {
return Err(compaction_abort(
"disk_reserve_exhausted",
format_args!("available {available} bytes, required {required} bytes"),
));
}
}
Ok(())
}
fn account_output_bytes(
&mut self,
bytes: u64,
ensure_live: &(dyn Fn() -> Result<()> + Send + Sync),
) -> Result<()> {
self.output_bytes = self.output_bytes.saturating_add(bytes);
if self.output_bytes > self.max_output_bytes {
return Err(compaction_abort(
"output_budget_exceeded",
format_args!(
"generated {} bytes, limit {} bytes",
self.output_bytes, self.max_output_bytes
),
));
}
self.throttle(ensure_live)?;
if let Some(reservation) = &mut self.disk_reservation {
reservation.account_output(bytes);
}
Ok(())
}
}
fn compaction_job_signature(table_name: &str, schema_epoch: u64, segment_ids: &[u64]) -> u64 {
let mut signature = runtime_owner_id(table_name) ^ schema_epoch.rotate_left(17);
for segment_id in segment_ids {
signature = signature
.wrapping_mul(0x100_0000_01b3)
.wrapping_add(*segment_id);
}
signature.max(1)
}
struct CompactionRetryEntry {
signature: u64,
until: Instant,
until_unix_millis: u64,
reason: String,
}
const MAX_COMPACTION_RETRY_ENTRIES: usize = 64;
#[derive(Default)]
struct CompactionRetryCooldown {
active: Mutex<Vec<CompactionRetryEntry>>,
suppressed: AtomicU64,
}
impl CompactionRetryCooldown {
fn should_defer(&self, signature: u64) -> bool {
let mut active = self
.active
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let now = Instant::now();
active.retain(|entry| entry.until > now);
if active.iter().any(|entry| entry.signature == signature) {
self.suppressed.fetch_add(1, Ordering::Relaxed);
return true;
}
false
}
fn record(&self, signature: u64, cooldown_ms: u64, reason: &str) {
let mut active = self
.active
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if cooldown_ms == 0 {
active.retain(|entry| entry.signature != signature);
return;
}
let now = Instant::now();
active.retain(|entry| entry.until > now && entry.signature != signature);
if active.len() == MAX_COMPACTION_RETRY_ENTRIES {
active.remove(0);
}
active.push(CompactionRetryEntry {
signature,
until: now + Duration::from_millis(cooldown_ms),
until_unix_millis: runtime_unix_millis().saturating_add(cooldown_ms),
reason: reason.chars().take(96).collect(),
});
}
fn clear(&self, signature: u64) {
let mut active = self
.active
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
active.retain(|entry| entry.signature != signature);
}
fn snapshot(&self) -> (bool, u64, u64, String) {
let mut active = self
.active
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let now = Instant::now();
active.retain(|entry| entry.until > now);
active.last().map_or_else(
|| {
(
false,
0,
self.suppressed.load(Ordering::Relaxed),
String::new(),
)
},
|entry| {
(
true,
entry.until_unix_millis,
self.suppressed.load(Ordering::Relaxed),
entry.reason.clone(),
)
},
)
}
}
struct CompactionRetryJobGuard<'a> {
cooldown: &'a CompactionRetryCooldown,
signature: u64,
cooldown_ms: u64,
reason: String,
finished: bool,
}
impl<'a> CompactionRetryJobGuard<'a> {
fn new(cooldown: &'a CompactionRetryCooldown, signature: u64, cooldown_ms: u64) -> Self {
Self {
cooldown,
signature,
cooldown_ms,
reason: "failed_unclassified".to_string(),
finished: false,
}
}
fn set_reason(&mut self, reason: &str) {
self.reason.clear();
self.reason.extend(reason.chars().take(96));
}
fn success(mut self) {
self.cooldown.clear(self.signature);
self.finished = true;
}
}
impl Drop for CompactionRetryJobGuard<'_> {
fn drop(&mut self) {
if !self.finished {
self.cooldown
.record(self.signature, self.cooldown_ms, &self.reason);
}
}
}
#[derive(Default)]
struct CompactionJobConcurrencyState {
active: AtomicU64,
peak: AtomicU64,
}
impl CompactionJobConcurrencyState {
fn start_job(&self) -> CompactionJobConcurrencyGuard<'_> {
let active = self.active.fetch_add(1, Ordering::AcqRel) + 1;
self.peak.fetch_max(active, Ordering::Relaxed);
CompactionJobConcurrencyGuard { state: self }
}
fn active(&self) -> u64 {
self.active.load(Ordering::Acquire)
}
fn peak(&self) -> u64 {
self.peak.load(Ordering::Relaxed)
}
}
struct CompactionJobConcurrencyGuard<'a> {
state: &'a CompactionJobConcurrencyState,
}
impl Drop for CompactionJobConcurrencyGuard<'_> {
fn drop(&mut self) {
self.state.active.fetch_sub(1, Ordering::AcqRel);
}
}
fn run_bounded_compaction_jobs<F>(
tables: &[String],
max_jobs: usize,
compact_table: &F,
) -> Result<()>
where
F: Fn(&str) -> Result<()> + Sync,
{
let mut seen = FxHashSet::default();
if tables.iter().any(|table| !seen.insert(table.as_str())) {
return Err(Error::internal(
"compaction scheduler received a duplicate table owner",
));
}
let max_jobs = max_jobs
.clamp(1, crate::config::MAX_COMPACTION_JOBS)
.min(tables.len().max(1));
if max_jobs == 1 {
for table in tables {
run_compaction_job_with_stale_replans(table, compact_table)?;
}
return Ok(());
}
for batch in tables.chunks(max_jobs) {
let results = std::thread::scope(|scope| {
let handles = batch
.iter()
.map(|table| scope.spawn(move || compact_table(table)))
.collect::<Vec<_>>();
handles
.into_iter()
.map(std::thread::ScopedJoinHandle::join)
.collect::<Vec<_>>()
});
let mut first_error = None;
let mut stale_tables = Vec::new();
for (table, result) in batch.iter().zip(results) {
match result {
Ok(Ok(())) => {}
Ok(Err(error)) if compaction_publication_is_stale(&error) => {
stale_tables.push(table.as_str());
}
Ok(Err(error)) if first_error.is_none() => first_error = Some(error),
Ok(Err(_)) => {}
Err(_) if first_error.is_none() => {
first_error = Some(Error::internal("compaction worker panicked"));
}
Err(_) => {}
}
}
if let Some(error) = first_error {
return Err(error);
}
for table in stale_tables {
run_compaction_job_with_stale_replans(table, compact_table)?;
}
}
Ok(())
}
const COMPACTION_STALE_PUBLICATION_REASON: &str = "physical_generation_stale";
const COMPACTION_STALE_REPLAN_LIMIT: usize = 2;
fn compaction_publication_is_stale(error: &Error) -> bool {
compaction_abort_reason(error) == Some(COMPACTION_STALE_PUBLICATION_REASON)
}
fn run_compaction_job_with_stale_replans<F>(table: &str, compact_table: &F) -> Result<()>
where
F: Fn(&str) -> Result<()> + Sync,
{
for replan in 0..=COMPACTION_STALE_REPLAN_LIMIT {
match compact_table(table) {
Ok(()) => return Ok(()),
Err(error) if compaction_publication_is_stale(&error) => {
if replan == COMPACTION_STALE_REPLAN_LIMIT {
return Err(error);
}
}
Err(error) => return Err(error),
}
}
unreachable!("bounded compaction replan loop always returns")
}
#[derive(Debug, Clone, Copy)]
struct L0PressureLimits {
soft_segments: u64,
hard_segments: u64,
soft_bytes: u64,
hard_bytes: u64,
soft_wait: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum L0PressureLevel {
Normal,
Soft,
Hard,
}
fn prioritize_compaction_tables(
candidates: &mut [(String, crate::volume::manifest::L0DebtSnapshot)],
limits: L0PressureLimits,
) {
candidates.sort_unstable_by(|(left_name, left_debt), (right_name, right_debt)| {
classify_l0_pressure(*right_debt, limits)
.cmp(&classify_l0_pressure(*left_debt, limits))
.then_with(|| right_debt.segments.cmp(&left_debt.segments))
.then_with(|| right_debt.physical_bytes.cmp(&left_debt.physical_bytes))
.then_with(|| left_name.cmp(right_name))
});
}
fn limit_compaction_wave<T>(candidates: &mut Vec<T>, max_tables: Option<usize>) -> bool {
let Some(max_tables) = max_tables else {
return false;
};
let max_tables = max_tables.max(1);
let more_candidates = candidates.len() > max_tables;
candidates.truncate(max_tables);
more_candidates
}
fn classify_l0_pressure(
debt: crate::volume::manifest::L0DebtSnapshot,
limits: L0PressureLimits,
) -> L0PressureLevel {
if debt.segments >= limits.hard_segments || debt.physical_bytes >= limits.hard_bytes {
L0PressureLevel::Hard
} else if debt.segments >= limits.soft_segments || debt.physical_bytes >= limits.soft_bytes {
L0PressureLevel::Soft
} else {
L0PressureLevel::Normal
}
}
fn is_artifact_adaptive_capacity_error(error: &Error) -> bool {
matches!(
error,
Error::Internal { message }
if message.starts_with("physical generation publication failed: V6 data artifact ")
|| message.starts_with(
"physical generation publication failed: V6 index artifact "
)
)
}
static NEXT_SCHEMA_SCOPE_ID: AtomicU64 = AtomicU64::new(1);
const VIEW_DEFINITION_MARKER_V1: &[u8; 4] = b"RVW1";
const VIEW_DEFINITION_MARKER_V2: &[u8; 4] = b"RVW2";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct CompactionRowRef {
row_id: i64,
vol_idx: u32,
row_idx: u32,
}
const COMPACTION_ROW_REF_ENCODED_LEN: usize = 16;
impl CompactionRowRef {
fn new(row_id: i64, vol_idx: usize, row_idx: usize) -> Result<Self> {
let vol_idx = u32::try_from(vol_idx).map_err(|_| {
Error::internal(format!(
"compaction volume index {} exceeds artifact-backed metadata limit {}",
vol_idx,
u32::MAX
))
})?;
let row_idx = u32::try_from(row_idx).map_err(|_| {
Error::internal(format!(
"compaction row index {} exceeds artifact-backed metadata limit {}",
row_idx,
u32::MAX
))
})?;
Ok(Self {
row_id,
vol_idx,
row_idx,
})
}
fn vol_idx(&self) -> usize {
self.vol_idx as usize
}
fn row_idx(&self) -> usize {
self.row_idx as usize
}
fn encode(self) -> [u8; COMPACTION_ROW_REF_ENCODED_LEN] {
let mut out = [0u8; COMPACTION_ROW_REF_ENCODED_LEN];
out[0..8].copy_from_slice(&self.row_id.to_le_bytes());
out[8..12].copy_from_slice(&self.vol_idx.to_le_bytes());
out[12..16].copy_from_slice(&self.row_idx.to_le_bytes());
out
}
fn decode(bytes: &[u8; COMPACTION_ROW_REF_ENCODED_LEN]) -> Self {
Self {
row_id: i64::from_le_bytes(bytes[0..8].try_into().expect("slice len checked")),
vol_idx: u32::from_le_bytes(bytes[8..12].try_into().expect("slice len checked")),
row_idx: u32::from_le_bytes(bytes[12..16].try_into().expect("slice len checked")),
}
}
}
enum CompactionRowRefs<'a> {
#[cfg(test)]
Slice(&'a [CompactionRowRef]),
Spool {
spool: &'a mut CompactionRowRefSpool,
range: Range<usize>,
},
}
impl CompactionRowRefs<'_> {
fn len(&self) -> usize {
match self {
#[cfg(test)]
Self::Slice(refs) => refs.len(),
Self::Spool { range, .. } => range.len(),
}
}
fn ref_at(&mut self, index: usize) -> Result<CompactionRowRef> {
match self {
#[cfg(test)]
Self::Slice(refs) => refs.get(index).copied().ok_or_else(|| {
Error::internal(format!("compaction row ref {} out of bounds", index))
}),
Self::Spool { spool, range } => {
if index >= range.len() {
return Err(Error::internal(format!(
"compaction row ref {} out of bounds",
index
)));
}
spool.ref_at(range.start + index)
}
}
}
}
struct CompactionRowRefSpool {
path: PathBuf,
file: File,
len: usize,
written_len: usize,
append_buffer: Vec<u8>,
cached_range: Option<Range<usize>>,
cached_refs: Vec<CompactionRowRef>,
#[cfg(test)]
append_flushes: usize,
}
const COMPACTION_SPOOL_PREFIX: &str = ".compaction_refs_";
const COMPACTION_SPOOL_SUFFIX: &str = ".tmp";
const COMPACTION_SPOOL_APPEND_BYTES: usize = 64 * 1024;
const COMPACTION_ABORT_PREFIX: &str = "compaction_cooperative_abort:";
static NEXT_COMPACTION_SPOOL_ID: AtomicU64 = AtomicU64::new(1);
fn compaction_abort(reason: &str, detail: impl std::fmt::Display) -> Error {
Error::internal(format!("{COMPACTION_ABORT_PREFIX}{reason}: {detail}"))
}
fn compaction_abort_reason(error: &Error) -> Option<&str> {
let Error::Internal { message } = error else {
return None;
};
let marker = message.find(COMPACTION_ABORT_PREFIX)?;
message[marker + COMPACTION_ABORT_PREFIX.len()..]
.split(':')
.next()
}
impl CompactionRowRefSpool {
fn create(volume_dir: &Path, table_name: &str) -> Result<Self> {
let table_dir = volume_dir.join(table_name);
fs::create_dir_all(&table_dir).map_err(|e| {
Error::internal(format!(
"compaction row ref spool: create {:?}: {}",
table_dir, e
))
})?;
for _ in 0..16 {
let path = table_dir.join(format!(
"{}{:016x}{}",
COMPACTION_SPOOL_PREFIX,
NEXT_COMPACTION_SPOOL_ID.fetch_add(1, Ordering::Relaxed),
COMPACTION_SPOOL_SUFFIX
));
match OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&path)
{
Ok(file) => {
return Ok(Self {
path,
file,
len: 0,
written_len: 0,
append_buffer: Vec::with_capacity(COMPACTION_SPOOL_APPEND_BYTES),
cached_range: None,
cached_refs: Vec::new(),
#[cfg(test)]
append_flushes: 0,
});
}
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(err) => {
return Err(Error::internal(format!(
"compaction row ref spool: create {:?}: {}",
path, err
)));
}
}
}
Err(Error::internal(format!(
"compaction row ref spool: failed to allocate unique temp file in {:?}",
table_dir
)))
}
fn len(&self) -> usize {
self.len
}
fn is_empty(&self) -> bool {
self.len == 0
}
fn append(&mut self, row_ref: CompactionRowRef) -> Result<()> {
self.append_buffer.extend_from_slice(&row_ref.encode());
self.len = self
.len
.checked_add(1)
.ok_or_else(|| Error::internal("compaction row ref spool length overflows"))?;
self.cached_range = None;
self.cached_refs.clear();
if self.append_buffer.len() >= COMPACTION_SPOOL_APPEND_BYTES {
self.flush_appends()?;
}
Ok(())
}
fn flush_appends(&mut self) -> Result<()> {
if self.append_buffer.is_empty() {
return Ok(());
}
let byte_offset = self
.written_len
.checked_mul(COMPACTION_ROW_REF_ENCODED_LEN)
.and_then(|offset| u64::try_from(offset).ok())
.ok_or_else(|| Error::internal("compaction row ref spool write offset overflows"))?;
self.file
.seek(SeekFrom::Start(byte_offset))
.map_err(|error| Error::internal(format!("compaction spool seek: {error}")))?;
let write_started = Instant::now();
self.file.write_all(&self.append_buffer).map_err(|error| {
Error::internal(format!("compaction spool buffered append: {error}"))
})?;
crate::instrumentation::record_compaction_spool_write(
self.append_buffer.len() as u64,
write_started.elapsed(),
);
let refs = self.append_buffer.len() / COMPACTION_ROW_REF_ENCODED_LEN;
self.written_len = self
.written_len
.checked_add(refs)
.ok_or_else(|| Error::internal("compaction written row count overflows"))?;
self.append_buffer.clear();
#[cfg(test)]
{
self.append_flushes += 1;
}
Ok(())
}
fn ref_at(&mut self, index: usize) -> Result<CompactionRowRef> {
if index >= self.len {
return Err(Error::internal(format!(
"compaction row ref {} out of bounds",
index
)));
}
if self
.cached_range
.as_ref()
.is_some_and(|range| range.start <= index && index < range.end)
{
let range = self.cached_range.as_ref().expect("range checked above");
return self
.cached_refs
.get(index - range.start)
.copied()
.ok_or_else(|| Error::internal("compaction row ref spool cache missing entry"));
}
self.load_ref_group(index)?;
self.ref_at(index)
}
fn load_ref_group(&mut self, index: usize) -> Result<()> {
self.flush_appends()?;
self.file
.flush()
.map_err(|e| Error::internal(format!("compaction row ref spool: flush: {}", e)))?;
let start = (index / ROW_GROUP_SIZE) * ROW_GROUP_SIZE;
let end = (start + ROW_GROUP_SIZE).min(self.len);
let count = end - start;
let byte_len = count
.checked_mul(COMPACTION_ROW_REF_ENCODED_LEN)
.ok_or_else(|| Error::internal("compaction row ref spool read length overflows"))?;
let byte_offset = start
.checked_mul(COMPACTION_ROW_REF_ENCODED_LEN)
.and_then(|offset| u64::try_from(offset).ok())
.ok_or_else(|| Error::internal("compaction row ref spool read offset overflows"))?;
let mut bytes = vec![0u8; byte_len];
self.file
.seek(SeekFrom::Start(byte_offset))
.map_err(|e| Error::internal(format!("compaction row ref spool: seek: {}", e)))?;
let read_started = Instant::now();
self.file
.read_exact(&mut bytes)
.map_err(|e| Error::internal(format!("compaction row ref spool: read: {}", e)))?;
crate::instrumentation::record_compaction_spool_read(
byte_len as u64,
read_started.elapsed(),
);
let mut refs = Vec::with_capacity(count);
for chunk in bytes.chunks_exact(COMPACTION_ROW_REF_ENCODED_LEN) {
refs.push(CompactionRowRef::decode(
chunk
.try_into()
.expect("chunks_exact yields exact ref bytes"),
));
}
self.cached_refs = refs;
self.cached_range = Some(start..end);
Ok(())
}
}
impl Drop for CompactionRowRefSpool {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
struct CompactionSealRowSource<'a> {
refs: CompactionRowRefs<'a>,
volumes: &'a [(u64, Arc<crate::volume::writer::FrozenVolume>)],
mappings: &'a [crate::volume::writer::ColumnMapping],
cache: &'a mut crate::volume::writer::CompactionBlockCache,
}
trait CompactionRowSource {
fn len(&self) -> usize;
fn row_id(&mut self, index: usize) -> Result<i64>;
fn with_value<T>(
&mut self,
row_idx: usize,
col_idx: usize,
f: impl FnOnce(&Value) -> Result<T>,
) -> Result<T>;
}
impl<'a> CompactionSealRowSource<'a> {
#[cfg(test)]
fn new(
refs: &'a [CompactionRowRef],
volumes: &'a [(u64, Arc<crate::volume::writer::FrozenVolume>)],
mappings: &'a [crate::volume::writer::ColumnMapping],
cache: &'a mut crate::volume::writer::CompactionBlockCache,
) -> Self {
Self {
refs: CompactionRowRefs::Slice(refs),
volumes,
mappings,
cache,
}
}
#[cfg(test)]
fn new_spooled(
refs: &'a mut CompactionRowRefSpool,
volumes: &'a [(u64, Arc<crate::volume::writer::FrozenVolume>)],
mappings: &'a [crate::volume::writer::ColumnMapping],
cache: &'a mut crate::volume::writer::CompactionBlockCache,
) -> Self {
let len = refs.len();
Self::new_spooled_range(refs, 0..len, volumes, mappings, cache)
}
fn new_spooled_range(
refs: &'a mut CompactionRowRefSpool,
range: Range<usize>,
volumes: &'a [(u64, Arc<crate::volume::writer::FrozenVolume>)],
mappings: &'a [crate::volume::writer::ColumnMapping],
cache: &'a mut crate::volume::writer::CompactionBlockCache,
) -> Self {
Self {
refs: CompactionRowRefs::Spool { spool: refs, range },
volumes,
mappings,
cache,
}
}
}
impl CompactionRowSource for CompactionSealRowSource<'_> {
fn len(&self) -> usize {
self.refs.len()
}
fn row_id(&mut self, index: usize) -> Result<i64> {
self.refs.ref_at(index).map(|row_ref| row_ref.row_id)
}
fn with_value<T>(
&mut self,
row_idx: usize,
col_idx: usize,
f: impl FnOnce(&Value) -> Result<T>,
) -> Result<T> {
let row_ref = self.refs.ref_at(row_idx)?;
let vol_idx = row_ref.vol_idx();
let input_row_idx = row_ref.row_idx();
let (_, volume) = self.volumes.get(vol_idx).ok_or_else(|| {
Error::internal(format!(
"compaction row ref {} has invalid volume index {}",
row_idx, vol_idx
))
})?;
let mapping = self.mappings.get(vol_idx).ok_or_else(|| {
Error::internal(format!(
"compaction row ref {} has no column mapping for volume {}",
row_idx, vol_idx
))
})?;
let value =
volume.get_value_for_compaction_cached(input_row_idx, col_idx, mapping, self.cache)?;
f(&value)
}
}
struct CompactionArtifactRows<'a> {
source: CompactionSealRowSource<'a>,
column_count: usize,
next_row: usize,
}
impl<'a> CompactionArtifactRows<'a> {
fn new(source: CompactionSealRowSource<'a>, column_count: usize) -> Self {
Self {
source,
column_count,
next_row: 0,
}
}
}
impl Iterator for CompactionArtifactRows<'_> {
type Item = crate::v6::FormatResult<crate::v6::SourceRow>;
fn next(&mut self) -> Option<Self::Item> {
if self.next_row >= CompactionRowSource::len(&self.source) {
return None;
}
let row_index = self.next_row;
self.next_row += 1;
Some((|| {
let row_id = crate::v6::encode_runtime_row_id(
CompactionRowSource::row_id(&mut self.source, row_index)
.map_err(compaction_source_format_error)?,
);
let mut values = Vec::with_capacity(self.column_count);
for column_index in 0..self.column_count {
let value = CompactionRowSource::with_value(
&mut self.source,
row_index,
column_index,
|value| Ok(value.clone()),
)
.map_err(compaction_source_format_error)?;
values.push(value);
}
Ok(crate::v6::SourceRow::new(row_id, values))
})())
}
}
fn compaction_source_format_error(error: Error) -> crate::v6::FormatError {
crate::v6::FormatError::InvalidPublicationGraph {
detail: format!("compaction source read failed: {error}"),
}
}
#[inline]
fn registry_as_visibility_checker(registry: &Arc<TransactionRegistry>) -> Arc<TransactionRegistry> {
Arc::clone(registry)
}
fn strip_fk_references(
schemas: &mut FxHashMap<String, CompactArc<Schema>>,
version_stores: &FxHashMap<String, Arc<VersionStore>>,
parent_table_lower: &str,
) {
let children_to_update: Vec<String> = schemas
.iter()
.filter(|(_, schema)| {
schema
.foreign_keys
.iter()
.any(|fk| fk.referenced_table == parent_table_lower)
})
.map(|(name, _)| name.clone())
.collect();
for child_name in &children_to_update {
if let Some(old_schema_arc) = schemas.get(child_name) {
let old_schema: &Schema = old_schema_arc;
let mut new_schema = old_schema.clone();
new_schema
.foreign_keys
.retain(|fk| fk.referenced_table != parent_table_lower);
if new_schema.foreign_keys.len() < old_schema.foreign_keys.len() {
new_schema.constraints.retain(|constraint| {
!matches!(
&constraint.kind,
SchemaConstraintKind::ForeignKey {
referenced_table,
..
} if referenced_table == parent_table_lower
)
});
new_schema
.finish_catalog_mutation()
.expect("dropping a referenced table must preserve child schema invariants");
let new_arc = CompactArc::new(new_schema);
if let Some(vs) = version_stores.get(child_name.as_str()) {
*vs.schema_mut() = new_arc.clone();
}
schemas.insert(child_name.clone(), new_arc);
}
}
}
}
fn try_parse_default_literal(expr: &str, data_type: DataType) -> Option<Value> {
let expr = expr.trim();
if expr.eq_ignore_ascii_case("null") {
return Some(Value::null(data_type));
}
if expr.eq_ignore_ascii_case("true") {
return Some(Value::Boolean(true));
}
if expr.eq_ignore_ascii_case("false") {
return Some(Value::Boolean(false));
}
if expr.len() >= 2 && expr.starts_with('\'') && expr.ends_with('\'') {
let inner = &expr[1..expr.len() - 1];
let unescaped = inner.replace("''", "'");
let text_val = Value::from(unescaped.as_str());
return Some(text_val.coerce_to_type(data_type));
}
if let Ok(v) = expr.parse::<i64>() {
return Some(Value::Integer(v).coerce_to_type(data_type));
}
if let Ok(v) = expr.parse::<f64>() {
return Some(Value::Float(v).coerce_to_type(data_type));
}
None
}
fn schema_derived_pk_index_name(schema: &Schema) -> Option<String> {
let pk_indices = schema.primary_key_indices();
if pk_indices.is_empty() {
return None;
}
debug_assert_eq!(pk_indices.len(), 1, "schema validation owns PK arity");
let pk_col = &schema.columns[pk_indices[0]];
Some(format!("__pk_{}_{}", schema.table_name_lower, pk_col.name))
}
fn is_schema_derived_pk_index(schema: &Schema, index: &dyn Index) -> bool {
if index.index_type() == radixdb_core::IndexType::PrimaryKey {
return true;
}
let Some(_index_name) = schema_derived_pk_index_name(schema) else {
return false;
};
let pk_indices = schema.primary_key_indices();
let pk_col = &schema.columns[pk_indices[0]];
index.name().starts_with("__pk_")
&& index.name().ends_with(&format!("_{}", pk_col.name_lower))
&& index.is_unique()
&& index.column_ids() == [pk_col.id as i32]
}
fn register_pk_index(schema: &Schema, version_store: &Arc<VersionStore>) -> Result<()> {
let Some(index_name) = schema_derived_pk_index_name(schema) else {
return Ok(());
};
let pk_indices = schema.primary_key_indices();
let pk_col = &schema.columns[pk_indices[0]];
let pk_index: Arc<dyn Index> = if pk_col.data_type == DataType::Integer {
Arc::new(PkIndex::new(
index_name.clone(),
schema.table_name.clone(),
pk_col.id as i32,
pk_col.name.clone(),
))
} else {
Arc::new(BTreeIndex::new(
index_name.clone(),
schema.table_name.clone(),
pk_col.id as i32,
pk_col.name.clone(),
pk_col.data_type,
true,
0,
))
};
version_store.add_index(index_name, pk_index)
}
#[derive(Debug, Clone)]
pub struct ViewDefinition {
pub name: String,
pub original_name: String,
pub query: String,
pub dependencies: Vec<String>,
bound_query: String,
bound_dependencies: Vec<String>,
}
pub type ViewDependencyBinder = fn(&str) -> Result<Vec<String>>;
fn validate_bound_view_dependencies(dependencies: &[String]) -> Result<()> {
if let Some(dependency) = dependencies.iter().find(|dependency| {
dependency.is_empty() || dependency.as_str() != dependency.to_lowercase()
}) {
return Err(Error::invalid_argument(format!(
"view dependency '{dependency}' is not a normalized non-empty name"
)));
}
if dependencies
.windows(2)
.any(|pair| pair[0].as_str() >= pair[1].as_str())
{
return Err(Error::invalid_argument(
"view dependencies must be sorted and unique",
));
}
Ok(())
}
impl ViewDefinition {
pub fn from_bound_query(name: &str, query: String, dependencies: Vec<String>) -> Result<Self> {
if query.trim().is_empty() {
return Err(Error::invalid_argument("view query must not be empty"));
}
validate_bound_view_dependencies(&dependencies)?;
Ok(Self {
name: name.to_lowercase(),
original_name: name.to_string(),
bound_query: query.clone(),
bound_dependencies: dependencies.clone(),
query,
dependencies,
})
}
pub fn serialize(&self) -> Result<Vec<u8>> {
use super::persistence::{checked_u16_len, checked_u32_len};
validate_bound_view_dependencies(&self.dependencies)?;
if self.query != self.bound_query || self.dependencies != self.bound_dependencies {
return Err(Error::invalid_argument(format!(
"view '{}' carries a stale dependency graph",
self.original_name
)));
}
let mut buf = Vec::new();
buf.extend_from_slice(VIEW_DEFINITION_MARKER_V2);
buf.extend_from_slice(
&checked_u16_len("view name", self.original_name.len())?.to_le_bytes(),
);
buf.extend_from_slice(self.original_name.as_bytes());
buf.extend_from_slice(&checked_u32_len("view query", self.query.len())?.to_le_bytes());
buf.extend_from_slice(self.query.as_bytes());
buf.extend_from_slice(
&checked_u16_len("view dependency count", self.dependencies.len())?.to_le_bytes(),
);
for dependency in &self.dependencies {
buf.extend_from_slice(
&checked_u16_len("view dependency", dependency.len())?.to_le_bytes(),
);
buf.extend_from_slice(dependency.as_bytes());
}
Ok(buf)
}
pub fn deserialize(
data: &[u8],
dependency_binder: ViewDependencyBinder,
) -> radixdb_core::Result<Self> {
let is_v1 = data.starts_with(VIEW_DEFINITION_MARKER_V1);
if !is_v1 && !data.starts_with(VIEW_DEFINITION_MARKER_V2) {
return Err(Error::internal(
"unsupported unversioned view definition; expected RVW1/RVW2 marker",
));
}
let mut pos = VIEW_DEFINITION_MARKER_V2.len();
if pos + 2 > data.len() {
return Err(radixdb_core::Error::internal(
"invalid view: missing name length",
));
}
let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap()) as usize;
pos += 2;
if pos + name_len > data.len() {
return Err(radixdb_core::Error::internal("invalid view: missing name"));
}
let original_name = String::from_utf8(data[pos..pos + name_len].to_vec())
.map_err(|e| radixdb_core::Error::internal(format!("invalid view name: {}", e)))?;
pos += name_len;
if pos + 4 > data.len() {
return Err(radixdb_core::Error::internal(
"invalid view: missing query length",
));
}
let query_len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
if pos + query_len > data.len() {
return Err(radixdb_core::Error::internal("invalid view: missing query"));
}
let query = String::from_utf8(data[pos..pos + query_len].to_vec())
.map_err(|e| radixdb_core::Error::internal(format!("invalid view query: {}", e)))?;
pos += query_len;
let bound_dependencies = dependency_binder(&query)?;
validate_bound_view_dependencies(&bound_dependencies)?;
let dependencies = if is_v1 {
bound_dependencies
} else {
if pos + 2 > data.len() {
return Err(Error::internal("invalid view: missing dependency count"));
}
let count = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap()) as usize;
pos += 2;
let mut dependencies = Vec::with_capacity(count);
for _ in 0..count {
if pos + 2 > data.len() {
return Err(Error::internal("invalid view: missing dependency length"));
}
let len = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap()) as usize;
pos += 2;
let end = pos
.checked_add(len)
.filter(|&end| end <= data.len())
.ok_or_else(|| Error::internal("invalid view: missing dependency"))?;
let dependency = String::from_utf8(data[pos..end].to_vec()).map_err(|error| {
Error::internal(format!("invalid view dependency: {error}"))
})?;
dependencies.push(dependency.to_lowercase());
pos = end;
}
dependencies.sort_unstable();
dependencies.dedup();
if dependencies != bound_dependencies {
return Err(Error::internal(
"invalid view definition: dependency graph does not match query",
));
}
dependencies
};
if pos != data.len() {
return Err(Error::internal("invalid view definition: trailing bytes"));
}
Self::from_bound_query(&original_name, query, dependencies)
}
}
type FkReverseCache = (u64, StringMap<Arc<Vec<(String, ForeignKeyConstraint)>>>);
pub struct MVCCEngine {
path: String,
config: RwLock<Config>,
partial_index_predicate_binder: crate::index::PartialIndexPredicateBinder,
row_validator_binder: OnceLock<crate::validation::RowValidatorBinder>,
view_dependency_binder: OnceLock<ViewDependencyBinder>,
catalog_runtime_binder: OnceLock<CatalogRuntimeBinder>,
schemas: Arc<RwLock<FxHashMap<String, CompactArc<Schema>>>>,
version_stores: Arc<RwLock<FxHashMap<String, Arc<VersionStore>>>>,
pending_tables: Arc<RwLock<FxHashMap<String, PendingTable>>>,
registry: Arc<TransactionRegistry>,
open: AtomicBool,
opened_once: AtomicBool,
lifecycle: RwLock<EngineLifecycleState>,
runtime_snapshot_sequence: AtomicU64,
runtime_maintenance: Arc<EngineMaintenanceState>,
shutdown_checkpoint_complete: AtomicBool,
txn_version_stores: Arc<RwLock<TxnVersionStoreMap>>,
views: Arc<RwLock<FxHashMap<String, Arc<ViewDefinition>>>>,
catalog_publisher: Arc<ArcSwap<CatalogPublisher>>,
physical_generation: Arc<ArcSwapOption<crate::v6::PhysicalGenerationPublisher>>,
persistence: Arc<ArcSwapOption<PersistenceManager>>,
loading_from_disk: Arc<AtomicBool>,
file_lock: Mutex<Option<FileLock>>,
startup_mutex: Mutex<()>,
schema_epoch: Arc<AtomicU64>,
schema_scope_id: u64,
cleanup_handle: Mutex<Option<CleanupHandle>>,
page_cache_warmup: Arc<crate::page_cache::PageCacheWarmupController>,
page_cache_warmup_handle: Mutex<Option<crate::page_cache::PageCacheWarmupHandle>>,
pressure_seal: Arc<PressureSealControl>,
fk_reverse_cache: RwLock<FkReverseCache>,
segment_managers: Arc<RwLock<FxHashMap<String, Arc<crate::volume::manifest::SegmentManager>>>>,
storage_cpu_runtime: Arc<crate::cpu_runtime::StorageCpuRuntime>,
force_seal_all: AtomicBool,
checkpoint_mutex: Mutex<()>,
seal_fence: Arc<parking_lot::RwLock<()>>,
ddl_fence: Arc<parking_lot::RwLock<()>>,
catalog_write_fence: Arc<parking_lot::Mutex<()>>,
visibility_fence: Arc<parking_lot::RwLock<()>>,
snapshot_maintenance_fence: Arc<parking_lot::RwLock<()>>,
compaction_running: Arc<AtomicBool>,
compaction_requested: Arc<AtomicBool>,
compaction_retry_cooldown: Arc<CompactionRetryCooldown>,
compaction_job_concurrency: Arc<CompactionJobConcurrencyState>,
compaction_soft_backpressure_waits: Arc<AtomicU64>,
compaction_soft_backpressure_wait_millis: Arc<AtomicU64>,
compaction_hard_backpressure_rejections: Arc<AtomicU64>,
eviction_epoch: AtomicU64,
}
struct AtomicBoolGuard<'a>(&'a AtomicBool);
impl Drop for AtomicBoolGuard<'_> {
fn drop(&mut self) {
self.0.store(false, Ordering::Release);
}
}
fn conventional_segment_file_path(table_name: &str, segment_id: u64) -> PathBuf {
PathBuf::from(table_name).join(format!("segment_{:016x}.data", segment_id))
}
fn missing_partial_index_predicate_binder(
canonical_sql: &str,
_schema: &Schema,
) -> Result<crate::index::PartialIndexPredicate> {
Err(Error::NotSupported(format!(
"cannot recover partial index predicate '{}' without a configured composition binder",
canonical_sql
)))
}
#[cfg(test)]
pub(crate) mod tests;