linkrs-storage 0.1.0

Storage engine with CSR and memory-mapped containers
Documentation
//! CSR with Properties — ladybug-style columnar storage.
//!
//! Properties are stored in parallel column arrays keyed by `EdgeId` through
//! the edge-to-row map. The topology CSR owns the only adjacency index; this
//! store keeps no per-vertex offsets, lengths, or append heads.
//!
//! This implementation uses `Column` (continuous arrays) instead of
//! `HashMap<u32, Value>` for cache-friendly scans and lower memory overhead.
//!
//! Visibility authority lives in `MVCCManager` (`edge_timestamps`): these CSR
//! row stamps are only a physical projection kept in sync on the write path
//! for garbage collection. They must never decide query visibility alone.

use std::collections::{HashMap, HashSet};
use std::sync::Arc;

use linkrs_core::types::{EdgeId, Timestamp};
use linkrs_core::Value;

use crate::edge::property_schema::PropertySchema;
use crate::vertex::column::Column;

/// Row visibility for MVCC.
///
/// Physical projection of the version authority for collection only.
/// Every visibility decision delegates to the single
/// [`crate::mvcc_visibility::Visibility`] predicate so this copy can never
/// drift into a second time comparison.
#[derive(Debug, Clone, Copy)]
struct RowVisibility {
    create_ts: Timestamp,
    delete_ts: Option<Timestamp>,
}

/// Explicit row lifecycle replacing the bare zero-timestamp sentinel check.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RowState {
    Virgin,
    Live,
    Deleted,
}

impl RowVisibility {
    fn state(&self) -> RowState {
        if self.create_ts == linkrs_core::types::RESERVED_ZERO_TIMESTAMP && self.delete_ts.is_none()
        {
            RowState::Virgin
        } else if self.delete_ts.is_some() {
            RowState::Deleted
        } else {
            RowState::Live
        }
    }

    fn is_virgin(&self) -> bool {
        self.state() == RowState::Virgin
    }

    fn is_live(&self) -> bool {
        self.state() == RowState::Live
    }
}

/// Columnar property storage keyed by edge id.
///
/// Every edge owns exactly one row, including edges without properties.
/// Row identity is the edge-to-row map; there is no per-vertex addressing.
///
/// Memory and persistence semantics: property before-images (row version
/// chains) stay memory-only; `dump` serializes current values plus row
/// visibility, per-column stable identifiers, encoding choices and refreshed
/// statistics. A reload restores plain values first, then re-applies the
/// recorded encodings and statistics, so encoded scans survive checkpoints.
/// Attribute time travel is therefore valid within a checkpoint epoch.
/// Row slot holding no edge mapping inside the dense edge map.
const UNMAPPED_ROW: u32 = u32::MAX;

/// FSST symbol-table ceiling applied whenever a column is encoded. One
/// value for every encode site, so a column encoded by maintenance and the
/// same column encoded by a checkpoint share identical parameters.
pub(crate) const FSST_MAX_SYMBOLS: usize = 255;

/// Exported property row: `(create_ts, delete_ts, per-column values)`.
pub type ExportedRow = (Timestamp, Option<Timestamp>, Vec<(Arc<str>, Option<Value>)>);

#[derive(Debug, Clone)]
pub struct CsrWithProperties {
    property_schema: Vec<PropertySchema>,
    property_columns: Vec<Column>,
    /// Column position by name, rebuilt on every schema mutation so hot
    /// paths never scan the schema linearly.
    column_index: HashMap<Arc<str>, usize>,
    /// Column position by stable identifier, rebuilt with the name index.
    /// Undo parameters keyed by id resolve through this instead of scanning.
    prop_id_index: HashMap<i32, usize>,
    visibility: Vec<RowVisibility>,
    /// Segmented sparse edge-to-row map indexed by the table-allocated edge id.
    /// One segment covers `EDGE_MAP_SEGMENT_ROWS` ids; untouched segments stay
    /// unallocated. Live entries hold row indexes, gaps hold `UNMAPPED_ROW`.
    /// Slot order is preserved by ascending segment iteration.
    edge_map_segments: Vec<Option<Box<[u32; 1024]>>>,
    /// Live mapping count, maintained alongside the dense map.
    edge_map_len: usize,
    /// Reverse index for O(1) row-to-edge lookup. Authoritative with
    /// `edge_to_row`; rebuilt on load, never persisted separately.
    row_to_edge: Vec<Option<EdgeId>>,
    free_list: Vec<u32>,
    row_count: usize,
    /// Column positions mutated since the last stats refresh or checkpoint.
    /// Drives per-column stats refresh so clean columns never pay recompute.
    /// Positions shift on schema mutation; the schema-mutating methods remap
    /// this set together with the schema.
    dirty_columns: HashSet<usize>,
    /// Stable column identifier allocator. Never reused or reassigned so
    /// stored undo parameters keyed by id stay valid across column drops.
    next_prop_id: i32,
    /// Checkpoint-epoch floor for attribute time travel.
    ///
    /// Row version chains stay memory-only, so a load drops every
    /// before-image. Strict reads below this floor for rows created at or
    /// below the query timestamp fail instead of returning the current
    /// value. Recomputed on every load as the maximum creation stamp;
    /// zero on fresh tables, disabling the fence.
    history_floor: Timestamp,
    /// Inline-form marker: the owning table stores its single scalar in the
    /// CSR value column, so this store keeps only the schema and the
    /// name/id indexes. Every row operation fails instead of forking a
    /// second property truth.
    inline: bool,
}

pub(crate) mod encoding;
pub(crate) mod maintenance;
pub(crate) mod mapping;
pub(crate) mod persistence;
pub(crate) mod read;
pub(crate) mod schema;
pub(crate) mod transfer;
pub(crate) mod visibility;
pub(crate) mod write;

#[cfg(test)]
mod tests;