use async_trait::async_trait;
use redb::{ReadableDatabase, ReadableTable, TableDefinition};
use crate::core::StoreError;
use crate::memory::{MemoryItem, MemoryStore, Recall, access_expiry};
use super::redb::{MAX_STR, RedbStore, be, begin_write};
const ITEMS: TableDefinition<(&str, &str, u64), &str> = TableDefinition::new("memory_items");
type SubjectKey<'a> = (&'a str, &'a str, &'a str, i64, &'a str);
type SubjectEntry = (u64, u8);
const BY_SUBJECT: TableDefinition<SubjectKey, SubjectEntry> =
TableDefinition::new("memory_by_subject");
const fn trust_rank(trust: crate::core::Trust) -> u8 {
match trust {
crate::core::Trust::Trusted => 0,
crate::core::Trust::Untrusted => 1,
}
}
const CURRENT: TableDefinition<(&str, &str), (&str, &str, i64, u64)> =
TableDefinition::new("memory_current");
const DERIVED: TableDefinition<(&str, &str, u64, &str, u64), ()> =
TableDefinition::new("memory_derived");
const DERIVED_BY_TARGET: TableDefinition<(&str, &str, u64, &str, u64), ()> =
TableDefinition::new("memory_derived_by_target");
type Edge = (String, u64, String, u64);
fn collect_edges_of_id(
forward: &impl ReadableTable<(&'static str, &'static str, u64, &'static str, u64), ()>,
reverse: &impl ReadableTable<(&'static str, &'static str, u64, &'static str, u64), ()>,
tenant: &str,
id: &str,
out: &mut Vec<Edge>,
) -> Result<(), StoreError> {
for entry in forward
.range((tenant, id, 0, "", 0)..=(tenant, id, u64::MAX, MAX_STR, u64::MAX))
.map_err(|e| be(&e))?
{
let (key, _) = entry.map_err(|e| be(&e))?;
let (_, source_id, source_version, derived_id, derived_version) = key.value();
out.push((
source_id.to_owned(),
source_version,
derived_id.to_owned(),
derived_version,
));
}
for entry in reverse
.range((tenant, id, 0, "", 0)..=(tenant, id, u64::MAX, MAX_STR, u64::MAX))
.map_err(|e| be(&e))?
{
let (key, _) = entry.map_err(|e| be(&e))?;
let (_, derived_id, derived_version, source_id, source_version) = key.value();
out.push((
source_id.to_owned(),
source_version,
derived_id.to_owned(),
derived_version,
));
}
Ok(())
}
fn collect_edges_of_version(
forward: &impl ReadableTable<(&'static str, &'static str, u64, &'static str, u64), ()>,
reverse: &impl ReadableTable<(&'static str, &'static str, u64, &'static str, u64), ()>,
tenant: &str,
id: &str,
version: u64,
out: &mut Vec<Edge>,
) -> Result<(), StoreError> {
for entry in forward
.range((tenant, id, version, "", 0)..=(tenant, id, version, MAX_STR, u64::MAX))
.map_err(|e| be(&e))?
{
let (key, _) = entry.map_err(|e| be(&e))?;
let (_, source_id, source_version, derived_id, derived_version) = key.value();
out.push((
source_id.to_owned(),
source_version,
derived_id.to_owned(),
derived_version,
));
}
for entry in reverse
.range((tenant, id, version, "", 0)..=(tenant, id, version, MAX_STR, u64::MAX))
.map_err(|e| be(&e))?
{
let (key, _) = entry.map_err(|e| be(&e))?;
let (_, derived_id, derived_version, source_id, source_version) = key.value();
out.push((
source_id.to_owned(),
source_version,
derived_id.to_owned(),
derived_version,
));
}
Ok(())
}
const FORGOTTEN: TableDefinition<(&str, &str), ()> = TableDefinition::new("memory_forgotten");
const HOLDS: TableDefinition<(&str, &str), ()> = TableDefinition::new("memory_legal_holds");
const ACCESS_EXPIRY: TableDefinition<(&str, &str), i64> =
TableDefinition::new("memory_access_expiry");
#[async_trait]
impl MemoryStore for RedbStore {
fn tenant(&self) -> &str {
self.tenant_str()
}
#[allow(clippy::too_many_lines)]
async fn remember(&self, item: &MemoryItem) -> Result<u64, StoreError> {
let tenant = self.tenant_name();
let id = item.id.clone();
let subject = item.subject.clone();
let purpose = item.purpose.clone();
let created = item.created_at.unix_timestamp();
let mut item = item.clone();
self.with_db(move |db| {
let w = begin_write(db)?;
let version = {
let mut items = w.open_table(ITEMS).map_err(|e| be(&e))?;
let mut current = w.open_table(CURRENT).map_err(|e| be(&e))?;
let mut by_subject = w.open_table(BY_SUBJECT).map_err(|e| be(&e))?;
let previous = current
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.map(|v| {
let (s, p, c, ver) = v.value();
(s.to_owned(), p.to_owned(), c, ver)
});
if previous.is_none()
&& w.open_table(FORGOTTEN)
.map_err(|e| be(&e))?
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.is_some()
{
return Err(StoreError::Backend(format!(
"memory id '{id}' was forgotten and cannot be reused"
)));
}
if let Some((previous_subject, previous_purpose, _, _)) = &previous
&& (previous_subject != &subject || previous_purpose != &purpose)
{
return Err(StoreError::Backend(format!(
"memory id '{id}' is scoped to subject '{previous_subject}' and purpose \
'{previous_purpose}'; use a new id instead of moving it to subject \
'{subject}' and purpose '{purpose}'"
)));
}
for source in &item.derived_from {
let raw = items
.get((tenant.as_str(), source.id.as_str(), source.version))
.map_err(|e| be(&e))?
.map(|raw| raw.value().to_owned())
.ok_or_else(|| {
StoreError::Backend(format!(
"derived memory '{id}' names missing source '{}' version {}",
source.id, source.version
))
})?;
let source_item: MemoryItem = serde_json::from_str(&raw)
.map_err(|e| StoreError::Backend(e.to_string()))?;
if source_item.selection_digest() != source.digest {
return Err(StoreError::Backend(format!(
"derived memory '{id}' names a changed source '{}' version {}",
source.id, source.version
)));
}
if source_item.subject != subject {
return Err(StoreError::Backend(format!(
"derived memory '{id}' must stay in source subject '{}' rather than \
'{subject}'",
source_item.subject
)));
}
}
let version = previous.as_ref().map_or(1, |(_, _, _, v)| v + 1);
item.version = version;
item.superseded_at = None;
if let Some((s, p, c, previous_version)) = &previous {
by_subject
.remove((tenant.as_str(), s.as_str(), p.as_str(), -*c, id.as_str()))
.map_err(|e| be(&e))?;
let previous_json = items
.get((tenant.as_str(), id.as_str(), *previous_version))
.map_err(|e| be(&e))?
.map(|raw| raw.value().to_owned());
if let Some(previous_json) = previous_json {
let mut superseded: MemoryItem = serde_json::from_str(&previous_json)
.map_err(|e| StoreError::Backend(e.to_string()))?;
superseded.superseded_at = Some(item.created_at);
let json = serde_json::to_string(&superseded)
.map_err(|e| StoreError::Backend(e.to_string()))?;
items
.insert(
(tenant.as_str(), id.as_str(), *previous_version),
json.as_str(),
)
.map_err(|e| be(&e))?;
}
}
let json =
serde_json::to_string(&item).map_err(|e| StoreError::Backend(e.to_string()))?;
items
.insert((tenant.as_str(), id.as_str(), version), json.as_str())
.map_err(|e| be(&e))?;
current
.insert(
(tenant.as_str(), id.as_str()),
(subject.as_str(), purpose.as_str(), created, version),
)
.map_err(|e| be(&e))?;
by_subject
.insert(
(
tenant.as_str(),
subject.as_str(),
purpose.as_str(),
-created,
id.as_str(),
),
(version, trust_rank(item.trust)),
)
.map_err(|e| be(&e))?;
let mut derived = w.open_table(DERIVED).map_err(|e| be(&e))?;
let mut derived_rev = w.open_table(DERIVED_BY_TARGET).map_err(|e| be(&e))?;
for source in &item.derived_from {
derived
.insert(
(
tenant.as_str(),
source.id.as_str(),
source.version,
id.as_str(),
version,
),
(),
)
.map_err(|e| be(&e))?;
derived_rev
.insert(
(
tenant.as_str(),
id.as_str(),
version,
source.id.as_str(),
source.version,
),
(),
)
.map_err(|e| be(&e))?;
}
drop(derived_rev);
drop(derived);
let mut access = w.open_table(ACCESS_EXPIRY).map_err(|e| be(&e))?;
match item.access_retention_seconds {
Some(window) => {
let expiry = access_expiry(item.created_at, window);
let prior = access
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.map_or(i64::MIN, |value| value.value());
if expiry > prior {
access
.insert((tenant.as_str(), id.as_str()), expiry)
.map_err(|e| be(&e))?;
}
}
None => {
access
.remove((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?;
}
}
version
};
w.commit().map_err(|e| be(&e))?;
Ok(version)
})
.await
}
async fn recall(&self, query: &Recall) -> Result<Vec<MemoryItem>, StoreError> {
let tenant = self.tenant_name();
let subject = query.subject.clone();
let purpose = query.purpose.clone();
let limit = query.limit;
let as_of = query.as_of;
self.with_db(move |db| {
let r = db.begin_read().map_err(|e| be(&e))?;
let Ok(by_subject) = r.open_table(BY_SUBJECT) else {
return Ok(Vec::new());
};
let Ok(items) = r.open_table(ITEMS) else {
return Ok(Vec::new());
};
let access = r.open_table(ACCESS_EXPIRY).ok();
let (from, to) = match &purpose {
Some(p) => (
(tenant.as_str(), subject.as_str(), p.as_str(), i64::MIN, ""),
(
tenant.as_str(),
subject.as_str(),
p.as_str(),
i64::MAX,
MAX_STR,
),
),
None => (
(tenant.as_str(), subject.as_str(), "", i64::MIN, ""),
(
tenant.as_str(),
subject.as_str(),
MAX_STR,
i64::MAX,
MAX_STR,
),
),
};
let mut keys: Vec<(u8, i64, String, u64)> = Vec::new();
for entry in by_subject.range(from..=to).map_err(|e| be(&e))? {
let (k, v) = entry.map_err(|e| be(&e))?;
let (version, rank) = v.value();
let (_, _, _, neg_created, id) = k.value();
keys.push((rank, neg_created, id.to_owned(), version));
}
keys.sort_unstable_by(|a, b| (a.0, a.1, a.2.as_str()).cmp(&(b.0, b.1, b.2.as_str())));
let mut out: Vec<MemoryItem> = Vec::new();
for (_, _, id, version) in keys {
if out.len() >= limit {
break;
}
let Some(raw) = items
.get((tenant.as_str(), id.as_str(), version))
.map_err(|e| be(&e))?
else {
continue;
};
let item: MemoryItem = serde_json::from_str(raw.value())
.map_err(|e| StoreError::Backend(e.to_string()))?;
let access_expiry = access
.as_ref()
.and_then(|table| table.get((tenant.as_str(), id.as_str())).ok().flatten())
.and_then(|value| {
crate::core::Timestamp::from_unix_timestamp(value.value()).ok()
});
let effective = match (item.expires_at, access_expiry) {
(Some(left), Some(right)) => Some(left.min(right)),
(left, right) => left.or(right),
};
if as_of.is_some_and(|at| effective.is_some_and(|expires| expires <= at)) {
continue;
}
out.push(item);
}
Ok(out)
})
.await
}
async fn subject_ids(&self, subject: &str) -> Result<Vec<String>, StoreError> {
let tenant = self.tenant_name();
let subject = subject.to_owned();
self.with_db(move |db| {
let r = db.begin_read().map_err(|e| be(&e))?;
let Ok(by_subject) = r.open_table(BY_SUBJECT) else {
return Ok(Vec::new());
};
let mut ids = std::collections::BTreeSet::new();
for entry in by_subject
.range(
(tenant.as_str(), subject.as_str(), "", i64::MIN, "")
..=(
tenant.as_str(),
subject.as_str(),
MAX_STR,
i64::MAX,
MAX_STR,
),
)
.map_err(|e| be(&e))?
{
let (key, _) = entry.map_err(|e| be(&e))?;
ids.insert(key.value().4.to_owned());
}
Ok(ids.into_iter().collect())
})
.await
}
async fn version(&self, id: &str, version: u64) -> Result<Option<MemoryItem>, StoreError> {
let tenant = self.tenant_name();
let id = id.to_owned();
self.with_db(move |db| {
let r = db.begin_read().map_err(|e| be(&e))?;
let Ok(items) = r.open_table(ITEMS) else {
return Ok(None);
};
let Some(raw) = items
.get((tenant.as_str(), id.as_str(), version))
.map_err(|e| be(&e))?
else {
return Ok(None);
};
serde_json::from_str(raw.value())
.map(Some)
.map_err(|e| StoreError::Backend(e.to_string()))
})
.await
}
async fn current(
&self,
id: &str,
as_of: Option<crate::core::Timestamp>,
) -> Result<Option<MemoryItem>, StoreError> {
let tenant = self.tenant_name();
let id = id.to_owned();
self.with_db(move |db| {
let r = db.begin_read().map_err(|e| be(&e))?;
let Ok(current) = r.open_table(CURRENT) else {
return Ok(None);
};
let Some(version) = current
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.map(|v| v.value().3)
else {
return Ok(None);
};
let items = r.open_table(ITEMS).map_err(|e| be(&e))?;
let Some(row) = items
.get((tenant.as_str(), id.as_str(), version))
.map_err(|e| be(&e))?
else {
return Ok(None);
};
let item: MemoryItem = serde_json::from_str(row.value())
.map_err(|e| StoreError::Backend(e.to_string()))?;
let access_expiry = r
.open_table(ACCESS_EXPIRY)
.ok()
.and_then(|table| {
table
.get((tenant.as_str(), id.as_str()))
.ok()
.flatten()
.map(|value| value.value())
})
.and_then(|value| crate::core::Timestamp::from_unix_timestamp(value).ok());
let effective = match (item.expires_at, access_expiry) {
(Some(left), Some(right)) => Some(left.min(right)),
(left, right) => left.or(right),
};
if as_of.is_some_and(|at| effective.is_some_and(|expires| expires <= at)) {
return Ok(None);
}
Ok(Some(item))
})
.await
}
async fn derivatives(&self, id: &str) -> Result<Vec<MemoryItem>, StoreError> {
let tenant = self.tenant_name();
let source = id.to_owned();
self.with_db(move |db| {
let r = db.begin_read().map_err(|e| be(&e))?;
let Ok(edges) = r.open_table(DERIVED) else {
return Ok(Vec::new());
};
let Ok(current) = r.open_table(CURRENT) else {
return Ok(Vec::new());
};
let Ok(items) = r.open_table(ITEMS) else {
return Ok(Vec::new());
};
let mut out = Vec::new();
let mut seen = std::collections::BTreeSet::new();
for e in edges
.range(
(tenant.as_str(), source.as_str(), 0, "", 0)
..=(
tenant.as_str(),
source.as_str(),
u64::MAX,
MAX_STR,
u64::MAX,
),
)
.map_err(|e| be(&e))?
{
let (k, _) = e.map_err(|e| be(&e))?;
let (_, _, _, derived_id, derived_version) = k.value();
let Some(v) = current
.get((tenant.as_str(), derived_id))
.map_err(|e| be(&e))?
else {
continue;
};
let version = v.value().3;
if version != derived_version || !seen.insert(derived_id.to_owned()) {
continue;
}
let Some(raw) = items
.get((tenant.as_str(), derived_id, version))
.map_err(|e| be(&e))?
else {
continue;
};
out.push(
serde_json::from_str(raw.value())
.map_err(|e| StoreError::Backend(e.to_string()))?,
);
}
Ok(out)
})
.await
}
#[allow(clippy::too_many_lines)]
async fn forget_cascading(&self, id: &str) -> Result<crate::memory::Cascade, StoreError> {
let tenant = self.tenant_name();
let root = id.to_owned();
self.with_db(move |db| {
let w = begin_write(db)?;
let removed = {
let mut items = w.open_table(ITEMS).map_err(|e| be(&e))?;
let mut current = w.open_table(CURRENT).map_err(|e| be(&e))?;
let mut by_subject = w.open_table(BY_SUBJECT).map_err(|e| be(&e))?;
let mut edges = w.open_table(DERIVED).map_err(|e| be(&e))?;
let mut edges_rev = w.open_table(DERIVED_BY_TARGET).map_err(|e| be(&e))?;
let mut forgotten = w.open_table(FORGOTTEN).map_err(|e| be(&e))?;
let mut id_queue = vec![root];
let mut version_queue: Vec<(String, u64)> = Vec::new();
let mut doomed = std::collections::BTreeSet::new();
let mut doomed_versions: std::collections::BTreeSet<(String, u64)> =
std::collections::BTreeSet::new();
loop {
if let Some(source) = id_queue.pop() {
if !doomed.insert(source.clone()) {
continue;
}
for entry in edges
.range(
(tenant.as_str(), source.as_str(), 0, "", 0)
..=(
tenant.as_str(),
source.as_str(),
u64::MAX,
MAX_STR,
u64::MAX,
),
)
.map_err(|e| be(&e))?
{
let (key, _) = entry.map_err(|e| be(&e))?;
let (_, _, _, derived_id, derived_version) = key.value();
version_queue.push((derived_id.to_owned(), derived_version));
}
} else if let Some((derived_id, derived_version)) = version_queue.pop() {
if doomed.contains(&derived_id) {
continue;
}
let current_version = current
.get((tenant.as_str(), derived_id.as_str()))
.map_err(|e| be(&e))?
.map(|value| value.value().3);
if current_version == Some(derived_version) {
id_queue.push(derived_id);
} else {
if !doomed_versions.insert((derived_id.clone(), derived_version)) {
continue;
}
for entry in edges
.range(
(tenant.as_str(), derived_id.as_str(), derived_version, "", 0)
..=(
tenant.as_str(),
derived_id.as_str(),
derived_version,
MAX_STR,
u64::MAX,
),
)
.map_err(|e| be(&e))?
{
let (key, _) = entry.map_err(|e| be(&e))?;
let (_, _, _, next_id, next_version) = key.value();
version_queue.push((next_id.to_owned(), next_version));
}
}
} else {
break;
}
}
let holds = w.open_table(HOLDS).map_err(|e| be(&e))?;
let mut access = w.open_table(ACCESS_EXPIRY).map_err(|e| be(&e))?;
let held_candidates = doomed
.iter()
.cloned()
.chain(doomed_versions.iter().map(|(id, _)| id.clone()));
for memory_id in held_candidates {
if holds
.get((tenant.as_str(), memory_id.as_str()))
.map_err(|e| be(&e))?
.is_some()
{
return Err(StoreError::UnderLegalHold { id: memory_id });
}
}
let mut erased = Vec::new();
for memory_id in &doomed {
let previous = current
.get((tenant.as_str(), memory_id.as_str()))
.map_err(|e| be(&e))?
.map(|value| {
let (subject, purpose, created, version) = value.value();
(subject.to_owned(), purpose.to_owned(), created, version)
});
if let Some((subject, purpose, created, _)) = &previous {
by_subject
.remove((
tenant.as_str(),
subject.as_str(),
purpose.as_str(),
-*created,
memory_id.as_str(),
))
.map_err(|e| be(&e))?;
}
current
.remove((tenant.as_str(), memory_id.as_str()))
.map_err(|e| be(&e))?;
forgotten
.insert((tenant.as_str(), memory_id.as_str()), ())
.map_err(|e| be(&e))?;
access
.remove((tenant.as_str(), memory_id.as_str()))
.map_err(|e| be(&e))?;
let versions: Vec<u64> = items
.range(
(tenant.as_str(), memory_id.as_str(), 0)
..=(tenant.as_str(), memory_id.as_str(), u64::MAX),
)
.map_err(|e| be(&e))?
.map(|entry| {
entry
.map(|(key, _)| key.value().2)
.map_err(|error| be(&error))
})
.collect::<Result<_, _>>()?;
if previous.is_some() || !versions.is_empty() {
let highest = versions
.iter()
.copied()
.chain(previous.as_ref().map(|(.., version)| *version))
.max()
.unwrap_or(0);
erased.push((memory_id.clone(), highest));
}
for version in versions {
items
.remove((tenant.as_str(), memory_id.as_str(), version))
.map_err(|e| be(&e))?;
}
}
let mut partly: std::collections::BTreeMap<&str, Vec<u64>> =
std::collections::BTreeMap::new();
for (memory_id, version) in &doomed_versions {
if doomed.contains(memory_id) {
continue;
}
if items
.remove((tenant.as_str(), memory_id.as_str(), *version))
.map_err(|e| be(&e))?
.is_some()
{
partly.entry(memory_id.as_str()).or_default().push(*version);
}
}
let trimmed: Vec<(String, Vec<u64>)> = partly
.into_iter()
.map(|(id, mut versions)| {
versions.sort_unstable();
versions.dedup();
((*id).to_owned(), versions)
})
.collect();
let mut stale: Vec<(String, u64, String, u64)> = Vec::new();
for memory_id in &doomed {
collect_edges_of_id(&edges, &edges_rev, &tenant, memory_id, &mut stale)?;
}
for (memory_id, version) in &doomed_versions {
if doomed.contains(memory_id) {
continue;
}
collect_edges_of_version(
&edges, &edges_rev, &tenant, memory_id, *version, &mut stale,
)?;
}
for (source_id, source_version, derived_id, derived_version) in stale {
edges
.remove((
tenant.as_str(),
source_id.as_str(),
source_version,
derived_id.as_str(),
derived_version,
))
.map_err(|e| be(&e))?;
edges_rev
.remove((
tenant.as_str(),
derived_id.as_str(),
derived_version,
source_id.as_str(),
source_version,
))
.map_err(|e| be(&e))?;
}
crate::memory::Cascade { erased, trimmed }
};
w.commit().map_err(|e| be(&e))?;
Ok(removed)
})
.await
}
async fn forget(&self, id: &str) -> Result<(), StoreError> {
let tenant = self.tenant_name();
let id = id.to_owned();
self.with_db(move |db| {
let w = begin_write(db)?;
{
if w.open_table(HOLDS)
.map_err(|e| be(&e))?
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.is_some()
{
return Err(StoreError::UnderLegalHold { id });
}
let mut items = w.open_table(ITEMS).map_err(|e| be(&e))?;
let mut current = w.open_table(CURRENT).map_err(|e| be(&e))?;
let mut by_subject = w.open_table(BY_SUBJECT).map_err(|e| be(&e))?;
let previous = current
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.map(|v| {
let (s, p, c, ver) = v.value();
(s.to_owned(), p.to_owned(), c, ver)
});
if let Some(v) = &previous {
by_subject
.remove((
tenant.as_str(),
v.0.as_str(),
v.1.as_str(),
-v.2,
id.as_str(),
))
.map_err(|e| be(&e))?;
}
current
.remove((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?;
w.open_table(ACCESS_EXPIRY)
.map_err(|e| be(&e))?
.remove((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?;
if previous.is_some() {
w.open_table(FORGOTTEN)
.map_err(|e| be(&e))?
.insert((tenant.as_str(), id.as_str()), ())
.map_err(|e| be(&e))?;
}
let doomed: Vec<u64> = items
.range(
(tenant.as_str(), id.as_str(), 0)
..=(tenant.as_str(), id.as_str(), u64::MAX),
)
.map_err(|e| be(&e))?
.map(|e| e.map(|(k, _)| k.value().2).map_err(|e| be(&e)))
.collect::<Result<_, _>>()?;
for version in doomed {
items
.remove((tenant.as_str(), id.as_str(), version))
.map_err(|e| be(&e))?;
}
}
w.commit().map_err(|e| be(&e))?;
Ok(())
})
.await
}
#[allow(clippy::too_many_lines)]
async fn forget_subject(&self, subject: &str) -> Result<usize, StoreError> {
let tenant = self.tenant_name();
let subject = subject.to_owned();
self.with_db(move |db| {
let w = begin_write(db)?;
let count = {
let mut items = w.open_table(ITEMS).map_err(|e| be(&e))?;
let mut current = w.open_table(CURRENT).map_err(|e| be(&e))?;
let mut by_subject = w.open_table(BY_SUBJECT).map_err(|e| be(&e))?;
let mut edges = w.open_table(DERIVED).map_err(|e| be(&e))?;
let mut edges_rev = w.open_table(DERIVED_BY_TARGET).map_err(|e| be(&e))?;
let mut forgotten = w.open_table(FORGOTTEN).map_err(|e| be(&e))?;
let mut access = w.open_table(ACCESS_EXPIRY).map_err(|e| be(&e))?;
let holds = w.open_table(HOLDS).map_err(|e| be(&e))?;
let ids: Vec<String> = by_subject
.range(
(tenant.as_str(), subject.as_str(), "", i64::MIN, "")
..=(
tenant.as_str(),
subject.as_str(),
MAX_STR,
i64::MAX,
MAX_STR,
),
)
.map_err(|e| be(&e))?
.map(|entry| {
entry
.map(|(key, _)| key.value().4.to_owned())
.map_err(|error| be(&error))
})
.collect::<Result<_, _>>()?;
for id in &ids {
if holds
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.is_some()
{
return Err(StoreError::UnderLegalHold { id: id.clone() });
}
}
for id in &ids {
let previous = current
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.map(|value| {
let (scope, purpose, created, _) = value.value();
(scope.to_owned(), purpose.to_owned(), created)
});
if let Some((scope, purpose, created)) = previous {
by_subject
.remove((
tenant.as_str(),
scope.as_str(),
purpose.as_str(),
-created,
id.as_str(),
))
.map_err(|e| be(&e))?;
}
current
.remove((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?;
forgotten
.insert((tenant.as_str(), id.as_str()), ())
.map_err(|e| be(&e))?;
access
.remove((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?;
let incoming: Vec<(String, u64, u64)> = edges_rev
.range(
(tenant.as_str(), id.as_str(), 0, "", 0)
..=(tenant.as_str(), id.as_str(), u64::MAX, MAX_STR, u64::MAX),
)
.map_err(|e| be(&e))?
.map(|entry| {
entry
.map(|(key, _)| {
let (_, _, derived_version, source_id, source_version) =
key.value();
(source_id.to_owned(), source_version, derived_version)
})
.map_err(|error| be(&error))
})
.collect::<Result<_, StoreError>>()?;
for (source_id, source_version, derived_version) in incoming {
edges
.remove((
tenant.as_str(),
source_id.as_str(),
source_version,
id.as_str(),
derived_version,
))
.map_err(|e| be(&e))?;
edges_rev
.remove((
tenant.as_str(),
id.as_str(),
derived_version,
source_id.as_str(),
source_version,
))
.map_err(|e| be(&e))?;
}
let versions: Vec<u64> = items
.range(
(tenant.as_str(), id.as_str(), 0)
..=(tenant.as_str(), id.as_str(), u64::MAX),
)
.map_err(|e| be(&e))?
.map(|entry| {
entry
.map(|(key, _)| key.value().2)
.map_err(|error| be(&error))
})
.collect::<Result<_, _>>()?;
for version in versions {
items
.remove((tenant.as_str(), id.as_str(), version))
.map_err(|e| be(&e))?;
}
}
ids.len()
};
w.commit().map_err(|e| be(&e))?;
Ok(count)
})
.await
}
async fn set_legal_hold(&self, id: &str, held: bool) -> Result<(), StoreError> {
let tenant = self.tenant_name();
let id = id.to_owned();
self.with_db(move |db| {
let w = begin_write(db)?;
{
let current = w.open_table(CURRENT).map_err(|e| be(&e))?;
if held
&& current
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.is_none()
{
return Err(StoreError::Backend(format!(
"cannot hold missing memory '{id}'"
)));
}
let mut holds = w.open_table(HOLDS).map_err(|e| be(&e))?;
if held {
holds
.insert((tenant.as_str(), id.as_str()), ())
.map_err(|e| be(&e))?;
} else {
holds
.remove((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?;
}
}
w.commit().map_err(|e| be(&e))
})
.await
}
async fn legal_hold(&self, id: &str) -> Result<bool, StoreError> {
let tenant = self.tenant_name();
let id = id.to_owned();
self.with_db(move |db| {
let r = db.begin_read().map_err(|e| be(&e))?;
let Ok(holds) = r.open_table(HOLDS) else {
return Ok(false);
};
holds
.get((tenant.as_str(), id.as_str()))
.map(|value| value.is_some())
.map_err(|e| be(&e))
})
.await
}
async fn legal_holds(
&self,
after: Option<&str>,
limit: usize,
) -> Result<Vec<String>, StoreError> {
let tenant = self.tenant_name();
let after = after.map(ToOwned::to_owned);
self.with_db(move |db| {
let r = db.begin_read().map_err(|e| be(&e))?;
let Ok(holds) = r.open_table(HOLDS) else {
return Ok(Vec::new());
};
let lo = after.as_deref().unwrap_or("");
let mut out = Vec::new();
for e in holds
.range((tenant.as_str(), lo)..=(tenant.as_str(), MAX_STR))
.map_err(|e| be(&e))?
{
if out.len() >= limit {
break;
}
let (k, _) = e.map_err(|e| be(&e))?;
let id = k.value().1;
if after.as_deref() == Some(id) {
continue;
}
out.push(id.to_owned());
}
Ok(out)
})
.await
}
#[allow(clippy::too_many_lines)]
async fn sweep_expired(
&self,
at: crate::core::Timestamp,
) -> Result<Vec<(String, u64)>, StoreError> {
let tenant = self.tenant_name();
self.with_db(move |db| {
let w = begin_write(db)?;
let removed = {
let mut items = w.open_table(ITEMS).map_err(|e| be(&e))?;
let mut current = w.open_table(CURRENT).map_err(|e| be(&e))?;
let mut by_subject = w.open_table(BY_SUBJECT).map_err(|e| be(&e))?;
let mut forgotten = w.open_table(FORGOTTEN).map_err(|e| be(&e))?;
let holds = w.open_table(HOLDS).map_err(|e| be(&e))?;
let mut access = w.open_table(ACCESS_EXPIRY).map_err(|e| be(&e))?;
let entries: Vec<(String, String, String, i64, u64)> = current
.range((tenant.as_str(), "")..=(tenant.as_str(), MAX_STR))
.map_err(|e| be(&e))?
.map(|entry| {
entry
.map(|(key, value)| {
let (_, id) = key.value();
let (subject, purpose, created, version) = value.value();
(
id.to_owned(),
subject.to_owned(),
purpose.to_owned(),
created,
version,
)
})
.map_err(|e| be(&e))
})
.collect::<Result<_, _>>()?;
let mut expired = Vec::new();
for (id, subject, purpose, created, version) in entries {
if holds
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.is_some()
{
continue;
}
let Some(raw) = items
.get((tenant.as_str(), id.as_str(), version))
.map_err(|e| be(&e))?
else {
continue;
};
let item: MemoryItem = serde_json::from_str(raw.value())
.map_err(|e| StoreError::Backend(e.to_string()))?;
let access_expiry = access
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.and_then(|value| {
crate::core::Timestamp::from_unix_timestamp(value.value()).ok()
});
let effective = match (item.expires_at, access_expiry) {
(Some(left), Some(right)) => Some(left.min(right)),
(left, right) => left.or(right),
};
if effective.is_some_and(|expires| expires <= at) {
expired.push((id, subject, purpose, created, version));
}
}
for (id, subject, purpose, created, _) in &expired {
by_subject
.remove((
tenant.as_str(),
subject.as_str(),
purpose.as_str(),
-*created,
id.as_str(),
))
.map_err(|e| be(&e))?;
current
.remove((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?;
forgotten
.insert((tenant.as_str(), id.as_str()), ())
.map_err(|e| be(&e))?;
access
.remove((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?;
let versions: Vec<u64> = items
.range(
(tenant.as_str(), id.as_str(), 0)
..=(tenant.as_str(), id.as_str(), u64::MAX),
)
.map_err(|e| be(&e))?
.map(|entry| {
entry
.map(|(key, _)| key.value().2)
.map_err(|error| be(&error))
})
.collect::<Result<_, _>>()?;
for version in versions {
items
.remove((tenant.as_str(), id.as_str(), version))
.map_err(|e| be(&e))?;
}
}
expired
.into_iter()
.map(|(id, .., version)| (id, version))
.collect::<Vec<_>>()
};
w.commit().map_err(|e| be(&e))?;
Ok(removed)
})
.await
}
async fn touch(&self, ids: &[String], at: crate::core::Timestamp) -> Result<(), StoreError> {
let tenant = self.tenant_name();
let ids = ids.to_vec();
self.with_db(move |db| {
let w = begin_write(db)?;
{
let current = w.open_table(CURRENT).map_err(|e| be(&e))?;
let items = w.open_table(ITEMS).map_err(|e| be(&e))?;
let mut access = w.open_table(ACCESS_EXPIRY).map_err(|e| be(&e))?;
for id in &ids {
let Some(pointer) = current
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
else {
continue;
};
let version = pointer.value().3;
let Some(raw) = items
.get((tenant.as_str(), id.as_str(), version))
.map_err(|e| be(&e))?
else {
continue;
};
let item: MemoryItem = serde_json::from_str(raw.value())
.map_err(|e| StoreError::Backend(e.to_string()))?;
let Some(window) = item.access_retention_seconds else {
continue;
};
let expiry = access_expiry(at, window);
let prior = access
.get((tenant.as_str(), id.as_str()))
.map_err(|e| be(&e))?
.map_or(i64::MIN, |value| value.value());
if expiry > prior {
access
.insert((tenant.as_str(), id.as_str()), expiry)
.map_err(|e| be(&e))?;
}
}
}
w.commit().map_err(|e| be(&e))
})
.await
}
}