use haematite::db::ReadOnlyDatabase;
use serde::{Deserialize, Serialize};
use super::{Captured, Failure, embedded, identifier, mutations::PreparedWrite, reads};
use crate::serve::protocol::{Change, IndexDeclaration, Version};
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Recorded {
format: u32,
change: Change,
declared_in: String,
declared_at: Version,
}
pub(in crate::serve) fn validate_names(collection: &str, field: &str) -> Result<(), Failure> {
identifier(collection)?;
if field.contains(['.', '/', '[', ']']) {
return Err(Failure::new(
"index_field_not_top_level",
"an equality field must name one top-level member without path syntax",
));
}
identifier(field)
}
pub(super) fn declare(
reader: &ReadOnlyDatabase,
at: &Captured,
write: &IndexDeclaration,
next: Version,
) -> Result<Vec<PreparedWrite>, Failure> {
validate_names(&write.collection, &write.field)?;
if reads::collection(reader, at, &write.collection)?.is_none() {
return Err(Failure::new("collection_missing", &write.collection));
}
if declared(reader, at, &write.collection, &write.field)? {
return Err(Failure::new(
"index_already_declared",
"this collection field already has an equality declaration",
));
}
let vector_key = format!("w\0{}\0{}", write.collection, write.field);
if let Some(bytes) = reads::get_bytes(reader, at, vector_key.as_bytes())? {
embedded::read_field(&bytes)?;
return Err(Failure::new(
"index_vector_field",
"a vector field cannot be declared for equality lookup",
));
}
let row = Recorded {
format: 1,
change: Change::IndexDeclare(write.clone()),
declared_in: at.branch.clone(),
declared_at: next,
};
Ok(vec![(
declaration_key(&write.collection, &write.field),
Some(serde_json::to_vec(&row).map_err(Failure::storage)?),
)])
}
pub(super) fn require_non_vector(
reader: &ReadOnlyDatabase,
at: &Captured,
collection: &str,
field: &str,
) -> Result<(), Failure> {
if declared(reader, at, collection, field)? {
return Err(Failure::new(
"index_vector_field",
"an equality field cannot become a vector field",
));
}
Ok(())
}
fn declared(
reader: &ReadOnlyDatabase,
at: &Captured,
collection: &str,
field: &str,
) -> Result<bool, Failure> {
let Some(bytes) = reads::get_bytes(reader, at, &declaration_key(collection, field))? else {
return Ok(false);
};
decode(collection, field, &bytes)?;
Ok(true)
}
pub(super) fn render(collection: &str, field: &str, bytes: &[u8]) -> Result<String, Failure> {
serde_json::to_string(&decode(collection, field, bytes)?).map_err(Failure::storage)
}
fn decode(collection: &str, field: &str, bytes: &[u8]) -> Result<Recorded, Failure> {
let corrupt = |message| Failure::new("index_declaration_corrupt", message);
let row: Recorded =
serde_json::from_slice(bytes).map_err(|error| corrupt(error.to_string()))?;
let Change::IndexDeclare(declaration) = &row.change else {
return Err(corrupt(
"declaration row has another change kind".to_owned(),
));
};
if row.format != 1 || declaration.collection != collection || declaration.field != field {
return Err(corrupt(
"declaration identity differs from its key".to_owned(),
));
}
validate_names(collection, field).map_err(|error| corrupt(error.to_string()))?;
identifier(&row.declared_in).map_err(|error| corrupt(error.to_string()))?;
Ok(row)
}
fn declaration_key(collection: &str, field: &str) -> Vec<u8> {
format!("f\0eq\0{collection}\0{field}").into_bytes()
}
#[cfg(test)]
mod tests {
use super::decode;
use crate::serve::protocol::Version;
use serde_json::json;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn index_declaration_decode_refuses_corrupt_identity_and_shape() -> TestResult {
let valid = json!({
"format":1,
"change":{"kind":"index.declare", "value":{"collection":"notes", "field":"ward"}},
"declared_in":"main", "declared_at":Version {generation:7, sequence:2}
});
decode("notes", "ward", &serde_json::to_vec(&valid)?)?;
let mut unsupported = valid.clone();
unsupported["format"] = json!(2);
let mut wrong_field = valid.clone();
wrong_field["change"]["value"]["field"] = json!("other");
let mut wrong_kind = valid.clone();
wrong_kind["change"] = json!({"kind":"relation.delete", "value":{"relation":"ward"}});
let mut authorless = valid.clone();
authorless["declared_in"] = json!("");
let mut extra = valid;
extra["members"] = json!([]);
for corrupt in [unsupported, wrong_field, wrong_kind, authorless, extra] {
let failure = decode("notes", "ward", &serde_json::to_vec(&corrupt)?)
.err()
.ok_or("corrupt declaration accepted")?;
assert_eq!(failure.code, "index_declaration_corrupt");
}
Ok(())
}
}