use std::collections::{BTreeMap, BTreeSet};
use std::num::{NonZeroU32, NonZeroUsize};
use haematite::db::ReadOnlyDatabase;
use serde::Serialize;
use serde_json::value::RawValue;
use super::edges::{decode_edge, decode_key, page_start};
use super::reads::{PageLimits, bounded_key_page};
use super::{Captured, Failure, collection_key, document_key, identifier, reads, relations};
use crate::serve::access::Access;
use crate::serve::protocol::{Edge, GraphPage, Node as Record, Unrelated, Version};
pub const MEMBER_NAMES_MOST: usize = 16;
pub const VALUE_BYTES_MOST: usize = 1024;
#[derive(Debug, Serialize)]
pub struct Node {
pub collection: String,
pub document_id: String,
pub members: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub over_bound: Vec<OverBound>,
}
#[derive(Debug, Serialize)]
pub struct OverBound {
pub member: String,
pub bytes: usize,
}
#[derive(Debug)]
pub struct Filtered {
pub edges: Vec<Edge>,
pub nodes: Vec<Node>,
pub next_after: Option<String>,
pub examination_ended: bool,
}
#[must_use]
pub const fn is_filtered(page: &GraphPage) -> bool {
page.relations.is_some()
|| page.collections.is_some()
|| page.members.is_some()
|| page.records.is_some()
|| page.unrelated.is_some()
}
#[derive(Serialize)]
struct Asked {
relations: Option<BTreeSet<String>>,
collections: Option<BTreeSet<String>>,
members: BTreeMap<String, Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
records: Option<BTreeSet<(String, String)>>,
#[serde(skip_serializing_if = "Option::is_none")]
unrelated: Option<(Unrelated, Version)>,
#[serde(skip)]
prefixes: Vec<Vec<u8>>,
#[serde(skip)]
listed: Vec<String>,
}
fn unknown(code: &str, what: &str, name: &str, path: String) -> Failure {
Failure {
path: Some(path),
..Failure::new(
code,
format!("{what} {name} does not exist at the captured version"),
)
}
}
fn collection_exists(
reader: &ReadOnlyDatabase,
at: &Captured,
name: &str,
) -> Result<bool, Failure> {
identifier(name)?;
Ok(reads::get_bytes(reader, at, &collection_key(name))?.is_some())
}
#[derive(Clone, Copy)]
struct Bounds {
examined: usize,
bytes: usize,
}
impl Bounds {
fn of(max_page: NonZeroU32, bytes: NonZeroUsize) -> Result<Self, Failure> {
let examined = usize::try_from(max_page.get())
.ok()
.and_then(|pages| pages.checked_mul(64))
.ok_or_else(|| {
Failure::new("limit_overflow", "graph examination bound is too large")
})?;
Ok(Self {
examined,
bytes: bytes.get(),
})
}
}
fn named_records(
reading: (&ReadOnlyDatabase, &Captured, &Access),
named: &[Record],
bounds: Bounds,
) -> Result<BTreeSet<(String, String)>, Failure> {
let (reader, at, access) = reading;
let most = bounds.examined;
if named.len() > most {
return Err(Failure {
path: Some(format!("/records/{most}")),
..Failure::new(
"graph_records_bound",
format!("at most {most} records may be named, the store's examination bound"),
)
});
}
let length = serde_json::to_vec(named).map_err(Failure::storage)?.len();
if length > bounds.bytes {
return Err(Failure {
path: Some("/records".to_owned()),
..Failure::new(
"graph_records_bytes",
format!(
"the named records are {length} bytes, over the frame's {}",
bounds.bytes
),
)
});
}
let mut records = BTreeSet::new();
for (index, record) in named.iter().enumerate() {
let (collection, id) = (&record.collection, &record.document_id);
if !access.can_read_document(collection, id)
|| !reads::document_exists_for_constraint(reader, at, collection, id)?
{
return Err(Failure {
path: Some(format!("/records/{index}")),
..Failure::new(
"graph_record_unknown",
"a named record does not exist at the captured version",
)
});
}
records.insert((collection.clone(), id.clone()));
}
Ok(records)
}
impl Asked {
fn read(
reader: &ReadOnlyDatabase,
at: &Captured,
access: &Access,
page: &GraphPage,
bounds: Bounds,
) -> Result<Self, Failure> {
let mut from = None;
let relations = match &page.relations {
None => None,
Some(names) => {
let mut froms = BTreeSet::new();
for (index, name) in names.iter().enumerate() {
identifier(name)?;
let declaration = relations::get(reader, at, name)?.ok_or_else(|| {
unknown(
"graph_relation_unknown",
"relation",
name,
format!("/relations/{index}"),
)
})?;
froms.insert(declaration.from_collection);
}
from = Some(froms);
Some(names.iter().cloned().collect::<BTreeSet<_>>())
}
};
let collections = match &page.collections {
None => None,
Some(names) => {
for (index, name) in names.iter().enumerate() {
if !collection_exists(reader, at, name)? {
return Err(unknown(
"graph_collection_unknown",
"collection",
name,
format!("/collections/{index}"),
));
}
}
Some(names.iter().cloned().collect::<BTreeSet<_>>())
}
};
let members = page.members.clone().unwrap_or_default();
members_named(reader, at, &members)?;
let from = match (from, &collections) {
(Some(from), Some(collections)) => Some(
from.intersection(collections)
.cloned()
.collect::<BTreeSet<_>>(),
),
(Some(from), None) => Some(from),
(None, Some(collections)) => Some(collections.clone()),
(None, None) => None,
};
let records = page
.records
.as_deref()
.map(|named| named_records((reader, at, access), named, bounds))
.transpose()?;
let prefixes = match (&records, from) {
(Some(records), from) => records
.iter()
.filter(|(collection, _)| {
from.as_ref().is_none_or(|from| from.contains(collection))
})
.map(|(collection, id)| format!("e\0{collection}\0{id}\0").into_bytes())
.collect(),
(None, Some(from)) => from
.iter()
.map(|collection| format!("e\0{collection}\0").into_bytes())
.collect(),
(None, None) => vec![b"e\0".to_vec()],
};
let (unrelated, listed) = match page.unrelated {
None => (None, Vec::new()),
Some(unrelated) => {
unrelated_within(unrelated, bounds)?;
let listed = match &collections {
Some(collections) => collections.iter().cloned().collect(),
None => every_collection(reader, at)?,
};
(Some((unrelated, at.version)), listed)
}
};
Ok(Self {
relations,
collections,
members,
records,
unrelated,
prefixes,
listed,
})
}
fn wants(&self, edge: &Edge) -> bool {
self.relations
.as_ref()
.is_none_or(|relations| relations.contains(&edge.relation))
&& self.collections.as_ref().is_none_or(|collections| {
collections.contains(&edge.from.collection)
&& collections.contains(&edge.to.collection)
})
&& self.records.as_ref().is_none_or(|records| {
among(records, &edge.from.collection, &edge.from.document_id)
&& among(records, &edge.to.collection, &edge.to.document_id)
})
}
fn fingerprint(&self) -> Result<u64, Failure> {
let bytes = serde_json::to_vec(self).map_err(Failure::storage)?;
Ok(bytes.iter().fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3)
}))
}
}
fn members_named(
reader: &ReadOnlyDatabase,
at: &Captured,
members: &BTreeMap<String, Vec<String>>,
) -> Result<(), Failure> {
for (collection, names) in members {
if !collection_exists(reader, at, collection)? {
return Err(unknown(
"graph_collection_unknown",
"collection",
collection,
format!("/members/{collection}"),
));
}
if names.len() > MEMBER_NAMES_MOST {
return Err(Failure {
path: Some(format!("/members/{collection}/{MEMBER_NAMES_MOST}")),
..Failure::new(
"graph_members_bound",
format!("at most {MEMBER_NAMES_MOST} members may be named per collection"),
)
});
}
if let Some(index) = names.iter().position(String::is_empty) {
return Err(Failure {
path: Some(format!("/members/{collection}/{index}")),
..Failure::new("graph_member_name", "a member name cannot be empty")
});
}
}
Ok(())
}
fn unrelated_within(unrelated: Unrelated, bounds: Bounds) -> Result<(), Failure> {
let most = u64::try_from(bounds.examined).unwrap_or(u64::MAX);
for (member, asked) in [
("per_collection", unrelated.per_collection),
("most", unrelated.most),
] {
if asked.get() > most {
return Err(Failure {
path: Some(format!("/unrelated/{member}")),
..Failure::new(
"graph_unrelated_bound",
format!("at most {most} records may be asked, the store's examination bound"),
)
});
}
}
Ok(())
}
fn every_collection(reader: &ReadOnlyDatabase, at: &Captured) -> Result<Vec<String>, Failure> {
let mut names = BTreeSet::new();
reads::scan_rows(reader, at, b"c\0", 1, |parts, _| {
names.insert(parts[0].to_owned());
Ok(())
})?;
Ok(names.into_iter().collect())
}
fn among(records: &BTreeSet<(String, String)>, collection: &str, id: &str) -> bool {
records.contains(&(collection.to_owned(), id.to_owned()))
}
fn continuation(text: &str, fingerprint: u64) -> Result<(usize, Option<String>), Failure> {
let invalid = || Failure::new("invalid_continuation", "graph continuation is malformed");
let mut parts = text.splitn(3, '.');
let (Some(made), Some(ordinal), Some(after)) = (parts.next(), parts.next(), parts.next())
else {
return Err(invalid());
};
let made = u64::from_str_radix(made, 16).map_err(|_| invalid())?;
if made != fingerprint {
return Err(Failure::new(
"graph_filter_changed",
"a continuation must carry the filter of the page that made it",
));
}
let ordinal = ordinal.parse().map_err(|_| invalid())?;
Ok((ordinal, (!after.is_empty()).then(|| after.to_owned())))
}
fn readable(access: &Access, edge_ends: (&str, &str, &str, &str)) -> bool {
let (from_collection, from_id, to_collection, to_id) = edge_ends;
access.can_read_document(from_collection, from_id)
&& access.can_read_document(to_collection, to_id)
}
struct Scanned {
resume: Option<String>,
overflow: bool,
stopped: Option<String>,
examined: usize,
bytes: usize,
}
struct Reading<'a> {
reader: &'a ReadOnlyDatabase,
at: &'a Captured,
access: &'a Access,
asked: &'a Asked,
}
fn scan_prefix(
reading: &Reading<'_>,
prefix: &[u8],
after: Option<&str>,
limits: PageLimits,
edges: &mut Vec<Edge>,
limit: usize,
) -> Result<Scanned, Failure> {
let Reading {
reader,
at,
access,
asked,
} = *reading;
let page_empty = edges.is_empty();
let mut taken = 0_usize;
let scanned = bounded_key_page(
reader,
at,
prefix,
&page_start(prefix, after)?,
limits,
|suffix, bytes| {
let edge = decode_edge(prefix, &suffix, &bytes)?;
let ends = (
edge.from.collection.as_str(),
edge.from.document_id.as_str(),
edge.to.collection.as_str(),
edge.to.document_id.as_str(),
);
let keep = asked.wants(&edge) && readable(access, ends);
Ok((suffix, keep.then_some(edge)))
},
|(_, edge)| {
let Some(edge) = edge else {
return Ok(0);
};
let length = serialized_len(edge)?;
if length > limits.bytes.get() && taken == 0 {
if page_empty {
return Err(Failure::new(
"graph_edge_bytes",
format!(
"edge {} is {length} bytes, over the {} bytes a page has",
address(edge),
limits.bytes
),
));
}
return Err(Failure::new(WIDE, "the next page starts at this edge"));
}
taken = taken.saturating_add(1);
Ok(length)
},
);
let (rows, stopped) = match scanned {
Err(failure) if failure.code == WIDE => {
return Ok(Scanned {
resume: None,
overflow: true,
stopped: None,
examined: 0,
bytes: 0,
});
}
scanned => scanned?,
};
let mut scanned = Scanned {
resume: None,
overflow: false,
stopped,
examined: rows.len(),
bytes: 0,
};
for (suffix, edge) in rows {
let Some(edge) = edge else {
continue;
};
if edges.len() == limit {
scanned.overflow = true;
break;
}
scanned.bytes = scanned.bytes.saturating_add(serialized_len(&edge)?);
edges.push(edge);
scanned.resume = Some(suffix);
}
Ok(scanned)
}
const WIDE: &str = "graph_edge_next_page";
fn address(edge: &Edge) -> String {
format!(
"{} {}/{} -> {}/{}",
edge.relation,
edge.from.collection,
edge.from.document_id,
edge.to.collection,
edge.to.document_id
)
}
fn serialized_len(edge: &Edge) -> Result<usize, Failure> {
serde_json::to_vec(edge)
.map(|bytes| bytes.len())
.map_err(Failure::storage)
}
fn readable_at(access: &Access, prefix: &[u8], key: &str) -> Result<(), Failure> {
let mut whole = prefix.to_vec();
whole.extend_from_slice(key.as_bytes());
let ends = decode_key(&whole)?;
let ends = (
ends.from.collection.as_str(),
ends.from.document_id.as_str(),
ends.to.collection.as_str(),
ends.to.document_id.as_str(),
);
if readable(access, ends) {
Ok(())
} else {
Err(Failure::new(
"access_scan_limit",
"graph examination ended on an edge the caller may not read",
))
}
}
pub fn export(
reader: &ReadOnlyDatabase,
at: &Captured,
access: &Access,
page: &GraphPage,
limits: PageLimits,
max_page: NonZeroU32,
) -> Result<Filtered, Failure> {
let bounds = Bounds::of(max_page, limits.bytes)?;
let asked = Asked::read(reader, at, access, page, bounds)?;
let fingerprint = asked.fingerprint()?;
let (mut ordinal, mut after) = match page.after.as_deref() {
None => (0, None),
Some(text) => continuation(text, fingerprint)?,
};
let mut examined = bounds.examined;
let mut bytes = limits.bytes.get();
let reading = Reading {
reader,
at,
access,
asked: &asked,
};
let mut edges = Vec::new();
let mut next_after = None;
let mut examination_ended = false;
while let Some(prefix) = asked.prefixes.get(ordinal) {
let (Some(rows), Some(budget)) = (NonZeroUsize::new(examined), NonZeroUsize::new(bytes))
else {
examination_ended = true;
next_after = Some(format!(
"{fingerprint:016x}.{ordinal}.{}",
after.take().unwrap_or_default()
));
break;
};
let scanned = scan_prefix(
&reading,
prefix,
after.as_deref(),
PageLimits {
rows,
bytes: budget,
},
&mut edges,
limits.rows.get(),
)?;
examined = examined.saturating_sub(scanned.examined);
bytes = bytes.saturating_sub(scanned.bytes);
if scanned.overflow {
let key = scanned.resume.or_else(|| after.take()).unwrap_or_default();
next_after = Some(format!("{fingerprint:016x}.{ordinal}.{key}"));
break;
}
if let Some(key) = scanned.stopped {
readable_at(access, prefix, &key)?;
examination_ended = true;
next_after = Some(format!("{fingerprint:016x}.{ordinal}.{key}"));
break;
}
ordinal = ordinal.saturating_add(1);
after = None;
}
let mut nodes = nodes(reader, at, &asked, &edges)?;
if next_after.is_none()
&& let Some((unrelated, _version)) = asked.unrelated
{
let listing = Listing {
reading: &reading,
fingerprint,
first: asked.prefixes.len(),
unrelated,
};
let budget = Budget {
examined,
bytes: bytes.saturating_sub(nodes_len(&nodes)?),
};
let listed = list_unrelated(&listing, ordinal, after.as_deref(), budget, &mut nodes)?;
next_after = listed.next_after;
examination_ended = listed.examination_ended;
}
Ok(Filtered {
edges,
nodes,
next_after,
examination_ended,
})
}
fn nodes_len(nodes: &[Node]) -> Result<usize, Failure> {
serde_json::to_vec(nodes)
.map(|bytes| bytes.len())
.map_err(Failure::storage)
}
struct Listing<'a> {
reading: &'a Reading<'a>,
fingerprint: u64,
first: usize,
unrelated: Unrelated,
}
#[derive(Clone, Copy)]
struct Budget {
examined: usize,
bytes: usize,
}
struct Listed {
next_after: Option<String>,
examination_ended: bool,
}
fn listing_place(after: Option<&str>) -> Result<(u64, u64, Option<String>), Failure> {
let invalid = || Failure::new("invalid_continuation", "graph continuation is malformed");
let Some(after) = after else {
return Ok((0, 0, None));
};
let mut parts = after.splitn(3, ':');
let (Some(total), Some(here), Some(id)) = (parts.next(), parts.next(), parts.next()) else {
return Err(invalid());
};
let total = total.parse().map_err(|_| invalid())?;
let here = here.parse().map_err(|_| invalid())?;
Ok((total, here, (!id.is_empty()).then(|| id.to_owned())))
}
fn list_unrelated(
listing: &Listing<'_>,
mut ordinal: usize,
after: Option<&str>,
mut budget: Budget,
nodes: &mut Vec<Node>,
) -> Result<Listed, Failure> {
let Reading {
reader,
at,
access,
asked,
} = *listing.reading;
let (mut total, mut here, mut resume) = if ordinal < listing.first {
(0, 0, None)
} else {
listing_place(after)?
};
ordinal = ordinal.max(listing.first);
let fingerprint = listing.fingerprint;
let place = |ordinal: usize, total: u64, here: u64, id: Option<&str>| {
format!(
"{fingerprint:016x}.{ordinal}.{total}:{here}:{}",
id.unwrap_or_default()
)
};
let (per_collection, most) = (
listing.unrelated.per_collection.get(),
listing.unrelated.most.get(),
);
while let Some(collection) = asked.listed.get(ordinal - listing.first) {
let need = per_collection
.saturating_sub(here)
.min(most.saturating_sub(total));
if most <= total {
break;
}
if need == 0 {
ordinal = ordinal.saturating_add(1);
(here, resume) = (0, None);
continue;
}
let rows = usize::try_from(need)
.unwrap_or(usize::MAX)
.min(budget.examined);
let Some(rows) = NonZeroUsize::new(rows) else {
return Ok(Listed {
next_after: Some(place(ordinal, total, here, resume.as_deref())),
examination_ended: true,
});
};
let prefix = format!("m\0{collection}\0").into_bytes();
let mut from = prefix.clone();
if let Some(id) = &resume {
from.extend_from_slice(id.as_bytes());
from.push(0);
}
let limits = PageLimits {
rows,
bytes: NonZeroUsize::MAX,
};
let (ids, more) = bounded_key_page(
reader,
at,
&prefix,
&from,
limits,
|id, _revision| Ok(id),
|_| Ok(0),
)?;
budget.examined = budget.examined.saturating_sub(ids.len());
for id in ids {
if access.can_read_document(collection, &id) {
let node = node(reader, at, asked, collection, &id)?;
let length = nodes_len(std::slice::from_ref(&node))?;
if length > budget.bytes && nodes.is_empty() {
return Err(Failure::new(
"graph_node_bytes",
format!(
"record {collection}/{id} as a node is {length} bytes, over the {} \
bytes a page has",
budget.bytes
),
));
}
if length > budget.bytes {
return Ok(Listed {
next_after: Some(place(ordinal, total, here, resume.as_deref())),
examination_ended: false,
});
}
budget.bytes = budget.bytes.saturating_sub(length);
nodes.push(node);
here = here.saturating_add(1);
total = total.saturating_add(1);
}
resume = Some(id);
}
if more.is_none() {
ordinal = ordinal.saturating_add(1);
(here, resume) = (0, None);
}
}
Ok(Listed {
next_after: None,
examination_ended: false,
})
}
fn nodes(
reader: &ReadOnlyDatabase,
at: &Captured,
asked: &Asked,
edges: &[Edge],
) -> Result<Vec<Node>, Failure> {
let ends: BTreeSet<(&str, &str)> = edges
.iter()
.flat_map(|edge| {
[
(
edge.from.collection.as_str(),
edge.from.document_id.as_str(),
),
(edge.to.collection.as_str(), edge.to.document_id.as_str()),
]
})
.collect();
let mut nodes = Vec::with_capacity(ends.len());
for (collection, document_id) in ends {
nodes.push(node(reader, at, asked, collection, document_id)?);
}
Ok(nodes)
}
fn node(
reader: &ReadOnlyDatabase,
at: &Captured,
asked: &Asked,
collection: &str,
document_id: &str,
) -> Result<Node, Failure> {
let mut node = Node {
collection: collection.to_owned(),
document_id: document_id.to_owned(),
members: BTreeMap::new(),
over_bound: Vec::new(),
};
if let Some(names) = asked.members.get(collection)
&& let Some(bytes) = reads::get_bytes(reader, at, &document_key(collection, document_id))?
{
let stored: BTreeMap<String, Box<RawValue>> =
serde_json::from_slice(&bytes).map_err(Failure::storage)?;
for name in names {
let Some(value) = stored.get(name) else {
continue;
};
let text = value.get();
if text.len() > VALUE_BYTES_MOST {
node.over_bound.push(OverBound {
member: name.clone(),
bytes: text.len(),
});
} else {
node.members.insert(name.clone(), text.to_owned());
}
}
}
Ok(node)
}
#[cfg(test)]
#[path = "graph_tests.rs"]
mod tests;