use crate::{
types::{Entity, EntityId, EntityProfile, Memory, MemoryId, Timestamp},
Error, Result,
};
use redb::{Database, ReadableTable, ReadableTableMetadata, TableDefinition};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use super::FileHeader;
const MEMORIES: TableDefinition<&[u8], &[u8]> = TableDefinition::new("memories");
pub(crate) const TEMPORAL_INDEX: TableDefinition<u64, &[u8]> =
TableDefinition::new("temporal_index");
const METADATA_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("metadata");
const MEMORY_ID_INDEX: TableDefinition<u64, &[u8]> = TableDefinition::new("memory_id_index");
const CAUSAL_GRAPH: TableDefinition<&str, &[u8]> = TableDefinition::new("causal_graph");
const ENTITIES: TableDefinition<&[u8], &[u8]> = TableDefinition::new("entities");
const ENTITY_NAMES: TableDefinition<&str, &[u8]> = TableDefinition::new("entity_names");
const CONTENT_HASH_INDEX: TableDefinition<&str, &[u8]> = TableDefinition::new("content_hash_index");
const LOGICAL_KEY_INDEX: TableDefinition<&str, &[u8]> = TableDefinition::new("logical_key_index");
const METADATA_INDEX: TableDefinition<&str, &[u8]> = TableDefinition::new("metadata_index");
const ENTITY_PROFILES: TableDefinition<&str, &[u8]> = TableDefinition::new("entity_profiles");
const FACT_EMBEDDINGS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("fact_embeddings");
pub struct StorageEngine {
db: Database,
path: PathBuf,
}
impl StorageEngine {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
if path.exists() {
let metadata = std::fs::metadata(path)?;
let file_size = metadata.len();
const MIN_FILE_SIZE: u64 = 512;
if file_size < MIN_FILE_SIZE {
return Err(Error::FileTruncated(format!(
"File size ({} bytes) is too small to be a valid database",
file_size
)));
}
}
let db = Database::create(path)?;
let mut engine = Self {
db,
path: path.to_path_buf(),
};
engine.init_tables()?;
engine.init_header()?;
if path.exists() {
engine.validate_database()?;
}
Ok(engine)
}
fn init_tables(&self) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let _ = write_txn.open_table(MEMORIES)?;
let _ = write_txn.open_table(TEMPORAL_INDEX)?;
let _ = write_txn.open_table(METADATA_TABLE)?;
let _ = write_txn.open_table(MEMORY_ID_INDEX)?;
let _ = write_txn.open_table(CAUSAL_GRAPH)?;
let _ = write_txn.open_table(ENTITIES)?;
let _ = write_txn.open_table(ENTITY_NAMES)?;
let _ = write_txn.open_table(CONTENT_HASH_INDEX)?;
let _ = write_txn.open_table(LOGICAL_KEY_INDEX)?;
let _ = write_txn.open_table(METADATA_INDEX)?;
let _ = write_txn.open_table(ENTITY_PROFILES)?;
let _ = write_txn.open_table(FACT_EMBEDDINGS)?;
}
write_txn.commit()?;
Ok(())
}
fn init_header(&mut self) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(METADATA_TABLE)?;
let header_exists = table.get("file_header")?.is_some();
if header_exists {
let existing = table.get("file_header")?.unwrap();
let existing_bytes = existing.value().to_vec();
let header = FileHeader::from_bytes(&existing_bytes)?;
header.validate()?;
} else {
let header = FileHeader::new();
table.insert("file_header", header.to_bytes().as_slice())?;
}
}
write_txn.commit()?;
Ok(())
}
fn validate_database(&self) -> Result<()> {
let read_txn = self.db.begin_read()?;
if let Err(e) = read_txn.open_table(MEMORIES) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'memories' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(TEMPORAL_INDEX) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'temporal_index' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(METADATA_TABLE) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'metadata' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(MEMORY_ID_INDEX) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'memory_id_index' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(CAUSAL_GRAPH) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'causal_graph' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(ENTITIES) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'entities' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(ENTITY_NAMES) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'entity_names' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(CONTENT_HASH_INDEX) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'content_hash_index' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(LOGICAL_KEY_INDEX) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'logical_key_index' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(METADATA_INDEX) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'metadata_index' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(ENTITY_PROFILES) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'entity_profiles' is missing or corrupt: {}",
e
)));
}
if let Err(e) = read_txn.open_table(FACT_EMBEDDINGS) {
return Err(Error::DatabaseCorruption(format!(
"Required table 'fact_embeddings' is missing or corrupt: {}",
e
)));
}
let metadata = read_txn.open_table(METADATA_TABLE)?;
match metadata.get("file_header")? {
Some(header_bytes) => {
let header = FileHeader::from_bytes(header_bytes.value())?;
header.validate()?;
}
None => {
return Err(Error::DatabaseCorruption(
"File header is missing".to_string(),
));
}
}
Ok(())
}
pub fn store_memory(&self, memory: &Memory) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut memories = write_txn.open_table(MEMORIES)?;
let mut temporal = write_txn.open_table(TEMPORAL_INDEX)?;
let mut id_index = write_txn.open_table(MEMORY_ID_INDEX)?;
let memory_data = self.serialize_memory(memory)?;
memories.insert(memory.id.as_bytes().as_slice(), memory_data.as_slice())?;
temporal.insert(
memory.created_at.as_micros(),
memory.id.as_bytes().as_slice(),
)?;
id_index.insert(memory.id.to_u64(), memory.id.as_bytes().as_slice())?;
}
write_txn.commit()?;
Ok(())
}
pub fn get_memory(&self, id: &MemoryId) -> Result<Option<Memory>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(MEMORIES)?;
match table.get(id.as_bytes().as_slice())? {
Some(data) => {
let memory = self.deserialize_memory(data.value())?;
Ok(Some(memory))
}
None => Ok(None),
}
}
pub fn get_memory_by_u64(&self, key: u64) -> Result<Option<Memory>> {
let read_txn = self.db.begin_read()?;
let id_index = read_txn.open_table(MEMORY_ID_INDEX)?;
let memories = read_txn.open_table(MEMORIES)?;
match id_index.get(key)? {
Some(id_bytes) => {
match memories.get(id_bytes.value())? {
Some(data) => {
let memory = self.deserialize_memory(data.value())?;
Ok(Some(memory))
}
None => Ok(None),
}
}
None => Ok(None),
}
}
pub fn delete_memory(&self, id: &MemoryId) -> Result<bool> {
let write_txn = self.db.begin_write()?;
let removed = {
let mut memories = write_txn.open_table(MEMORIES)?;
let mut id_index = write_txn.open_table(MEMORY_ID_INDEX)?;
let result = memories.remove(id.as_bytes().as_slice())?;
if result.is_some() {
id_index.remove(id.to_u64())?;
}
result.is_some()
};
write_txn.commit()?;
Ok(removed)
}
pub fn list_memory_ids(&self) -> Result<Vec<MemoryId>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(MEMORIES)?;
let mut ids = Vec::new();
for item in table.iter()? {
let (key, _) = item?;
let id = MemoryId::from_bytes(key.value())?;
ids.push(id);
}
Ok(ids)
}
pub fn count_memories(&self) -> Result<usize> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(MEMORIES)?;
Ok(table.len()? as usize)
}
pub fn find_memory_by_dialog_id(&self, dialog_id: &str) -> Result<Option<Memory>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(MEMORIES)?;
for item in table.iter()? {
let (_, val) = item?;
let memory = self.deserialize_memory(val.value())?;
if memory.metadata.get("dialog_id").map(|s| s.as_str()) == Some(dialog_id) {
return Ok(Some(memory));
}
}
Ok(None)
}
fn serialize_memory(&self, memory: &Memory) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
bytes.extend_from_slice(memory.id.as_bytes());
bytes.extend_from_slice(&memory.created_at.to_bytes());
let content_bytes = memory.content.as_bytes();
bytes.extend_from_slice(&(content_bytes.len() as u32).to_le_bytes());
bytes.extend_from_slice(content_bytes);
bytes.extend_from_slice(&(memory.embedding.len() as u32).to_le_bytes());
for val in &memory.embedding {
bytes.extend_from_slice(&val.to_le_bytes());
}
let metadata_str = serde_json::to_string(&memory.metadata)
.map_err(|e| Error::Serialization(e.to_string()))?;
let metadata_bytes = metadata_str.as_bytes();
bytes.extend_from_slice(&(metadata_bytes.len() as u32).to_le_bytes());
bytes.extend_from_slice(metadata_bytes);
Ok(bytes)
}
fn deserialize_memory(&self, bytes: &[u8]) -> Result<Memory> {
let mut offset = 0;
if bytes.len() < offset + 16 {
return Err(Error::Deserialization("Incomplete memory data".to_string()));
}
let id = MemoryId::from_bytes(&bytes[offset..offset + 16])?;
offset += 16;
if bytes.len() < offset + 8 {
return Err(Error::Deserialization(
"Incomplete timestamp data".to_string(),
));
}
let created_at = Timestamp::from_bytes(&bytes[offset..offset + 8])?;
offset += 8;
if bytes.len() < offset + 4 {
return Err(Error::Deserialization(
"Incomplete content length".to_string(),
));
}
let content_len = u32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
]) as usize;
offset += 4;
if bytes.len() < offset + content_len {
return Err(Error::Deserialization(
"Incomplete content data".to_string(),
));
}
let content = String::from_utf8(bytes[offset..offset + content_len].to_vec())
.map_err(|e| Error::Deserialization(e.to_string()))?;
offset += content_len;
if bytes.len() < offset + 4 {
return Err(Error::Deserialization(
"Incomplete embedding length".to_string(),
));
}
let embedding_len = u32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
]) as usize;
offset += 4;
if bytes.len() < offset + embedding_len * 4 {
return Err(Error::Deserialization(
"Incomplete embedding data".to_string(),
));
}
let mut embedding = Vec::with_capacity(embedding_len);
for _ in 0..embedding_len {
let val = f32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
]);
embedding.push(val);
offset += 4;
}
if bytes.len() < offset + 4 {
return Err(Error::Deserialization(
"Incomplete metadata length".to_string(),
));
}
let metadata_len = u32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
]) as usize;
offset += 4;
if bytes.len() < offset + metadata_len {
return Err(Error::Deserialization(
"Incomplete metadata data".to_string(),
));
}
let metadata_str = String::from_utf8(bytes[offset..offset + metadata_len].to_vec())
.map_err(|e| Error::Deserialization(e.to_string()))?;
let metadata: HashMap<String, String> = serde_json::from_str(&metadata_str)
.map_err(|e| Error::Deserialization(e.to_string()))?;
Ok(Memory {
id,
content,
embedding,
created_at,
metadata,
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub(crate) fn db(&self) -> &Database {
&self.db
}
pub fn store_vector_index(&self, buffer: &[u8]) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(METADATA_TABLE)?;
table.insert("vector_index", buffer)?;
}
write_txn.commit()?;
Ok(())
}
pub fn load_vector_index(&self) -> Result<Option<Vec<u8>>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(METADATA_TABLE)?;
match table.get("vector_index")? {
Some(data) => Ok(Some(data.value().to_vec())),
None => Ok(None),
}
}
pub fn store_bm25_index(&self, buffer: &[u8]) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(METADATA_TABLE)?;
table.insert("bm25_index", buffer)?;
}
write_txn.commit()?;
Ok(())
}
pub fn load_bm25_index(&self) -> Result<Option<Vec<u8>>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(METADATA_TABLE)?;
match table.get("bm25_index")? {
Some(data) => Ok(Some(data.value().to_vec())),
None => Ok(None),
}
}
pub fn store_causal_graph(&self, data: &[u8]) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(CAUSAL_GRAPH)?;
table.insert("graph", data)?;
}
write_txn.commit()?;
Ok(())
}
pub fn load_causal_graph(&self) -> Result<Option<Vec<u8>>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(CAUSAL_GRAPH)?;
match table.get("graph")? {
Some(data) => Ok(Some(data.value().to_vec())),
None => Ok(None),
}
}
pub fn store_entity(&self, entity: &Entity) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut entities = write_txn.open_table(ENTITIES)?;
let mut names = write_txn.open_table(ENTITY_NAMES)?;
let entity_data =
serde_json::to_vec(entity).map_err(|e| Error::Serialization(e.to_string()))?;
entities.insert(entity.id.as_bytes().as_slice(), entity_data.as_slice())?;
let normalized_name = entity.normalized_name();
names.insert(normalized_name.as_str(), entity.id.as_bytes().as_slice())?;
}
write_txn.commit()?;
Ok(())
}
pub fn get_entity(&self, id: &EntityId) -> Result<Option<Entity>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(ENTITIES)?;
match table.get(id.as_bytes().as_slice())? {
Some(data) => {
let entity: Entity = serde_json::from_slice(data.value())
.map_err(|e| Error::Deserialization(e.to_string()))?;
Ok(Some(entity))
}
None => Ok(None),
}
}
pub fn find_entity_by_name(&self, name: &str) -> Result<Option<Entity>> {
let read_txn = self.db.begin_read()?;
let names_table = read_txn.open_table(ENTITY_NAMES)?;
let entities_table = read_txn.open_table(ENTITIES)?;
let normalized = name.to_lowercase();
match names_table.get(normalized.as_str())? {
Some(id_bytes) => {
let id_bytes = id_bytes.value().to_vec();
let entity_id = EntityId::from_bytes(&id_bytes)?;
match entities_table.get(entity_id.as_bytes().as_slice())? {
Some(data) => {
let entity: Entity = serde_json::from_slice(data.value())
.map_err(|e| Error::Deserialization(e.to_string()))?;
Ok(Some(entity))
}
None => Ok(None),
}
}
None => Ok(None),
}
}
pub fn delete_entity(&self, id: &EntityId) -> Result<bool> {
let write_txn = self.db.begin_write()?;
let deleted = {
let mut entities = write_txn.open_table(ENTITIES)?;
let mut names = write_txn.open_table(ENTITY_NAMES)?;
let normalized_name = if let Some(data) = entities.get(id.as_bytes().as_slice())? {
let entity: Entity = serde_json::from_slice(data.value())
.map_err(|e| Error::Deserialization(e.to_string()))?;
Some(entity.normalized_name())
} else {
None
};
if let Some(name) = normalized_name {
names.remove(name.as_str())?;
entities.remove(id.as_bytes().as_slice())?;
true
} else {
false
}
};
write_txn.commit()?;
Ok(deleted)
}
pub fn list_entities(&self) -> Result<Vec<Entity>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(ENTITIES)?;
let mut entities = Vec::new();
for result in table.iter()? {
let (_, value) = result?;
let entity: Entity = serde_json::from_slice(value.value())
.map_err(|e| Error::Deserialization(e.to_string()))?;
entities.push(entity);
}
Ok(entities)
}
pub fn count_entities(&self) -> Result<usize> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(ENTITIES)?;
Ok(table.len()? as usize)
}
pub fn store_entity_graph(&self, data: &[u8]) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(METADATA_TABLE)?;
table.insert("entity_graph", data)?;
}
write_txn.commit()?;
Ok(())
}
pub fn load_entity_graph(&self) -> Result<Option<Vec<u8>>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(METADATA_TABLE)?;
match table.get("entity_graph")? {
Some(data) => Ok(Some(data.value().to_vec())),
None => Ok(None),
}
}
pub fn store_relationship_graph(&self, data: &[u8]) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(METADATA_TABLE)?;
table.insert("relationship_graph", data)?;
}
write_txn.commit()?;
Ok(())
}
pub fn load_relationship_graph(&self) -> Result<Option<Vec<u8>>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(METADATA_TABLE)?;
match table.get("relationship_graph")? {
Some(data) => Ok(Some(data.value().to_vec())),
None => Ok(None),
}
}
pub fn has_relationship_graph(&self) -> Result<bool> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(METADATA_TABLE)?;
Ok(table.get("relationship_graph")?.is_some())
}
pub fn store_content_hash(&self, hash: &str, memory_id: &MemoryId) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(CONTENT_HASH_INDEX)?;
table.insert(hash, memory_id.as_bytes() as &[u8])?;
}
write_txn.commit()?;
Ok(())
}
pub fn find_by_content_hash(&self, hash: &str) -> Result<Option<MemoryId>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(CONTENT_HASH_INDEX)?;
match table.get(hash)? {
Some(bytes) => {
let id = MemoryId::from_bytes(bytes.value())?;
Ok(Some(id))
}
None => Ok(None),
}
}
pub fn delete_content_hash(&self, hash: &str) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(CONTENT_HASH_INDEX)?;
table.remove(hash)?;
}
write_txn.commit()?;
Ok(())
}
pub fn store_logical_key(&self, key: &str, memory_id: &MemoryId) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(LOGICAL_KEY_INDEX)?;
table.insert(key, memory_id.as_bytes() as &[u8])?;
}
write_txn.commit()?;
Ok(())
}
pub fn find_by_logical_key(&self, key: &str) -> Result<Option<MemoryId>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(LOGICAL_KEY_INDEX)?;
match table.get(key)? {
Some(bytes) => {
let id = MemoryId::from_bytes(bytes.value())?;
Ok(Some(id))
}
None => Ok(None),
}
}
pub fn delete_logical_key(&self, key: &str) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(LOGICAL_KEY_INDEX)?;
table.remove(key)?;
}
write_txn.commit()?;
Ok(())
}
pub fn update_logical_key(&self, key: &str, new_memory_id: &MemoryId) -> Result<()> {
self.store_logical_key(key, new_memory_id)
}
pub fn list_namespaces(&self) -> Result<Vec<String>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(MEMORIES)?;
let mut namespaces = std::collections::HashSet::new();
for entry in table.iter()? {
let (_, value) = entry?;
let memory_data = value.value();
if let Ok(memory) = self.deserialize_memory(memory_data) {
let ns = memory.get_namespace();
if !ns.is_empty() {
namespaces.insert(ns);
}
}
}
let mut result: Vec<String> = namespaces.into_iter().collect();
result.sort();
Ok(result)
}
pub fn count_namespace(&self, namespace: &str) -> Result<usize> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(MEMORIES)?;
let mut count = 0;
for entry in table.iter()? {
let (_, value) = entry?;
let memory_data = value.value();
if let Ok(memory) = self.deserialize_memory(memory_data) {
if memory.get_namespace() == namespace {
count += 1;
}
}
}
Ok(count)
}
pub fn list_namespace_ids(&self, namespace: &str) -> Result<Vec<MemoryId>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(MEMORIES)?;
let mut ids = Vec::new();
for entry in table.iter()? {
let (key, value) = entry?;
let memory_data = value.value();
if let Ok(memory) = self.deserialize_memory(memory_data) {
if memory.get_namespace() == namespace {
let id = MemoryId::from_bytes(key.value())?;
ids.push(id);
}
}
}
Ok(ids)
}
fn metadata_index_key(field: &str, value: &str, namespace: &str) -> String {
format!("{}:{}:{}", field, value, namespace)
}
pub fn add_to_metadata_index(
&self,
field: &str,
value: &str,
namespace: &str,
memory_id: &MemoryId,
) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(METADATA_INDEX)?;
let key = Self::metadata_index_key(field, value, namespace);
let mut ids: Vec<MemoryId> = match table.get(key.as_str())? {
Some(data) => serde_json::from_slice(data.value())?,
None => Vec::new(),
};
if !ids.contains(memory_id) {
ids.push(memory_id.clone());
let data = serde_json::to_vec(&ids)?;
table.insert(key.as_str(), data.as_slice())?;
}
}
write_txn.commit()?;
Ok(())
}
pub fn remove_from_metadata_index(
&self,
field: &str,
value: &str,
namespace: &str,
memory_id: &MemoryId,
) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(METADATA_INDEX)?;
let key = Self::metadata_index_key(field, value, namespace);
let ids_data = table.get(key.as_str())?.map(|data| data.value().to_vec());
if let Some(data_vec) = ids_data {
let mut ids: Vec<MemoryId> = serde_json::from_slice(&data_vec)?;
ids.retain(|id| id != memory_id);
if ids.is_empty() {
table.remove(key.as_str())?;
} else {
let data = serde_json::to_vec(&ids)?;
table.insert(key.as_str(), data.as_slice())?;
}
}
}
write_txn.commit()?;
Ok(())
}
pub fn find_by_metadata(
&self,
field: &str,
value: &str,
namespace: &str,
) -> Result<Vec<MemoryId>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(METADATA_INDEX)?;
let key = Self::metadata_index_key(field, value, namespace);
match table.get(key.as_str())? {
Some(data) => {
let ids: Vec<MemoryId> = serde_json::from_slice(data.value())?;
Ok(ids)
}
None => Ok(Vec::new()),
}
}
pub fn remove_metadata_indexes_for_memory(
&self,
memory: &Memory,
indexed_fields: &[String],
) -> Result<()> {
let namespace = memory.get_namespace();
for field in indexed_fields {
if let Some(value) = memory.metadata.get(field) {
self.remove_from_metadata_index(field, value, &namespace, &memory.id)?;
}
}
Ok(())
}
pub fn store_entity_profile(&self, profile: &EntityProfile) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(ENTITY_PROFILES)?;
let key = profile.name.to_lowercase();
let data =
serde_json::to_vec(profile).map_err(|e| Error::Serialization(e.to_string()))?;
table.insert(key.as_str(), data.as_slice())?;
}
write_txn.commit()?;
Ok(())
}
pub fn get_entity_profile(&self, name: &str) -> Result<Option<EntityProfile>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(ENTITY_PROFILES)?;
let key = name.to_lowercase();
match table.get(key.as_str())? {
Some(data) => {
let profile: EntityProfile = serde_json::from_slice(data.value())
.map_err(|e| Error::Deserialization(e.to_string()))?;
Ok(Some(profile))
}
None => Ok(None),
}
}
pub fn list_entity_profiles(&self) -> Result<Vec<EntityProfile>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(ENTITY_PROFILES)?;
let mut profiles = Vec::new();
for result in table.iter()? {
let (_, value) = result?;
let profile: EntityProfile = serde_json::from_slice(value.value())
.map_err(|e| Error::Deserialization(e.to_string()))?;
profiles.push(profile);
}
Ok(profiles)
}
pub fn delete_entity_profile(&self, name: &str) -> Result<bool> {
let write_txn = self.db.begin_write()?;
let deleted = {
let mut table = write_txn.open_table(ENTITY_PROFILES)?;
let key = name.to_lowercase();
let result = table.remove(key.as_str())?;
result.is_some()
};
write_txn.commit()?;
Ok(deleted)
}
pub fn count_entity_profiles(&self) -> Result<usize> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(ENTITY_PROFILES)?;
Ok(table.len()? as usize)
}
pub fn list_entity_profile_names(&self) -> Result<Vec<String>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(ENTITY_PROFILES)?;
let mut names = Vec::new();
for result in table.iter()? {
let (key, _) = result?;
names.push(key.value().to_string());
}
Ok(names)
}
pub fn store_fact_embedding(&self, key: &[u8], embedding: &[f32]) -> Result<()> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(FACT_EMBEDDINGS)?;
let bytes: Vec<u8> = embedding.iter().flat_map(|f| f.to_le_bytes()).collect();
table.insert(key, bytes.as_slice())?;
}
write_txn.commit()?;
Ok(())
}
pub fn get_fact_embedding(&self, key: &[u8]) -> Result<Option<Vec<f32>>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(FACT_EMBEDDINGS)?;
match table.get(key)? {
Some(data) => {
let bytes = data.value();
if bytes.len() % 4 != 0 {
return Err(Error::Deserialization(
"Invalid fact embedding data length".to_string(),
));
}
let embedding: Vec<f32> = bytes
.chunks_exact(4)
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
Ok(Some(embedding))
}
None => Ok(None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_storage_engine_open() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
assert_eq!(engine.path(), path);
}
#[test]
fn test_storage_engine_store_and_retrieve() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let memory = Memory::new("test content".to_string(), vec![0.1, 0.2, 0.3]);
let id = memory.id.clone();
engine.store_memory(&memory).unwrap();
let retrieved = engine.get_memory(&id).unwrap();
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.id, id);
assert_eq!(retrieved.content, "test content");
assert_eq!(retrieved.embedding, vec![0.1, 0.2, 0.3]);
}
#[test]
fn test_storage_engine_delete() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let memory = Memory::new("test".to_string(), vec![0.1]);
let id = memory.id.clone();
engine.store_memory(&memory).unwrap();
assert!(engine.get_memory(&id).unwrap().is_some());
let deleted = engine.delete_memory(&id).unwrap();
assert!(deleted);
assert!(engine.get_memory(&id).unwrap().is_none());
}
#[test]
fn test_storage_engine_not_found() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let id = MemoryId::new();
assert!(engine.get_memory(&id).unwrap().is_none());
}
#[test]
fn test_storage_engine_multiple_memories() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mem1 = Memory::new("first".to_string(), vec![0.1]);
let mem2 = Memory::new("second".to_string(), vec![0.2]);
let mem3 = Memory::new("third".to_string(), vec![0.3]);
engine.store_memory(&mem1).unwrap();
engine.store_memory(&mem2).unwrap();
engine.store_memory(&mem3).unwrap();
assert_eq!(engine.count_memories().unwrap(), 3);
let ids = engine.list_memory_ids().unwrap();
assert_eq!(ids.len(), 3);
}
#[test]
fn test_storage_engine_with_metadata() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mut metadata = HashMap::new();
metadata.insert("source".to_string(), "test".to_string());
metadata.insert("category".to_string(), "example".to_string());
let memory = Memory::new_with_metadata("test".to_string(), vec![0.1], metadata);
let id = memory.id.clone();
engine.store_memory(&memory).unwrap();
let retrieved = engine.get_memory(&id).unwrap().unwrap();
assert_eq!(retrieved.metadata.len(), 2);
assert_eq!(retrieved.metadata.get("source"), Some(&"test".to_string()));
assert_eq!(
retrieved.metadata.get("category"),
Some(&"example".to_string())
);
}
#[test]
fn test_storage_engine_reopen() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let memory = Memory::new("persistent".to_string(), vec![0.5]);
let id = memory.id.clone();
{
let engine = StorageEngine::open(&path).unwrap();
engine.store_memory(&memory).unwrap();
}
{
let engine = StorageEngine::open(&path).unwrap();
let retrieved = engine.get_memory(&id).unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().content, "persistent");
}
}
#[test]
fn test_storage_list_namespaces_empty() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let namespaces = engine.list_namespaces().unwrap();
assert!(namespaces.is_empty());
}
#[test]
fn test_storage_list_namespaces() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mut mem1 = Memory::new("content 1".to_string(), vec![0.1; 384]);
mem1.set_namespace("user_123");
engine.store_memory(&mem1).unwrap();
let mut mem2 = Memory::new("content 2".to_string(), vec![0.2; 384]);
mem2.set_namespace("user_456");
engine.store_memory(&mem2).unwrap();
let mut mem3 = Memory::new("content 3".to_string(), vec![0.3; 384]);
mem3.set_namespace("user_123"); engine.store_memory(&mem3).unwrap();
let mem4 = Memory::new("content 4".to_string(), vec![0.4; 384]);
engine.store_memory(&mem4).unwrap();
let namespaces = engine.list_namespaces().unwrap();
assert_eq!(namespaces.len(), 2);
assert_eq!(namespaces[0], "user_123");
assert_eq!(namespaces[1], "user_456");
}
#[test]
fn test_storage_count_namespace() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mut mem1 = Memory::new("content 1".to_string(), vec![0.1; 384]);
mem1.set_namespace("user_123");
engine.store_memory(&mem1).unwrap();
let mut mem2 = Memory::new("content 2".to_string(), vec![0.2; 384]);
mem2.set_namespace("user_123");
engine.store_memory(&mem2).unwrap();
let mut mem3 = Memory::new("content 3".to_string(), vec![0.3; 384]);
mem3.set_namespace("user_456");
engine.store_memory(&mem3).unwrap();
let mem4 = Memory::new("content 4".to_string(), vec![0.4; 384]);
engine.store_memory(&mem4).unwrap();
assert_eq!(engine.count_namespace("user_123").unwrap(), 2);
assert_eq!(engine.count_namespace("user_456").unwrap(), 1);
assert_eq!(engine.count_namespace("").unwrap(), 1); assert_eq!(engine.count_namespace("nonexistent").unwrap(), 0);
}
#[test]
fn test_storage_list_namespace_ids() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mut mem1 = Memory::new("content 1".to_string(), vec![0.1; 384]);
mem1.set_namespace("user_123");
let id1 = mem1.id.clone();
engine.store_memory(&mem1).unwrap();
let mut mem2 = Memory::new("content 2".to_string(), vec![0.2; 384]);
mem2.set_namespace("user_123");
let id2 = mem2.id.clone();
engine.store_memory(&mem2).unwrap();
let mut mem3 = Memory::new("content 3".to_string(), vec![0.3; 384]);
mem3.set_namespace("user_456");
engine.store_memory(&mem3).unwrap();
let ids = engine.list_namespace_ids("user_123").unwrap();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&id1));
assert!(ids.contains(&id2));
let ids_456 = engine.list_namespace_ids("user_456").unwrap();
assert_eq!(ids_456.len(), 1);
let ids_empty = engine.list_namespace_ids("nonexistent").unwrap();
assert!(ids_empty.is_empty());
}
#[test]
fn test_storage_namespace_default() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mem = Memory::new("default content".to_string(), vec![0.1; 384]);
let id = mem.id.clone();
engine.store_memory(&mem).unwrap();
assert_eq!(engine.count_namespace("").unwrap(), 1);
let ids = engine.list_namespace_ids("").unwrap();
assert_eq!(ids.len(), 1);
assert_eq!(ids[0], id);
let namespaces = engine.list_namespaces().unwrap();
assert!(namespaces.is_empty());
}
#[test]
fn test_metadata_index_add_and_find() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mut mem = Memory::new("test content".to_string(), vec![0.1; 384]);
mem.metadata.insert("type".to_string(), "event".to_string());
mem.metadata
.insert("priority".to_string(), "high".to_string());
let id = mem.id.clone();
engine
.add_to_metadata_index("type", "event", "", &id)
.unwrap();
engine
.add_to_metadata_index("priority", "high", "", &id)
.unwrap();
let ids = engine.find_by_metadata("type", "event", "").unwrap();
assert_eq!(ids.len(), 1);
assert_eq!(ids[0], id);
let ids = engine.find_by_metadata("priority", "high", "").unwrap();
assert_eq!(ids.len(), 1);
assert_eq!(ids[0], id);
let ids = engine.find_by_metadata("type", "task", "").unwrap();
assert!(ids.is_empty());
}
#[test]
fn test_metadata_index_multiple_memories() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mut mem1 = Memory::new("content 1".to_string(), vec![0.1; 384]);
mem1.metadata
.insert("type".to_string(), "event".to_string());
let id1 = mem1.id.clone();
let mut mem2 = Memory::new("content 2".to_string(), vec![0.2; 384]);
mem2.metadata
.insert("type".to_string(), "event".to_string());
let id2 = mem2.id.clone();
let mut mem3 = Memory::new("content 3".to_string(), vec![0.3; 384]);
mem3.metadata.insert("type".to_string(), "task".to_string());
let id3 = mem3.id.clone();
engine
.add_to_metadata_index("type", "event", "", &id1)
.unwrap();
engine
.add_to_metadata_index("type", "event", "", &id2)
.unwrap();
engine
.add_to_metadata_index("type", "task", "", &id3)
.unwrap();
let ids = engine.find_by_metadata("type", "event", "").unwrap();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&id1));
assert!(ids.contains(&id2));
let ids = engine.find_by_metadata("type", "task", "").unwrap();
assert_eq!(ids.len(), 1);
assert_eq!(ids[0], id3);
}
#[test]
fn test_metadata_index_with_namespace() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mut mem1 = Memory::new("content 1".to_string(), vec![0.1; 384]);
mem1.set_namespace("user_123");
mem1.metadata
.insert("type".to_string(), "event".to_string());
let id1 = mem1.id.clone();
let mut mem2 = Memory::new("content 2".to_string(), vec![0.2; 384]);
mem2.set_namespace("user_456");
mem2.metadata
.insert("type".to_string(), "event".to_string());
let id2 = mem2.id.clone();
engine
.add_to_metadata_index("type", "event", "user_123", &id1)
.unwrap();
engine
.add_to_metadata_index("type", "event", "user_456", &id2)
.unwrap();
let ids = engine
.find_by_metadata("type", "event", "user_123")
.unwrap();
assert_eq!(ids.len(), 1);
assert_eq!(ids[0], id1);
let ids = engine
.find_by_metadata("type", "event", "user_456")
.unwrap();
assert_eq!(ids.len(), 1);
assert_eq!(ids[0], id2);
let ids = engine.find_by_metadata("type", "event", "").unwrap();
assert!(ids.is_empty());
}
#[test]
fn test_metadata_index_remove() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mut mem = Memory::new("test content".to_string(), vec![0.1; 384]);
mem.metadata.insert("type".to_string(), "event".to_string());
let id = mem.id.clone();
engine
.add_to_metadata_index("type", "event", "", &id)
.unwrap();
let ids = engine.find_by_metadata("type", "event", "").unwrap();
assert_eq!(ids.len(), 1);
engine
.remove_from_metadata_index("type", "event", "", &id)
.unwrap();
let ids = engine.find_by_metadata("type", "event", "").unwrap();
assert!(ids.is_empty());
}
#[test]
fn test_metadata_index_remove_one_of_many() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mut mem1 = Memory::new("content 1".to_string(), vec![0.1; 384]);
mem1.metadata
.insert("type".to_string(), "event".to_string());
let id1 = mem1.id.clone();
let mut mem2 = Memory::new("content 2".to_string(), vec![0.2; 384]);
mem2.metadata
.insert("type".to_string(), "event".to_string());
let id2 = mem2.id.clone();
engine
.add_to_metadata_index("type", "event", "", &id1)
.unwrap();
engine
.add_to_metadata_index("type", "event", "", &id2)
.unwrap();
let ids = engine.find_by_metadata("type", "event", "").unwrap();
assert_eq!(ids.len(), 2);
engine
.remove_from_metadata_index("type", "event", "", &id1)
.unwrap();
let ids = engine.find_by_metadata("type", "event", "").unwrap();
assert_eq!(ids.len(), 1);
assert_eq!(ids[0], id2);
}
#[test]
fn test_metadata_index_remove_all_for_memory() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mut mem = Memory::new("test content".to_string(), vec![0.1; 384]);
mem.metadata.insert("type".to_string(), "event".to_string());
mem.metadata
.insert("priority".to_string(), "high".to_string());
mem.metadata
.insert("category".to_string(), "work".to_string());
let id = mem.id.clone();
engine
.add_to_metadata_index("type", "event", "", &id)
.unwrap();
engine
.add_to_metadata_index("priority", "high", "", &id)
.unwrap();
engine
.add_to_metadata_index("category", "work", "", &id)
.unwrap();
assert!(!engine
.find_by_metadata("type", "event", "")
.unwrap()
.is_empty());
assert!(!engine
.find_by_metadata("priority", "high", "")
.unwrap()
.is_empty());
assert!(!engine
.find_by_metadata("category", "work", "")
.unwrap()
.is_empty());
let indexed_fields = vec![
"type".to_string(),
"priority".to_string(),
"category".to_string(),
];
engine
.remove_metadata_indexes_for_memory(&mem, &indexed_fields)
.unwrap();
assert!(engine
.find_by_metadata("type", "event", "")
.unwrap()
.is_empty());
assert!(engine
.find_by_metadata("priority", "high", "")
.unwrap()
.is_empty());
assert!(engine
.find_by_metadata("category", "work", "")
.unwrap()
.is_empty());
}
#[test]
fn test_truncated_file_detection() {
let dir = tempdir().unwrap();
let path = dir.path().join("truncated.mfdb");
std::fs::write(&path, b"MF").unwrap();
let result = StorageEngine::open(&path);
assert!(result.is_err());
if let Err(err) = result {
assert!(matches!(err, Error::FileTruncated(_)));
}
}
#[test]
fn test_validate_database_integrity() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
assert!(engine.validate_database().is_ok());
}
#[test]
fn test_validate_database_with_data() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let mem = Memory::new("test content".to_string(), vec![0.1; 384]);
engine.store_memory(&mem).unwrap();
assert!(engine.validate_database().is_ok());
drop(engine);
let engine = StorageEngine::open(&path).unwrap();
assert!(engine.validate_database().is_ok());
}
#[test]
fn test_open_validates_existing_database() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
{
let _engine = StorageEngine::open(&path).unwrap();
}
let engine = StorageEngine::open(&path).unwrap();
assert!(engine.validate_database().is_ok());
}
#[test]
fn test_entity_profile_store_and_retrieve() {
use crate::types::{EntityFact, EntityId, EntityProfile};
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let entity_id = EntityId::new();
let mut profile =
EntityProfile::new(entity_id.clone(), "Alice".to_string(), "person".to_string());
let memory_id = MemoryId::new();
profile.add_fact(EntityFact::new(
"occupation",
"engineer",
0.9,
memory_id.clone(),
));
profile.add_source_memory(memory_id);
engine.store_entity_profile(&profile).unwrap();
let retrieved = engine.get_entity_profile("Alice").unwrap();
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.name, "Alice");
assert_eq!(retrieved.entity_type, "person");
assert_eq!(retrieved.facts.get("occupation").unwrap().len(), 1);
assert_eq!(
retrieved.facts.get("occupation").unwrap()[0].value,
"engineer"
);
}
#[test]
fn test_entity_profile_case_insensitive_lookup() {
use crate::types::{EntityId, EntityProfile};
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let profile =
EntityProfile::new(EntityId::new(), "Alice".to_string(), "person".to_string());
engine.store_entity_profile(&profile).unwrap();
assert!(engine.get_entity_profile("alice").unwrap().is_some());
assert!(engine.get_entity_profile("ALICE").unwrap().is_some());
assert!(engine.get_entity_profile("Alice").unwrap().is_some());
assert!(engine.get_entity_profile("aLiCe").unwrap().is_some());
}
#[test]
fn test_entity_profile_not_found() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let result = engine.get_entity_profile("Nonexistent").unwrap();
assert!(result.is_none());
}
#[test]
fn test_entity_profile_list() {
use crate::types::{EntityId, EntityProfile};
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let profile1 =
EntityProfile::new(EntityId::new(), "Alice".to_string(), "person".to_string());
let profile2 = EntityProfile::new(EntityId::new(), "Bob".to_string(), "person".to_string());
let profile3 = EntityProfile::new(
EntityId::new(),
"Acme Corp".to_string(),
"organization".to_string(),
);
engine.store_entity_profile(&profile1).unwrap();
engine.store_entity_profile(&profile2).unwrap();
engine.store_entity_profile(&profile3).unwrap();
let profiles = engine.list_entity_profiles().unwrap();
assert_eq!(profiles.len(), 3);
let names: Vec<_> = profiles.iter().map(|p| p.name.as_str()).collect();
assert!(names.contains(&"Alice"));
assert!(names.contains(&"Bob"));
assert!(names.contains(&"Acme Corp"));
}
#[test]
fn test_entity_profile_delete() {
use crate::types::{EntityId, EntityProfile};
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let profile =
EntityProfile::new(EntityId::new(), "Alice".to_string(), "person".to_string());
engine.store_entity_profile(&profile).unwrap();
assert!(engine.get_entity_profile("Alice").unwrap().is_some());
let deleted = engine.delete_entity_profile("Alice").unwrap();
assert!(deleted);
assert!(engine.get_entity_profile("Alice").unwrap().is_none());
let deleted = engine.delete_entity_profile("Alice").unwrap();
assert!(!deleted);
}
#[test]
fn test_entity_profile_count() {
use crate::types::{EntityId, EntityProfile};
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
assert_eq!(engine.count_entity_profiles().unwrap(), 0);
engine
.store_entity_profile(&EntityProfile::new(
EntityId::new(),
"Alice".to_string(),
"person".to_string(),
))
.unwrap();
assert_eq!(engine.count_entity_profiles().unwrap(), 1);
engine
.store_entity_profile(&EntityProfile::new(
EntityId::new(),
"Bob".to_string(),
"person".to_string(),
))
.unwrap();
assert_eq!(engine.count_entity_profiles().unwrap(), 2);
}
#[test]
fn test_entity_profile_update() {
use crate::types::{EntityFact, EntityId, EntityProfile};
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let engine = StorageEngine::open(&path).unwrap();
let entity_id = EntityId::new();
let mut profile =
EntityProfile::new(entity_id.clone(), "Alice".to_string(), "person".to_string());
profile.add_fact(EntityFact::new(
"occupation",
"engineer",
0.9,
MemoryId::new(),
));
engine.store_entity_profile(&profile).unwrap();
profile.add_fact(EntityFact::new("skill", "Rust", 0.85, MemoryId::new()));
engine.store_entity_profile(&profile).unwrap();
let retrieved = engine.get_entity_profile("Alice").unwrap().unwrap();
assert_eq!(retrieved.facts.len(), 2);
assert!(retrieved.facts.contains_key("occupation"));
assert!(retrieved.facts.contains_key("skill"));
}
#[test]
fn test_entity_profile_persistence() {
use crate::types::{EntityFact, EntityId, EntityProfile};
let dir = tempdir().unwrap();
let path = dir.path().join("test.mfdb");
let entity_id = EntityId::new();
{
let engine = StorageEngine::open(&path).unwrap();
let mut profile =
EntityProfile::new(entity_id.clone(), "Alice".to_string(), "person".to_string());
profile.add_fact(EntityFact::new(
"occupation",
"engineer",
0.9,
MemoryId::new(),
));
engine.store_entity_profile(&profile).unwrap();
}
{
let engine = StorageEngine::open(&path).unwrap();
let retrieved = engine.get_entity_profile("Alice").unwrap().unwrap();
assert_eq!(retrieved.name, "Alice");
assert_eq!(
retrieved.facts.get("occupation").unwrap()[0].value,
"engineer"
);
}
}
}