#![cfg(all(test, feature = "persistence"))]
use crate::collection::types::CollectionConfig;
use crate::collection::Collection;
use crate::distance::DistanceMetric;
use crate::index::hnsw::HnswParams;
use crate::quantization::StorageMode;
use std::path::PathBuf;
#[test]
fn test_hnsw_params_persisted_in_config_json() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let params = HnswParams::custom(64, 400, 50_000);
let collection = Collection::create_with_hnsw_params(
PathBuf::from(temp_dir.path()),
128,
DistanceMetric::Cosine,
StorageMode::Full,
params,
)
.expect("collection should be created");
let cfg = collection.config();
assert_eq!(cfg.hnsw_params, Some(params));
let config_path = temp_dir.path().join("config.json");
let raw = std::fs::read_to_string(&config_path).expect("config.json should exist");
let deserialized: CollectionConfig =
serde_json::from_str(&raw).expect("config.json should deserialize");
assert_eq!(deserialized.hnsw_params, Some(params));
}
#[test]
fn test_config_without_hnsw_params_loads_as_none() {
let json = r#"{
"name": "legacy",
"dimension": 128,
"metric": "Cosine",
"point_count": 0,
"storage_mode": "full",
"metadata_only": false
}"#;
let cfg: CollectionConfig =
serde_json::from_str(json).expect("legacy config should deserialize");
assert!(cfg.hnsw_params.is_none());
}
#[test]
fn test_reopen_collection_uses_persisted_hnsw_params() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let params = HnswParams::custom(64, 400, 50_000);
let _collection = Collection::create_with_hnsw_params(
PathBuf::from(temp_dir.path()),
128,
DistanceMetric::Cosine,
StorageMode::Full,
params,
)
.expect("collection should be created");
assert!(
!temp_dir.path().join("hnsw.bin").exists(),
"hnsw.bin should not exist for empty collection"
);
let reopened =
Collection::open(PathBuf::from(temp_dir.path())).expect("collection should reopen");
let cfg = reopened.config();
assert_eq!(
cfg.hnsw_params,
Some(params),
"reopened collection should preserve custom HNSW params"
);
}
#[test]
fn test_default_collection_omits_hnsw_params_from_json() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let _collection =
Collection::create(PathBuf::from(temp_dir.path()), 128, DistanceMetric::Cosine)
.expect("collection should be created");
let config_path = temp_dir.path().join("config.json");
let raw = std::fs::read_to_string(&config_path).expect("config.json should exist");
assert!(
!raw.contains("hnsw_params"),
"config.json should not contain hnsw_params when None"
);
}
fn expect_err(result: crate::error::Result<Collection>) -> crate::Error {
match result {
Err(e) => e,
Ok(_) => panic!("expected Err, got Ok"),
}
}
#[test]
fn test_create_rejects_zero_dimension() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let result = Collection::create(PathBuf::from(temp_dir.path()), 0, DistanceMetric::Cosine);
let err = expect_err(result);
assert_eq!(err.code(), "VELES-032");
}
#[test]
fn test_create_rejects_oversized_dimension() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let result = Collection::create(
PathBuf::from(temp_dir.path()),
100_000,
DistanceMetric::Cosine,
);
let err = expect_err(result);
assert_eq!(err.code(), "VELES-032");
}
#[test]
fn test_create_accepts_min_dimension() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let result = Collection::create(PathBuf::from(temp_dir.path()), 1, DistanceMetric::Cosine);
assert!(result.is_ok(), "dimension 1 should be accepted");
}
#[test]
fn test_create_accepts_max_dimension() {
use crate::validation::validate_dimension;
assert!(
validate_dimension(65_536).is_ok(),
"dimension 65_536 should pass validation"
);
assert!(
validate_dimension(65_537).is_err(),
"dimension 65_537 should be rejected"
);
}
#[test]
fn test_create_with_hnsw_params_rejects_zero_dimension() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let params = HnswParams::custom(16, 200, 10_000);
let result = Collection::create_with_hnsw_params(
PathBuf::from(temp_dir.path()),
0,
DistanceMetric::Cosine,
StorageMode::Full,
params,
);
let err = expect_err(result);
assert_eq!(err.code(), "VELES-032");
}
#[test]
fn test_graph_collection_rejects_zero_embedding_dim() {
use crate::collection::graph::GraphSchema;
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let schema = GraphSchema::new();
let result = Collection::create_graph_collection(
PathBuf::from(temp_dir.path()),
"test_graph",
schema,
Some(0),
DistanceMetric::Cosine,
);
let err = expect_err(result);
assert_eq!(err.code(), "VELES-032");
}
#[test]
fn test_graph_collection_accepts_none_embedding_dim() {
use crate::collection::graph::GraphSchema;
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let schema = GraphSchema::new();
let result = Collection::create_graph_collection(
PathBuf::from(temp_dir.path()),
"test_graph",
schema,
None,
DistanceMetric::Cosine,
);
assert!(
result.is_ok(),
"embedding_dim None should be accepted for graph collections"
);
}
#[test]
fn test_reopen_collection_reconciles_point_count_from_storage() {
use crate::point::Point;
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let n = 25_usize;
let collection = Collection::create(PathBuf::from(temp_dir.path()), 4, DistanceMetric::Cosine)
.expect("collection should be created");
#[allow(clippy::cast_precision_loss)]
let points: Vec<Point> = (0..n)
.map(|i| {
let f = i as f32 / n as f32;
Point::without_payload(i as u64, vec![f, 1.0 - f, 0.5, 0.1])
})
.collect();
collection.upsert(points).expect("upsert should succeed");
assert_eq!(collection.len(), n, "in-memory len should equal N");
drop(collection);
let reopened =
Collection::open(PathBuf::from(temp_dir.path())).expect("collection should reopen");
assert_eq!(
reopened.config().point_count,
n,
"config.point_count must be reconciled from storage on open"
);
assert_eq!(
reopened.len(),
n,
"len() must reflect actual vector count after reopen"
);
}
#[test]
fn test_flush_drains_delta_buffer_into_hnsw() {
use crate::index::VectorIndex;
use crate::point::Point;
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let collection = Collection::create(PathBuf::from(temp_dir.path()), 4, DistanceMetric::Cosine)
.expect("collection should be created");
let initial_points = vec![
Point::without_payload(1, vec![1.0, 0.0, 0.0, 0.0]),
Point::without_payload(2, vec![0.0, 1.0, 0.0, 0.0]),
];
collection.upsert(initial_points).expect("initial upsert");
{
use crate::storage::VectorStorage;
let mut vs = collection.vector_storage.write();
vs.store(10, &[0.5, 0.5, 0.0, 0.0]).expect("store 10");
vs.store(11, &[0.0, 0.0, 0.5, 0.5]).expect("store 11");
}
collection.delta_buffer.activate();
assert!(
collection.delta_buffer.is_active(),
"delta should be active"
);
collection.delta_buffer.push(10, vec![0.5, 0.5, 0.0, 0.0]);
collection.delta_buffer.push(11, vec![0.0, 0.0, 0.5, 0.5]);
assert_eq!(
collection.delta_buffer.len(),
2,
"delta should hold 2 entries"
);
collection.flush().expect("flush should succeed");
assert!(
!collection.delta_buffer.is_active(),
"delta buffer must be inactive after flush"
);
assert!(
collection.delta_buffer.is_empty(),
"delta buffer must be empty after flush"
);
let results = collection.index.search(&[0.5, 0.5, 0.0, 0.0], 5);
let result_ids: Vec<u64> = results.iter().map(|r| r.id).collect();
assert!(
result_ids.contains(&10),
"id=10 should be in HNSW after flush (was: {result_ids:?})"
);
assert!(
result_ids.contains(&11),
"id=11 should be in HNSW after flush (was: {result_ids:?})"
);
}
#[test]
fn test_flush_with_inactive_delta_buffer_is_noop() {
use crate::point::Point;
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let collection = Collection::create(PathBuf::from(temp_dir.path()), 4, DistanceMetric::Cosine)
.expect("collection should be created");
let points = vec![Point::without_payload(1, vec![1.0, 0.0, 0.0, 0.0])];
collection.upsert(points).expect("upsert");
assert!(!collection.delta_buffer.is_active());
collection
.flush()
.expect("flush with inactive delta should succeed");
let results = collection.search(&[1.0, 0.0, 0.0, 0.0], 1).expect("search");
assert_eq!(results.len(), 1, "search should still work after flush");
}
#[test]
fn test_open_without_edge_store_bin_recovers_gracefully() {
use crate::collection::graph::{GraphEdge, GraphSchema};
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let col_path = temp_dir.path().join("graph_col");
{
let schema = GraphSchema::new();
let collection = Collection::create_graph_collection(
col_path.clone(),
"graph_col",
schema,
None,
DistanceMetric::Cosine,
)
.expect("graph collection should be created");
let edge = GraphEdge::new(1, 100, 200, "KNOWS").expect("valid edge");
collection.add_edge(edge).expect("add edge should succeed");
collection.flush().expect("flush should succeed");
}
let edge_store_path = col_path.join("edge_store.bin");
assert!(
edge_store_path.exists(),
"edge_store.bin should exist after flush"
);
std::fs::remove_file(&edge_store_path).expect("remove edge_store.bin");
assert!(
!edge_store_path.exists(),
"edge_store.bin should be deleted"
);
let reopened = Collection::open(col_path).expect("open should succeed without edge_store.bin");
assert_eq!(
reopened.edge_count(),
0,
"edge store should be empty after recovery without edge_store.bin"
);
let edge_a = GraphEdge::new(10, 1, 2, "LIKES").expect("valid edge");
reopened
.add_edge(edge_a)
.expect("add edge should succeed after recovery");
assert_eq!(reopened.edge_count(), 1, "edge count after add");
let outgoing = reopened.get_outgoing_edges(1);
assert_eq!(outgoing.len(), 1, "should have one outgoing edge");
assert_eq!(outgoing[0].target(), 2, "target should be 2");
assert_eq!(outgoing[0].label(), "LIKES", "label should be LIKES");
reopened
.flush()
.expect("flush should succeed after recovery");
assert!(
edge_store_path.exists(),
"edge_store.bin should be re-created after flush"
);
}
#[test]
fn test_schema_version_set_on_new_collection() {
use crate::collection::types::CURRENT_SCHEMA_VERSION;
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let collection = Collection::create(PathBuf::from(temp_dir.path()), 4, DistanceMetric::Cosine)
.expect("collection should be created");
let cfg = collection.config();
assert_eq!(
cfg.schema_version, CURRENT_SCHEMA_VERSION,
"new collection must carry the current schema version"
);
}
#[test]
fn test_schema_version_persisted_in_config_json() {
use crate::collection::types::CURRENT_SCHEMA_VERSION;
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let _collection = Collection::create(PathBuf::from(temp_dir.path()), 4, DistanceMetric::Cosine)
.expect("collection should be created");
let raw = std::fs::read_to_string(temp_dir.path().join("config.json"))
.expect("config.json should exist");
let deserialized: CollectionConfig =
serde_json::from_str(&raw).expect("config.json should deserialize");
assert_eq!(deserialized.schema_version, CURRENT_SCHEMA_VERSION);
}
#[test]
fn test_schema_version_defaults_to_1_for_legacy_config() {
let json = r#"{
"name": "legacy_no_version",
"dimension": 128,
"metric": "Cosine",
"point_count": 0,
"storage_mode": "full",
"metadata_only": false
}"#;
let cfg: CollectionConfig =
serde_json::from_str(json).expect("legacy config should deserialize");
assert_eq!(
cfg.schema_version, 1,
"missing schema_version must default to 1"
);
}
#[test]
fn test_open_rejects_future_schema_version() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let collection = Collection::create(PathBuf::from(temp_dir.path()), 4, DistanceMetric::Cosine)
.expect("collection should be created");
collection.flush().expect("flush should succeed");
drop(collection);
let config_path = temp_dir.path().join("config.json");
let raw = std::fs::read_to_string(&config_path).expect("read config");
let mut cfg: serde_json::Value = serde_json::from_str(&raw).expect("parse config");
cfg["schema_version"] = serde_json::Value::from(999);
std::fs::write(
&config_path,
serde_json::to_string_pretty(&cfg).expect("serialize"),
)
.expect("write tampered config");
let result = Collection::open(PathBuf::from(temp_dir.path()));
let err = expect_err(result);
assert_eq!(err.code(), "VELES-036");
let msg = err.to_string();
assert!(
msg.contains("999"),
"error must mention the found version: {msg}"
);
}
#[test]
fn test_schema_version_zero_treated_as_v1() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let collection = Collection::create(PathBuf::from(temp_dir.path()), 4, DistanceMetric::Cosine)
.expect("collection should be created");
collection.flush().expect("flush should succeed");
drop(collection);
let config_path = temp_dir.path().join("config.json");
let raw = std::fs::read_to_string(&config_path).expect("read config");
let mut cfg: serde_json::Value = serde_json::from_str(&raw).expect("parse config");
cfg["schema_version"] = serde_json::Value::from(0);
std::fs::write(
&config_path,
serde_json::to_string_pretty(&cfg).expect("serialize"),
)
.expect("write config with version 0");
let _reopened = Collection::open(PathBuf::from(temp_dir.path()))
.expect("collection with schema_version=0 should open");
}
#[test]
fn test_incompatible_schema_version_is_not_recoverable() {
let err = crate::Error::IncompatibleSchemaVersion {
found: 99,
supported: 1,
};
assert!(
!err.is_recoverable(),
"IncompatibleSchemaVersion must not be recoverable"
);
}