use super::*;
impl UnifiedStore {
pub(crate) fn insert_vault_version(
&self,
collection: &str,
entity: UnifiedEntity,
metadata: Metadata,
) -> Result<(), StoreError> {
assert!(matches!(entity.data, EntityData::Row(_)));
let manager = self
.get_collection(collection)
.ok_or_else(|| StoreError::CollectionNotFound(collection.into()))?;
self.pending_vault_versions.write().insert(entity.id);
let id = manager.insert(entity)?;
self.register_entity_id(id);
if !metadata.fields.is_empty() {
manager.set_metadata(id, metadata)?;
}
let entity = manager
.get(id)
.expect("inserted vault version remains in memory");
self.persist_entities_to_pager_wal_only(collection, &[entity])
}
pub(crate) fn persist_entities_to_pager(
&self,
collection: &str,
entities: &[UnifiedEntity],
) -> Result<(), StoreError> {
self.persist_entities_impl(collection, entities, false)
}
pub(crate) fn persist_entity_refs_to_pager(
&self,
collection: &str,
entities: &[&UnifiedEntity],
) -> Result<(), StoreError> {
self.persist_entity_refs_impl(collection, entities, false)
}
pub(crate) fn persist_entity_refs_to_pager_wal_only(
&self,
collection: &str,
entities: &[&UnifiedEntity],
) -> Result<(), StoreError> {
self.persist_entity_refs_impl(collection, entities, true)
}
fn persist_entity_refs_impl(
&self,
collection: &str,
entities: &[&UnifiedEntity],
skip_btree: bool,
) -> Result<(), StoreError> {
let skip_btree = skip_btree || self.config.resident_memory_bytes_max.is_some();
if entities.is_empty() {
return Ok(());
}
if self.pager.is_none() && self.config.embedded_wal_path.is_none() {
return Ok(());
}
let fv = self.format_version();
let manager = self
.get_collection(collection)
.ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
let mut serialized: Vec<(Vec<u8>, Vec<u8>)> = entities
.iter()
.map(|entity| {
let metadata = manager.get_metadata(entity.id);
(
entity.id.raw().to_be_bytes().to_vec(),
Self::serialize_entity_record(entity, metadata.as_ref(), fv),
)
})
.collect();
serialized.sort_by(|a, b| a.0.cmp(&b.0));
if !skip_btree
&& !Self::deferred_store_wal_capture_active()
&& self.config.resident_memory_bytes_max.is_none()
{
let Some(pager) = &self.pager else {
let records: Vec<Vec<u8>> =
serialized.into_iter().map(|(_id, record)| record).collect();
self.finish_paged_write([StoreWalAction::BulkUpsertEntityRecords {
collection: collection.to_string(),
records,
}])?;
return Ok(());
};
let mut btree_indices = self.btree_indices.write();
let btree = btree_indices
.entry(collection.to_string())
.or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
let root_before = btree.root_page_id();
btree.upsert_batch_sorted(&serialized).map_err(|e| {
StoreError::Io(std::io::Error::other(format!("B-tree upsert error: {}", e)))
})?;
let root_after = btree.root_page_id();
drop(btree_indices);
if root_before != root_after {
self.mark_paged_registry_dirty();
}
}
let records: Vec<Vec<u8>> = serialized.into_iter().map(|(_id, record)| record).collect();
self.finish_paged_write_for_projection(
[StoreWalAction::BulkUpsertEntityRecords {
collection: collection.to_string(),
records,
}],
skip_btree,
)?;
Ok(())
}
pub(crate) fn persist_entities_to_pager_wal_only(
&self,
collection: &str,
entities: &[UnifiedEntity],
) -> Result<(), StoreError> {
self.persist_entities_impl(collection, entities, true)
}
fn persist_entities_impl(
&self,
collection: &str,
entities: &[UnifiedEntity],
skip_btree: bool,
) -> Result<(), StoreError> {
let skip_btree = skip_btree || self.config.resident_memory_bytes_max.is_some();
if entities.is_empty() {
return Ok(());
}
if self.pager.is_none() && self.config.embedded_wal_path.is_none() {
return Ok(());
}
let fv = self.format_version();
let manager = self
.get_collection(collection)
.ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
let mut serialized: Vec<(Vec<u8>, Vec<u8>)> = entities
.iter()
.map(|entity| {
let metadata = manager.get_metadata(entity.id);
(
entity.id.raw().to_be_bytes().to_vec(),
Self::serialize_entity_record(entity, metadata.as_ref(), fv),
)
})
.collect();
serialized.sort_by(|a, b| a.0.cmp(&b.0));
if !skip_btree
&& !Self::deferred_store_wal_capture_active()
&& self.config.resident_memory_bytes_max.is_none()
{
let Some(pager) = &self.pager else {
let records: Vec<Vec<u8>> =
serialized.into_iter().map(|(_id, record)| record).collect();
self.finish_paged_write([StoreWalAction::BulkUpsertEntityRecords {
collection: collection.to_string(),
records,
}])?;
return Ok(());
};
let mut btree_indices = self.btree_indices.write();
let btree = btree_indices
.entry(collection.to_string())
.or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
let root_before = btree.root_page_id();
btree.upsert_batch_sorted(&serialized).map_err(|e| {
StoreError::Io(std::io::Error::other(format!("B-tree upsert error: {}", e)))
})?;
let root_after = btree.root_page_id();
drop(btree_indices);
if root_before != root_after {
self.mark_paged_registry_dirty();
}
}
let records: Vec<Vec<u8>> = serialized.into_iter().map(|(_id, record)| record).collect();
self.finish_paged_write_for_projection(
[StoreWalAction::BulkUpsertEntityRecords {
collection: collection.to_string(),
records,
}],
skip_btree,
)?;
Ok(())
}
pub(crate) fn update_graph_label_index(
&self,
collection: &str,
label: &str,
entity_id: EntityId,
) {
self.resident
.update_graph_label_index(collection, label, entity_id)
}
pub(crate) fn remove_from_graph_label_index(&self, collection: &str, entity_id: EntityId) {
self.resident
.remove_from_graph_label_index(collection, entity_id)
}
pub(crate) fn remove_from_graph_label_index_batch(
&self,
collection: &str,
entity_ids: &[EntityId],
) {
self.resident
.remove_from_graph_label_index_batch(collection, entity_ids)
}
pub fn lookup_graph_nodes_by_label(&self, label: &str) -> Vec<EntityId> {
self.resident.lookup_graph_nodes_by_label(label)
}
pub fn lookup_graph_nodes_by_label_in(&self, collection: &str, label: &str) -> Vec<EntityId> {
self.resident
.lookup_graph_nodes_by_label_in(collection, label)
}
pub fn create_collection(&self, name: impl Into<String>) -> Result<(), StoreError> {
let name = name.into();
let mut collections = self.resident.collections.write();
if collections.contains_key(&name) {
return Err(StoreError::CollectionExists(name));
}
let manager = SegmentManager::with_config(&name, self.config.manager_config.clone());
collections.insert(name.clone(), Arc::new(manager));
drop(collections);
let create = match self.prepare_operational_collection_create(&name) {
Ok(create) => create,
Err(err) => {
let mut collections = self.resident.collections.write();
collections.remove(&name);
return Err(err);
}
};
if let Some(create) = create {
if let Err(err) = create.publish().map_err(StoreError::Io) {
let mut collections = self.resident.collections.write();
collections.remove(&name);
return Err(err);
}
}
self.mark_paged_registry_dirty();
self.finish_paged_write([StoreWalAction::CreateCollection { name }])?;
Ok(())
}
pub fn get_or_create_collection(&self, name: impl Into<String>) -> Arc<SegmentManager> {
self.resident
.get_or_create_collection(name, &self.config.manager_config)
}
pub fn get_collection(&self, name: &str) -> Option<Arc<SegmentManager>> {
self.resident.get_collection(name)
}
pub fn context_index(&self) -> &ContextIndex {
self.resident.context_index()
}
pub fn take_replayed_turbo_inserts(&self, collection: &str) -> Option<Vec<(u64, Vec<f32>)>> {
let mut map = self.replayed_turbo_inserts.lock();
map.remove(collection)
}
pub(crate) fn take_replayed_probabilistic_deltas(&self) -> Vec<(u8, u8, String, Vec<Vec<u8>>)> {
let mut deltas = self.replayed_probabilistic_deltas.lock();
std::mem::take(&mut *deltas)
}
pub fn set_config_tree(&self, prefix: &str, json: &crate::serde_json::Value) -> usize {
self.try_set_config_tree(prefix, json).unwrap_or_default()
}
pub fn try_set_config_tree(
&self,
prefix: &str,
json: &crate::serde_json::Value,
) -> Result<usize, StoreError> {
let _publication = self.vault_publication_lock.lock();
let manager = self.get_or_create_collection("red_config");
let mut pairs = Vec::new();
flatten_config_json(prefix, json, &mut pairs);
let mut previous: HashMap<String, Vec<(EntityId, Option<reddb_types::Value>)>> = pairs
.iter()
.map(|(key, _)| (key.clone(), Vec::new()))
.collect();
let mut capture = |entity: &UnifiedEntity| {
if let EntityData::Row(row) = &entity.data {
if let Some(reddb_types::Value::Text(key)) = row.get_field("key") {
if let Some(rows) = previous.get_mut(key.as_ref()) {
rows.push((entity.id, row.get_field("value").cloned()));
}
}
}
true
};
if manager.has_backing() {
for (key, _) in &pairs {
manager.try_for_each_key_entity(key, &mut capture)?;
}
} else {
manager.try_for_each_entity(usize::MAX, &mut capture)?;
}
let mut saved = 0;
for (key, value) in pairs {
let stale = previous.remove(&key).unwrap_or_default();
if let [(_, Some(current))] = stale.as_slice() {
if *current == value {
saved += 1;
continue;
}
}
let stale: Vec<EntityId> = stale.into_iter().map(|(id, _)| id).collect();
let entity = UnifiedEntity::new(
EntityId::new(0),
EntityKind::TableRow {
table: Arc::from("red_config"),
row_id: 0,
},
EntityData::Row(RowData {
columns: Vec::new(),
named: Some(
[
("key".to_string(), reddb_types::Value::text(key)),
("value".to_string(), value),
]
.into_iter()
.collect(),
),
schema: None,
}),
);
self.insert_auto("red_config", entity)?;
saved += 1;
if !stale.is_empty() {
self.delete_batch("red_config", &stale)?;
}
}
Ok(saved)
}
pub fn get_config(&self, key: &str) -> Option<reddb_types::Value> {
self.try_get_config(key).ok().flatten()
}
pub fn try_get_config(&self, key: &str) -> Result<Option<reddb_types::Value>, StoreError> {
let Some(manager) = self.get_collection("red_config") else {
return Ok(None);
};
let _publication = manager
.has_backing()
.then(|| self.vault_publication_lock.lock());
let mut latest: Option<(u64, reddb_types::Value)> = None;
manager.try_for_each_key_entity(key, |entity| {
if let EntityData::Row(row) = &entity.data {
let key_matches = matches!(row.get_field("key"), Some(reddb_types::Value::Text(stored)) if stored.as_ref() == key);
if key_matches && latest.as_ref().is_none_or(|(id, _)| entity.id.raw() >= *id) {
if let Some(value) = row.get_field("value") {
latest = Some((entity.id.raw(), value.clone()));
}
}
}
true
})?;
Ok(latest.map(|(_, value)| value))
}
pub fn set_aux_metadata(&self, bytes: Vec<u8>) {
self.resident.set_aux_metadata(bytes)
}
pub(crate) fn persist_aux_metadata(&self, bytes: Vec<u8>) -> Result<(), StoreError> {
let _publication = self.vault_publication_lock.lock();
self.finish_paged_write([StoreWalAction::SetAuxMetadata {
bytes: bytes.clone(),
}])?;
self.resident.set_aux_metadata(bytes);
Ok(())
}
pub fn aux_metadata(&self) -> Vec<u8> {
self.resident.aux_metadata()
}
pub fn list_collections(&self) -> Vec<String> {
self.resident.list_collections()
}
pub fn drop_collection(&self, name: &str) -> Result<(), StoreError> {
let _vault_guard = self.vault_publication_lock.lock();
let drop = self.prepare_operational_collection_drop(name)?;
let manager = {
let mut collections = self.resident.collections.write();
collections
.remove(name)
.ok_or_else(|| StoreError::CollectionNotFound(name.to_string()))?
};
let entities = manager.query_all(|_| true);
let entity_ids: Vec<EntityId> = entities.iter().map(|entity| entity.id).collect();
for entity_id in &entity_ids {
self.resident.context_index.remove_entity(*entity_id);
let _ = self.unindex_cross_refs(*entity_id);
}
if let Some(tree) = self.btree_indices.write().remove(name) {
self.retire_btree(tree);
}
if let Some(index) = self.key_indices.write().remove(name) {
for tree in index.trees() {
self.retire_btree(Arc::clone(tree));
}
}
self.secondary_index_specs.write().remove(name);
self.resident
.entity_cache
.retain(|entity_id, (collection, _)| {
collection != name && !entity_ids.iter().any(|id| id.raw() == entity_id)
});
self.resident.cross_refs.write().retain(|source_id, refs| {
refs.retain(|(target_id, _, target_collection)| {
target_collection != name && !entity_ids.iter().any(|id| id == target_id)
});
!entity_ids.iter().any(|id| id == source_id)
});
self.resident
.reverse_refs
.write()
.retain(|target_id, refs| {
refs.retain(|(source_id, _, source_collection)| {
source_collection != name && !entity_ids.iter().any(|id| id == source_id)
});
!entity_ids.iter().any(|id| id == target_id)
});
self.mark_paged_registry_dirty();
self.finish_paged_write([StoreWalAction::DropCollection {
name: name.to_string(),
}])?;
if let Some(drop) = drop {
drop.finish().map_err(StoreError::Io)?;
}
Ok(())
}
pub fn insert(&self, collection: &str, entity: UnifiedEntity) -> Result<EntityId, StoreError> {
let manager = self
.get_collection(collection)
.ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
let mut entity = entity;
if entity.id.raw() == 0 {
entity.id = self.next_entity_id();
} else {
self.register_entity_id(entity.id);
}
if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
if *row_id == 0 {
*row_id = manager.next_row_id();
} else {
manager.register_row_id(*row_id);
}
}
entity.ensure_table_logical_id();
let graph_node_label: Option<String> = if let EntityKind::GraphNode(ref node) = entity.kind
{
Some(node.label.clone())
} else {
None
};
let id = manager.insert(entity)?;
self.register_entity_id(id);
if let Some(ref label) = graph_node_label {
self.update_graph_label_index(collection, label, id);
}
let mut registry_dirty = false;
if let Some(pager) = self.pager.as_ref().filter(|_| {
!Self::deferred_store_wal_capture_active()
&& self.config.resident_memory_bytes_max.is_none()
}) {
if let Some(entity) = manager.get(id) {
let mut btree_indices = self.btree_indices.write();
let btree = btree_indices
.entry(collection.to_string())
.or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
let root_before = btree.root_page_id();
let key = id.raw().to_be_bytes();
let metadata = manager.get_metadata(id);
let value = Self::serialize_entity_record(
&entity,
metadata.as_ref(),
self.format_version(),
);
let _ = btree.insert(&key, &value);
registry_dirty = root_before != btree.root_page_id();
}
}
if self.config.auto_index_refs {
if let Some(entity) = manager.get(id) {
self.index_cross_refs(&entity, collection)?;
}
}
if self.pager.is_some() || self.config.embedded_wal_path.is_some() {
let actions = manager
.get(id)
.map(|entity| {
let metadata = manager.get_metadata(id);
vec![StoreWalAction::upsert_entity(
collection,
&entity,
metadata.as_ref(),
self.format_version(),
)]
})
.unwrap_or_default();
if registry_dirty {
self.mark_paged_registry_dirty();
}
self.finish_paged_write(actions)?;
}
Ok(id)
}
pub fn bulk_insert(
&self,
collection: &str,
mut entities: Vec<UnifiedEntity>,
) -> Result<Vec<EntityId>, StoreError> {
let _publication = self.vault_publication_lock.lock();
let trace = matches!(
std::env::var("REDDB_BULK_TIMING").ok().as_deref(),
Some("1") | Some("true") | Some("on")
);
let t_start = std::time::Instant::now();
let n = entities.len();
let manager = self.get_or_create_collection(collection);
let t_get_coll = t_start.elapsed();
let t0 = std::time::Instant::now();
let n_missing_entity_ids = entities.iter().filter(|e| e.id.raw() == 0).count() as u64;
let n_missing_row_ids = entities
.iter()
.filter(|e| matches!(e.kind, EntityKind::TableRow { row_id: 0, .. }))
.count() as u64;
let mut entity_id_range = if n_missing_entity_ids > 0 {
self.reserve_entity_ids(n_missing_entity_ids)
} else {
0..0
};
let mut row_id_range = if n_missing_row_ids > 0 {
manager.reserve_row_ids(n_missing_row_ids)
} else {
0..0
};
for entity in &mut entities {
if entity.id.raw() == 0 {
let next = entity_id_range
.next()
.expect("reserved entity-id range exhausted");
entity.id = EntityId::new(next);
} else {
self.register_entity_id(entity.id);
}
if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
if *row_id == 0 {
*row_id = row_id_range
.next()
.expect("reserved row-id range exhausted");
} else {
manager.register_row_id(*row_id);
}
}
entity.ensure_table_logical_id();
}
let t_assign_ids = t0.elapsed();
let graph_labels: Vec<Option<(String, EntityId)>> = entities
.iter()
.map(|e| {
if let EntityKind::GraphNode(ref node) = e.kind {
Some((node.label.clone(), e.id))
} else {
None
}
})
.collect();
let t0 = std::time::Instant::now();
let ids = manager.bulk_insert(entities)?;
let t_manager = t0.elapsed();
for id in &ids {
self.register_entity_id(*id);
}
let t0 = std::time::Instant::now();
let serialized: Option<Vec<(Vec<u8>, Vec<u8>)>> =
if self.pager.is_some() || self.config.embedded_wal_path.is_some() {
let fv = self.format_version();
let serial_map = |id: &EntityId| {
manager
.get_with(*id, |entity| {
(
id.raw().to_be_bytes().to_vec(),
Self::serialize_entity_record(entity, None, fv),
)
})
.expect("bulk-inserted entity must remain in the fenced segment")
};
if ids.len() >= 512 {
use rayon::prelude::*;
Some(ids.par_iter().map(serial_map).collect())
} else {
Some(ids.iter().map(serial_map).collect())
}
} else {
None
};
let t_serialize = t0.elapsed();
for (label, entity_id) in graph_labels.iter().flatten() {
self.update_graph_label_index(collection, label, *entity_id);
}
let skip_btree_requested = matches!(
std::env::var("REDDB_BULK_SKIP_PERSIST_UNSAFE")
.ok()
.as_deref(),
Some("1") | Some("true") | Some("on")
);
let skip_btree = skip_btree_requested && self.pager.is_none();
if skip_btree_requested && !skip_btree {
static IGNORED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
IGNORED.get_or_init(|| {
tracing::warn!(
"REDDB_BULK_SKIP_PERSIST_UNSAFE set but durable pager is \
active — flag ignored; bulk inserts will be persisted normally"
);
});
} else if skip_btree {
static WARNED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
WARNED.get_or_init(|| {
tracing::warn!(
"REDDB_BULK_SKIP_PERSIST_UNSAFE set (ephemeral/no-pager mode) — \
bulk inserts NOT durable; data will be lost on restart"
);
});
}
let mut t_btree_lock = std::time::Duration::ZERO;
let mut t_btree_insert = std::time::Duration::ZERO;
let mut t_flush = std::time::Duration::ZERO;
if !skip_btree
&& !Self::deferred_store_wal_capture_active()
&& self.config.resident_memory_bytes_max.is_none()
{
if let (Some(pager), Some(batch)) = (&self.pager, serialized.as_ref()) {
let t0 = std::time::Instant::now();
let mut btree_indices = self.btree_indices.write();
let btree = btree_indices
.entry(collection.to_string())
.or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
let root_before = btree.root_page_id();
t_btree_lock = t0.elapsed();
let t0 = std::time::Instant::now();
let _ = btree.bulk_insert_sorted(batch);
t_btree_insert = t0.elapsed();
let registry_dirty = root_before != btree.root_page_id();
let t0 = std::time::Instant::now();
if registry_dirty {
self.mark_paged_registry_dirty();
}
t_flush = t0.elapsed();
}
}
let actions = serialized
.map(|batch| {
let records: Vec<Vec<u8>> =
batch.into_iter().map(|(_key, record)| record).collect();
vec![StoreWalAction::BulkUpsertEntityRecords {
collection: collection.to_string(),
records,
}]
})
.unwrap_or_default();
self.finish_paged_write(actions)?;
if trace {
tracing::debug!(
n,
total = ?t_start.elapsed(),
get_coll = ?t_get_coll,
assign = ?t_assign_ids,
serialize = ?t_serialize,
manager = ?t_manager,
btree_lock = ?t_btree_lock,
btree = ?t_btree_insert,
flush = ?t_flush,
"bulk_insert timing"
);
}
Ok(ids)
}
pub fn insert_auto(
&self,
collection: &str,
entity: UnifiedEntity,
) -> Result<EntityId, StoreError> {
let _publication = self.vault_publication_lock.lock();
let manager = self.get_or_create_collection(collection);
let mut entity = entity;
if entity.id.raw() == 0 {
entity.id = self.next_entity_id();
} else {
self.register_entity_id(entity.id);
}
if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
if *row_id == 0 {
*row_id = manager.next_row_id();
} else {
manager.register_row_id(*row_id);
}
}
entity.ensure_table_logical_id();
let graph_node_label: Option<String> = if let EntityKind::GraphNode(ref node) = entity.kind
{
Some(node.label.clone())
} else {
None
};
self.resident
.context_index
.index_entity(collection, &entity);
let persist_record = self.pager.is_some() || self.config.embedded_wal_path.is_some();
if self.config.auto_index_refs {
self.index_cross_refs(&entity, collection)?;
}
let paged_btree = if persist_record
&& !Self::deferred_store_wal_capture_active()
&& self.config.resident_memory_bytes_max.is_none()
{
self.get_or_create_btree(collection)
} else {
None
};
let persist_fence = paged_btree
.as_ref()
.map(|_| self.btree_indices.read_recursive());
let before = paged_btree.as_ref().map(|_| manager.checkpoint_revisions());
let unversioned = entity.xmin == 0 && entity.xmax == 0;
let id = manager.insert(entity)?;
let serialized_record = if persist_record {
Some(
manager
.get_with(id, |entity| {
Self::serialize_entity_record(entity, None, self.format_version())
})
.ok_or(StoreError::EntityNotFound(id))?,
)
} else {
None
};
if let Some(ref label) = graph_node_label {
self.update_graph_label_index(collection, label, id);
}
let mut registry_dirty = false;
if let (Some(btree), Some(record)) = (paged_btree.as_ref(), serialized_record.as_ref()) {
let root_before = btree.root_page_id();
let key = id.raw().to_be_bytes();
btree.insert(&key, record).map_err(|e| {
StoreError::Io(std::io::Error::other(format!(
"B-tree insert error while inserting '{collection}'/{id}: {e}"
)))
})?;
registry_dirty = root_before != btree.root_page_id();
}
drop(persist_fence);
if self.pager.is_some() || self.config.embedded_wal_path.is_some() {
let actions = serialized_record
.map(|record| {
vec![StoreWalAction::UpsertEntityRecord {
collection: collection.to_string(),
record,
}]
})
.unwrap_or_default();
if registry_dirty {
self.mark_paged_registry_dirty();
}
if let Some(pending) = self.enqueue_grouped_actions(&actions)? {
drop(_publication);
self.complete_grouped_actions(&pending, &actions)?;
} else {
self.finish_paged_write(actions)?;
}
}
if unversioned && !Self::deferred_store_wal_capture_active() {
if let Some(before) = before {
self.note_paged_insert(collection, &manager, &before, id);
}
}
Ok(id)
}
pub fn get(&self, collection: &str, id: EntityId) -> Option<UnifiedEntity> {
self.try_get(collection, id, usize::MAX).ok().flatten()
}
pub fn try_get(
&self,
collection: &str,
id: EntityId,
limit_bytes: usize,
) -> Result<Option<UnifiedEntity>, StoreError> {
let Some(manager) = self.get_collection(collection) else {
return Ok(None);
};
manager.try_get(id, limit_bytes)
}
pub fn get_table_row_by_logical_id(
&self,
collection: &str,
logical_id: EntityId,
) -> Option<UnifiedEntity> {
if let Some(entity) = self.get(collection, logical_id) {
if matches!(entity.kind, EntityKind::TableRow { .. })
&& entity.logical_id() == logical_id
&& entity.xmax == 0
{
return Some(entity);
}
}
let manager = self.get_collection(collection)?;
let mut matches = manager.query_all(|entity| {
matches!(entity.kind, EntityKind::TableRow { .. }) && entity.logical_id() == logical_id
});
matches
.iter()
.find(|entity| entity.xmax == 0)
.cloned()
.or_else(|| matches.pop())
}
pub fn table_row_versions_by_logical_id(
&self,
collection: &str,
logical_id: EntityId,
) -> Vec<UnifiedEntity> {
self.resident
.table_row_versions_by_logical_id(collection, logical_id)
}
pub(crate) fn try_for_each_table_row_version(
&self,
collection: &str,
logical_id: EntityId,
mut visit: impl FnMut(&UnifiedEntity) -> bool,
) -> Result<(), StoreError> {
let Some(manager) = self.get_collection(collection) else {
return Ok(());
};
manager.try_for_each_entity(usize::MAX, |entity| {
!matches!(entity.kind, EntityKind::TableRow { .. })
|| entity.logical_id() != logical_id
|| visit(entity)
})
}
pub(crate) fn try_get_table_row_by_logical_id(
&self,
collection: &str,
logical_id: EntityId,
) -> Result<Option<UnifiedEntity>, StoreError> {
let Some(manager) = self.get_collection(collection) else {
return Ok(None);
};
if let Some(entity) = manager.try_get(logical_id, usize::MAX)? {
if matches!(entity.kind, EntityKind::TableRow { .. })
&& entity.logical_id() == logical_id
&& entity.xmax == 0
{
return Ok(Some(entity));
}
}
let mut selected = None;
self.try_for_each_table_row_version(collection, logical_id, |entity| {
selected = Some(entity.clone());
entity.xmax != 0
})?;
manager.restore_owned_row_schemas(selected.iter_mut());
Ok(selected)
}
pub fn vacuum_mvcc_history(
&self,
collection: &str,
cutoff_xid: u64,
) -> Result<MvccVacuumStats, StoreError> {
let manager = self
.get_collection(collection)
.ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
let mut logical = HashMap::<EntityId, (bool, u64)>::new();
manager.for_each_entity(|entity| {
if !matches!(entity.kind, EntityKind::TableRow { .. }) {
return true;
}
let entry = logical.entry(entity.logical_id()).or_insert((false, 0));
if entity.xmax == 0 {
entry.0 = true;
}
entry.1 = entry.1.max(entity.xmin);
true
});
let mut stats = MvccVacuumStats::default();
let mut reclaim_ids = Vec::new();
manager.for_each_entity(|entity| {
if !matches!(entity.kind, EntityKind::TableRow { .. }) || entity.xmax == 0 {
return true;
}
stats.scanned_versions += 1;
let (has_live_version, max_xmin) = logical
.get(&entity.logical_id())
.copied()
.unwrap_or((false, entity.xmin));
let is_delete_tombstone = !has_live_version && entity.xmin == max_xmin;
if entity.xmax < cutoff_xid {
stats.reclaimed_versions += 1;
if is_delete_tombstone {
stats.reclaimed_tombstones += 1;
} else {
stats.reclaimed_history_versions += 1;
}
reclaim_ids.push(entity.id);
} else {
stats.retained_versions += 1;
if is_delete_tombstone {
stats.retained_tombstones += 1;
} else {
stats.retained_history_versions += 1;
}
}
true
});
if !reclaim_ids.is_empty() {
self.delete_batch(collection, &reclaim_ids)?;
}
Ok(stats)
}
pub(crate) fn install_versioned_table_row_update(
&self,
collection: &str,
old_version: UnifiedEntity,
mut new_version: UnifiedEntity,
metadata: Option<&Metadata>,
) -> Result<(), StoreError> {
let manager = self
.get_collection(collection)
.ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
let old_id = old_version.id;
let new_id = new_version.id;
let inherited_metadata = metadata.cloned().or_else(|| manager.get_metadata(old_id));
self.resident.entity_cache.remove(old_id.raw());
self.resident.entity_cache.remove(new_id.raw());
manager.update(old_version.clone())?;
self.resident.context_index.remove_entity(old_id);
self.register_entity_id(new_version.id);
if let EntityKind::TableRow { ref mut row_id, .. } = new_version.kind {
if *row_id == 0 {
*row_id = manager.next_row_id();
} else {
manager.register_row_id(*row_id);
}
}
new_version.ensure_table_logical_id();
manager.insert(new_version.clone())?;
if let Some(metadata) = inherited_metadata {
manager.set_metadata(new_id, metadata)?;
}
self.resident
.context_index
.index_entity(collection, &new_version);
if self.config.auto_index_refs {
self.index_cross_refs(&new_version, collection)?;
}
let old_metadata = manager.get_metadata(old_id);
let new_metadata = manager.get_metadata(new_id);
let fv = self.format_version();
let records = vec![
manager
.get_with(old_id, |entity| {
Self::serialize_entity_record(entity, old_metadata.as_ref(), fv)
})
.ok_or(StoreError::EntityNotFound(old_id))?,
manager
.get_with(new_id, |entity| {
Self::serialize_entity_record(entity, new_metadata.as_ref(), fv)
})
.ok_or(StoreError::EntityNotFound(new_id))?,
];
self.finish_paged_write_for_projection(
[StoreWalAction::BulkUpsertEntityRecords {
collection: collection.to_string(),
records,
}],
true,
)?;
Ok(())
}
pub fn get_batch(&self, collection: &str, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
match self.get_collection(collection) {
Some(manager) => manager.get_many(ids),
None => vec![None; ids.len()],
}
}
pub fn try_get_batch(
&self,
collection: &str,
ids: &[EntityId],
read_bytes_max: usize,
) -> Result<Vec<Option<UnifiedEntity>>, StoreError> {
match self.get_collection(collection) {
Some(manager) => manager.try_get_many(ids, read_bytes_max),
None => Ok(vec![None; ids.len()]),
}
}
pub fn get_any(&self, id: EntityId) -> Option<(String, UnifiedEntity)> {
self.resident.get_any(id)
}
pub fn entity_cache_hit_rate(&self) -> Option<f64> {
self.resident.entity_cache_hit_rate()
}
pub fn entity_cache_stats(&self) -> super::super::entity_cache::EntityCacheStats {
self.resident.entity_cache_stats()
}
pub fn delete(&self, collection: &str, id: EntityId) -> Result<bool, StoreError> {
let publication = self.vault_publication_lock.lock();
self.resident.entity_cache.remove(id.raw());
let manager = self
.get_collection(collection)
.ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
let deleted = manager.delete(id)?;
if !deleted {
return Ok(false);
}
let mut registry_dirty = false;
if self.pager.is_some() && self.config.resident_memory_bytes_max.is_none() {
let btree_indices = self.btree_indices.read();
if let Some(btree) = btree_indices.get(collection) {
let root_before = btree.root_page_id();
let key = id.raw().to_be_bytes();
let _ = btree.delete(&key);
registry_dirty = root_before != btree.root_page_id();
}
}
self.unindex_cross_refs(id)?;
self.remove_from_graph_label_index(collection, id);
if registry_dirty {
self.mark_paged_registry_dirty();
}
let actions = [StoreWalAction::DeleteEntityRecord {
collection: collection.to_string(),
entity_id: id.raw(),
}];
if let Some(pending) = self.enqueue_grouped_actions(&actions)? {
drop(publication);
self.complete_grouped_actions(&pending, &actions)?;
} else {
self.finish_paged_write(actions)?;
}
Ok(true)
}
pub fn delete_batch(
&self,
collection: &str,
ids: &[EntityId],
) -> Result<Vec<EntityId>, StoreError> {
if ids.is_empty() {
return Ok(Vec::new());
}
let _publication = self.vault_publication_lock.lock();
self.resident
.entity_cache
.remove_many(ids.iter().map(|id| id.raw()));
let manager = self
.get_collection(collection)
.ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
let before = self.pager.as_ref().map(|_| manager.checkpoint_revisions());
let deleted_ids = manager.delete_batch(ids)?;
if deleted_ids.is_empty() {
return Ok(deleted_ids);
}
let mut registry_dirty = false;
let mut updated_tree = false;
if self.pager.is_some() && self.config.resident_memory_bytes_max.is_none() {
let btree_indices = self.btree_indices.read();
if let Some(btree) = btree_indices.get(collection) {
updated_tree = true;
let root_before = btree.root_page_id();
for id in &deleted_ids {
let key = id.raw().to_be_bytes();
updated_tree &= btree.delete(&key).is_ok();
}
registry_dirty = root_before != btree.root_page_id();
}
}
self.unindex_cross_refs_batch(&deleted_ids)?;
self.remove_from_graph_label_index_batch(collection, &deleted_ids);
if registry_dirty {
self.mark_paged_registry_dirty();
}
let actions = deleted_ids
.iter()
.map(|id| StoreWalAction::DeleteEntityRecord {
collection: collection.to_string(),
entity_id: id.raw(),
})
.collect::<Vec<_>>();
self.finish_paged_write(actions)?;
if updated_tree {
if let Some(before) = before {
self.note_paged_delete(collection, &manager, &before, &deleted_ids);
}
self.flush_paged_registry()?;
}
Ok(deleted_ids)
}
pub fn set_metadata(
&self,
collection: &str,
id: EntityId,
metadata: Metadata,
) -> Result<(), StoreError> {
let manager = self
.get_collection(collection)
.ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
manager.set_metadata(id, metadata)?;
if let Some(entity) = manager.get(id) {
self.persist_entities_to_pager(collection, std::slice::from_ref(&entity))?;
}
Ok(())
}
pub fn get_metadata(&self, collection: &str, id: EntityId) -> Option<Metadata> {
self.get_collection(collection)?.get_metadata(id)
}
pub fn try_get_metadata(
&self,
collection: &str,
id: EntityId,
limit_bytes: usize,
) -> Result<Option<Metadata>, StoreError> {
let Some(manager) = self.get_collection(collection) else {
return Ok(None);
};
manager.try_get_metadata(id, limit_bytes)
}
pub fn add_cross_ref(
&self,
source_collection: &str,
source_id: EntityId,
target_collection: &str,
target_id: EntityId,
ref_type: RefType,
weight: f32,
) -> Result<(), StoreError> {
let registry_dirty = self.resident.add_cross_ref_in_memory(
source_collection,
source_id,
target_collection,
target_id,
ref_type,
weight,
self.config.max_cross_refs,
|entity| {
self.persist_entities_to_pager(source_collection, std::slice::from_ref(entity))
},
)?;
if registry_dirty
&& matches!(
self.config.durability_mode,
crate::api::DurabilityMode::Strict
)
{
self.flush_paged_state()?;
}
Ok(())
}
pub fn get_refs_from(&self, id: EntityId) -> Vec<(EntityId, RefType, String)> {
self.resident.get_refs_from(id)
}
pub fn get_refs_to(&self, id: EntityId) -> Vec<(EntityId, RefType, String)> {
self.resident.get_refs_to(id)
}
pub fn expand_refs(
&self,
id: EntityId,
depth: u32,
ref_types: Option<&[RefType]>,
) -> Vec<(UnifiedEntity, u32, RefType)> {
let mut results = Vec::new();
let mut visited = std::collections::HashSet::new();
visited.insert(id);
self.expand_refs_recursive(id, depth, ref_types, &mut visited, &mut results, 1);
results
}
fn expand_refs_recursive(
&self,
id: EntityId,
max_depth: u32,
ref_types: Option<&[RefType]>,
visited: &mut std::collections::HashSet<EntityId>,
results: &mut Vec<(UnifiedEntity, u32, RefType)>,
current_depth: u32,
) {
if current_depth > max_depth {
return;
}
for (target_id, ref_type, target_collection) in self.get_refs_from(id) {
if visited.contains(&target_id) {
continue;
}
if let Some(types) = ref_types {
if !types.contains(&ref_type) {
continue;
}
}
visited.insert(target_id);
if let Some(entity) = self.get(&target_collection, target_id) {
results.push((entity, current_depth, ref_type));
self.expand_refs_recursive(
target_id,
max_depth,
ref_types,
visited,
results,
current_depth + 1,
);
}
}
}
pub(crate) fn index_cross_refs(
&self,
entity: &UnifiedEntity,
collection: &str,
) -> Result<(), StoreError> {
self.resident.index_cross_refs(entity, collection)
}
pub(crate) fn unindex_cross_refs(&self, id: EntityId) -> Result<(), StoreError> {
self.resident.unindex_cross_refs(id)
}
pub(crate) fn unindex_cross_refs_batch(&self, ids: &[EntityId]) -> Result<(), StoreError> {
self.resident.unindex_cross_refs_batch(ids)
}
pub fn unindex_cross_refs_fast_path_hits(&self) -> u64 {
self.resident.unindex_cross_refs_fast_path_hits()
}
pub fn query_all<F>(&self, filter: F) -> Vec<(String, UnifiedEntity)>
where
F: Fn(&UnifiedEntity) -> bool + Clone + Send + Sync,
{
self.resident.query_all(filter)
}
pub fn scan<F>(
&self,
snapshot: Option<&crate::runtime::impl_core::SnapshotContext>,
filter: F,
) -> Vec<(String, UnifiedEntity)>
where
F: Fn(&UnifiedEntity) -> bool + Clone + Send + Sync,
{
self.resident.scan(snapshot, filter)
}
pub fn filter_metadata_all(
&self,
filters: &[(String, MetadataFilter)],
) -> Vec<(String, EntityId)> {
self.resident.filter_metadata_all(filters)
}
pub fn stats(&self) -> StoreStats {
let pairs: Vec<(String, Arc<SegmentManager>)> = {
let collections = self.resident.collections.read();
collections
.iter()
.map(|(name, mgr)| (name.clone(), Arc::clone(mgr)))
.collect()
};
let mut stats = StoreStats {
collection_count: pairs.len(),
..Default::default()
};
for (name, manager) in &pairs {
let manager_stats = manager.stats();
stats.total_entities += manager_stats.total_entities;
stats.total_memory_bytes += manager_stats.total_memory_bytes;
stats.total_memory_bytes += manager.checkpoint_tracking_bytes();
stats.collections.insert(name.clone(), manager_stats);
}
stats.total_memory_bytes += self.checkpoint_durable_rows.read().memory_bytes();
stats.total_memory_bytes += self
.checkpoint_versions
.read()
.values()
.filter_map(|version| version.deferred_rows.as_ref())
.map(|rows| rows.capacity() * std::mem::size_of::<EntityId>())
.sum::<usize>();
stats
}
pub fn run_maintenance(&self) -> Result<(), StoreError> {
let collections = self.resident.collections.read();
for manager in collections.values() {
manager.run_maintenance()?;
}
Ok(())
}
pub fn reclaimable_segment_bytes(&self) -> u64 {
let collections = self.resident.collections.read();
collections
.values()
.map(|manager| manager.reclaimable_bytes())
.fold(0, u64::saturating_add)
}
pub fn pressure_consolidation_tick(&self) -> bool {
let managers: Vec<_> = self.resident.collections.read().values().cloned().collect();
let mut advanced = false;
for manager in &managers {
advanced |= manager.pressure_consolidation_tick();
}
advanced
}
}
fn flatten_config_json(
prefix: &str,
value: &crate::serde_json::Value,
out: &mut Vec<(String, reddb_types::Value)>,
) {
use reddb_types::Value;
match value {
crate::serde_json::Value::Object(map) => {
for (k, v) in map {
let key = if prefix.is_empty() {
k.clone()
} else {
format!("{prefix}.{k}")
};
flatten_config_json(&key, v, out);
}
}
crate::serde_json::Value::String(s) => {
out.push((prefix.to_string(), Value::text(s.clone())));
}
crate::serde_json::Value::Integer(n) => {
out.push((prefix.to_string(), Value::Integer(*n)));
}
crate::serde_json::Value::Number(n) => {
if n.fract().abs() < f64::EPSILON {
out.push((prefix.to_string(), Value::UnsignedInteger(*n as u64)));
} else {
out.push((prefix.to_string(), Value::Float(*n)));
}
}
crate::serde_json::Value::Decimal(n) => {
out.push((prefix.to_string(), Value::DecimalText(n.clone())));
}
crate::serde_json::Value::Bool(b) => {
out.push((prefix.to_string(), Value::Boolean(*b)));
}
crate::serde_json::Value::Null => {
out.push((prefix.to_string(), Value::Null));
}
crate::serde_json::Value::Array(arr) => {
let json_str = crate::serde_json::to_string(value).unwrap_or_default();
out.push((prefix.to_string(), Value::text(json_str)));
}
}
}