use super::node_id::NodeId;
use super::term::Term;
use crate::btree::BTree;
use crate::compression::prefix::PrefixCompressor;
use crate::error::Result;
use crate::storage::BufferPool;
use parking_lot::RwLock;
use std::sync::Arc;
pub struct NodeTable {
term_to_id: RwLock<BTree<Term, NodeId>>,
id_to_term: RwLock<BTree<NodeId, Term>>,
next_id: RwLock<NodeId>,
prefix_compressor: RwLock<PrefixCompressor>,
compression_enabled: bool,
}
impl NodeTable {
pub fn new(buffer_pool: Arc<BufferPool>) -> Self {
Self::with_compression(buffer_pool, true)
}
pub fn with_compression(buffer_pool: Arc<BufferPool>, compression_enabled: bool) -> Self {
NodeTable {
term_to_id: RwLock::new(BTree::new(buffer_pool.clone())),
id_to_term: RwLock::new(BTree::new(buffer_pool)),
next_id: RwLock::new(NodeId::FIRST),
prefix_compressor: RwLock::new(PrefixCompressor::new(15)), compression_enabled,
}
}
pub fn compression_stats(&self) -> crate::compression::prefix::CompressionStats {
let compressor = self.prefix_compressor.read();
compressor.stats()
}
pub fn get_or_create(&self, term: &Term) -> Result<NodeId> {
{
let term_to_id = self.term_to_id.read();
if let Some(id) = term_to_id.search(term)? {
return Ok(id);
}
}
let mut term_to_id = self.term_to_id.write();
let mut id_to_term = self.id_to_term.write();
let mut next_id = self.next_id.write();
if let Some(id) = term_to_id.search(term)? {
return Ok(id);
}
if self.compression_enabled {
if let Some(iri) = term.as_iri() {
let mut compressor = self.prefix_compressor.write();
let _ = compressor.compress(iri);
}
}
let id = *next_id;
*next_id = next_id.next();
term_to_id.insert(term.clone(), id)?;
id_to_term.insert(id, term.clone())?;
Ok(id)
}
pub fn get_id(&self, term: &Term) -> Result<Option<NodeId>> {
let term_to_id = self.term_to_id.read();
term_to_id.search(term)
}
pub fn get_term(&self, id: NodeId) -> Result<Option<Term>> {
let id_to_term = self.id_to_term.read();
id_to_term.search(&id)
}
pub fn size(&self) -> u64 {
let next_id = self.next_id.read();
next_id.as_u64() - NodeId::FIRST.as_u64()
}
pub fn contains(&self, term: &Term) -> Result<bool> {
Ok(self.get_id(term)?.is_some())
}
pub fn contains_id(&self, id: NodeId) -> Result<bool> {
Ok(self.get_term(id)?.is_some())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::{Allocator, FileManager};
use tempfile::TempDir;
fn create_test_node_table() -> (TempDir, NodeTable) {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_manager = Arc::new(FileManager::open(&db_path, true).unwrap());
let buffer_pool = Arc::new(BufferPool::new(100, file_manager));
let node_table = NodeTable::new(buffer_pool);
(temp_dir, node_table)
}
#[test]
fn test_node_table_get_or_create() -> Result<()> {
let (_temp_dir, table) = create_test_node_table();
let term1 = Term::iri("http://example.org/resource1");
let term2 = Term::iri("http://example.org/resource2");
let id1 = table.get_or_create(&term1)?;
let id2 = table.get_or_create(&term2)?;
assert_ne!(id1, id2);
let id1_again = table.get_or_create(&term1)?;
assert_eq!(id1, id1_again);
Ok(())
}
#[test]
fn test_node_table_bidirectional_mapping() -> Result<()> {
let (_temp_dir, table) = create_test_node_table();
let term = Term::literal_with_lang("Hello World", "en");
let id = table.get_or_create(&term)?;
assert_eq!(table.get_id(&term)?, Some(id));
assert_eq!(table.get_term(id)?, Some(term));
Ok(())
}
#[test]
fn test_node_table_get_id_not_found() -> Result<()> {
let (_temp_dir, table) = create_test_node_table();
let term = Term::iri("http://not-exists.com");
assert_eq!(table.get_id(&term)?, None);
Ok(())
}
#[test]
fn test_node_table_get_term_not_found() -> Result<()> {
let (_temp_dir, table) = create_test_node_table();
let id = NodeId::new(999);
assert_eq!(table.get_term(id)?, None);
Ok(())
}
#[test]
fn test_node_table_size() -> Result<()> {
let (_temp_dir, table) = create_test_node_table();
assert_eq!(table.size(), 0);
table.get_or_create(&Term::iri("http://a.com"))?;
assert_eq!(table.size(), 1);
table.get_or_create(&Term::iri("http://b.com"))?;
assert_eq!(table.size(), 2);
table.get_or_create(&Term::iri("http://a.com"))?;
assert_eq!(table.size(), 2);
Ok(())
}
#[test]
fn test_node_table_contains() -> Result<()> {
let (_temp_dir, table) = create_test_node_table();
let term1 = Term::iri("http://exists.com");
let term2 = Term::iri("http://not-exists.com");
table.get_or_create(&term1)?;
assert!(table.contains(&term1)?);
assert!(!table.contains(&term2)?);
Ok(())
}
#[test]
fn test_node_table_contains_id() -> Result<()> {
let (_temp_dir, table) = create_test_node_table();
let term = Term::literal("test");
let id = table.get_or_create(&term)?;
assert!(table.contains_id(id)?);
assert!(!table.contains_id(NodeId::new(999))?);
Ok(())
}
#[test]
fn test_node_table_multiple_term_types() -> Result<()> {
let (_temp_dir, table) = create_test_node_table();
let iri = Term::iri("http://example.org");
let literal = Term::literal("test value");
let blank = Term::blank_node("b0");
let id1 = table.get_or_create(&iri)?;
let id2 = table.get_or_create(&literal)?;
let id3 = table.get_or_create(&blank)?;
assert_ne!(id1, id2);
assert_ne!(id2, id3);
assert_ne!(id1, id3);
assert_eq!(table.get_term(id1)?, Some(iri));
assert_eq!(table.get_term(id2)?, Some(literal));
assert_eq!(table.get_term(id3)?, Some(blank));
Ok(())
}
#[test]
fn test_prefix_compression_integration() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_manager = Arc::new(FileManager::open(&db_path, true).unwrap());
let buffer_pool = Arc::new(BufferPool::new(100, file_manager));
let table = NodeTable::with_compression(buffer_pool, true);
let iri1 = Term::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
let iri2 = Term::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#Property");
let iri3 = Term::iri("http://www.w3.org/2000/01/rdf-schema#label");
let iri4 = Term::iri("http://www.w3.org/2000/01/rdf-schema#comment");
table.get_or_create(&iri1)?;
table.get_or_create(&iri2)?;
table.get_or_create(&iri3)?;
table.get_or_create(&iri4)?;
let stats = table.compression_stats();
assert_eq!(stats.num_prefixes, 2);
assert!(stats.total_prefix_bytes > 0);
Ok(())
}
#[test]
fn test_prefix_compression_disabled() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_manager = Arc::new(FileManager::open(&db_path, true).unwrap());
let buffer_pool = Arc::new(BufferPool::new(100, file_manager));
let table = NodeTable::with_compression(buffer_pool, false);
let iri1 = Term::iri("http://example.org/Person");
let iri2 = Term::iri("http://example.org/name");
table.get_or_create(&iri1)?;
table.get_or_create(&iri2)?;
let stats = table.compression_stats();
assert_eq!(stats.num_prefixes, 0);
Ok(())
}
#[test]
fn test_prefix_compression_with_short_iris() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_manager = Arc::new(FileManager::open(&db_path, true).unwrap());
let buffer_pool = Arc::new(BufferPool::new(100, file_manager));
let table = NodeTable::with_compression(buffer_pool, true);
let short_iri = Term::iri("http://a.b");
table.get_or_create(&short_iri)?;
let stats = table.compression_stats();
assert_eq!(stats.num_prefixes, 0);
Ok(())
}
#[test]
fn test_prefix_compression_with_literals() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_manager = Arc::new(FileManager::open(&db_path, true).unwrap());
let buffer_pool = Arc::new(BufferPool::new(100, file_manager));
let table = NodeTable::with_compression(buffer_pool, true);
let lit1 = Term::literal("This is a long literal value that could be compressed");
let lit2 = Term::literal("Another long literal value");
table.get_or_create(&lit1)?;
table.get_or_create(&lit2)?;
let stats = table.compression_stats();
assert_eq!(stats.num_prefixes, 0);
Ok(())
}
#[test]
fn test_prefix_compression_realistic_dataset() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let file_manager = Arc::new(FileManager::open(&db_path, true).unwrap());
let buffer_pool = Arc::new(BufferPool::new(100, file_manager));
let table = NodeTable::with_compression(buffer_pool, true);
let vocab_iris = vec![
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
"http://www.w3.org/1999/02/22-rdf-syntax-ns#Property",
"http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement",
"http://www.w3.org/2000/01/rdf-schema#label",
"http://www.w3.org/2000/01/rdf-schema#comment",
"http://www.w3.org/2000/01/rdf-schema#Class",
"http://xmlns.com/foaf/0.1/Person",
"http://xmlns.com/foaf/0.1/name",
"http://xmlns.com/foaf/0.1/knows",
"http://example.org/vocab/Employee",
"http://example.org/vocab/salary",
"http://example.org/vocab/department",
];
for iri in vocab_iris {
table.get_or_create(&Term::iri(iri))?;
}
let stats = table.compression_stats();
assert!(stats.num_prefixes >= 4);
assert!(stats.total_prefix_bytes > 100);
Ok(())
}
}