use crate::PropertyType;
use crate::content::node::NodeState;
use crate::content::{PropertyValues, SegmentProvider};
use crate::error::{Error, Result};
use crate::segment::record::RecordIdentifier;
use crate::writer::commit::{ChildEdits, NodeEdits, rewrite_node_with_edits};
use crate::writer::record_writer::{
PropertyToWrite, PropertyValuesToWrite, RecordWriter, SegmentSink,
};
pub const DISABLE_ON_NEXT_CYCLE_PROPERTY: &str = ":disableIndexesOnNextCycle";
const RETAIN_PROPERTY: &str = "retainNodeInReindex";
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ReindexCount {
Increment,
Set(u64),
Keep,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DisablerVerdict {
Flag,
Leave,
}
pub struct DefinitionEdits {
pub reindex_count: ReindexCount,
pub disabler_verdict: DisablerVerdict,
pub property_replacements: Vec<PropertyToWrite>,
pub property_removals: Vec<String>,
pub visible_children: ChildEdits,
pub reindex_flag_raised: bool,
pub hidden_children: Vec<(String, RecordIdentifier)>,
}
impl DefinitionEdits {
#[must_use]
pub fn reindexed(
disabler_verdict: DisablerVerdict,
hidden_children: Vec<(String, RecordIdentifier)>,
) -> Self {
Self {
reindex_count: ReindexCount::Increment,
disabler_verdict,
property_replacements: Vec::new(),
property_removals: Vec::new(),
visible_children: ChildEdits::new(),
hidden_children,
reindex_flag_raised: false,
}
}
#[must_use]
pub fn reset() -> Self {
Self {
reindex_count: ReindexCount::Keep,
disabler_verdict: DisablerVerdict::Leave,
property_replacements: Vec::new(),
property_removals: Vec::new(),
visible_children: ChildEdits::new(),
hidden_children: Vec::new(),
reindex_flag_raised: true,
}
}
}
pub fn rewrite_definition<Sink: SegmentSink>(
provider: &dyn SegmentProvider,
writer: &mut RecordWriter<Sink>,
definition: &NodeState<'_>,
edits: &DefinitionEdits,
) -> Result<RecordIdentifier> {
let node_edits = definition_node_edits(writer, definition, edits)?;
rewrite_node_with_edits(
provider,
writer,
Some(definition.record_identifier()),
&node_edits,
)
}
fn definition_node_edits<Sink: SegmentSink>(
writer: &mut RecordWriter<Sink>,
definition: &NodeState<'_>,
edits: &DefinitionEdits,
) -> Result<NodeEdits> {
let mut node_edits = NodeEdits {
property_replacements: edits.property_replacements.clone(),
property_removals: edits.property_removals.clone(),
child_edits: edits.visible_children.clone(),
};
if edits.reindex_count != ReindexCount::Keep {
let truth = writer.write_string("false")?;
node_edits.property_replacements.push(PropertyToWrite {
name: "reindex".to_owned(),
property_type: PropertyType::Boolean,
values: PropertyValuesToWrite::Single(truth),
});
let next = match edits.reindex_count {
ReindexCount::Increment => current_reindex_count(definition)?.saturating_add(1),
ReindexCount::Set(value) => value,
ReindexCount::Keep => unreachable!("excluded by the branch"),
};
let value = writer.write_string(&next.to_string())?;
node_edits.property_replacements.push(PropertyToWrite {
name: "reindexCount".to_owned(),
property_type: PropertyType::Long,
values: PropertyValuesToWrite::Single(value),
});
node_edits.property_removals.push("corrupt".to_owned());
}
if edits.reindex_flag_raised {
let truth = writer.write_string("true")?;
node_edits.property_replacements.push(PropertyToWrite {
name: "reindex".to_owned(),
property_type: PropertyType::Boolean,
values: PropertyValuesToWrite::Single(truth),
});
}
if edits.disabler_verdict == DisablerVerdict::Flag {
let truth = writer.write_string("true")?;
node_edits.property_replacements.push(PropertyToWrite {
name: DISABLE_ON_NEXT_CYCLE_PROPERTY.to_owned(),
property_type: PropertyType::Boolean,
values: PropertyValuesToWrite::Single(truth),
});
}
let produced: Vec<&str> = edits
.hidden_children
.iter()
.map(|(name, _)| name.as_str())
.collect();
for (name, child) in definition.child_node_entries()? {
if !name.starts_with(':') || produced.contains(&name.as_str()) {
continue;
}
if retains_across_reindex(&child)? {
continue;
}
node_edits.child_edits.insert(name, None);
}
for (name, record) in &edits.hidden_children {
node_edits.child_edits.insert(name.clone(), Some(*record));
}
Ok(node_edits)
}
pub fn clone_updated_definition_state<Sink: SegmentSink>(
provider: &dyn SegmentProvider,
writer: &mut RecordWriter<Sink>,
definition: &NodeState<'_>,
edits: &DefinitionEdits,
) -> Result<RecordIdentifier> {
let mut node_edits = definition_node_edits(writer, definition, edits)?;
for name in node_edits
.child_edits
.keys()
.filter(|name| name.starts_with(':'))
.cloned()
.collect::<Vec<String>>()
{
node_edits.child_edits.insert(name, None);
}
for (name, child) in definition.child_node_entries()? {
if name.starts_with(':') {
node_edits.child_edits.insert(name, None);
continue;
}
if node_edits.child_edits.contains_key(&name) {
continue;
}
let cloned = clone_visible_state(provider, writer, &child)?;
if cloned != child.record_identifier() {
node_edits.child_edits.insert(name, Some(cloned));
}
}
rewrite_node_with_edits(
provider,
writer,
Some(definition.record_identifier()),
&node_edits,
)
}
fn current_reindex_count(definition: &NodeState<'_>) -> Result<u64> {
let Some(property) = definition.property("reindexCount")? else {
return Ok(0);
};
match &property.values {
PropertyValues::Single(value) => Ok(value
.as_text()
.and_then(|text| text.parse::<i64>().ok())
.and_then(|count| u64::try_from(count).ok())
.unwrap_or(0)),
PropertyValues::Multiple(_) => Err(Error::InvalidFormat {
details: "reindexCount is multi-valued, which Oak's own increment refuses; \
a rebuild cannot guess which value to carry forward"
.to_owned(),
}),
}
}
fn retains_across_reindex(child: &NodeState<'_>) -> Result<bool> {
let property = child.property(RETAIN_PROPERTY)?;
Ok(crate::index::strict_boolean(property.as_ref()))
}
const MAXIMUM_SUPERSEDES_DEPTH: usize = 64;
pub fn disabler_verdict(
content_root: &NodeState<'_>,
definition: &NodeState<'_>,
) -> Result<DisablerVerdict> {
let superseded = crate::index::converting_strings(definition.property("supersedes")?.as_ref());
for entry in superseded {
let (path, node_type) = match entry.rsplit_once('/') {
Some((parent, last)) if last.starts_with('@') => (parent, Some(&last[1..])),
_ => (entry.as_str(), None),
};
let Some(node) = resolve_under(content_root, path)? else {
continue;
};
if let Some(node_type) = node_type {
let declared =
crate::index::converting_strings(node.property("declaringNodeTypes")?.as_ref());
if declared.iter().any(|declared| declared == node_type) {
return Ok(DisablerVerdict::Flag);
}
} else if crate::index::strict_string(node.property("type")?.as_ref()) != Some("disabled") {
return Ok(DisablerVerdict::Flag);
}
}
Ok(DisablerVerdict::Leave)
}
fn resolve_under<'store>(
root: &NodeState<'store>,
path: &str,
) -> Result<Option<NodeState<'store>>> {
let mut node = *root;
for (depth, element) in path.split('/').filter(|part| !part.is_empty()).enumerate() {
if depth >= MAXIMUM_SUPERSEDES_DEPTH {
return Ok(None);
}
let Some(child) = node.child_node(element)? else {
return Ok(None);
};
node = child;
}
Ok(Some(node))
}
pub const INDEX_VERSION_PROPERTY: &str = ":version";
pub const STORED_DEFINITION_CHILD: &str = ":index-definition";
pub fn fresh_index_format_version(definition: &NodeState<'_>) -> Result<i64> {
let Some(property) = definition.property("compatMode")? else {
return Ok(2);
};
match crate::index::converting_long(Some(&property)) {
Some(version @ (1 | 2)) => Ok(version),
other => Err(Error::InvalidFormat {
details: format!(
"compatMode reads as {}, and Oak's IndexFormatVersion knows only 1 and 2",
other.map_or_else(|| "no number".to_owned(), |value| value.to_string())
),
}),
}
}
const MAXIMUM_CLONE_DEPTH: usize = 64;
pub fn clone_visible_state<Sink: SegmentSink>(
provider: &dyn SegmentProvider,
writer: &mut RecordWriter<Sink>,
node: &NodeState<'_>,
) -> Result<RecordIdentifier> {
clone_visible_state_bounded(provider, writer, node, 0)
}
fn clone_visible_state_bounded<Sink: SegmentSink>(
provider: &dyn SegmentProvider,
writer: &mut RecordWriter<Sink>,
node: &NodeState<'_>,
depth: usize,
) -> Result<RecordIdentifier> {
if depth >= MAXIMUM_CLONE_DEPTH {
return Err(Error::InvalidFormat {
details: format!(
"a definition nests more than {MAXIMUM_CLONE_DEPTH} levels deep, which no \
index definition does and a corrupt record can claim"
),
});
}
let mut edits = ChildEdits::new();
for (name, child) in node.child_node_entries()? {
if name.starts_with(':') {
edits.insert(name, None);
continue;
}
let cloned = clone_visible_state_bounded(provider, writer, &child, depth + 1)?;
if cloned != child.record_identifier() {
edits.insert(name, Some(cloned));
}
}
crate::writer::commit::rewrite_node_with_child_edits(
provider,
writer,
Some(node.record_identifier()),
&edits,
)
}