use std::fs;
use selene_core::{
Change, GraphId, GraphTypeId, LabelSet, PredefinedValueType, PropertyValueType,
RecordFieldStructure, RecordFieldStructureDef, RecordFieldStructureType, SchemaChange,
ValueType, db_string,
};
use smallvec::smallvec;
use crate::{
MAX_RECORD_TYPE_NESTING, PropertyTypeDef, RecordFieldType, RecordFieldTypeDef,
RecordFieldTypes, SharedGraph, ValidationMode,
};
use super::{append_wal, empty_closed_graph_type, temp_dir};
fn nested_record_field_types() -> RecordFieldTypes {
RecordFieldTypes(vec![
RecordFieldTypeDef {
name: db_string("a").unwrap(),
field_type: RecordFieldType::Scalar(PropertyValueType::Int),
required: true,
},
RecordFieldTypeDef {
name: db_string("b").unwrap(),
field_type: RecordFieldType::List(Box::new(RecordFieldType::NotNull(Box::new(
RecordFieldType::Scalar(PropertyValueType::String),
)))),
required: false,
},
RecordFieldTypeDef {
name: db_string("c").unwrap(),
field_type: RecordFieldType::Record(Box::new(RecordFieldTypes(vec![
RecordFieldTypeDef {
name: db_string("d").unwrap(),
field_type: RecordFieldType::NotNull(Box::new(RecordFieldType::Scalar(
PropertyValueType::Bool,
))),
required: true,
},
]))),
required: true,
},
RecordFieldTypeDef {
name: db_string("meta").unwrap(),
field_type: RecordFieldType::OpenRecord,
required: false,
},
])
}
#[test]
fn recover_closed_wal_only_preserves_closed_record_property() {
let dir = temp_dir("closed-schema-record-wal-only");
let graph_id = GraphId::new(31);
let base = empty_closed_graph_type();
let shared = SharedGraph::builder(graph_id)
.bound_to(base.clone())
.unwrap()
.build()
.unwrap();
let sensor = db_string("RecordSensor").unwrap();
let config = db_string("config").unwrap();
let field_types = nested_record_field_types();
let changes = {
let mut txn = shared.begin_write();
txn.mutator()
.create_node_type(
sensor.clone(),
LabelSet::single(sensor),
vec![PropertyTypeDef {
name: config.clone(),
value_type: PropertyValueType::RecordTyped,
list_element_type: None,
required: false,
default: None,
immutable: false,
unique: false,
decimal_type: None,
character_string_type: None,
byte_string_type: None,
record_field_types: Some(field_types.clone()),
}],
ValidationMode::Strict,
)
.unwrap();
txn.commit().unwrap().changes
};
append_wal(&dir, 0, &changes);
let recovered = SharedGraph::recover_closed(&dir, graph_id, base).unwrap();
let graph_type = recovered.graph_type().unwrap();
let property = &graph_type.node_types[0].properties[0];
assert_eq!(property.name, config);
assert_eq!(property.value_type, PropertyValueType::RecordTyped);
assert_eq!(property.record_field_types, Some(field_types));
let _ = fs::remove_dir_all(dir);
}
#[test]
fn recover_closed_wal_only_preserves_open_record_property() {
let dir = temp_dir("closed-schema-open-record-wal-only");
let graph_id = GraphId::new(32);
let base = empty_closed_graph_type();
let shared = SharedGraph::builder(graph_id)
.bound_to(base.clone())
.unwrap()
.build()
.unwrap();
let sensor = db_string("OpenRecordSensor").unwrap();
let payload = db_string("payload").unwrap();
let changes = {
let mut txn = shared.begin_write();
txn.mutator()
.create_node_type(
sensor.clone(),
LabelSet::single(sensor),
vec![PropertyTypeDef {
name: payload.clone(),
value_type: PropertyValueType::RecordTyped,
list_element_type: None,
required: false,
default: None,
immutable: false,
unique: false,
decimal_type: None,
character_string_type: None,
byte_string_type: None,
record_field_types: None,
}],
ValidationMode::Strict,
)
.unwrap();
txn.commit().unwrap().changes
};
append_wal(&dir, 0, &changes);
let recovered = SharedGraph::recover_closed(&dir, graph_id, base).unwrap();
let graph_type = recovered.graph_type().unwrap();
let property = &graph_type.node_types[0].properties[0];
assert_eq!(property.name, payload);
assert_eq!(property.value_type, PropertyValueType::RecordTyped);
assert_eq!(property.record_field_types, None);
let _ = fs::remove_dir_all(dir);
}
fn deep_record_structure(levels: u32) -> RecordFieldStructure {
let mut structure = RecordFieldStructure::Closed(vec![RecordFieldStructureDef {
name: db_string("leaf").unwrap(),
field_type: RecordFieldStructureType::Scalar(PropertyValueType::Bool),
required: true,
}]);
for _ in 0..levels {
structure = RecordFieldStructure::Closed(vec![RecordFieldStructureDef {
name: db_string("nest").unwrap(),
field_type: RecordFieldStructureType::Record(Box::new(structure)),
required: true,
}]);
}
structure
}
#[test]
fn recover_closed_rejects_overdeep_record_property() {
let dir = temp_dir("closed-schema-record-depth");
let graph_id = GraphId::new(33);
let base = empty_closed_graph_type();
let graph_type = GraphTypeId::new(1).unwrap();
let sensor = db_string("DeepRecordSensor").unwrap();
let value_type = ValueType::predefined(PredefinedValueType::String);
let record_fields = Some(Box::new(deep_record_structure(MAX_RECORD_TYPE_NESTING + 1)));
append_wal(
&dir,
0,
&[Change::SchemaChanged {
graph: graph_id,
change: SchemaChange::NodeTypeAddedV2 {
graph_type,
label: sensor.clone(),
def: selene_core::NodeTypeDef {
labels: LabelSet::single(sensor),
properties: smallvec![selene_core::PropertyDef {
name: db_string("too_deep").unwrap(),
value_type,
nullable: true,
default: None,
immutable: false,
unique: false,
record_fields,
}],
key: None,
validation_mode: selene_core::ValidationMode::Strict,
},
},
}],
);
let err = match SharedGraph::recover_closed(&dir, graph_id, base) {
Ok(_) => panic!("overdeep RECORD property recovery should fail"),
Err(err) => err,
};
assert!(format!("{err}").contains("RECORD nesting limit"));
let _ = fs::remove_dir_all(dir);
}