use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use nodedb_strict::{StrictError, TupleDecoder, TupleEncoder};
use nodedb_types::Namespace;
use nodedb_types::columnar::StrictSchema;
use nodedb_types::value::Value;
use crate::error::LiteError;
use crate::storage::engine::StorageEngine;
pub(super) const META_STRICT_SCHEMA_PREFIX: &str = "strict_schema:";
pub(super) const META_STRICT_COLLECTIONS: &[u8] = b"meta:strict_collections";
pub struct StrictEngine<S: StorageEngine> {
pub(super) storage: Arc<S>,
pub(super) collections: RwLock<HashMap<String, Arc<CollectionState>>>,
pub(super) indexes: std::sync::OnceLock<Arc<crate::index::IndexCatalog>>,
}
pub struct CollectionState {
pub(super) schema: StrictSchema,
pub(super) encoder: TupleEncoder,
pub(super) decoder: TupleDecoder,
pub(super) pk_indices: Vec<usize>,
pub(super) version_column_counts: HashMap<u16, usize>,
}
impl CollectionState {
pub(super) fn new(schema: StrictSchema) -> Self {
let pk_indices: Vec<usize> = schema
.columns
.iter()
.enumerate()
.filter(|(_, c)| c.primary_key)
.map(|(i, _)| i)
.collect();
let encoder = TupleEncoder::new(&schema);
let decoder = TupleDecoder::new(&schema);
let mut version_column_counts = HashMap::new();
version_column_counts.insert(schema.version as u16, schema.columns.len());
Self {
schema,
encoder,
decoder,
pk_indices,
version_column_counts,
}
}
pub(super) fn storage_key(&self, collection: &str, values: &[Value]) -> Vec<u8> {
let mut key = collection.as_bytes().to_vec();
key.push(b':');
for &pk_idx in &self.pk_indices {
encode_pk_component(&mut key, &values[pk_idx]);
}
key
}
pub(super) fn storage_key_from_pk(&self, collection: &str, pk: &Value) -> Vec<u8> {
let mut key = collection.as_bytes().to_vec();
key.push(b':');
encode_pk_component(&mut key, pk);
key
}
}
fn encode_pk_component(key: &mut Vec<u8>, value: &Value) {
match value {
Value::Integer(v) => {
let sortable = (*v as u64) ^ (1u64 << 63);
key.extend_from_slice(&sortable.to_be_bytes());
}
Value::String(s) => {
key.extend_from_slice(s.as_bytes());
}
Value::Uuid(s) => {
key.extend_from_slice(s.as_bytes());
}
Value::Decimal(d) => {
key.extend_from_slice(&d.serialize());
}
_ => {
key.extend_from_slice(format!("{value:?}").as_bytes());
}
}
}
pub(super) fn strict_err_to_lite(e: StrictError) -> LiteError {
LiteError::BadRequest {
detail: e.to_string(),
}
}
impl<S: StorageEngine> StrictEngine<S> {
pub fn new(storage: Arc<S>) -> Self {
Self {
storage,
collections: RwLock::new(HashMap::new()),
indexes: std::sync::OnceLock::new(),
}
}
pub async fn restore(storage: Arc<S>) -> Result<Self, LiteError> {
let engine = Self::new(Arc::clone(&storage));
let list_bytes = storage
.get(Namespace::Meta, META_STRICT_COLLECTIONS)
.await?;
let names: Vec<String> = match list_bytes {
Some(bytes) => zerompk::from_msgpack(&bytes).map_err(|e| LiteError::Storage {
detail: format!("strict collection list deserialization failed: {e}"),
})?,
None => Vec::new(),
};
let mut loaded: HashMap<String, Arc<CollectionState>> = HashMap::new();
for name in names {
let meta_key = format!("{META_STRICT_SCHEMA_PREFIX}{name}");
if let Some(schema_bytes) = storage.get(Namespace::Meta, meta_key.as_bytes()).await?
&& let Ok(schema) = zerompk::from_msgpack::<StrictSchema>(&schema_bytes)
{
loaded.insert(name, Arc::new(CollectionState::new(schema)));
}
}
*engine
.collections
.write()
.map_err(|_| LiteError::LockPoisoned)? = loaded;
Ok(engine)
}
pub(super) fn get_state(&self, collection: &str) -> Result<Arc<CollectionState>, LiteError> {
let guard = self
.collections
.read()
.map_err(|_| LiteError::LockPoisoned)?;
guard
.get(collection)
.cloned()
.ok_or_else(|| LiteError::collection_not_found("strict", collection))
}
}