ordinary-storage 0.11.0

Storage for Ordinary
Documentation
#![doc = include_str!("../README.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc, clippy::cast_sign_loss)]

// Copyright (C) 2026 The Ordinary Authors.
//
// SPDX-License-Identifier: BSD-3-Clause

mod stores;

pub use stores::{
    artifact::{ArtifactKind, ArtifactStore},
    assets::AssetStore,
    cache::{CacheDependency, CacheKind, CacheStore, Lookup},
    database::UpdateError,
    secrets::SecretsStore,
};

use flexbuffers::Reader;
use ordinary_config::{DatabaseModelConfig, StorageLimits};
use ordinary_types::{Field, Kind, TimeUnit};
use saferlmdb::{
    ConstAccessor, Environment, ReadTransaction, Stat, WriteAccessor, WriteTransaction,
};
use std::sync::Arc;

pub use bytes;
use bytes::{BufMut, Bytes, BytesMut};

use crate::stores::database::DatabaseStore;
pub use saferlmdb;

fn field_to_bytes(field: &Field, reader: &Reader<&[u8]>) -> Bytes {
    let mut out = BytesMut::new();

    match &field.kind {
        Kind::Uuid => {
            out.put(reader.as_blob().0);
        }
        Kind::Bool => {
            if reader.as_bool() {
                out.put_u8(1);
            } else {
                out.put_u8(0);
            }
        }
        Kind::F32 => out.put_f32(reader.as_f32()),
        Kind::F64 => out.put_f64(reader.as_f64()),
        Kind::U8 => out.put_u8(reader.as_u8()),
        Kind::U16 => out.put_u16(reader.as_u16()),
        Kind::U32 => out.put_u32(reader.as_u32()),
        Kind::U64 => out.put_u64(reader.as_u64()),
        Kind::I8 => out.put_i8(reader.as_i8()),
        Kind::I16 => out.put_i16(reader.as_i16()),
        Kind::I32 => out.put_i32(reader.as_i32()),
        Kind::I64 => out.put_i64(reader.as_i64()),
        Kind::String | Kind::Markdown | Kind::Json | Kind::Url => {
            out.put(reader.as_str().as_bytes());
        }
        Kind::Timestamp { unit, .. } => match unit {
            TimeUnit::Seconds => out.put_i64(reader.as_i64()),
        },
        _ => {
            tracing::error!(
                "kind '{:?}' does not support encrypted/compressed",
                field.kind
            );
        }
    }

    out.into()
}

fn push_field_from_bytes(
    field: &Field,
    bytes: &[u8],
    dest: &mut flexbuffers::VectorBuilder,
) -> anyhow::Result<()> {
    match &field.kind {
        Kind::Uuid => dest.push(flexbuffers::Blob(bytes)),
        Kind::Bool => dest.push(if bytes.len() == 1 {
            bytes[0] == 1
        } else {
            false
        }),
        Kind::F32 => dest.push(f32::from_be_bytes(bytes.try_into()?)),
        Kind::F64 => dest.push(f64::from_be_bytes(bytes.try_into()?)),
        Kind::U8 => dest.push(u8::from_be_bytes(bytes.try_into()?)),
        Kind::U16 => dest.push(u16::from_be_bytes(bytes.try_into()?)),
        Kind::U32 => dest.push(u32::from_be_bytes(bytes.try_into()?)),
        Kind::U64 => dest.push(u64::from_be_bytes(bytes.try_into()?)),
        Kind::I8 => dest.push(i8::from_be_bytes(bytes.try_into()?)),
        Kind::I16 => dest.push(i16::from_be_bytes(bytes.try_into()?)),
        Kind::I32 => dest.push(i32::from_be_bytes(bytes.try_into()?)),
        Kind::I64 => dest.push(i64::from_be_bytes(bytes.try_into()?)),
        Kind::String | Kind::Markdown | Kind::Json | Kind::Url => {
            dest.push(std::str::from_utf8(bytes)?);
        }
        Kind::Timestamp { unit, .. } => match unit {
            TimeUnit::Seconds => dest.push(i64::from_be_bytes(bytes.try_into()?)),
        },
        _ => {
            tracing::error!(
                "kind '{:?}' does not support encrypted/compressed",
                field.kind
            );
        }
    }

    Ok(())
}

pub enum Transaction<'a> {
    Read(&'a ReadTransaction<'a>),
    Write(WriteTransaction<'a>),
}

enum Accessor<'a> {
    Const(&'a ConstAccessor<'a>),
    Write(&'a WriteAccessor<'a>),
}

/// For non-many relationships, limit can be 1 or 0 and cursor is never evaluated.
/// ((field idx, limit, cursor), next depth)
#[derive(Clone, Debug)]
#[allow(clippy::type_complexity)]
pub struct RefDepth(pub Vec<((u8, u8, Option<[u8; 16]>), RefDepth)>);

/// used for queryable fields.
#[derive(Clone, Debug)]
pub enum QueryExpression {
    Gte,
    Gt,
    Lte,
    Lt,
    Eq,
    BeginsWith,
}

impl QueryExpression {
    #[must_use]
    pub fn as_byte(&self) -> u8 {
        match self {
            Self::Gte => 0,
            Self::Lte => 1,
            Self::Eq => 2,
            Self::Gt => 3,
            Self::Lt => 4,
            Self::BeginsWith => 5,
        }
    }
}

pub struct OrdinaryStorage {
    /// DB env
    env: Arc<Environment>,

    pub artifact: ArtifactStore,
    pub asset: AssetStore,
    pub cache: CacheStore,
    pub secrets: SecretsStore,
    pub database: DatabaseStore,
}

/// storage mechanism for ordinary applications.
impl OrdinaryStorage {
    #[allow(clippy::too_many_lines)]
    pub fn new(
        limits: StorageLimits,
        model_configs: Vec<DatabaseModelConfig>,
        encryption_key: [u8; 32],
        env: &Arc<Environment>,
        log_sizes: bool,
    ) -> anyhow::Result<Self> {
        Ok(Self {
            env: env.clone(),

            database: DatabaseStore::new(
                limits.database,
                model_configs,
                encryption_key,
                env,
                log_sizes,
            )?,
            artifact: ArtifactStore::new(limits.artifact, env, log_sizes)?,
            asset: AssetStore::new(limits.assets, env, log_sizes)?,
            cache: CacheStore::new(limits.cache, env, log_sizes)?,
            secrets: SecretsStore::new(env, encryption_key)?,
        })
    }

    pub fn stat(&self) -> anyhow::Result<Stat> {
        let stat = self.env.stat()?;
        Ok(stat)
    }

    pub fn write_txn(&self) -> anyhow::Result<WriteTransaction<'_>> {
        let txn = WriteTransaction::new(self.env.clone())?;
        Ok(txn)
    }

    pub fn read_txn(&self) -> anyhow::Result<ReadTransaction<'_>> {
        let txn = ReadTransaction::new(self.env.clone())?;
        Ok(txn)
    }
}