use std::collections::HashMap;
use std::sync::Arc;
use super::columnar_write::{set_via_column_master, ColumnMasterWrite};
use super::identity_fields::IdentityAliases;
use super::write::{enforce_write_scope, set_node_property_direct};
use crate::datatypes::values::Value;
use crate::graph::languages::cypher::result::MutationStats;
use crate::graph::schema::{DirGraph, InternedKey};
use crate::graph::storage::GraphRead;
use petgraph::graph::NodeIndex;
use std::borrow::Cow;
pub(super) struct NodePropertySet<'a> {
pub node_idx: NodeIndex,
pub property: &'a str,
pub value: Value,
}
#[derive(Default)]
pub(super) struct SetMemos<'a> {
types: HashMap<InternedKey, TypeFacts>,
properties: HashMap<(InternedKey, &'a str), PropertyFacts>,
}
struct TypeFacts {
name: String,
aliases: IdentityAliases,
maintains_indexes: bool,
auto_timestamp: bool,
}
struct PropertyFacts {
recorded_type: &'static str,
key: InternedKey,
}
#[derive(Clone, Copy)]
struct RowBookkeeping {
maintains_indexes: bool,
register_schema_key: bool,
record_metadata: Option<&'static str>,
auto_timestamp: bool,
key: InternedKey,
}
struct LandedPropertyWrite<'a> {
node_idx: NodeIndex,
node_type: &'a str,
property: &'a str,
write_field: &'a str,
old_value: Option<&'a Value>,
value: &'a Value,
constraint_plan: &'a crate::graph::dir_graph::constraints::PropertyWritePlan,
owed: RowBookkeeping,
}
fn enforce_schema_lock(
graph: &DirGraph,
node_type: &str,
property: &str,
value: &Value,
) -> Result<(), String> {
if !graph.schema_locked {
return Ok(());
}
crate::graph::mutation::validation::validate_property_known(
node_type,
property,
value,
&graph.node_type_metadata,
graph.schema_definition.as_ref(),
)?;
if graph.property_type_for(node_type, property).is_none() {
crate::graph::mutation::validation::validate_property_type(
node_type,
property,
value,
&graph.node_type_metadata,
)?;
}
Ok(())
}
pub(super) fn apply_node_property_set<'a>(
graph: &mut DirGraph,
write: NodePropertySet<'a>,
memos: &mut SetMemos<'a>,
stats: &mut MutationStats,
nodes_to_stamp: &mut HashMap<NodeIndex, String>,
) -> Result<(), String> {
let NodePropertySet {
node_idx,
property,
value,
} = write;
let Some(type_key) = ({
let _arena_guard = graph.graph.begin_query();
GraphRead::node_type_of(&graph.graph, node_idx)
}) else {
return Ok(());
};
let facts = type_facts_for(&mut memos.types, graph, type_key);
let node_type_str = facts.name.as_str();
let write_field = match facts.aliases.canonical(property) {
"id" => {
return Err(format!(
"Cannot SET node id — it is immutable ('{property}' is the id field of \
node type '{node_type_str}', so it names the identity)"
))
}
"title" => "title",
_ => property,
};
let old_value = if facts.maintains_indexes {
let _arena_guard = graph.graph.begin_query();
let Some(node) = graph.node_view(node_idx) else {
return Ok(());
};
match write_field {
"name" => node
.get_field_ref("name")
.map(Cow::into_owned)
.or_else(|| Some(node.title().into_owned())),
"title" => Some(node.title().into_owned()),
_ => node.get_field_ref(write_field).map(Cow::into_owned),
}
} else {
None
};
enforce_write_scope(graph, node_type_str)?;
enforce_schema_lock(graph, node_type_str, property, &value)?;
let constraint_plan = graph
.plan_property_write(node_type_str, node_idx, property, Some(&value))
.map_err(|violation| violation.to_string())?;
let owed = row_bookkeeping(
&mut memos.properties,
type_key,
property,
value.type_name(),
facts,
&mut graph.interner,
);
let columnar_row_id = {
let _arena_guard = graph.graph.begin_query();
graph
.graph
.node_weight(node_idx)
.and_then(|n| n.properties.columnar_row_id())
};
let wrote_via_master = set_via_column_master(
graph,
ColumnMasterWrite {
node_idx,
node_type: node_type_str,
type_key,
property: write_field,
key: owed.key,
value: &value,
row_id: columnar_row_id,
},
);
if wrote_via_master {
stats.properties_set += 1;
}
if !wrote_via_master {
if set_node_property_direct(graph, node_idx, write_field, value.clone()) {
stats.properties_set += 1;
}
}
finish_node_property_write(
graph,
LandedPropertyWrite {
node_idx,
node_type: node_type_str,
property,
write_field,
old_value: old_value.as_ref(),
value: &value,
constraint_plan: &constraint_plan,
owed,
},
nodes_to_stamp,
);
Ok(())
}
fn finish_node_property_write(
graph: &mut DirGraph,
write: LandedPropertyWrite<'_>,
nodes_to_stamp: &mut HashMap<NodeIndex, String>,
) {
if write.owed.register_schema_key && write.write_field != "title" {
let ik = write.owed.key;
let needs_key = graph
.type_schemas
.get(write.node_type)
.is_some_and(|schema| schema.slot(ik).is_none());
if needs_key {
if let Some(schema_arc) = graph.type_schemas_mut().get_mut(write.node_type) {
Arc::make_mut(schema_arc).add_key(ik);
}
}
}
if write.owed.maintains_indexes {
graph.update_property_indices_for_set(
write.node_type,
write.node_idx,
write.property,
write.old_value,
write.value,
);
}
graph.apply_property_write_plan(write.constraint_plan, write.node_idx);
if let Some(value_type) = write.owed.record_metadata {
let mut prop_type = HashMap::new();
prop_type.insert(write.property.to_string(), value_type.to_string());
graph.upsert_node_type_metadata(write.node_type, prop_type);
}
if write.owed.auto_timestamp && write.property != "updated_at" {
nodes_to_stamp.insert(write.node_idx, write.node_type.to_string());
}
}
fn type_facts_for<'m>(
memo: &'m mut HashMap<InternedKey, TypeFacts>,
graph: &DirGraph,
type_key: InternedKey,
) -> &'m TypeFacts {
memo.entry(type_key).or_insert_with(|| {
let name = graph.interner.resolve(type_key).to_string();
TypeFacts {
maintains_indexes: graph.type_has_user_indexes(&name),
auto_timestamp: graph.auto_timestamp_for(&name),
aliases: IdentityAliases::for_type(graph, &name),
name,
}
})
}
fn row_bookkeeping<'a>(
memo: &mut HashMap<(InternedKey, &'a str), PropertyFacts>,
type_key: InternedKey,
property: &'a str,
value_type: &'static str,
type_facts: &TypeFacts,
interner: &mut crate::graph::storage::interner::StringInterner,
) -> RowBookkeeping {
use std::collections::hash_map::Entry;
let mut register_schema_key = true;
let mut record_metadata = Some(value_type);
let key = match memo.entry((type_key, property)) {
Entry::Occupied(mut seen) => {
register_schema_key = false;
let key = seen.get().key;
if seen.get().recorded_type == value_type {
record_metadata = None;
} else {
seen.insert(PropertyFacts {
recorded_type: value_type,
key,
});
}
key
}
Entry::Vacant(slot) => {
let key = interner.get_or_intern(property);
slot.insert(PropertyFacts {
recorded_type: value_type,
key,
});
key
}
};
RowBookkeeping {
maintains_indexes: type_facts.maintains_indexes,
register_schema_key,
record_metadata,
auto_timestamp: type_facts.auto_timestamp,
key,
}
}