Skip to main content

ordinary_storage/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(clippy::all, clippy::pedantic)]
3#![allow(clippy::missing_errors_doc, clippy::cast_sign_loss)]
4
5// Copyright (C) 2026 The Ordinary Authors.
6//
7// SPDX-License-Identifier: BSD-3-Clause
8
9mod stores;
10
11pub use stores::{
12    artifact::{ArtifactKind, ArtifactStore},
13    assets::AssetStore,
14    cache::{CacheDependency, CacheKind, CacheStore, Lookup},
15    database::UpdateError,
16    secrets::SecretsStore,
17};
18
19use flexbuffers::Reader;
20use ordinary_config::{DatabaseModelConfig, StorageLimits};
21use ordinary_types::{Field, Kind, TimeUnit};
22use saferlmdb::{
23    ConstAccessor, Environment, ReadTransaction, Stat, WriteAccessor, WriteTransaction,
24};
25use std::sync::Arc;
26
27pub use bytes;
28use bytes::{BufMut, Bytes, BytesMut};
29
30use crate::stores::database::DatabaseStore;
31pub use saferlmdb;
32
33fn field_to_bytes(field: &Field, reader: &Reader<&[u8]>) -> Bytes {
34    let mut out = BytesMut::new();
35
36    match &field.kind {
37        Kind::Uuid => {
38            out.put(reader.as_blob().0);
39        }
40        Kind::Bool => {
41            if reader.as_bool() {
42                out.put_u8(1);
43            } else {
44                out.put_u8(0);
45            }
46        }
47        Kind::F32 => out.put_f32(reader.as_f32()),
48        Kind::F64 => out.put_f64(reader.as_f64()),
49        Kind::U8 => out.put_u8(reader.as_u8()),
50        Kind::U16 => out.put_u16(reader.as_u16()),
51        Kind::U32 => out.put_u32(reader.as_u32()),
52        Kind::U64 => out.put_u64(reader.as_u64()),
53        Kind::I8 => out.put_i8(reader.as_i8()),
54        Kind::I16 => out.put_i16(reader.as_i16()),
55        Kind::I32 => out.put_i32(reader.as_i32()),
56        Kind::I64 => out.put_i64(reader.as_i64()),
57        Kind::String | Kind::Markdown | Kind::Json | Kind::Url => {
58            out.put(reader.as_str().as_bytes());
59        }
60        Kind::Timestamp { unit, .. } => match unit {
61            TimeUnit::Seconds => out.put_i64(reader.as_i64()),
62        },
63        _ => {
64            tracing::error!(
65                "kind '{:?}' does not support encrypted/compressed",
66                field.kind
67            );
68        }
69    }
70
71    out.into()
72}
73
74fn push_field_from_bytes(
75    field: &Field,
76    bytes: &[u8],
77    dest: &mut flexbuffers::VectorBuilder,
78) -> anyhow::Result<()> {
79    match &field.kind {
80        Kind::Uuid => dest.push(flexbuffers::Blob(bytes)),
81        Kind::Bool => dest.push(if bytes.len() == 1 {
82            bytes[0] == 1
83        } else {
84            false
85        }),
86        Kind::F32 => dest.push(f32::from_be_bytes(bytes.try_into()?)),
87        Kind::F64 => dest.push(f64::from_be_bytes(bytes.try_into()?)),
88        Kind::U8 => dest.push(u8::from_be_bytes(bytes.try_into()?)),
89        Kind::U16 => dest.push(u16::from_be_bytes(bytes.try_into()?)),
90        Kind::U32 => dest.push(u32::from_be_bytes(bytes.try_into()?)),
91        Kind::U64 => dest.push(u64::from_be_bytes(bytes.try_into()?)),
92        Kind::I8 => dest.push(i8::from_be_bytes(bytes.try_into()?)),
93        Kind::I16 => dest.push(i16::from_be_bytes(bytes.try_into()?)),
94        Kind::I32 => dest.push(i32::from_be_bytes(bytes.try_into()?)),
95        Kind::I64 => dest.push(i64::from_be_bytes(bytes.try_into()?)),
96        Kind::String | Kind::Markdown | Kind::Json | Kind::Url => {
97            dest.push(std::str::from_utf8(bytes)?);
98        }
99        Kind::Timestamp { unit, .. } => match unit {
100            TimeUnit::Seconds => dest.push(i64::from_be_bytes(bytes.try_into()?)),
101        },
102        _ => {
103            tracing::error!(
104                "kind '{:?}' does not support encrypted/compressed",
105                field.kind
106            );
107        }
108    }
109
110    Ok(())
111}
112
113pub enum Transaction<'a> {
114    Read(&'a ReadTransaction<'a>),
115    Write(WriteTransaction<'a>),
116}
117
118enum Accessor<'a> {
119    Const(&'a ConstAccessor<'a>),
120    Write(&'a WriteAccessor<'a>),
121}
122
123/// For non-many relationships, limit can be 1 or 0 and cursor is never evaluated.
124/// ((field idx, limit, cursor), next depth)
125#[derive(Clone, Debug)]
126#[allow(clippy::type_complexity)]
127pub struct RefDepth(pub Vec<((u8, u8, Option<[u8; 16]>), RefDepth)>);
128
129/// used for queryable fields.
130#[derive(Clone, Debug)]
131pub enum QueryExpression {
132    Gte,
133    Gt,
134    Lte,
135    Lt,
136    Eq,
137    BeginsWith,
138}
139
140impl QueryExpression {
141    #[must_use]
142    pub fn as_byte(&self) -> u8 {
143        match self {
144            Self::Gte => 0,
145            Self::Lte => 1,
146            Self::Eq => 2,
147            Self::Gt => 3,
148            Self::Lt => 4,
149            Self::BeginsWith => 5,
150        }
151    }
152}
153
154pub struct OrdinaryStorage {
155    /// DB env
156    env: Arc<Environment>,
157
158    pub artifact: ArtifactStore,
159    pub asset: AssetStore,
160    pub cache: CacheStore,
161    pub secrets: SecretsStore,
162    pub database: DatabaseStore,
163}
164
165/// storage mechanism for ordinary applications.
166impl OrdinaryStorage {
167    #[allow(clippy::too_many_lines)]
168    pub fn new(
169        limits: StorageLimits,
170        model_configs: Vec<DatabaseModelConfig>,
171        encryption_key: [u8; 32],
172        env: &Arc<Environment>,
173        log_sizes: bool,
174    ) -> anyhow::Result<Self> {
175        Ok(Self {
176            env: env.clone(),
177
178            database: DatabaseStore::new(
179                limits.database,
180                model_configs,
181                encryption_key,
182                env,
183                log_sizes,
184            )?,
185            artifact: ArtifactStore::new(limits.artifact, env, log_sizes)?,
186            asset: AssetStore::new(limits.assets, env, log_sizes)?,
187            cache: CacheStore::new(limits.cache, env, log_sizes)?,
188            secrets: SecretsStore::new(env, encryption_key)?,
189        })
190    }
191
192    pub fn stat(&self) -> anyhow::Result<Stat> {
193        let stat = self.env.stat()?;
194        Ok(stat)
195    }
196
197    pub fn write_txn(&self) -> anyhow::Result<WriteTransaction<'_>> {
198        let txn = WriteTransaction::new(self.env.clone())?;
199        Ok(txn)
200    }
201
202    pub fn read_txn(&self) -> anyhow::Result<ReadTransaction<'_>> {
203        let txn = ReadTransaction::new(self.env.clone())?;
204        Ok(txn)
205    }
206}