use hyperdb_mcp::engine::Engine;
use hyperdb_mcp::error::ErrorCode;
use hyperdb_mcp::server::HyperMcpServer;
use hyperdb_mcp::table_catalog::{self, MetadataFields, TABLE_CATALOG_TABLE};
use tempfile::TempDir;
fn workspace_engine() -> (Engine, TempDir) {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let engine = Engine::new_no_daemon(Some(path.to_str().unwrap().into())).unwrap();
(engine, dir)
}
fn table_exists(engine: &Engine, name: &str) -> bool {
let sql = format!(
"SELECT tablename FROM \"persistent\".pg_catalog.pg_tables \
WHERE schemaname = 'public' AND tablename = '{}'",
name.replace('\'', "''")
);
engine
.execute_query_to_json(&sql)
.is_ok_and(|rows| !rows.is_empty())
}
#[test]
fn ensure_exists_is_idempotent() {
let (engine, _dir) = workspace_engine();
table_catalog::ensure_exists(&engine).unwrap();
table_catalog::ensure_exists(&engine).unwrap();
assert!(table_exists(&engine, TABLE_CATALOG_TABLE));
}
#[test]
fn upsert_stub_creates_row_with_null_prose_fields() {
let (engine, _dir) = workspace_engine();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".widgets (id INT)")
.unwrap();
table_catalog::upsert_stub(
&engine,
"widgets",
"load_file",
Some(r#"{"source_path":"/tmp/widgets.csv"}"#),
Some(42),
true,
)
.unwrap();
let entry = table_catalog::get(&engine, "widgets").unwrap().unwrap();
assert_eq!(entry.table_name, "widgets");
assert_eq!(entry.load_tool.as_deref(), Some("load_file"));
assert_eq!(entry.row_count, Some(42));
assert!(entry
.load_params
.as_deref()
.unwrap()
.contains("widgets.csv"));
assert!(entry.source_url.is_none());
assert!(entry.purpose.is_none());
}
#[test]
fn upsert_stub_preserves_prose_on_reload() {
let (engine, _dir) = workspace_engine();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".widgets (id INT)")
.unwrap();
table_catalog::upsert_stub(&engine, "widgets", "load_file", None, Some(10), true).unwrap();
table_catalog::set_metadata(
&engine,
"widgets",
&MetadataFields {
purpose: Some("test data".into()),
source_url: Some("https://example.com/widgets".into()),
..Default::default()
},
)
.unwrap();
table_catalog::upsert_stub(&engine, "widgets", "load_file", None, Some(25), true).unwrap();
let entry = table_catalog::get(&engine, "widgets").unwrap().unwrap();
assert_eq!(entry.row_count, Some(25), "row count should refresh");
assert_eq!(
entry.purpose.as_deref(),
Some("test data"),
"prose should survive a reload"
);
assert_eq!(
entry.source_url.as_deref(),
Some("https://example.com/widgets"),
);
}
#[test]
fn upsert_stub_bump_refresh_updates_last_refreshed_not_loaded_at() {
let (engine, _dir) = workspace_engine();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".widgets (id INT)")
.unwrap();
table_catalog::upsert_stub(&engine, "widgets", "load_file", None, Some(1), true).unwrap();
let first = table_catalog::get(&engine, "widgets").unwrap().unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
table_catalog::upsert_stub(&engine, "widgets", "load_file", None, Some(2), true).unwrap();
let second = table_catalog::get(&engine, "widgets").unwrap().unwrap();
assert_eq!(
first.loaded_at, second.loaded_at,
"loaded_at should be stable across reloads"
);
assert!(
second.last_refreshed_at > first.last_refreshed_at,
"last_refreshed_at should advance on bump_refresh=true"
);
}
#[test]
fn set_metadata_updates_prose_without_touching_mechanical_fields() {
let (engine, _dir) = workspace_engine();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".widgets (id INT)")
.unwrap();
table_catalog::upsert_stub(&engine, "widgets", "load_file", None, Some(10), true).unwrap();
let before = table_catalog::get(&engine, "widgets").unwrap().unwrap();
let entry = table_catalog::set_metadata(
&engine,
"widgets",
&MetadataFields {
purpose: Some("answering demo questions".into()),
notes: Some("refreshed weekly".into()),
..Default::default()
},
)
.unwrap();
assert_eq!(entry.purpose.as_deref(), Some("answering demo questions"));
assert_eq!(entry.notes.as_deref(), Some("refreshed weekly"));
assert_eq!(entry.load_tool, before.load_tool);
assert_eq!(entry.row_count, before.row_count);
assert_eq!(entry.loaded_at, before.loaded_at);
}
#[test]
fn set_metadata_empty_string_clears_field() {
let (engine, _dir) = workspace_engine();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".widgets (id INT)")
.unwrap();
table_catalog::upsert_stub(&engine, "widgets", "load_file", None, Some(1), true).unwrap();
table_catalog::set_metadata(
&engine,
"widgets",
&MetadataFields {
purpose: Some("to be cleared".into()),
..Default::default()
},
)
.unwrap();
let cleared = table_catalog::set_metadata(
&engine,
"widgets",
&MetadataFields {
purpose: Some(String::new()),
..Default::default()
},
)
.unwrap();
assert!(cleared.purpose.is_none());
}
#[test]
fn set_metadata_rejects_empty_payload() {
let (engine, _dir) = workspace_engine();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".widgets (id INT)")
.unwrap();
table_catalog::upsert_stub(&engine, "widgets", "load_file", None, Some(1), true).unwrap();
let err =
table_catalog::set_metadata(&engine, "widgets", &MetadataFields::default()).unwrap_err();
assert_eq!(err.code, ErrorCode::EmptyData);
}
#[test]
fn set_metadata_unknown_table_errors_with_table_not_found() {
let (engine, _dir) = workspace_engine();
table_catalog::ensure_exists(&engine).unwrap();
let err = table_catalog::set_metadata(
&engine,
"no_such_table",
&MetadataFields {
purpose: Some("x".into()),
..Default::default()
},
)
.unwrap_err();
assert_eq!(err.code, ErrorCode::TableNotFound);
}
#[test]
fn reconcile_inserts_stubs_drops_orphans_refreshes_counts() {
let (engine, _dir) = workspace_engine();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".alpha (id INT)")
.unwrap();
engine
.execute_command("INSERT INTO \"persistent\".\"public\".alpha VALUES (1), (2)")
.unwrap();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".beta (id INT)")
.unwrap();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".gamma (id INT)")
.unwrap();
table_catalog::upsert_stub(&engine, "alpha", "load_file", None, Some(0), true).unwrap();
table_catalog::upsert_stub(&engine, "zeta", "load_file", None, Some(100), true).unwrap();
table_catalog::reconcile(&engine).unwrap();
let all = table_catalog::list(&engine).unwrap();
let names: Vec<_> = all.iter().map(|e| e.table_name.clone()).collect();
assert!(names.contains(&"alpha".to_string()));
assert!(names.contains(&"beta".to_string()));
assert!(names.contains(&"gamma".to_string()));
assert!(
!names.contains(&"zeta".to_string()),
"orphan must be dropped"
);
let alpha = all.iter().find(|e| e.table_name == "alpha").unwrap();
assert_eq!(
alpha.row_count,
Some(2),
"existing row count must be refreshed"
);
assert_eq!(alpha.load_tool.as_deref(), Some("load_file"));
let beta = all.iter().find(|e| e.table_name == "beta").unwrap();
assert_eq!(
beta.load_tool.as_deref(),
Some("unknown"),
"newly discovered tables get load_tool='unknown'"
);
}
#[test]
fn reconcile_skips_internal_tables() {
let (engine, _dir) = workspace_engine();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".alpha (id INT)")
.unwrap();
engine
.execute_command("CREATE TABLE _hyperdb_saved_queries (name TEXT NOT NULL)")
.unwrap();
table_catalog::reconcile(&engine).unwrap();
let entries = table_catalog::list(&engine).unwrap();
let names: Vec<_> = entries.iter().map(|e| e.table_name.clone()).collect();
assert!(names.contains(&"alpha".to_string()));
assert!(!names.contains(&TABLE_CATALOG_TABLE.to_string()));
assert!(!names.contains(&"_hyperdb_saved_queries".to_string()));
}
#[test]
fn delete_for_is_idempotent() {
let (engine, _dir) = workspace_engine();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".widgets (id INT)")
.unwrap();
table_catalog::upsert_stub(&engine, "widgets", "load_file", None, Some(1), true).unwrap();
assert!(table_catalog::delete_for(&engine, "widgets").unwrap());
assert!(!table_catalog::delete_for(&engine, "widgets").unwrap());
assert!(table_catalog::get(&engine, "widgets").unwrap().is_none());
}
#[test]
fn default_server_auto_creates_catalog_on_first_engine_use() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let path_str = path.to_str().unwrap().to_string();
{
let server = HyperMcpServer::with_no_daemon(Some(path_str.clone()), false, true);
let _ = server.resource_body_for_uri("hyper://workspace").unwrap();
}
let engine = Engine::new_no_daemon(Some(path_str)).unwrap();
assert!(
table_exists(&engine, TABLE_CATALOG_TABLE),
"_table_catalog must be present in the workspace after a default server has touched it"
);
}
#[test]
fn read_only_server_does_not_create_catalog() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let path_str = path.to_str().unwrap().to_string();
{
let engine = Engine::new_no_daemon(Some(path_str.clone())).unwrap();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".widgets (id INT)")
.unwrap();
}
{
let server = HyperMcpServer::with_no_daemon(Some(path_str.clone()), true, true);
let _ = server.resource_body_for_uri("hyper://workspace").unwrap();
}
let engine = Engine::new_no_daemon(Some(path_str)).unwrap();
assert!(
!table_exists(&engine, TABLE_CATALOG_TABLE),
"_table_catalog must NOT be created by a read-only server"
);
}
#[test]
fn backfill_stubs_preexisting_tables_on_reopen() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let path_str = path.to_str().unwrap().to_string();
{
let engine = Engine::new_no_daemon(Some(path_str.clone())).unwrap();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".alpha (id INT)")
.unwrap();
engine
.execute_command("INSERT INTO \"persistent\".\"public\".alpha VALUES (1), (2), (3)")
.unwrap();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".beta (id INT)")
.unwrap();
}
{
let server = HyperMcpServer::with_no_daemon(Some(path_str.clone()), false, true);
let _ = server.resource_body_for_uri("hyper://workspace").unwrap();
}
let engine = Engine::new_no_daemon(Some(path_str)).unwrap();
let entries = table_catalog::list(&engine).unwrap();
let names: Vec<_> = entries.iter().map(|e| e.table_name.clone()).collect();
assert!(names.contains(&"alpha".to_string()));
assert!(names.contains(&"beta".to_string()));
let alpha = entries.iter().find(|e| e.table_name == "alpha").unwrap();
assert_eq!(alpha.row_count, Some(3));
assert_eq!(
alpha.load_tool.as_deref(),
Some("unknown"),
"backfilled rows are tagged as unknown origin"
);
}
#[test]
fn rename_table_preserves_catalog_metadata() {
let tmp = TempDir::new().unwrap();
let path_str = tmp
.path()
.join("workspace.hyper")
.to_str()
.unwrap()
.to_string();
let engine = Engine::new_no_daemon(Some(path_str.clone())).unwrap();
engine
.execute_command(
"CREATE TABLE \"persistent\".\"public\".population (id INT, name TEXT NOT NULL)",
)
.unwrap();
engine
.execute_command(
"INSERT INTO \"persistent\".\"public\".population VALUES (1, 'Earth'), (2, 'Mars'), (3, 'Venus')"
)
.unwrap();
table_catalog::upsert_stub(
&engine,
"population",
"load_file",
Some("{\"path\":\"/tmp/population.csv\"}"),
Some(3),
false,
)
.unwrap();
table_catalog::set_metadata(
&engine,
"population",
&MetadataFields {
source_url: Some("https://example.com/population".into()),
purpose: Some("Test rename metadata preservation".into()),
notes: Some("Refresh: curl ...".into()),
..Default::default()
},
)
.unwrap();
let before = table_catalog::get(&engine, "population").unwrap().unwrap();
assert_eq!(
before.source_url.as_deref(),
Some("https://example.com/population")
);
assert_eq!(
before.purpose.as_deref(),
Some("Test rename metadata preservation")
);
assert_eq!(before.notes.as_deref(), Some("Refresh: curl ..."));
assert_eq!(before.load_tool.as_deref(), Some("load_file"));
assert_eq!(before.row_count, Some(3));
engine
.execute_command(
"ALTER TABLE \"persistent\".\"public\".population RENAME TO owid_population",
)
.unwrap();
table_catalog::reconcile(&engine).unwrap();
assert!(
table_catalog::get(&engine, "population").unwrap().is_none(),
"old table name should not exist in catalog after rename"
);
let after = table_catalog::get(&engine, "owid_population")
.unwrap()
.expect("renamed table should have a catalog entry");
assert_eq!(
after.source_url.as_deref(),
Some("https://example.com/population"),
"source_url must survive rename"
);
assert_eq!(
after.purpose.as_deref(),
Some("Test rename metadata preservation"),
"purpose must survive rename"
);
assert_eq!(
after.notes.as_deref(),
Some("Refresh: curl ..."),
"notes must survive rename"
);
assert_eq!(
after.load_tool.as_deref(),
Some("load_file"),
"load_tool must survive rename"
);
assert_eq!(after.row_count, Some(3), "row_count must survive rename");
assert_eq!(
after.loaded_at, before.loaded_at,
"loaded_at should be anchored to the original load time, not the rename time"
);
assert_eq!(
after.last_refreshed_at, before.last_refreshed_at,
"last_refreshed_at should be anchored to the original load time, not bumped on rename"
);
}
#[test]
fn set_metadata_data_url_roundtrip() {
let (engine, _dir) = workspace_engine();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".widgets (id INT)")
.unwrap();
table_catalog::upsert_stub(&engine, "widgets", "load_file", None, Some(10), true).unwrap();
let entry = table_catalog::set_metadata(
&engine,
"widgets",
&MetadataFields {
data_url: Some("https://example.com/widgets.csv?v=2".into()),
..Default::default()
},
)
.unwrap();
assert_eq!(
entry.data_url.as_deref(),
Some("https://example.com/widgets.csv?v=2"),
"data_url should round-trip through set_metadata"
);
let fresh = table_catalog::get(&engine, "widgets").unwrap().unwrap();
assert_eq!(
fresh.data_url.as_deref(),
Some("https://example.com/widgets.csv?v=2")
);
table_catalog::set_metadata(
&engine,
"widgets",
&MetadataFields {
data_url: Some(String::new()),
..Default::default()
},
)
.unwrap();
let cleared = table_catalog::get(&engine, "widgets").unwrap().unwrap();
assert!(
cleared.data_url.is_none(),
"empty string should clear data_url"
);
}