use revision::revisioned;
use surrealdb_datastore::values::diskann::DiskAnnRecordPendingUpdate;
use surrealdb_datastore::values::hnsw::HnswRecordPendingUpdate;
use surrealdb_datastore::values::ids::DocId;
use surrealdb_datastore::values::vector::SerializedVector;
use crate::key::KVValue;
#[revisioned(revision = 1)]
#[derive(Clone, Debug, PartialEq)]
struct HnswRecordPendingUpdateV1 {
doc_id: Option<DocId>,
old_vectors: Vec<SerializedVector>,
new_vectors: Vec<SerializedVector>,
}
#[revisioned(revision = 1)]
#[derive(Clone, Debug, PartialEq)]
struct DiskAnnRecordPendingUpdateV1 {
doc_id: Option<DocId>,
old_vectors: Vec<SerializedVector>,
new_vectors: Vec<SerializedVector>,
}
fn hnsw_v1() -> HnswRecordPendingUpdateV1 {
HnswRecordPendingUpdateV1 {
doc_id: Some(42),
old_vectors: vec![SerializedVector::F32(vec![1.0, 2.0, 3.0, 4.0])],
new_vectors: vec![SerializedVector::F32(vec![5.0, 6.0, 7.0, 8.0])],
}
}
fn diskann_v1() -> DiskAnnRecordPendingUpdateV1 {
DiskAnnRecordPendingUpdateV1 {
doc_id: Some(42),
old_vectors: vec![SerializedVector::F32(vec![1.0, 2.0, 3.0, 4.0])],
new_vectors: vec![SerializedVector::F32(vec![5.0, 6.0, 7.0, 8.0])],
}
}
#[rustfmt::skip]
const HNSW_PENDING_V1: &[u8] = &[
1, 1, 42, 1, 2, 1, 4, 0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64, 0, 0, 128, 64, 1, 2, 1, 4, 0,
0, 160, 64, 0, 0, 192, 64, 0, 0, 224, 64, 0, 0, 0, 65,
];
#[rustfmt::skip]
const DISKANN_PENDING_V1: &[u8] = &[
1, 1, 42, 1, 2, 1, 4, 0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64, 0, 0, 128, 64, 1, 2, 1, 4, 0,
0, 160, 64, 0, 0, 192, 64, 0, 0, 224, 64, 0, 0, 0, 65,
];
#[test]
fn v1_ann_pending_entries_decode_with_no_record_id() {
let hnsw = HnswRecordPendingUpdate::kv_decode_value(HNSW_PENDING_V1, ()).unwrap_or_else(|e| {
panic!(
"BACKWARDS COMPATIBILITY BROKEN: revision-1 HnswRecordPendingUpdate bytes no longer \
decode.\nError: {e}\n\nEvery `!hr` entry written by a previous version is revision \
1. A node that cannot decode them cannot compact the backlog it inherits."
)
});
let diskann = DiskAnnRecordPendingUpdate::kv_decode_value(DISKANN_PENDING_V1, ())
.unwrap_or_else(|e| {
panic!(
"BACKWARDS COMPATIBILITY BROKEN: revision-1 DiskAnnRecordPendingUpdate bytes no \
longer decode.\nError: {e}\n\nEvery `!dr`/`!dw` entry written by a previous \
version is revision 1."
)
});
let expected_hnsw = hnsw_v1();
assert_eq!(hnsw.id, None, "the field added at revision 2 must default to None");
assert_eq!(hnsw.doc_id, expected_hnsw.doc_id);
assert_eq!(hnsw.old_vectors, expected_hnsw.old_vectors);
assert_eq!(hnsw.new_vectors, expected_hnsw.new_vectors);
let expected_diskann = diskann_v1();
assert_eq!(diskann.id, None, "the field added at revision 2 must default to None");
assert_eq!(diskann.doc_id, expected_diskann.doc_id);
assert_eq!(diskann.old_vectors, expected_diskann.old_vectors);
assert_eq!(diskann.new_vectors, expected_diskann.new_vectors);
}
#[test]
fn re_encoding_an_upgraded_pending_entry_writes_revision_2_losslessly() {
let decoded = HnswRecordPendingUpdate::kv_decode_value(HNSW_PENDING_V1, ()).expect("decode");
let re_encoded = decoded.kv_encode_value().expect("encode");
assert_eq!(HNSW_PENDING_V1.first(), Some(&1), "the frozen fixture must be revision 1");
assert_eq!(re_encoded.first(), Some(&2), "an upgraded node must write revision 2");
assert_eq!(
re_encoded.len(),
HNSW_PENDING_V1.len() + 1,
"the only added byte is the absent id's `None` marker"
);
let round_tripped =
HnswRecordPendingUpdate::kv_decode_value(&re_encoded, ()).expect("re-decode");
assert_eq!(round_tripped.id, None);
assert_eq!(round_tripped.doc_id, decoded.doc_id);
assert_eq!(round_tripped.new_vectors, decoded.new_vectors);
let decoded =
DiskAnnRecordPendingUpdate::kv_decode_value(DISKANN_PENDING_V1, ()).expect("decode");
let re_encoded = decoded.kv_encode_value().expect("encode");
assert_eq!(DISKANN_PENDING_V1.first(), Some(&1), "the frozen fixture must be revision 1");
assert_eq!(re_encoded.first(), Some(&2), "an upgraded node must write revision 2");
assert_eq!(
re_encoded.len(),
DISKANN_PENDING_V1.len() + 1,
"the only added byte is the absent id's `None` marker"
);
let round_tripped =
DiskAnnRecordPendingUpdate::kv_decode_value(&re_encoded, ()).expect("re-decode");
assert_eq!(round_tripped.id, None);
assert_eq!(round_tripped.doc_id, decoded.doc_id);
assert_eq!(round_tripped.new_vectors, decoded.new_vectors);
}
#[test]
#[ignore = "prints the frozen fixtures; run by hand when a revision-1 shape is restated"]
fn generate() {
for (name, bytes) in [
("HNSW_PENDING_V1", revision::to_vec(&hnsw_v1()).expect("encode")),
("DISKANN_PENDING_V1", revision::to_vec(&diskann_v1()).expect("encode")),
] {
let body = bytes
.chunks(16)
.map(|row| {
let cells: Vec<String> = row.iter().map(|b| b.to_string()).collect();
format!("\t{},", cells.join(", "))
})
.collect::<Vec<_>>()
.join("\n");
println!("const {name}: &[u8] = &[\n{body}\n];");
}
}