use std::sync::Arc;
use selene_core::{
ByteStringType, Change, CharacterStringType, DbString, EdgeEndpointDef as CoreEdgeEndpointDef,
GraphTypeId, LabelSet, PredefinedValueType, PropertyDef, PropertyValueType, SchemaChange,
ValueType,
};
use smallvec::SmallVec;
use crate::{
DropBehavior, EdgeEndpointDef, EdgeTypeDef, GraphError, GraphResult, GraphTypeDef, Mutator,
NodeTypeDef, PropertyElementType, PropertyTypeDef, RecordFieldType, RecordFieldTypes,
ValidationMode,
graph_types::{MAX_LIST_TYPE_NESTING, MAX_RECORD_TYPE_NESTING},
};
const OPEN_GRAPH_CATALOG_DDL: &str =
"open graph (GG01) does not support catalog type DDL -- use a closed graph (GG02)";
impl<'tx, 'g> Mutator<'tx, 'g> {
pub fn create_node_type(
&mut self,
name: DbString,
key_labels: LabelSet,
properties: Vec<PropertyTypeDef>,
validation_mode: ValidationMode,
) -> GraphResult<()> {
let mut graph_type = self.current_graph_type()?;
if graph_type
.node_types
.iter()
.any(|node_type| node_type.name == name)
{
return Err(GraphError::Inconsistent {
reason: format!("node type {name} already exists"),
});
}
let node_type = NodeTypeDef {
name: name.clone(),
key_labels,
properties,
validation_mode,
};
graph_type.node_types.push(node_type.clone());
graph_type.validate_ref()?;
let graph_id = self.txn.read().graph_id();
self.txn.guard_mut().meta.bound_type = Some(Arc::new(graph_type));
self.txn.changes.push(Change::SchemaChanged {
graph: graph_id,
change: SchemaChange::NodeTypeAddedV2 {
graph_type: implicit_graph_type_id(),
label: name,
def: core_node_type_def(&node_type)?,
},
});
Ok(())
}
pub fn create_edge_type(
&mut self,
name: DbString,
label: DbString,
source_node_type: EdgeEndpointDef,
target_node_type: EdgeEndpointDef,
properties: Vec<PropertyTypeDef>,
validation_mode: ValidationMode,
) -> GraphResult<()> {
let mut graph_type = self.current_graph_type()?;
if graph_type
.edge_types
.iter()
.any(|edge_type| edge_type.name == name)
{
return Err(GraphError::Inconsistent {
reason: format!("edge type {name} already exists"),
});
}
let edge_type = EdgeTypeDef {
name,
label: label.clone(),
source_node_type,
target_node_type,
properties,
validation_mode,
};
graph_type.edge_types.push(edge_type.clone());
graph_type.validate_ref()?;
let graph_id = self.txn.read().graph_id();
self.txn.guard_mut().meta.bound_type = Some(Arc::new(graph_type.clone()));
self.txn.changes.push(Change::SchemaChanged {
graph: graph_id,
change: SchemaChange::EdgeTypeAddedV2 {
graph_type: implicit_graph_type_id(),
label,
def: core_edge_type_def(&graph_type, &edge_type)?,
},
});
Ok(())
}
pub fn drop_node_type(&mut self, name: DbString, behavior: DropBehavior) -> GraphResult<()> {
let graph_type = self.current_graph_type()?;
let removed_index = graph_type
.node_type_index_for(name.clone())
.ok_or_else(|| GraphError::Inconsistent {
reason: format!("node type {name} does not exist"),
})?;
match behavior {
DropBehavior::Restrict => {
let live = self
.txn
.read()
.nodes_with_label(&name)
.map_or(0, roaring::RoaringBitmap::len);
if live > 0 {
return Err(GraphError::Inconsistent {
reason: format!(
"cannot drop node type {name}: {live} instance(s) still exist; use CASCADE to remove them"
),
});
}
for edge_type in &graph_type.edge_types {
if endpoint_references_node(&edge_type.source_node_type, removed_index)
|| endpoint_references_node(&edge_type.target_node_type, removed_index)
{
return Err(GraphError::Inconsistent {
reason: format!(
"cannot drop node type {name}: edge type {} still references it",
edge_type.name
),
});
}
}
}
DropBehavior::Cascade => {
self.truncate_node_type(name.clone())?;
}
}
for edge_type in &graph_type.edge_types {
if endpoint_depends_on_shifted_node(&edge_type.source_node_type, removed_index)
|| endpoint_depends_on_shifted_node(&edge_type.target_node_type, removed_index)
{
return Err(GraphError::Inconsistent {
reason: format!(
"cannot drop node type {name}: edge type {} still depends on node-type indexes that would require reindexing",
edge_type.name
),
});
}
}
let next = graph_type
.without_node_type(name.clone())
.expect("node type existed above");
next.validate_ref()?;
let graph_id = self.txn.read().graph_id();
self.txn.guard_mut().meta.bound_type = Some(Arc::new(next));
self.txn.changes.push(Change::SchemaChanged {
graph: graph_id,
change: SchemaChange::NodeTypeDropped {
graph_type: implicit_graph_type_id(),
name,
},
});
Ok(())
}
pub fn drop_edge_type(&mut self, name: DbString, behavior: DropBehavior) -> GraphResult<()> {
let graph_type = self.current_graph_type()?;
if graph_type.edge_type_index_for(name.clone()).is_none() {
return Err(GraphError::Inconsistent {
reason: format!("edge type {name} does not exist"),
});
}
match behavior {
DropBehavior::Restrict => {
let live = self
.txn
.read()
.edges_with_label(&name)
.map_or(0, roaring::RoaringBitmap::len);
if live > 0 {
return Err(GraphError::Inconsistent {
reason: format!(
"cannot drop edge type {name}: {live} instance(s) still exist; use CASCADE to remove them"
),
});
}
}
DropBehavior::Cascade => {
self.truncate_edge_type(name.clone())?;
}
}
let next = graph_type
.without_edge_type(name.clone())
.expect("edge type existed above");
next.validate_ref()?;
let graph_id = self.txn.read().graph_id();
self.txn.guard_mut().meta.bound_type = Some(Arc::new(next));
self.txn.changes.push(Change::SchemaChanged {
graph: graph_id,
change: SchemaChange::EdgeTypeDropped {
graph_type: implicit_graph_type_id(),
name,
},
});
Ok(())
}
fn current_graph_type(&self) -> GraphResult<GraphTypeDef> {
self.txn
.read()
.meta
.bound_type
.as_deref()
.cloned()
.ok_or_else(|| GraphError::Inconsistent {
reason: OPEN_GRAPH_CATALOG_DDL.to_owned(),
})
}
}
fn implicit_graph_type_id() -> GraphTypeId {
GraphTypeId::new(1).expect("implicit graph type id")
}
fn endpoint_references_node(endpoint: &EdgeEndpointDef, removed_index: u32) -> bool {
match endpoint {
EdgeEndpointDef::Any => false,
EdgeEndpointDef::NodeType(index) => *index == removed_index,
EdgeEndpointDef::OneOf(indices) => indices.contains(&removed_index),
}
}
fn endpoint_depends_on_shifted_node(endpoint: &EdgeEndpointDef, removed_index: u32) -> bool {
match endpoint {
EdgeEndpointDef::Any => false,
EdgeEndpointDef::NodeType(index) => *index >= removed_index,
EdgeEndpointDef::OneOf(indices) => indices.iter().any(|index| *index >= removed_index),
}
}
fn core_node_type_def(node_type: &NodeTypeDef) -> GraphResult<selene_core::NodeTypeDef> {
Ok(selene_core::NodeTypeDef {
labels: node_type.key_labels.clone(),
properties: core_node_properties(&node_type.properties)?,
key: None,
validation_mode: core_validation_mode(node_type.validation_mode),
})
}
fn core_edge_type_def(
graph_type: &GraphTypeDef,
edge_type: &EdgeTypeDef,
) -> GraphResult<selene_core::EdgeTypeDef> {
Ok(selene_core::EdgeTypeDef {
label: edge_type.label.clone(),
source_node_type: core_edge_endpoint_def(
graph_type,
edge_type.name.clone(),
&edge_type.source_node_type,
)?,
target_node_type: core_edge_endpoint_def(
graph_type,
edge_type.name.clone(),
&edge_type.target_node_type,
)?,
properties: core_edge_properties(&edge_type.properties)?,
validation_mode: core_validation_mode(edge_type.validation_mode),
})
}
fn core_edge_endpoint_def(
graph_type: &GraphTypeDef,
edge_name: DbString,
endpoint: &EdgeEndpointDef,
) -> GraphResult<CoreEdgeEndpointDef> {
match endpoint {
EdgeEndpointDef::Any => Ok(CoreEdgeEndpointDef::Any),
EdgeEndpointDef::NodeType(index) => graph_type
.node_types
.get(*index as usize)
.map(|node_type| {
CoreEdgeEndpointDef::NodeType(selene_core::NodeTypeRef(node_type.name.clone()))
})
.ok_or_else(|| GraphError::Inconsistent {
reason: format!("edge type {edge_name} references invalid node type {index}"),
}),
EdgeEndpointDef::OneOf(indices) => {
let mut refs: SmallVec<[selene_core::NodeTypeRef; 4]> = SmallVec::new();
for index in indices {
let node_type = graph_type.node_types.get(*index as usize).ok_or_else(|| {
GraphError::Inconsistent {
reason: format!(
"edge type {edge_name} OneOf endpoint references invalid node type {index}"
),
}
})?;
refs.push(selene_core::NodeTypeRef(node_type.name.clone()));
}
Ok(CoreEdgeEndpointDef::OneOf(refs))
}
}
}
fn core_node_properties(properties: &[PropertyTypeDef]) -> GraphResult<SmallVec<[PropertyDef; 8]>> {
let mut out = SmallVec::new();
for property in properties {
out.push(PropertyDef {
name: property.name.clone(),
value_type: core_value_type(
property.value_type,
property.list_element_type.as_ref(),
property.decimal_type,
property.character_string_type,
property.byte_string_type,
property.required,
)?,
nullable: !property.required,
default: property
.default
.as_ref()
.map(|default| default.to_value())
.transpose()?,
immutable: property.immutable,
unique: property.unique,
record_fields: core_record_fields(
property.value_type,
property.record_field_types.as_ref(),
)?,
});
}
Ok(out)
}
fn core_edge_properties(properties: &[PropertyTypeDef]) -> GraphResult<SmallVec<[PropertyDef; 4]>> {
let mut out = SmallVec::new();
for property in properties {
out.push(PropertyDef {
name: property.name.clone(),
value_type: core_value_type(
property.value_type,
property.list_element_type.as_ref(),
property.decimal_type,
property.character_string_type,
property.byte_string_type,
property.required,
)?,
nullable: !property.required,
default: property
.default
.as_ref()
.map(|default| default.to_value())
.transpose()?,
immutable: property.immutable,
unique: property.unique,
record_fields: core_record_fields(
property.value_type,
property.record_field_types.as_ref(),
)?,
});
}
Ok(out)
}
const fn core_validation_mode(mode: ValidationMode) -> selene_core::ValidationMode {
match mode {
ValidationMode::Strict => selene_core::ValidationMode::Strict,
ValidationMode::Warn => selene_core::ValidationMode::Warn,
}
}
fn core_value_type(
value_type: PropertyValueType,
list_element_type: Option<&PropertyElementType>,
decimal_type: Option<selene_core::DecimalType>,
character_string_type: Option<CharacterStringType>,
byte_string_type: Option<ByteStringType>,
required: bool,
) -> GraphResult<ValueType> {
let mut value_type = if value_type == PropertyValueType::List {
let element_type = list_element_type.ok_or_else(|| GraphError::Inconsistent {
reason: "LIST property definition is missing element type".to_owned(),
})?;
ValueType::list_of(core_element_value_type(element_type, 1)?)
} else {
core_scalar_value_type(
value_type,
decimal_type,
character_string_type,
byte_string_type,
)
};
value_type.not_null = required;
Ok(value_type)
}
fn core_element_value_type(
element_type: &PropertyElementType,
depth: u32,
) -> GraphResult<ValueType> {
if depth > MAX_LIST_TYPE_NESTING {
return Err(GraphError::Inconsistent {
reason: "LIST property definition exceeds nesting limit".to_owned(),
});
}
match element_type {
PropertyElementType::Scalar(value_type) => {
Ok(core_scalar_value_type(*value_type, None, None, None))
}
PropertyElementType::CharacterString(character_string_type) => Ok(core_scalar_value_type(
PropertyValueType::String,
None,
Some(*character_string_type),
None,
)),
PropertyElementType::Decimal(decimal_type) => Ok(core_scalar_value_type(
PropertyValueType::Decimal,
Some(*decimal_type),
None,
None,
)),
PropertyElementType::ByteString(byte_string_type) => Ok(core_scalar_value_type(
PropertyValueType::Bytes,
None,
None,
Some(*byte_string_type),
)),
PropertyElementType::List(inner) => Ok(ValueType::list_of(core_element_value_type(
inner,
depth + 1,
)?)),
PropertyElementType::NotNull(inner) => {
let mut value_type = core_element_value_type(inner, depth)?;
value_type.not_null = true;
Ok(value_type)
}
}
}
fn core_scalar_value_type(
value_type: PropertyValueType,
decimal_type: Option<selene_core::DecimalType>,
character_string_type: Option<CharacterStringType>,
byte_string_type: Option<ByteStringType>,
) -> ValueType {
let predefined = match value_type {
PropertyValueType::Bool => Some(PredefinedValueType::Bool),
PropertyValueType::Int => Some(PredefinedValueType::Int),
PropertyValueType::Uint => Some(PredefinedValueType::Uint),
PropertyValueType::Int128 => Some(PredefinedValueType::Int128),
PropertyValueType::Uint128 => Some(PredefinedValueType::Uint128),
PropertyValueType::Float => Some(PredefinedValueType::Float),
PropertyValueType::Float32 => Some(PredefinedValueType::Float32),
PropertyValueType::Decimal => Some(PredefinedValueType::Decimal),
PropertyValueType::String => Some(PredefinedValueType::String),
PropertyValueType::Bytes => Some(PredefinedValueType::Bytes),
PropertyValueType::Path => Some(PredefinedValueType::Path),
PropertyValueType::NodeRef => Some(PredefinedValueType::NodeRef),
PropertyValueType::EdgeRef => Some(PredefinedValueType::EdgeRef),
PropertyValueType::GraphRef => Some(PredefinedValueType::GraphRef),
PropertyValueType::TableRef => Some(PredefinedValueType::TableRef),
PropertyValueType::ZonedDateTime => Some(PredefinedValueType::ZonedDateTime),
PropertyValueType::LocalDateTime => Some(PredefinedValueType::LocalDateTime),
PropertyValueType::Date => Some(PredefinedValueType::Date),
PropertyValueType::ZonedTime => Some(PredefinedValueType::ZonedTime),
PropertyValueType::LocalTime => Some(PredefinedValueType::LocalTime),
PropertyValueType::Duration => Some(PredefinedValueType::Duration),
PropertyValueType::DurationYearToMonth => Some(PredefinedValueType::DurationYearToMonth),
PropertyValueType::DurationDayToSecond => Some(PredefinedValueType::DurationDayToSecond),
PropertyValueType::Uuid => Some(PredefinedValueType::Uuid),
PropertyValueType::Vector => Some(PredefinedValueType::Vector),
PropertyValueType::Json => Some(PredefinedValueType::Json),
PropertyValueType::List
| PropertyValueType::Record
| PropertyValueType::RecordTyped
| PropertyValueType::Null => None,
};
ValueType {
predefined,
decimal_type: if value_type == PropertyValueType::Decimal {
decimal_type
} else {
None
},
character_string_type: if value_type == PropertyValueType::String {
character_string_type
} else {
None
},
byte_string_type: if value_type == PropertyValueType::Bytes {
byte_string_type
} else {
None
},
union: None,
list_of: None,
record: None,
not_null: false,
cardinality: selene_core::ValueTypeCardinality::ExactlyOne,
}
}
fn core_record_fields(
value_type: PropertyValueType,
fields: Option<&RecordFieldTypes>,
) -> GraphResult<Option<Box<selene_core::RecordFieldStructure>>> {
match (value_type, fields) {
(PropertyValueType::RecordTyped, Some(fields)) => {
Ok(Some(Box::new(core_record_field_structure(fields, 1)?)))
}
(PropertyValueType::RecordTyped, None) => {
Ok(Some(Box::new(selene_core::RecordFieldStructure::Open)))
}
_ => Ok(None),
}
}
fn core_record_field_structure(
fields: &RecordFieldTypes,
depth: u32,
) -> GraphResult<selene_core::RecordFieldStructure> {
if depth > MAX_RECORD_TYPE_NESTING {
return Err(GraphError::Inconsistent {
reason: "RECORD property definition exceeds nesting limit".to_owned(),
});
}
let defs = fields
.0
.iter()
.map(|field| {
Ok(selene_core::RecordFieldStructureDef {
name: field.name.clone(),
field_type: core_record_field_structure_type(&field.field_type, depth)?,
required: field.required,
})
})
.collect::<GraphResult<Vec<_>>>()?;
Ok(selene_core::RecordFieldStructure::Closed(defs))
}
fn core_record_field_structure_type(
field_type: &RecordFieldType,
depth: u32,
) -> GraphResult<selene_core::RecordFieldStructureType> {
Ok(match field_type {
RecordFieldType::Scalar(value_type) => {
selene_core::RecordFieldStructureType::Scalar(*value_type)
}
RecordFieldType::CharacterString(character_string_type) => {
selene_core::RecordFieldStructureType::CharacterString(*character_string_type)
}
RecordFieldType::Decimal(decimal_type) => {
selene_core::RecordFieldStructureType::Decimal(*decimal_type)
}
RecordFieldType::ByteString(byte_string_type) => {
selene_core::RecordFieldStructureType::ByteString(*byte_string_type)
}
RecordFieldType::List(inner) => selene_core::RecordFieldStructureType::List(Box::new(
core_record_field_structure_type(inner, depth + 1)?,
)),
RecordFieldType::OpenRecord => selene_core::RecordFieldStructureType::Record(Box::new(
selene_core::RecordFieldStructure::Open,
)),
RecordFieldType::Record(inner) => selene_core::RecordFieldStructureType::Record(Box::new(
core_record_field_structure(inner, depth + 1)?,
)),
RecordFieldType::NotNull(inner) => selene_core::RecordFieldStructureType::NotNull(
Box::new(core_record_field_structure_type(inner, depth)?),
),
})
}
#[cfg(test)]
mod tests;