use redb::TableDefinition;
use serde_json::Value;
use crate::error::NookError;
use crate::storage::{ReadTx, WriteTx};
pub(crate) const INDEX_ENTRIES: TableDefinition<&[u8], &[u8]> =
TableDefinition::new("index_entries");
const SEP: u8 = 0;
#[must_use]
pub fn encode_index_value(v: &Value) -> Vec<u8> {
match v {
Value::String(s) => s.as_bytes().to_vec(),
Value::Bool(b) => vec![u8::from(*b)],
Value::Number(n) => n.to_string().into_bytes(),
Value::Null => b"\0null".to_vec(),
other => other.to_string().into_bytes(),
}
}
fn key(collection: &str, field: &str, enc_val: &[u8], doc_id: &[u8]) -> Vec<u8> {
debug_assert!(
!doc_id.contains(&0),
"index doc_id must be NUL-free: the \\0-delimited, non-length-prefixed \
index key relies on this for uniqueness and lookup_eq range-exactness",
);
let mut k =
Vec::with_capacity(collection.len() + field.len() + enc_val.len() + doc_id.len() + 3);
k.extend_from_slice(collection.as_bytes());
k.push(SEP);
k.extend_from_slice(field.as_bytes());
k.push(SEP);
k.extend_from_slice(enc_val);
k.push(SEP);
k.extend_from_slice(doc_id);
k
}
fn prefix(collection: &str, field: &str, enc_val: &[u8]) -> (Vec<u8>, Vec<u8>) {
let mut lo = Vec::with_capacity(collection.len() + field.len() + enc_val.len() + 3);
lo.extend_from_slice(collection.as_bytes());
lo.push(SEP);
lo.extend_from_slice(field.as_bytes());
lo.push(SEP);
lo.extend_from_slice(enc_val);
lo.push(SEP);
let mut hi = lo.clone();
let last = lo.len() - 1;
hi[last] = SEP + 1;
(lo, hi)
}
pub fn put_index_entry(
tx: &mut WriteTx<'_>,
collection: &str,
field: &str,
value: &Value,
doc_id: &[u8],
) -> Result<(), NookError> {
let k = key(collection, field, &encode_index_value(value), doc_id);
tx.index_put(&k, doc_id)
}
pub fn delete_index_entry(
tx: &mut WriteTx<'_>,
collection: &str,
field: &str,
value: &Value,
doc_id: &[u8],
) -> Result<(), NookError> {
let k = key(collection, field, &encode_index_value(value), doc_id);
tx.index_delete(&k)
}
pub fn lookup_eq(
tx: &ReadTx,
collection: &str,
field: &str,
value: &Value,
) -> Result<Vec<Vec<u8>>, NookError> {
let (lo, hi) = prefix(collection, field, &encode_index_value(value));
tx.index_range_values(&lo, &hi)
}
pub fn index_value_exists(
tx: &ReadTx,
collection: &str,
field: &str,
value: &Value,
) -> Result<bool, NookError> {
Ok(!lookup_eq(tx, collection, field, value)?.is_empty())
}
pub fn index_value_exists_writing(
tx: &WriteTx<'_>,
collection: &str,
field: &str,
value: &Value,
) -> Result<bool, NookError> {
let (lo, hi) = prefix(collection, field, &encode_index_value(value));
Ok(!tx.index_range_values(&lo, &hi)?.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::Database;
use serde_json::json;
fn db() -> (tempfile::TempDir, Database) {
let d = tempfile::tempdir().unwrap();
let db = Database::open(d.path().join("t.db")).unwrap();
(d, db)
}
#[test]
fn encode_index_value_is_deterministic_per_type() {
assert_eq!(
encode_index_value(&json!("ab")),
encode_index_value(&json!("ab"))
);
assert_eq!(encode_index_value(&json!(true)), b"\x01");
assert_ne!(encode_index_value(&json!(1)), encode_index_value(&json!(2)));
}
#[test]
fn put_then_lookup_returns_doc_ids_and_delete_removes_them() {
let (_d, db) = db();
db.write(|tx| {
put_index_entry(tx, "u", "role", &json!("admin"), b"u1")?;
put_index_entry(tx, "u", "role", &json!("admin"), b"u2")?;
put_index_entry(tx, "u", "role", &json!("user"), b"u3")?;
Ok(())
})
.unwrap();
let mut hits = db
.read(|tx| lookup_eq(tx, "u", "role", &json!("admin")))
.unwrap();
hits.sort();
assert_eq!(hits, vec![b"u1".to_vec(), b"u2".to_vec()]);
db.write(|tx| delete_index_entry(tx, "u", "role", &json!("admin"), b"u1"))
.unwrap();
let hits2 = db
.read(|tx| lookup_eq(tx, "u", "role", &json!("admin")))
.unwrap();
assert_eq!(hits2, vec![b"u2".to_vec()]);
}
#[test]
fn unique_violation_is_detected() {
let (_d, db) = db();
db.write(|tx| put_index_entry(tx, "u", "email", &json!("a@b"), b"u1"))
.unwrap();
let dup = db
.read(|tx| index_value_exists(tx, "u", "email", &json!("a@b")))
.unwrap();
assert!(dup);
let absent = db
.read(|tx| index_value_exists(tx, "u", "email", &json!("z@z")))
.unwrap();
assert!(!absent);
}
#[test]
fn value_prefix_does_not_cross_match() {
let (_d, db) = db();
db.write(|tx| {
put_index_entry(tx, "u", "name", &json!("a"), b"d_a")?;
put_index_entry(tx, "u", "name", &json!("ab"), b"d_ab")?;
Ok(())
})
.unwrap();
let mut for_a = db
.read(|tx| lookup_eq(tx, "u", "name", &json!("a")))
.unwrap();
for_a.sort();
assert_eq!(for_a, vec![b"d_a".to_vec()]);
let mut for_ab = db
.read(|tx| lookup_eq(tx, "u", "name", &json!("ab")))
.unwrap();
for_ab.sort();
assert_eq!(for_ab, vec![b"d_ab".to_vec()]);
}
#[test]
fn adjacent_field_names_do_not_bleed() {
let (_d, db) = db();
db.write(|tx| {
put_index_entry(tx, "u", "role", &json!("x"), b"r1")?;
put_index_entry(tx, "u", "roles", &json!("x"), b"r2")?;
Ok(())
})
.unwrap();
let mut for_role = db
.read(|tx| lookup_eq(tx, "u", "role", &json!("x")))
.unwrap();
for_role.sort();
assert_eq!(for_role, vec![b"r1".to_vec()]);
let mut for_roles = db
.read(|tx| lookup_eq(tx, "u", "roles", &json!("x")))
.unwrap();
for_roles.sort();
assert_eq!(for_roles, vec![b"r2".to_vec()]);
}
}