use std::collections::{BTreeMap, BTreeSet};
use crate::error::{Error, Result};
use crate::field::index::{self, FsIndexStore, SEL_PAGE, SelectorKey};
use crate::field::node::{MAX_NODE_BYTES, NodeKind, SeedNode, u32_params};
use crate::field::{FieldId, FieldStore};
use crate::store::{IoSnapshot, NodeId};
pub const MAX_EDIT_CONTENT_BYTES: usize = 48 * 1024;
const EDIT_SEED_NODES: u64 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditReport {
pub field: FieldId,
pub previous: FieldId,
pub page: u32,
pub page_content: NodeId,
pub content_literal: NodeId,
pub index_root: NodeId,
pub index_entries: u64,
pub index_entries_reused: u64,
pub index_entries_replaced: u64,
pub seed_nodes_new: u64,
pub seed_nodes_reused: u64,
pub index_nodes_reused: u64,
pub index_nodes_new: u64,
pub bytes_newly_persisted: u64,
pub descriptor_bytes_read: u64,
pub manifest_bytes_read: u64,
pub index_bytes_read: u64,
pub seed_bytes_read: u64,
}
pub fn replace_page_content(
store: &mut FieldStore,
field: &FieldId,
page: u32,
new_content: &[u8],
) -> Result<EditReport> {
if page == 0 {
return Err(Error::usage("edit page numbers are 1-based"));
}
if new_content.len() > MAX_EDIT_CONTENT_BYTES {
return Err(Error::resource_limit(format!(
"edit content is {} bytes > {MAX_EDIT_CONTENT_BYTES}",
new_content.len()
)));
}
let io_before = store.io().snapshot();
let io_handle = store.io().handle();
let previous = *field;
let manifest = store.get_field(field)?;
if !manifest.has_index() {
return Err(Error::unsupported_feature(
"immutable edit requires an indexed field (ingest with a recovered index)",
));
}
let old_root = NodeId::from_bytes(manifest.index_root);
let istore = FsIndexStore::open_with_io(store.root(), io_handle.clone())?;
let before = index::inspect(&istore, &old_root)?;
let preexisting: BTreeSet<NodeId> = istore.list_ids()?.into_iter().collect();
let old_by_key: BTreeMap<SelectorKey, NodeId> =
before.entries.iter().map(|e| (e.key, e.node_id)).collect();
let page_key = SelectorKey::new(SEL_PAGE, page);
if !old_by_key.contains_key(&page_key) {
return Err(Error::usage(format!(
"field {previous} has no page {page} in its index"
)));
}
let literal = SeedNode::new(
NodeKind::Literal,
new_content.len() as u64,
new_content.to_vec(),
Vec::new(),
format!("field:edit;literal;page={page}"),
);
let literal_canonical = literal.encode_canonical();
if literal_canonical.len() > MAX_NODE_BYTES {
return Err(Error::resource_limit(format!(
"edit content encodes to {} bytes > the {MAX_NODE_BYTES}-byte node framing limit",
literal_canonical.len()
)));
}
let literal_id = literal.content_id();
let page_content = SeedNode::new(
NodeKind::PageContent,
new_content.len() as u64,
u32_params(page),
vec![literal_id],
format!("field:edit;page-content;page={page}"),
);
let page_content_id = page_content.content_id();
let mut seed_nodes_new = 0u64;
let mut seed_nodes_reused = 0u64;
let mut bytes_newly_persisted = 0u64;
for node in [&literal, &page_content] {
let canonical = node.encode_canonical();
let id = NodeId::of_node(&canonical);
if store.seeds().contains_node(&id)? {
seed_nodes_reused = seed_nodes_reused.saturating_add(1);
} else {
seed_nodes_new = seed_nodes_new.saturating_add(1);
bytes_newly_persisted = bytes_newly_persisted.saturating_add(canonical.len() as u64);
}
store.seeds_mut().put_node(&canonical)?;
}
let mut entries = before.entries.clone();
let mut index_entries_reused = 0u64;
let mut index_entries_replaced = 0u64;
for e in entries.iter_mut() {
if e.key == page_key {
e.node_id = page_content_id;
e.out_off = 0;
e.out_len = 0;
index_entries_replaced = index_entries_replaced.saturating_add(1);
} else if old_by_key.get(&e.key).copied() == Some(e.node_id) {
index_entries_reused = index_entries_reused.saturating_add(1);
}
}
let mut new_istore = FsIndexStore::open_with_io(store.root(), io_handle)?;
let new_root = index::build(&mut new_istore, &entries)?;
let (index_node_count, _depth, after_nodes) = index::validate_nodes(&new_istore, &new_root)?;
let index_nodes_reused = after_nodes
.iter()
.filter(|id| preexisting.contains(id))
.count() as u64;
let index_nodes_new = after_nodes.len() as u64 - index_nodes_reused;
for id in &after_nodes {
if !preexisting.contains(id) {
bytes_newly_persisted =
bytes_newly_persisted.saturating_add(new_istore.get(id)?.len() as u64);
}
}
let mut new_manifest = manifest.clone();
new_manifest.index_root = *new_root.as_bytes();
new_manifest.index_node_count = index_node_count;
new_manifest.node_count = manifest.node_count.saturating_add(EDIT_SEED_NODES);
new_manifest.provenance = format!("field:edit;page={page};prev={previous}");
let manifest_bytes = new_manifest.encode_canonical();
let new_id = new_manifest.content_id();
if !manifest_exists(store, &new_id)? {
bytes_newly_persisted = bytes_newly_persisted.saturating_add(manifest_bytes.len() as u64);
}
store.put_field(&new_manifest)?;
let io = io_before.delta(&store.io().snapshot());
Ok(EditReport {
field: new_id,
previous,
page,
page_content: page_content_id,
content_literal: literal_id,
index_root: new_root,
index_entries: entries.len() as u64,
index_entries_reused,
index_entries_replaced,
seed_nodes_new,
seed_nodes_reused,
index_nodes_reused,
index_nodes_new,
bytes_newly_persisted,
descriptor_bytes_read: io.descriptor_bytes,
manifest_bytes_read: io.manifest_bytes,
index_bytes_read: io.index_bytes,
seed_bytes_read: io.seed_bytes,
})
}
pub fn io_of(report: &EditReport) -> IoSnapshot {
IoSnapshot {
descriptor_bytes: report.descriptor_bytes_read,
manifest_bytes: report.manifest_bytes_read,
index_bytes: report.index_bytes_read,
seed_bytes: report.seed_bytes_read,
}
}
fn manifest_exists(store: &FieldStore, id: &FieldId) -> Result<bool> {
let path = store.root().join("field").join(id.to_hex());
if path.exists() {
return Ok(true);
}
Ok(store.get_field(id).is_ok())
}