use crate::config::StorageConfig;
use crate::error::Result;
use crate::storage_trait::{StorageBackend, StorageStats};
use crate::types::{
Action, ActionType, AgentPubKey, Entry, Hash, Link, Record, Signature, Timestamp,
};
use rusqlite::{params, Connection};
pub struct Storage {
conn: Connection,
config: StorageConfig,
}
impl Storage {
pub fn open(config: StorageConfig) -> Result<Self> {
let conn = Connection::open(&config.db_path)?;
let storage = Self { conn, config };
storage.init_schema()?;
Ok(storage)
}
pub fn memory() -> Result<Self> {
let conn = Connection::open_in_memory()?;
let config = StorageConfig::default();
let storage = Self { conn, config };
storage.init_schema()?;
Ok(storage)
}
fn init_schema(&self) -> Result<()> {
self.conn.execute_batch(
r#"
-- Actions table (source chain)
CREATE TABLE IF NOT EXISTS actions (
hash BLOB PRIMARY KEY,
seq INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
action_type TEXT NOT NULL,
author BLOB NOT NULL,
prev_action BLOB,
entry_hash BLOB,
data BLOB NOT NULL
);
-- Entries table
CREATE TABLE IF NOT EXISTS entries (
hash BLOB PRIMARY KEY,
entry_type TEXT NOT NULL,
content BLOB NOT NULL,
created_at INTEGER NOT NULL
);
-- Links table
CREATE TABLE IF NOT EXISTS links (
id INTEGER PRIMARY KEY,
base BLOB NOT NULL,
target BLOB NOT NULL,
link_type INTEGER NOT NULL,
tag BLOB,
timestamp INTEGER NOT NULL,
deleted INTEGER DEFAULT 0
);
-- Indices for efficient queries
CREATE INDEX IF NOT EXISTS idx_actions_seq ON actions(seq);
CREATE INDEX IF NOT EXISTS idx_actions_timestamp ON actions(timestamp);
CREATE INDEX IF NOT EXISTS idx_entries_created ON entries(created_at);
CREATE INDEX IF NOT EXISTS idx_links_base ON links(base);
CREATE INDEX IF NOT EXISTS idx_links_target ON links(target);
-- Metadata table
CREATE TABLE IF NOT EXISTS metadata (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
"#,
)?;
Ok(())
}
pub fn put_action(&self, action: &Action) -> Result<Hash> {
let data = serde_json::to_vec(action)?;
let hash = Hash::from_bytes(&data);
let action_type = format!("{:?}", action.action_type);
self.conn.execute(
r#"INSERT OR REPLACE INTO actions
(hash, seq, timestamp, action_type, author, prev_action, entry_hash, data)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"#,
params![
hash.as_bytes().as_ref(),
action.seq,
action.timestamp.0 as i64,
action_type,
action.author.as_bytes().as_ref(),
action.prev_action.as_ref().map(|h| h.as_bytes().to_vec()),
action.entry_hash.as_ref().map(|h| h.as_bytes().to_vec()),
data,
],
)?;
if self.config.aggressive_pruning {
self.prune_old_actions()?;
}
Ok(hash)
}
pub fn put_entry(&self, entry: &Entry) -> Result<Hash> {
let hash = entry.hash();
let entry_type = format!("{:?}", entry.entry_type);
let timestamp = Timestamp::now().0 as i64;
self.conn.execute(
r#"INSERT OR REPLACE INTO entries
(hash, entry_type, content, created_at)
VALUES (?1, ?2, ?3, ?4)"#,
params![
hash.as_bytes().as_ref(),
entry_type,
&entry.content,
timestamp,
],
)?;
if self.config.aggressive_pruning {
self.prune_old_entries()?;
}
Ok(hash)
}
pub fn put_record(&self, record: &Record) -> Result<Hash> {
if let Some(entry) = &record.entry {
self.put_entry(entry)?;
}
self.put_action(&record.action)
}
pub fn put_records_batch(&self, records: &[Record]) -> Result<Vec<Hash>> {
if records.is_empty() {
return Ok(Vec::new());
}
self.conn.execute("BEGIN IMMEDIATE", [])?;
let result = (|| {
let mut hashes = Vec::with_capacity(records.len());
let mut entry_stmt = self.conn.prepare_cached(
r#"INSERT OR REPLACE INTO entries
(hash, entry_type, content, created_at)
VALUES (?1, ?2, ?3, ?4)"#,
)?;
let mut action_stmt = self.conn.prepare_cached(
r#"INSERT OR REPLACE INTO actions
(hash, seq, timestamp, action_type, author, prev_action, entry_hash, data)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"#,
)?;
let timestamp = crate::types::Timestamp::now().0 as i64;
for record in records {
if let Some(entry) = &record.entry {
let entry_hash = entry.hash();
let entry_type = format!("{:?}", entry.entry_type);
entry_stmt.execute(params![
entry_hash.as_bytes().as_ref(),
entry_type,
&entry.content,
timestamp,
])?;
}
let action = &record.action;
let data = serde_json::to_vec(action)?;
let hash = Hash::from_bytes(&data);
let action_type = format!("{:?}", action.action_type);
action_stmt.execute(params![
hash.as_bytes().as_ref(),
action.seq,
action.timestamp.0 as i64,
action_type,
action.author.as_bytes().as_ref(),
action.prev_action.as_ref().map(|h| h.as_bytes().to_vec()),
action.entry_hash.as_ref().map(|h| h.as_bytes().to_vec()),
data,
])?;
hashes.push(hash);
}
Ok(hashes)
})();
match result {
Ok(hashes) => {
self.conn.execute("COMMIT", [])?;
if self.config.aggressive_pruning {
self.prune_old_actions()?;
self.prune_old_entries()?;
}
Ok(hashes)
}
Err(e) => {
let _ = self.conn.execute("ROLLBACK", []);
Err(e)
}
}
}
pub fn get_action(&self, hash: &Hash) -> Result<Option<Action>> {
let mut stmt = self
.conn
.prepare("SELECT data FROM actions WHERE hash = ?1")?;
let result: Option<Vec<u8>> = stmt
.query_row(params![hash.as_bytes().as_ref()], |row| row.get(0))
.ok();
match result {
Some(data) => Ok(Some(serde_json::from_slice(&data)?)),
None => Ok(None),
}
}
pub fn get_entry(&self, hash: &Hash) -> Result<Option<Entry>> {
let mut stmt = self
.conn
.prepare("SELECT entry_type, content FROM entries WHERE hash = ?1")?;
let result: Option<(String, Vec<u8>)> = stmt
.query_row(params![hash.as_bytes().as_ref()], |row| {
Ok((row.get(0)?, row.get(1)?))
})
.ok();
match result {
Some((_entry_type, content)) => {
Ok(Some(Entry {
entry_type: crate::types::EntryType::App,
content,
}))
}
None => Ok(None),
}
}
pub fn get_latest_seq(&self) -> Result<u32> {
let mut stmt = self.conn.prepare("SELECT MAX(seq) FROM actions")?;
let seq: Option<u32> = stmt.query_row([], |row| row.get(0)).ok().flatten();
Ok(seq.unwrap_or(0))
}
pub fn get_records_by_seq_range(
&self,
from_seq: u32,
to_seq: u32,
limit: u32,
) -> Result<Vec<Record>> {
let mut stmt = self.conn.prepare(
"SELECT hash, seq, timestamp, action_type, author, prev_action, entry_hash, data
FROM actions
WHERE seq >= ? AND seq < ?
ORDER BY seq ASC
LIMIT ?",
)?;
let rows = stmt.query_map(params![from_seq, to_seq, limit], |row| {
let hash_bytes: Vec<u8> = row.get(0)?;
let seq: u32 = row.get(1)?;
let timestamp_ms: i64 = row.get(2)?;
let action_type_str: String = row.get(3)?;
let author_bytes: Vec<u8> = row.get(4)?;
let prev_action_bytes: Option<Vec<u8>> = row.get(5)?;
let entry_hash_bytes: Option<Vec<u8>> = row.get(6)?;
let _data: Vec<u8> = row.get(7)?;
Ok((
hash_bytes,
seq,
timestamp_ms,
action_type_str,
author_bytes,
prev_action_bytes,
entry_hash_bytes,
))
})?;
let mut records = Vec::new();
for row_result in rows {
let (
_hash_bytes,
seq,
timestamp_ms,
action_type_str,
author_bytes,
prev_action_bytes,
entry_hash_bytes,
) = row_result?;
let action_type = match action_type_str.as_str() {
"Create" => ActionType::Create,
"Update" => ActionType::Update,
"Delete" => ActionType::Delete,
_ => ActionType::Create,
};
let mut author = [0u8; 32];
if author_bytes.len() >= 32 {
author.copy_from_slice(&author_bytes[..32]);
}
let prev_action = prev_action_bytes.map(|bytes| {
let mut arr = [0u8; 32];
if bytes.len() >= 32 {
arr.copy_from_slice(&bytes[..32]);
}
Hash(arr)
});
let entry_hash = entry_hash_bytes.as_ref().map(|bytes| {
let mut arr = [0u8; 32];
if bytes.len() >= 32 {
arr.copy_from_slice(&bytes[..32]);
}
Hash(arr)
});
let action = Action {
action_type,
author: AgentPubKey(author),
timestamp: Timestamp::from_millis(timestamp_ms as u64),
seq,
prev_action,
entry_hash: entry_hash.clone(),
signature: Signature([0u8; 64]), };
let entry = if let Some(ref eh) = entry_hash {
self.get_entry(eh)?
} else {
None
};
records.push(Record { action, entry });
}
Ok(records)
}
pub fn stats(&self) -> Result<StorageStats> {
let action_count: u64 = self
.conn
.query_row("SELECT COUNT(*) FROM actions", [], |row| row.get(0))?;
let entry_count: u64 = self
.conn
.query_row("SELECT COUNT(*) FROM entries", [], |row| row.get(0))?;
let link_count: u64 =
self.conn
.query_row("SELECT COUNT(*) FROM links WHERE deleted = 0", [], |row| {
row.get(0)
})?;
let db_size = std::fs::metadata(&self.config.db_path)
.map(|m| m.len() as usize)
.unwrap_or(0);
Ok(StorageStats {
action_count,
entry_count,
link_count,
db_size,
})
}
fn prune_old_actions(&self) -> Result<()> {
let keep = self.config.keep_recent as i64;
self.conn.execute(
r#"DELETE FROM actions WHERE seq NOT IN
(SELECT seq FROM actions ORDER BY seq DESC LIMIT ?1)"#,
params![keep],
)?;
Ok(())
}
fn prune_old_entries(&self) -> Result<()> {
let keep = self.config.keep_recent as i64;
self.conn.execute(
r#"DELETE FROM entries WHERE hash NOT IN
(SELECT hash FROM entries ORDER BY created_at DESC LIMIT ?1)"#,
params![keep],
)?;
Ok(())
}
pub fn vacuum(&self) -> Result<()> {
self.conn.execute("VACUUM", [])?;
Ok(())
}
pub fn check_limits(&self) -> Result<bool> {
let stats = self.stats()?;
Ok(stats.db_size <= self.config.max_size)
}
pub fn add_link(&self, link: &Link) -> Result<i64> {
self.conn.execute(
r#"INSERT INTO links (base, target, link_type, tag, timestamp, deleted)
VALUES (?1, ?2, ?3, ?4, ?5, 0)"#,
params![
link.base.as_bytes().as_ref(),
link.target.as_bytes().as_ref(),
link.link_type as i64,
&link.tag,
link.timestamp.0 as i64,
],
)?;
Ok(self.conn.last_insert_rowid())
}
pub fn delete_link(&self, link_id: i64) -> Result<()> {
self.conn.execute(
"UPDATE links SET deleted = 1 WHERE id = ?1",
params![link_id],
)?;
Ok(())
}
pub fn get_links(&self, base: &Hash, link_type: Option<u8>) -> Result<Vec<Link>> {
let mut links = Vec::new();
let sql = if link_type.is_some() {
"SELECT base, target, link_type, tag, timestamp FROM links
WHERE base = ?1 AND link_type = ?2 AND deleted = 0"
} else {
"SELECT base, target, link_type, tag, timestamp FROM links
WHERE base = ?1 AND deleted = 0"
};
let mut stmt = self.conn.prepare(sql)?;
let rows: Box<dyn Iterator<Item = _>> = if let Some(lt) = link_type {
Box::new(
stmt.query_map(params![base.as_bytes().as_ref(), lt as i64], |row| {
let base_bytes: Vec<u8> = row.get(0)?;
let target_bytes: Vec<u8> = row.get(1)?;
let tag_bytes: Vec<u8> = row.get(3)?;
Ok((
base_bytes,
target_bytes,
row.get::<_, i64>(2)?,
tag_bytes,
row.get::<_, i64>(4)?,
))
})?,
)
} else {
Box::new(stmt.query_map(params![base.as_bytes().as_ref()], |row| {
let base_bytes: Vec<u8> = row.get(0)?;
let target_bytes: Vec<u8> = row.get(1)?;
let tag_bytes: Vec<u8> = row.get(3)?;
Ok((
base_bytes,
target_bytes,
row.get::<_, i64>(2)?,
tag_bytes,
row.get::<_, i64>(4)?,
))
})?)
};
for row_result in rows {
let (base_bytes, target_bytes, link_type, tag, timestamp) = row_result?;
links.push(Link {
base: Hash::from_raw(&base_bytes),
target: Hash::from_raw(&target_bytes),
link_type: link_type as u8,
tag,
timestamp: Timestamp(timestamp as u64),
});
}
Ok(links)
}
pub fn set_metadata(&self, key: &str, value: &str) -> Result<()> {
self.conn.execute(
"INSERT OR REPLACE INTO metadata (key, value) VALUES (?1, ?2)",
params![key, value],
)?;
Ok(())
}
pub fn get_metadata(&self, key: &str) -> Result<Option<String>> {
let mut stmt = self
.conn
.prepare("SELECT value FROM metadata WHERE key = ?1")?;
let result: Option<String> = stmt.query_row(params![key], |row| row.get(0)).ok();
Ok(result)
}
pub fn delete_metadata(&self, key: &str) -> Result<()> {
self.conn
.execute("DELETE FROM metadata WHERE key = ?1", params![key])?;
Ok(())
}
pub fn all_metadata(&self) -> Result<Vec<(String, String)>> {
let mut stmt = self.conn.prepare("SELECT key, value FROM metadata")?;
let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
let mut result = Vec::new();
for row in rows {
result.push(row?);
}
Ok(result)
}
}
impl StorageBackend for Storage {
fn put_action(&self, action: &Action) -> Result<Hash> {
Storage::put_action(self, action)
}
fn put_entry(&self, entry: &Entry) -> Result<Hash> {
Storage::put_entry(self, entry)
}
fn put_record(&self, record: &Record) -> Result<Hash> {
Storage::put_record(self, record)
}
fn put_records_batch(&self, records: &[Record]) -> Result<Vec<Hash>> {
Storage::put_records_batch(self, records)
}
fn get_action(&self, hash: &Hash) -> Result<Option<Action>> {
Storage::get_action(self, hash)
}
fn get_entry(&self, hash: &Hash) -> Result<Option<Entry>> {
Storage::get_entry(self, hash)
}
fn get_latest_seq(&self) -> Result<u32> {
Storage::get_latest_seq(self)
}
fn get_records_by_seq_range(
&self,
from_seq: u32,
to_seq: u32,
limit: u32,
) -> Result<Vec<Record>> {
Storage::get_records_by_seq_range(self, from_seq, to_seq, limit)
}
fn stats(&self) -> Result<StorageStats> {
Storage::stats(self)
}
fn add_link(&self, link: &Link) -> Result<i64> {
Storage::add_link(self, link)
}
fn delete_link(&self, link_id: i64) -> Result<()> {
Storage::delete_link(self, link_id)
}
fn get_links(&self, base: &Hash, link_type: Option<u8>) -> Result<Vec<Link>> {
Storage::get_links(self, base, link_type)
}
fn set_metadata(&self, key: &str, value: &str) -> Result<()> {
Storage::set_metadata(self, key, value)
}
fn get_metadata(&self, key: &str) -> Result<Option<String>> {
Storage::get_metadata(self, key)
}
fn vacuum(&self) -> Result<()> {
Storage::vacuum(self)
}
fn check_limits(&self) -> Result<bool> {
Storage::check_limits(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage_trait::StorageStats;
use crate::types::*;
fn create_test_action(seq: u32) -> Action {
Action {
action_type: ActionType::Create,
author: AgentPubKey([0u8; 32]),
timestamp: Timestamp::now(),
seq,
prev_action: None,
entry_hash: None,
signature: Signature([0u8; 64]),
}
}
#[test]
fn test_storage_open() {
let storage = Storage::memory().unwrap();
let stats = storage.stats().unwrap();
assert_eq!(stats.action_count, 0);
}
#[test]
fn test_put_get_action() {
let storage = Storage::memory().unwrap();
let action = create_test_action(1);
let hash = storage.put_action(&action).unwrap();
let retrieved = storage.get_action(&hash).unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().seq, 1);
}
#[test]
fn test_latest_seq() {
let storage = Storage::memory().unwrap();
for seq in 1..=5 {
let action = create_test_action(seq);
storage.put_action(&action).unwrap();
}
assert_eq!(storage.get_latest_seq().unwrap(), 5);
}
fn create_test_link() -> Link {
Link {
base: Hash::from_bytes(b"base_entry"),
target: Hash::from_bytes(b"target_entry"),
link_type: 1,
tag: vec![1, 2, 3],
timestamp: Timestamp::now(),
}
}
#[test]
fn test_add_link() {
let storage = Storage::memory().unwrap();
let link = create_test_link();
let link_id = storage.add_link(&link).unwrap();
assert!(link_id > 0);
}
#[test]
fn test_get_links() {
let storage = Storage::memory().unwrap();
let link = create_test_link();
let base = link.base.clone();
storage.add_link(&link).unwrap();
let links = storage.get_links(&base, None).unwrap();
assert_eq!(links.len(), 1);
assert_eq!(links[0].link_type, 1);
}
#[test]
fn test_delete_link() {
let storage = Storage::memory().unwrap();
let link = create_test_link();
let base = link.base.clone();
let link_id = storage.add_link(&link).unwrap();
storage.delete_link(link_id).unwrap();
let links = storage.get_links(&base, None).unwrap();
assert_eq!(links.len(), 0);
}
#[test]
fn test_get_links_by_type() {
let storage = Storage::memory().unwrap();
let link1 = Link {
base: Hash::from_bytes(b"base"),
target: Hash::from_bytes(b"target1"),
link_type: 1,
tag: vec![],
timestamp: Timestamp::now(),
};
let link2 = Link {
base: Hash::from_bytes(b"base"),
target: Hash::from_bytes(b"target2"),
link_type: 2,
tag: vec![],
timestamp: Timestamp::now(),
};
storage.add_link(&link1).unwrap();
storage.add_link(&link2).unwrap();
let base = Hash::from_bytes(b"base");
let all_links = storage.get_links(&base, None).unwrap();
assert_eq!(all_links.len(), 2);
let type1_links = storage.get_links(&base, Some(1)).unwrap();
assert_eq!(type1_links.len(), 1);
}
#[test]
fn test_set_get_metadata() {
let storage = Storage::memory().unwrap();
storage.set_metadata("key1", "value1").unwrap();
let value = storage.get_metadata("key1").unwrap();
assert_eq!(value, Some("value1".to_string()));
}
#[test]
fn test_metadata_update() {
let storage = Storage::memory().unwrap();
storage.set_metadata("key", "old").unwrap();
storage.set_metadata("key", "new").unwrap();
let value = storage.get_metadata("key").unwrap();
assert_eq!(value, Some("new".to_string()));
}
#[test]
fn test_delete_metadata() {
let storage = Storage::memory().unwrap();
storage.set_metadata("key", "value").unwrap();
storage.delete_metadata("key").unwrap();
let value = storage.get_metadata("key").unwrap();
assert!(value.is_none());
}
#[test]
fn test_all_metadata() {
let storage = Storage::memory().unwrap();
storage.set_metadata("a", "1").unwrap();
storage.set_metadata("b", "2").unwrap();
let all = storage.all_metadata().unwrap();
assert_eq!(all.len(), 2);
}
#[test]
fn test_file_persistence() {
let db_path = "/tmp/aingle_test_persistence.db";
let _ = std::fs::remove_file(db_path);
{
let config = StorageConfig::sqlite(db_path);
let storage = Storage::open(config).unwrap();
for seq in 1..=5 {
let action = create_test_action(seq);
storage.put_action(&action).unwrap();
}
storage.set_metadata("node_id", "test-node-001").unwrap();
storage.set_metadata("version", "1.0.0").unwrap();
let link = create_test_link();
storage.add_link(&link).unwrap();
assert_eq!(storage.get_latest_seq().unwrap(), 5);
}
{
let config = StorageConfig::sqlite(db_path);
let storage = Storage::open(config).unwrap();
assert_eq!(storage.get_latest_seq().unwrap(), 5);
assert_eq!(
storage.get_metadata("node_id").unwrap(),
Some("test-node-001".to_string())
);
assert_eq!(
storage.get_metadata("version").unwrap(),
Some("1.0.0".to_string())
);
let base = Hash::from_bytes(b"base_entry");
let links = storage.get_links(&base, None).unwrap();
assert_eq!(links.len(), 1);
let stats = storage.stats().unwrap();
assert_eq!(stats.action_count, 5);
assert_eq!(stats.link_count, 1);
}
let _ = std::fs::remove_file(db_path);
}
#[test]
fn test_storage_backend_trait() {
fn use_backend<B: StorageBackend>(backend: &B) -> Result<StorageStats> {
backend.stats()
}
let storage = Storage::memory().unwrap();
let stats = use_backend(&storage).unwrap();
assert_eq!(stats.action_count, 0);
}
#[test]
fn test_get_records_by_seq_range() {
let storage = Storage::memory().unwrap();
for seq in 1..=10 {
let action = create_test_action(seq);
storage.put_action(&action).unwrap();
}
let records = storage.get_records_by_seq_range(3, 8, 10).unwrap();
assert_eq!(records.len(), 5); assert_eq!(records[0].action.seq, 3);
assert_eq!(records[4].action.seq, 7);
let records = storage.get_records_by_seq_range(1, 10, 3).unwrap();
assert_eq!(records.len(), 3);
assert_eq!(records[0].action.seq, 1);
assert_eq!(records[2].action.seq, 3);
let records = storage.get_records_by_seq_range(100, 200, 10).unwrap();
assert!(records.is_empty());
}
}