use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
use velesdb_core::{DistanceMetric, GraphEdge, GraphSchema, Point};
fn velesdb_cmd() -> Command {
Command::cargo_bin("velesdb").unwrap()
}
fn repl_run(db_path: &std::path::Path, commands: &[&str]) -> assert_cmd::assert::Assert {
let mut input = commands.join("\n");
input.push_str("\n.quit\n");
velesdb_cmd()
.arg("repl")
.arg(db_path)
.write_stdin(input)
.assert()
.success()
}
fn setup_vector(name: &str, dim: usize) -> (std::path::PathBuf, TempDir) {
let temp = TempDir::new().unwrap();
let db_path = temp.path().join("db");
let db = velesdb_core::Database::open(&db_path).unwrap();
db.create_vector_collection(name, dim, DistanceMetric::Cosine)
.unwrap();
let col = db.get_vector_collection(name).unwrap();
for i in 1u64..=5 {
#[allow(clippy::cast_precision_loss)]
let vec: Vec<f32> = (0..dim).map(|j| (i as f32 + j as f32) / 100.0).collect();
col.upsert(vec![Point {
id: i,
vector: vec,
payload: Some(serde_json::json!({"label": format!("vec_{}", i)})),
sparse_vectors: None,
}])
.unwrap();
}
drop(db);
(db_path, temp)
}
fn setup_graph(name: &str) -> (std::path::PathBuf, TempDir) {
let temp = TempDir::new().unwrap();
let db_path = temp.path().join("db");
let db = velesdb_core::Database::open(&db_path).unwrap();
db.create_graph_collection(name, GraphSchema::schemaless())
.unwrap();
let col = db.get_graph_collection(name).unwrap();
for i in 1u64..=3 {
let edge = GraphEdge::new(i, i * 10, i * 10 + 1, "LINKS").unwrap();
col.add_edge(edge).unwrap();
col.upsert_node_payload(
i * 10,
&serde_json::json!({"name": format!("node_{}", i * 10)}),
)
.unwrap();
}
col.flush().unwrap();
drop(db);
(db_path, temp)
}
fn setup_metadata(name: &str) -> (std::path::PathBuf, TempDir) {
let temp = TempDir::new().unwrap();
let db_path = temp.path().join("db");
let db = velesdb_core::Database::open(&db_path).unwrap();
db.create_metadata_collection(name).unwrap();
let col = db.get_metadata_collection(name).unwrap();
let points: Vec<Point> = (1u64..=5)
.map(|i| Point::metadata_only(i, serde_json::json!({"title": format!("item_{}", i)})))
.collect();
col.upsert(points).unwrap();
drop(db);
(db_path, temp)
}
#[test]
fn test_repl_collections_lists_all_types() {
let temp = TempDir::new().unwrap();
let db_path = temp.path().join("db");
let db = velesdb_core::Database::open(&db_path).unwrap();
db.create_vector_collection("vecs", 4, DistanceMetric::Cosine)
.unwrap();
db.create_graph_collection("kg", GraphSchema::schemaless())
.unwrap();
db.create_metadata_collection("meta").unwrap();
drop(db);
repl_run(&db_path, &[".collections"])
.stdout(predicate::str::contains("vecs"))
.stdout(predicate::str::contains("kg"))
.stdout(predicate::str::contains("meta"));
}
#[test]
fn test_repl_count_vector_collection() {
let (db_path, _temp) = setup_vector("vecs", 8);
repl_run(&db_path, &[".count vecs"]).stdout(predicate::str::contains("5"));
}
#[test]
fn test_repl_count_metadata_collection() {
let (db_path, _temp) = setup_metadata("meta");
repl_run(&db_path, &[".count meta"]).stdout(predicate::str::contains("5"));
}
#[test]
fn test_repl_sample_vector_shows_vector_column() {
let (db_path, _temp) = setup_vector("vecs", 4);
repl_run(&db_path, &[".sample vecs 3"])
.stdout(predicate::str::contains("sample(s) from"))
.stdout(predicate::str::contains("vector"));
}
#[test]
fn test_repl_sample_graph_no_vector_column() {
let (db_path, _temp) = setup_graph("kg");
let assert = repl_run(&db_path, &[".sample kg 5"]);
assert
.stdout(predicate::str::contains("sample(s) from").or(predicate::str::contains("node")))
.stdout(predicate::str::contains("vector").not());
}
#[test]
fn test_repl_sample_metadata_no_vector_column() {
let (db_path, _temp) = setup_metadata("meta");
repl_run(&db_path, &[".sample meta 3"])
.stdout(predicate::str::contains("sample(s) from"))
.stdout(predicate::str::contains("vector").not());
}
#[test]
fn test_repl_sample_unknown_collection_returns_error() {
let temp = TempDir::new().unwrap();
let db_path = temp.path().join("db");
velesdb_core::Database::open(&db_path).unwrap();
repl_run(&db_path, &[".sample nonexistent"])
.stdout(predicate::str::contains("not found").or(predicate::str::contains("Error")));
}
#[test]
fn test_repl_browse_vector_page1() {
let (db_path, _temp) = setup_vector("vecs", 4);
repl_run(&db_path, &[".browse vecs 1"])
.stdout(predicate::str::contains("Page 1"))
.stdout(predicate::str::contains("total records"));
}
#[test]
fn test_repl_browse_graph_shows_unique_nodes() {
let (db_path, _temp) = setup_graph("kg");
repl_run(&db_path, &[".browse kg 1"])
.stdout(predicate::str::contains("Page 1"))
.stdout(predicate::str::contains("unique nodes"));
}
#[test]
fn test_repl_browse_metadata_no_vector_column() {
let (db_path, _temp) = setup_metadata("meta");
repl_run(&db_path, &[".browse meta 1"])
.stdout(predicate::str::contains("Page 1"))
.stdout(predicate::str::contains("vector").not());
}
#[test]
fn test_repl_nodes_graph_shows_header() {
let (db_path, _temp) = setup_graph("kg");
repl_run(&db_path, &[".nodes kg 1"])
.stdout(predicate::str::contains("Nodes"))
.stdout(predicate::str::contains("kg"));
}
#[test]
fn test_repl_nodes_graph_shows_node_ids() {
let (db_path, _temp) = setup_graph("kg");
repl_run(&db_path, &[".nodes kg"])
.stdout(predicate::str::contains("10").or(predicate::str::contains("20")));
}
#[test]
fn test_repl_nodes_non_graph_returns_error() {
let (db_path, _temp) = setup_vector("vecs", 4);
repl_run(&db_path, &[".nodes vecs"])
.stdout(predicate::str::contains("not found").or(predicate::str::contains("Error")));
}
#[test]
fn test_repl_export_vector_creates_file() {
let (db_path, _temp) = setup_vector("vecs", 4);
let export_path = db_path.parent().unwrap().join("vecs_export.json");
repl_run(
&db_path,
&[&format!(".export vecs {}", export_path.display())],
)
.stdout(predicate::str::contains("Exported"));
let content = std::fs::read_to_string(&export_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).expect("should be valid JSON");
assert!(parsed.is_array());
assert_eq!(parsed.as_array().unwrap().len(), 5);
}
#[test]
fn test_repl_export_metadata_no_vector_field() {
let (db_path, _temp) = setup_metadata("meta");
let export_path = db_path.parent().unwrap().join("meta_export.json");
repl_run(
&db_path,
&[&format!(".export meta {}", export_path.display())],
)
.stdout(predicate::str::contains("Exported"));
let content = std::fs::read_to_string(&export_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).expect("should be valid JSON");
let records = parsed.as_array().unwrap();
assert_eq!(records.len(), 5);
for record in records {
assert!(
record.get("vector").is_none(),
"metadata export should not contain 'vector' field"
);
assert!(record.get("id").is_some());
}
}
#[test]
fn test_repl_export_graph_returns_informative_error() {
let (db_path, _temp) = setup_graph("kg");
repl_run(&db_path, &[".export kg"])
.stdout(predicate::str::contains("not supported").or(predicate::str::contains("Error")));
}
#[test]
fn test_repl_stats_vector() {
let (db_path, _temp) = setup_vector("vecs", 8);
repl_run(&db_path, &[".stats vecs"])
.stdout(predicate::str::contains("Vector"))
.stdout(predicate::str::contains("Point Count"));
}
#[test]
fn test_repl_stats_graph() {
let (db_path, _temp) = setup_graph("kg");
repl_run(&db_path, &[".stats kg"])
.stdout(predicate::str::contains("Graph"))
.stdout(predicate::str::contains("Edge Count"));
}
#[test]
fn test_repl_stats_metadata() {
let (db_path, _temp) = setup_metadata("meta");
repl_run(&db_path, &[".stats meta"])
.stdout(predicate::str::contains("Metadata"))
.stdout(predicate::str::contains("Item Count"));
}
#[test]
fn test_repl_diagnostics_command() {
let (db_path, _temp) = setup_vector("vecs", 8);
repl_run(&db_path, &[".diagnostics vecs"])
.stdout(predicate::str::contains("Diagnostics"))
.stdout(predicate::str::contains("Search Ready"))
.stdout(predicate::str::contains("Index Health"))
.stdout(predicate::str::contains("Point Count"))
.stdout(predicate::str::contains("5"));
}
#[test]
fn test_repl_diagnostics_unknown_collection_returns_error() {
let temp = TempDir::new().unwrap();
let db_path = temp.path().join("db");
velesdb_core::Database::open(&db_path).unwrap();
repl_run(&db_path, &[".diagnostics nonexistent"])
.stdout(predicate::str::contains("not found").or(predicate::str::contains("Error")));
}
#[test]
fn test_repl_velesql_select_vector() {
let (db_path, _temp) = setup_vector("vecs", 4);
repl_run(&db_path, &["SELECT * FROM vecs LIMIT 3"])
.stdout(predicate::str::contains("id").and(predicate::str::contains("3 rows")));
}
#[test]
fn test_repl_velesql_select_metadata() {
let (db_path, _temp) = setup_metadata("meta");
repl_run(&db_path, &["SELECT * FROM meta LIMIT 5"])
.stdout(predicate::str::contains("id").and(predicate::str::contains("rows")));
}
#[test]
fn test_repl_graph_edges_command() {
let (db_path, _temp) = setup_graph("kg");
repl_run(&db_path, &[".graph edges kg"])
.stdout(predicate::str::contains("LINKS").or(predicate::str::contains("edge")));
}
#[test]
fn test_repl_graph_degree_command() {
let (db_path, _temp) = setup_graph("kg");
repl_run(&db_path, &[".graph degree kg 10"])
.stdout(predicate::str::contains("Node Degree").or(predicate::str::contains("degree")));
}
#[test]
fn test_repl_schema_vector() {
let (db_path, _temp) = setup_vector("vecs", 4);
repl_run(&db_path, &[".schema vecs"])
.stdout(predicate::str::contains("Vector"))
.stdout(predicate::str::contains("4").or(predicate::str::contains("dim")));
}
#[test]
fn test_repl_schema_graph() {
let (db_path, _temp) = setup_graph("kg");
repl_run(&db_path, &[".schema kg"]).stdout(predicate::str::contains("Graph"));
}
#[test]
fn test_repl_schema_metadata() {
let (db_path, _temp) = setup_metadata("meta");
repl_run(&db_path, &[".schema meta"]).stdout(predicate::str::contains("Metadata"));
}
#[test]
fn test_repl_multi_command_session() {
let (db_path, _temp) = setup_vector("vecs", 4);
repl_run(
&db_path,
&[
".collections",
".count vecs",
".sample vecs 2",
".stats vecs",
],
)
.stdout(predicate::str::contains("vecs"))
.stdout(predicate::str::contains("5"))
.stdout(predicate::str::contains("Vector"));
}
fn setup_cross_collection_enrichment() -> (std::path::PathBuf, TempDir) {
let temp = TempDir::new().unwrap();
let db_path = temp.path().join("db");
let db = velesdb_core::Database::open(&db_path).unwrap();
db.create_graph_collection("catalog", GraphSchema::schemaless())
.unwrap();
let gc = db.get_graph_collection("catalog").unwrap();
gc.upsert_node_payload(
1,
&serde_json::json!({"_labels": ["Product"], "name": "Headphones"}),
)
.unwrap();
gc.upsert_node_payload(
2,
&serde_json::json!({"_labels": ["Warehouse"], "name": "Paris HQ"}),
)
.unwrap();
gc.add_edge(GraphEdge::new(1, 1, 2, "STORED_IN").unwrap())
.unwrap();
gc.flush().unwrap();
db.create_metadata_collection("pricing").unwrap();
let mc = db.get_metadata_collection("pricing").unwrap();
mc.upsert(vec![Point::metadata_only(
2,
serde_json::json!({"price": 99.99, "stock": 50}),
)])
.unwrap();
drop(db);
(db_path, temp)
}
#[test]
fn test_repl_match_at_collection_enrichment() {
let (db_path, _temp) = setup_cross_collection_enrichment();
repl_run(
&db_path,
&[
".use catalog",
"MATCH (p:Product)-[:STORED_IN]->(w:Warehouse@pricing) RETURN p, w LIMIT 10",
],
)
.stdout(predicate::str::contains("w.price"));
}