use std::collections::BTreeMap;
use sim_table_core::TablePath;
use crate::{CellId, DirId, EffectiveCodecPolicy};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BackendKind {
Memory,
Filesystem,
Database,
ReadOnly,
MountedNamespace,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MountEpoch(u64);
impl MountEpoch {
pub fn new(value: u64) -> Self {
Self(value)
}
pub fn value(self) -> u64 {
self.0
}
pub fn next_after(self) -> Self {
Self(self.0.saturating_add(1))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MountResource {
Table,
Dir,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MountDescriptor {
path: TablePath,
resource: MountResource,
backend: BackendKind,
epoch: MountEpoch,
}
impl MountDescriptor {
pub fn table(path: TablePath, backend: BackendKind, epoch: MountEpoch) -> Self {
Self {
path,
resource: MountResource::Table,
backend,
epoch,
}
}
pub fn dir(path: TablePath, backend: BackendKind, epoch: MountEpoch) -> Self {
Self {
path,
resource: MountResource::Dir,
backend,
epoch,
}
}
pub fn path(&self) -> &TablePath {
&self.path
}
pub fn resource(&self) -> MountResource {
self.resource
}
pub fn backend(&self) -> BackendKind {
self.backend
}
pub fn epoch(&self) -> MountEpoch {
self.epoch
}
fn set_epoch(&mut self, epoch: MountEpoch) {
self.epoch = epoch;
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SourceEntry {
expr: String,
codec: Option<String>,
}
impl SourceEntry {
pub fn new(expr: impl Into<String>) -> Self {
Self {
expr: expr.into(),
codec: None,
}
}
pub fn with_codec(mut self, codec: impl Into<String>) -> Self {
self.codec = Some(codec.into());
self
}
pub fn expr(&self) -> &str {
&self.expr
}
pub fn codec(&self) -> Option<&str> {
self.codec.as_deref()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ControlEntry {
Counter(u64),
Policy(EffectiveCodecPolicy),
UiPreference(String),
MountEpoch(MountEpoch),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DerivedEntry {
Graph(String),
CachedValue(String),
Receipt(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingCommit {
source_writes: BTreeMap<CellId, SourceEntry>,
control_writes: BTreeMap<String, ControlEntry>,
phase: CommitPhase,
}
impl PendingCommit {
fn new(
source_writes: BTreeMap<CellId, SourceEntry>,
control_writes: BTreeMap<String, ControlEntry>,
) -> Self {
Self {
source_writes,
control_writes,
phase: CommitPhase::Prepared,
}
}
pub fn source_committed(&self) -> bool {
self.phase >= CommitPhase::SourceCommitted
}
pub fn control_committed(&self) -> bool {
self.phase >= CommitPhase::ControlCommitted
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum CommitPhase {
Prepared,
SourceCommitted,
ControlCommitted,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StoreError {
MissingRootDir,
InvalidMount(String),
TableMountIsLeaf(TablePath),
CorruptMount(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExprTreeStores {
root_dir: DirId,
source: BTreeMap<CellId, SourceEntry>,
control: BTreeMap<String, ControlEntry>,
derived: BTreeMap<CellId, DerivedEntry>,
mounts: BTreeMap<String, MountDescriptor>,
}
impl ExprTreeStores {
pub fn new(root_dir: DirId) -> Result<Self, StoreError> {
if root_dir.as_str().is_empty() {
return Err(StoreError::MissingRootDir);
}
Ok(Self {
root_dir,
source: BTreeMap::new(),
control: BTreeMap::new(),
derived: BTreeMap::new(),
mounts: BTreeMap::new(),
})
}
pub fn reopen(
root_dir: DirId,
source: BTreeMap<CellId, SourceEntry>,
control: BTreeMap<String, ControlEntry>,
derived: BTreeMap<CellId, DerivedEntry>,
mounts: Vec<MountDescriptor>,
) -> Result<Self, StoreError> {
let mut stores = Self::new(root_dir)?;
stores.source = source;
stores.control = control;
stores.derived = derived;
for descriptor in mounts {
stores.mount(descriptor)?;
}
Ok(stores)
}
pub fn root_dir(&self) -> &DirId {
&self.root_dir
}
pub fn source_entry(&self, id: &CellId) -> Option<&SourceEntry> {
self.source.get(id)
}
pub fn control_entry(&self, key: &str) -> Option<&ControlEntry> {
self.control.get(key)
}
pub fn derived_entry(&self, id: &CellId) -> Option<&DerivedEntry> {
self.derived.get(id)
}
pub fn mounts(&self) -> impl Iterator<Item = &MountDescriptor> {
self.mounts.values()
}
pub fn return_value_without_mounting(&self, _resource: MountResource) -> usize {
self.mounts.len()
}
pub fn mount(&mut self, descriptor: MountDescriptor) -> Result<(), StoreError> {
validate_mount(&descriptor)?;
let key = mount_key(descriptor.path());
if self.mounts.contains_key(&key) {
return Err(StoreError::InvalidMount(format!(
"duplicate mount point {}",
descriptor.path()
)));
}
for existing in self.mounts.values() {
if is_prefix(existing.path(), descriptor.path())
&& existing.resource() == MountResource::Table
&& existing.path() != descriptor.path()
{
return Err(StoreError::TableMountIsLeaf(existing.path().clone()));
}
if is_prefix(descriptor.path(), existing.path())
&& descriptor.resource() == MountResource::Table
{
return Err(StoreError::InvalidMount(format!(
"table mount {} would parent existing mount {}",
descriptor.path(),
existing.path()
)));
}
}
self.mounts.insert(key, descriptor);
Ok(())
}
pub fn unmount(&mut self, path: &TablePath) -> Result<MountDescriptor, StoreError> {
let key = mount_key(path);
let descriptor = self
.mounts
.remove(&key)
.ok_or_else(|| StoreError::InvalidMount(format!("missing mount point {path}")))?;
self.control.remove(&format!("mount-epoch:{path}"));
Ok(descriptor)
}
pub fn observe_mount_epoch(
&mut self,
path: &TablePath,
epoch: MountEpoch,
) -> Result<(), StoreError> {
let mount = self
.mounts
.get_mut(&mount_key(path))
.ok_or_else(|| StoreError::CorruptMount(format!("missing mount {}", path)))?;
mount.set_epoch(epoch);
self.control.insert(
format!("mount-epoch:{}", path),
ControlEntry::MountEpoch(epoch),
);
Ok(())
}
pub fn prepare_source_control_commit(
source_writes: BTreeMap<CellId, SourceEntry>,
control_writes: BTreeMap<String, ControlEntry>,
) -> PendingCommit {
PendingCommit::new(source_writes, control_writes)
}
pub fn commit_source(&mut self, pending: &mut PendingCommit) {
self.source.extend(pending.source_writes.clone());
pending.phase = CommitPhase::SourceCommitted;
}
pub fn commit_control(&mut self, pending: &mut PendingCommit) {
self.control.extend(pending.control_writes.clone());
pending.phase = CommitPhase::ControlCommitted;
}
pub fn recover_commit(&mut self, pending: &mut PendingCommit) {
if !pending.source_committed() {
self.commit_source(pending);
}
if !pending.control_committed() {
self.commit_control(pending);
}
}
pub fn put_derived(&mut self, id: CellId, entry: DerivedEntry) {
self.derived.insert(id, entry);
}
pub fn put_control(&mut self, key: impl Into<String>, entry: ControlEntry) {
self.control.insert(key.into(), entry);
}
pub fn remove_source(&mut self, id: &CellId) -> Option<SourceEntry> {
self.source.remove(id)
}
}
fn validate_mount(descriptor: &MountDescriptor) -> Result<(), StoreError> {
if descriptor.path().is_root() {
return Err(StoreError::InvalidMount(
"root is supplied as the required root Dir, not as a mount".to_owned(),
));
}
if descriptor.backend() == BackendKind::ReadOnly && descriptor.resource() == MountResource::Dir
{
return Ok(());
}
Ok(())
}
fn is_prefix(candidate: &TablePath, path: &TablePath) -> bool {
let candidate_segments = segments(candidate);
let path_segments = segments(path);
candidate_segments.len() <= path_segments.len()
&& candidate_segments
.iter()
.zip(path_segments.iter())
.all(|(left, right)| left == right)
}
fn segments(path: &TablePath) -> Vec<&str> {
path.segments().iter().map(String::as_str).collect()
}
fn mount_key(path: &TablePath) -> String {
path.to_absolute_reference()
}
#[cfg(test)]
pub(crate) fn source_keys_for_test(stores: &ExprTreeStores) -> std::collections::BTreeSet<CellId> {
stores.source.keys().cloned().collect()
}