mod common;
use common::TestEngine;
use hyperdb_mcp::error::ErrorCode;
#[test]
fn engine_starts_and_connects() {
let te = TestEngine::new_ephemeral();
assert!(te.engine.is_running());
}
#[test]
fn engine_hides_hyperdb_internal_tables() {
let te = TestEngine::new_ephemeral();
te.engine
.execute_command("CREATE TABLE widgets (id INT, label TEXT)")
.unwrap();
te.engine
.execute_command(
"CREATE TABLE _hyperdb_saved_queries (\
name TEXT, sql TEXT, description TEXT, created_at TIMESTAMP)",
)
.unwrap();
te.engine
.execute_command("INSERT INTO widgets VALUES (1, 'alpha'), (2, 'beta')")
.unwrap();
let tables = te.engine.describe_tables().unwrap();
assert_eq!(tables.len(), 1, "meta-table should be hidden from describe");
assert_eq!(tables[0]["name"], "widgets");
let status = te.engine.status().unwrap();
assert_eq!(
status["table_count"], 1,
"table_count should exclude meta-tables: {status}"
);
assert_eq!(
status["total_rows"], 2,
"total_rows should not count meta-table rows: {status}"
);
}
#[test]
fn engine_create_table_and_query() {
let te = TestEngine::new_ephemeral();
te.engine
.execute_command("CREATE TABLE test (id INT, name TEXT)")
.unwrap();
te.engine
.execute_command("INSERT INTO test VALUES (1, 'Alice')")
.unwrap();
let rows = te
.engine
.execute_query_to_json("SELECT * FROM test")
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["id"], 1);
assert_eq!(rows[0]["name"], "Alice");
}
#[test]
fn engine_describe_tables() {
let te = TestEngine::new_ephemeral();
te.engine
.execute_command("CREATE TABLE orders (id INT NOT NULL, amount DOUBLE PRECISION)")
.unwrap();
let tables = te.engine.describe_tables().unwrap();
assert_eq!(tables.len(), 1);
assert_eq!(tables[0]["name"], "orders");
}
#[test]
fn engine_describe_single_table() {
let te = TestEngine::new_ephemeral();
te.engine
.execute_command("CREATE TABLE widgets (id INT NOT NULL, label TEXT)")
.unwrap();
te.engine
.execute_command("CREATE TABLE gadgets (sku TEXT, price DOUBLE PRECISION)")
.unwrap();
te.engine
.execute_command("INSERT INTO widgets VALUES (1, 'alpha'), (2, 'beta')")
.unwrap();
let widgets = te.engine.describe_table("widgets").unwrap();
assert_eq!(widgets["name"], "widgets");
assert_eq!(widgets["row_count"], 2);
let cols = widgets["columns"].as_array().expect("columns array");
assert_eq!(cols.len(), 2);
assert_eq!(cols[0]["name"], "id");
assert_eq!(cols[0]["nullable"], false);
assert_eq!(cols[1]["name"], "label");
assert_eq!(cols[1]["nullable"], true);
let err = te.engine.describe_table("does_not_exist").unwrap_err();
assert_eq!(err.code, ErrorCode::TableNotFound);
}
#[test]
fn engine_describe_table_hides_internal() {
let te = TestEngine::new_ephemeral();
te.engine
.execute_command(
"CREATE TABLE _hyperdb_saved_queries (\
name TEXT, sql TEXT, description TEXT, created_at TIMESTAMP)",
)
.unwrap();
let err = te
.engine
.describe_table("_hyperdb_saved_queries")
.unwrap_err();
assert_eq!(err.code, ErrorCode::TableNotFound);
}
#[test]
fn engine_status() {
let te = TestEngine::new_ephemeral();
let status = te.engine.status().unwrap();
assert_eq!(status["hyperd_running"], true);
assert_eq!(status["table_count"], 0);
let version = status["hyper_rust_api_version"]
.as_str()
.expect("hyper_rust_api_version string");
assert!(
version.contains(".r"),
"hyper_rust_api_version should carry .r<hash>: {version}"
);
}
#[test]
fn create_table_append_is_idempotent() {
use hyperdb_mcp::schema::ColumnSchema;
let te = TestEngine::new_ephemeral();
let cols = vec![
ColumnSchema {
name: "id".into(),
hyper_type: "INT".into(),
nullable: false,
},
ColumnSchema {
name: "name".into(),
hyper_type: "TEXT".into(),
nullable: true,
},
];
te.engine.create_table("idempotent", &cols, false).unwrap();
te.engine.create_table("idempotent", &cols, false).unwrap();
let other = vec![ColumnSchema {
name: "different".into(),
hyper_type: "BIGINT".into(),
nullable: true,
}];
te.engine.create_table("idempotent", &other, false).unwrap();
let tables = te.engine.describe_tables().unwrap();
let t = tables.iter().find(|t| t["name"] == "idempotent").unwrap();
let cols = t["columns"].as_array().unwrap();
let names: Vec<&str> = cols.iter().filter_map(|c| c["name"].as_str()).collect();
assert!(names.contains(&"id"));
assert!(names.contains(&"name"));
assert!(!names.contains(&"different"));
}
#[test]
fn create_table_replace_overwrites() {
use hyperdb_mcp::schema::ColumnSchema;
let te = TestEngine::new_ephemeral();
let cols_a = vec![ColumnSchema {
name: "a".into(),
hyper_type: "INT".into(),
nullable: true,
}];
let cols_b = vec![ColumnSchema {
name: "b".into(),
hyper_type: "TEXT".into(),
nullable: true,
}];
te.engine.create_table("t", &cols_a, true).unwrap();
te.engine.create_table("t", &cols_b, true).unwrap();
let tables = te.engine.describe_tables().unwrap();
let t = tables.iter().find(|t| t["name"] == "t").unwrap();
let names: Vec<&str> = t["columns"]
.as_array()
.unwrap()
.iter()
.filter_map(|c| c["name"].as_str())
.collect();
assert_eq!(names, vec!["b"]);
}
#[test]
fn create_table_rejects_unknown_type() {
use hyperdb_mcp::error::ErrorCode;
use hyperdb_mcp::schema::ColumnSchema;
let te = TestEngine::new_ephemeral();
let cols = vec![ColumnSchema {
name: "x".into(),
hyper_type: "NOT_A_REAL_TYPE".into(),
nullable: true,
}];
let err = te.engine.create_table("t", &cols, false).unwrap_err();
assert_eq!(err.code, ErrorCode::SchemaMismatch);
te.engine.execute_command("CREATE TABLE u (v INT)").unwrap();
}
#[test]
fn engine_exposes_log_paths() {
let te = TestEngine::new_ephemeral();
let log_dir = te.engine.log_dir();
assert!(log_dir.exists(), "log_dir should have been created");
let status = te.engine.status().unwrap();
let logs = status.get("logs").expect("status should include logs");
assert!(logs["log_dir"].is_string());
assert!(logs.get("hyperd_log").is_some());
assert!(logs.get("client_log").is_some());
}
#[test]
fn engine_numeric_columns_and_aggregates_render_as_json_numbers() {
let te = TestEngine::new_ephemeral();
te.engine
.execute_command("CREATE TABLE sales (id INT, price NUMERIC(10, 2))")
.unwrap();
te.engine
.execute_command("INSERT INTO sales VALUES (1, 12.50), (2, 7.25), (3, 100.00)")
.unwrap();
let rows = te
.engine
.execute_query_to_json("SELECT id, price FROM sales ORDER BY id")
.unwrap();
assert_eq!(rows.len(), 3);
let prices: Vec<f64> = rows
.iter()
.map(|r| {
r["price"]
.as_f64()
.unwrap_or_else(|| panic!("price rendered as non-number: {:?}", r["price"]))
})
.collect();
assert!((prices[0] - 12.50).abs() < 1e-9, "got {}", prices[0]);
assert!((prices[1] - 7.25).abs() < 1e-9, "got {}", prices[1]);
assert!((prices[2] - 100.00).abs() < 1e-9, "got {}", prices[2]);
let avg_rows = te
.engine
.execute_query_to_json("SELECT AVG(id) AS avg_id FROM sales")
.unwrap();
assert_eq!(avg_rows.len(), 1);
let avg = avg_rows[0]["avg_id"]
.as_f64()
.unwrap_or_else(|| panic!("AVG rendered as non-number: {:?}", avg_rows[0]["avg_id"]));
assert!((avg - 2.0).abs() < 1e-9, "got {avg}");
}
#[test]
fn resolve_log_dir_picks_expected_paths() {
use hyperdb_mcp::engine::resolve_log_dir;
let persistent = resolve_log_dir(Some("/tmp/subdir/project.hyper"));
assert_eq!(persistent, std::path::PathBuf::from("/tmp/subdir"));
let ephemeral = resolve_log_dir(None);
let expected_prefix = format!("hyperdb-mcp-{}", std::process::id());
assert!(
ephemeral
.file_name()
.and_then(|s| s.to_str())
.is_some_and(|s| s == expected_prefix),
"ephemeral log dir should end with {expected_prefix}, got {ephemeral:?}"
);
}