use std::borrow::Cow;
use anyhow::Result;
use storekey::{BorrowDecode, BorrowReader, Encode};
use crate::catalog::{DatabaseId, NamespaceId};
use crate::expr::dir::Dir;
use crate::key::category::{Categorise, Category};
use crate::kvs::{KVKey, impl_kv_key_storekey};
use crate::val::{RecordId, RecordIdKey, TableName};
#[derive(Clone, Debug, Eq, PartialEq, Encode, BorrowDecode)]
#[storekey(format = "()")]
struct Prefix<'a> {
__: u8,
_a: u8,
pub ns: NamespaceId,
_b: u8,
pub db: DatabaseId,
_c: u8,
pub tb: Cow<'a, TableName>,
_d: u8,
pub id: RecordIdKey,
}
impl_kv_key_storekey!(Prefix<'_> => Vec<u8>);
impl<'a> Prefix<'a> {
fn new(ns: NamespaceId, db: DatabaseId, tb: &'a TableName, id: &RecordIdKey) -> Self {
Self {
__: b'/',
_a: b'*',
ns,
_b: b'*',
db,
_c: b'*',
tb: Cow::Borrowed(tb),
_d: b'~',
id: id.to_owned(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Encode, BorrowDecode)]
#[storekey(format = "()")]
struct PrefixEg<'a> {
__: u8,
_a: u8,
pub ns: NamespaceId,
_b: u8,
pub db: DatabaseId,
_c: u8,
pub tb: Cow<'a, TableName>,
_d: u8,
pub id: RecordIdKey,
pub eg: Dir,
}
impl_kv_key_storekey!(PrefixEg<'_> => Vec<u8>);
impl<'a> PrefixEg<'a> {
fn new(ns: NamespaceId, db: DatabaseId, tb: &'a TableName, id: &RecordIdKey, eg: Dir) -> Self {
Self {
__: b'/',
_a: b'*',
ns,
_b: b'*',
db,
_c: b'*',
tb: Cow::Borrowed(tb),
_d: b'~',
id: id.clone(),
eg,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Encode, BorrowDecode)]
#[storekey(format = "()")]
struct PrefixFt<'a> {
__: u8,
_a: u8,
pub ns: NamespaceId,
_b: u8,
pub db: DatabaseId,
_c: u8,
pub tb: Cow<'a, TableName>,
_d: u8,
pub id: RecordIdKey,
pub eg: Dir,
pub ft: Cow<'a, str>,
}
impl_kv_key_storekey!(PrefixFt<'_> => Vec<u8>);
impl<'a> PrefixFt<'a> {
fn new(
ns: NamespaceId,
db: DatabaseId,
tb: &'a TableName,
id: &RecordIdKey,
eg: Dir,
ft: &'a str,
) -> Self {
Self {
__: b'/',
_a: b'*',
ns,
_b: b'*',
db,
_c: b'*',
tb: Cow::Borrowed(tb),
_d: b'~',
id: id.to_owned(),
eg,
ft: Cow::Borrowed(ft),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Encode, BorrowDecode)]
#[storekey(format = "()")]
pub(crate) struct Graph<'a> {
__: u8,
_a: u8,
pub ns: NamespaceId,
_b: u8,
pub db: DatabaseId,
_c: u8,
pub tb: Cow<'a, TableName>,
_d: u8,
pub id: RecordIdKey,
pub eg: Dir,
pub ft: Cow<'a, TableName>,
pub fk: Cow<'a, RecordIdKey>,
}
impl_kv_key_storekey!(Graph<'_> => ());
#[derive(Clone, Debug, Eq, PartialEq, Encode, BorrowDecode)]
#[storekey(format = "()")]
pub(crate) struct GraphWithTarget<'a> {
__: u8,
_a: u8,
pub ns: NamespaceId,
_b: u8,
pub db: DatabaseId,
_c: u8,
pub tb: Cow<'a, TableName>,
_d: u8,
pub id: RecordIdKey,
pub eg: Dir,
pub ft: Cow<'a, TableName>,
pub fk: Cow<'a, RecordIdKey>,
pub tt: Cow<'a, TableName>,
pub tk: Cow<'a, RecordIdKey>,
}
impl_kv_key_storekey!(GraphWithTarget<'_> => ());
#[derive(Debug, Clone)]
pub(crate) struct DecodedGraph {
pub edge: RecordId,
pub target: Option<RecordId>,
}
impl Graph<'_> {
pub fn decode_key(k: &[u8]) -> Result<DecodedGraph> {
let mut reader = BorrowReader::new(k);
let g = <Graph as BorrowDecode>::borrow_decode(&mut reader)?;
let edge = RecordId {
table: g.ft.into_owned(),
key: g.fk.into_owned(),
};
let target = if reader.is_empty() {
None
} else {
let tt = <Cow<TableName> as BorrowDecode>::borrow_decode(&mut reader)?;
let tk = <Cow<RecordIdKey> as BorrowDecode>::borrow_decode(&mut reader)?;
Some(RecordId {
table: tt.into_owned(),
key: tk.into_owned(),
})
};
Ok(DecodedGraph {
edge,
target,
})
}
}
pub fn new<'a>(
ns: NamespaceId,
db: DatabaseId,
tb: &'a TableName,
id: &RecordIdKey,
eg: Dir,
fk: &'a RecordId,
) -> Graph<'a> {
Graph::new(ns, db, tb, id.to_owned(), eg, fk)
}
pub fn new_pointer<'a>(
ns: NamespaceId,
db: DatabaseId,
tb: &'a TableName,
id: &RecordIdKey,
eg: Dir,
fk: &'a RecordId,
target: &'a RecordId,
) -> GraphWithTarget<'a> {
GraphWithTarget {
__: b'/',
_a: b'*',
ns,
_b: b'*',
db,
_c: b'*',
tb: Cow::Borrowed(tb),
_d: b'~',
id: id.to_owned(),
eg,
ft: Cow::Borrowed(&fk.table),
fk: Cow::Borrowed(&fk.key),
tt: Cow::Borrowed(&target.table),
tk: Cow::Borrowed(&target.key),
}
}
pub fn prefix(
ns: NamespaceId,
db: DatabaseId,
tb: &TableName,
id: &RecordIdKey,
) -> Result<Vec<u8>> {
let mut k = Prefix::new(ns, db, tb, id).encode_key()?;
k.extend_from_slice(&[0x00]);
Ok(k)
}
pub fn suffix(
ns: NamespaceId,
db: DatabaseId,
tb: &TableName,
id: &RecordIdKey,
) -> Result<Vec<u8>> {
let mut k = Prefix::new(ns, db, tb, id).encode_key()?;
k.extend_from_slice(&[0xff]);
Ok(k)
}
pub fn egprefix(
ns: NamespaceId,
db: DatabaseId,
tb: &TableName,
id: &RecordIdKey,
eg: Dir,
) -> Result<Vec<u8>> {
let mut k = PrefixEg::new(ns, db, tb, id, eg).encode_key()?;
k.extend_from_slice(&[0x00]);
Ok(k)
}
pub fn egsuffix(
ns: NamespaceId,
db: DatabaseId,
tb: &TableName,
id: &RecordIdKey,
eg: Dir,
) -> Result<Vec<u8>> {
let mut k = PrefixEg::new(ns, db, tb, id, eg).encode_key()?;
k.extend_from_slice(&[0xff]);
Ok(k)
}
pub fn ftprefix(
ns: NamespaceId,
db: DatabaseId,
tb: &TableName,
id: &RecordIdKey,
eg: Dir,
ft: &str,
) -> Result<Vec<u8>> {
let mut k = PrefixFt::new(ns, db, tb, id, eg, ft).encode_key()?;
k.extend_from_slice(&[0x00]);
Ok(k)
}
pub fn ftsuffix(
ns: NamespaceId,
db: DatabaseId,
tb: &TableName,
id: &RecordIdKey,
eg: Dir,
ft: &str,
) -> Result<Vec<u8>> {
let mut k = PrefixFt::new(ns, db, tb, id, eg, ft).encode_key()?;
k.extend_from_slice(&[0xff]);
Ok(k)
}
impl Categorise for Graph<'_> {
fn categorise(&self) -> Category {
Category::Graph
}
}
impl Categorise for GraphWithTarget<'_> {
fn categorise(&self) -> Category {
Category::Graph
}
}
impl<'a> Graph<'a> {
pub fn new(
ns: NamespaceId,
db: DatabaseId,
tb: &'a TableName,
id: RecordIdKey,
eg: Dir,
fk: &'a RecordId,
) -> Self {
Self {
__: b'/',
_a: b'*',
ns,
_b: b'*',
db,
_c: b'*',
tb: Cow::Borrowed(tb),
_d: b'~',
id,
eg,
ft: Cow::Borrowed(&fk.table),
fk: Cow::Borrowed(&fk.key),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::syn;
use crate::types::PublicValue;
#[test]
fn key() {
let Ok(PublicValue::RecordId(fk)) = syn::value("other:test") else {
panic!()
};
let fk = fk.into();
let tb: TableName = "testtb".into();
let val = Graph::new(
NamespaceId(1),
DatabaseId(2),
&tb,
"testid".to_owned().into(),
Dir::Out,
&fk,
);
let enc = Graph::encode_key(&val).unwrap();
assert_eq!(
enc,
b"/*\x00\x00\x00\x01*\x00\x00\x00\x02*testtb\0~\x03testid\0\x03other\0\x03test\0"
);
let dec = Graph::decode_key(&enc).unwrap();
assert_eq!(dec.edge.table.as_str(), "other");
assert!(dec.target.is_none());
}
#[test]
fn key_with_target() {
let Ok(PublicValue::RecordId(edge)) = syn::value("likes:abc") else {
panic!()
};
let Ok(PublicValue::RecordId(target)) = syn::value("person:bob") else {
panic!()
};
let edge: RecordId = edge.into();
let target: RecordId = target.into();
let tb: TableName = "person".into();
let id: RecordIdKey = "alice".to_owned().into();
let new_key =
new_pointer(NamespaceId(1), DatabaseId(2), &tb, &id, Dir::Out, &edge, &target);
let new_bytes = GraphWithTarget::encode_key(&new_key).unwrap();
let legacy = Graph::new(NamespaceId(1), DatabaseId(2), &tb, id.clone(), Dir::Out, &edge);
let legacy_bytes = Graph::encode_key(&legacy).unwrap();
assert!(new_bytes.starts_with(&legacy_bytes));
assert!(new_bytes.len() > legacy_bytes.len());
let boundary_byte = new_bytes[legacy_bytes.len()];
assert!(
boundary_byte < 0xff,
"first byte after legacy graph-key prefix must be < 0xff but was 0x{:02x}; \
the `0xff` sentinel used by graph range bounds is no longer safe",
boundary_byte,
);
let dec = Graph::decode_key(&new_bytes).unwrap();
assert_eq!(dec.edge.table.as_str(), "likes");
assert_eq!(dec.edge.key, RecordIdKey::from("abc".to_owned()));
let tgt = dec.target.expect("new-format key must carry a target");
assert_eq!(tgt.table.as_str(), "person");
assert_eq!(tgt.key, RecordIdKey::from("bob".to_owned()));
}
#[test]
fn legacy_key_with_trailing_bytes_errors() {
let Ok(PublicValue::RecordId(fk)) = syn::value("other:test") else {
panic!()
};
let fk: RecordId = fk.into();
let tb: TableName = "person".into();
let legacy = Graph::new(
NamespaceId(1),
DatabaseId(2),
&tb,
"alice".to_owned().into(),
Dir::Out,
&fk,
);
let mut bytes = Graph::encode_key(&legacy).unwrap();
bytes.extend_from_slice(b"trailing-garbage");
Graph::decode_key(&bytes)
.expect_err("trailing bytes on a legacy key must surface as a decode error");
}
#[test]
fn key_with_trailing_bytes_decodes() {
let Ok(PublicValue::RecordId(edge)) = syn::value("likes:abc") else {
panic!()
};
let Ok(PublicValue::RecordId(target)) = syn::value("person:bob") else {
panic!()
};
let edge: RecordId = edge.into();
let target: RecordId = target.into();
let tb: TableName = "person".into();
let id: RecordIdKey = "alice".to_owned().into();
let key = new_pointer(NamespaceId(1), DatabaseId(2), &tb, &id, Dir::Out, &edge, &target);
let mut bytes = GraphWithTarget::encode_key(&key).unwrap();
bytes.extend_from_slice(b"future-extension");
let dec = Graph::decode_key(&bytes).expect("trailing bytes must not fail decode");
assert_eq!(dec.edge.table.as_str(), "likes");
let tgt = dec.target.expect("target still decoded from prefix");
assert_eq!(tgt.table.as_str(), "person");
}
}