use std::collections::HashSet;
use std::sync::Arc;
use nodedb_client::NodeDb;
use nodedb_lite::{NodeDbLite, PagedbStorageMem};
use nodedb_types::document::Document;
use nodedb_types::value::Value;
use crate::common::origin::OriginServer;
use crate::common::sql::{OriginPgwire, open_lite};
async fn lite_register_collection(db: &Arc<NodeDbLite<PagedbStorageMem>>, name: &str) {
let mut doc = Document::new("__bootstrap__");
doc.set("_init", Value::Bool(true));
db.document_put(name, doc)
.await
.unwrap_or_else(|e| panic!("lite_register_collection({name}): {e}"));
db.document_delete(name, "__bootstrap__")
.await
.unwrap_or_else(|e| panic!("lite_register_collection delete({name}): {e}"));
}
async fn origin_create_schemaless(pg: &OriginPgwire, name: &str) {
pg.execute(&format!(
"CREATE COLLECTION {name} WITH (engine='document_schemaless')"
))
.await;
}
async fn origin_insert(pg: &OriginPgwire, coll: &str, id: &str, name: &str, age: i64) {
pg.execute(&format!(
"INSERT INTO {coll} (id, name, age) VALUES ('{id}', '{name}', {age})"
))
.await;
}
async fn lite_insert(
db: &Arc<NodeDbLite<PagedbStorageMem>>,
coll: &str,
id: &str,
name: &str,
age: i64,
) {
db.execute_sql(
&format!("INSERT INTO {coll} (id, name, age) VALUES ('{id}', '{name}', {age})"),
&[],
)
.await
.unwrap_or_else(|e| panic!("Lite INSERT failed: {e}"));
}
async fn lite_ids(db: &Arc<NodeDbLite<PagedbStorageMem>>, coll: &str) -> HashSet<String> {
let result = db
.execute_sql(&format!("SELECT id, document FROM {coll}"), &[])
.await
.unwrap_or_else(|e| panic!("Lite SELECT failed: {e}"));
result
.rows
.iter()
.filter_map(|row| {
row.first().and_then(|v| {
if let nodedb_types::value::Value::String(s) = v {
Some(s.clone())
} else {
None
}
})
})
.collect()
}
async fn origin_ids(pg: &OriginPgwire, coll: &str) -> HashSet<String> {
let rows = pg.query(&format!("SELECT id FROM {coll}")).await;
rows.iter()
.map(|r| {
let id: String = r
.try_get::<_, String>(0)
.unwrap_or_else(|_| r.try_get::<_, i64>(0).unwrap_or(0).to_string());
id
})
.collect()
}
#[tokio::test]
async fn document_insert_parity() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let pg = OriginPgwire::connect().await;
let db = open_lite().await;
origin_create_schemaless(&pg, "parity_doc_insert").await;
lite_register_collection(&db, "parity_doc_insert").await;
origin_insert(&pg, "parity_doc_insert", "d1", "Alice", 30).await;
origin_insert(&pg, "parity_doc_insert", "d2", "Bob", 25).await;
lite_insert(&db, "parity_doc_insert", "d1", "Alice", 30).await;
lite_insert(&db, "parity_doc_insert", "d2", "Bob", 25).await;
let lite = lite_ids(&db, "parity_doc_insert").await;
let origin = origin_ids(&pg, "parity_doc_insert").await;
assert_eq!(
lite, origin,
"document ID sets must match after INSERT\nlite={lite:?}\norigin={origin:?}"
);
}
#[tokio::test]
async fn document_delete_parity() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let pg = OriginPgwire::connect().await;
let db = open_lite().await;
origin_create_schemaless(&pg, "parity_doc_delete").await;
lite_register_collection(&db, "parity_doc_delete").await;
origin_insert(&pg, "parity_doc_delete", "x1", "Carol", 40).await;
origin_insert(&pg, "parity_doc_delete", "x2", "Dave", 35).await;
lite_insert(&db, "parity_doc_delete", "x1", "Carol", 40).await;
lite_insert(&db, "parity_doc_delete", "x2", "Dave", 35).await;
pg.execute("DELETE FROM parity_doc_delete WHERE id = 'x1'")
.await;
db.execute_sql("DELETE FROM parity_doc_delete WHERE id = 'x1'", &[])
.await
.unwrap_or_else(|e| panic!("Lite DELETE failed: {e}"));
let lite = lite_ids(&db, "parity_doc_delete").await;
let origin = origin_ids(&pg, "parity_doc_delete").await;
assert_eq!(
lite, origin,
"document ID sets must match after DELETE\nlite={lite:?}\norigin={origin:?}"
);
assert!(
!lite.contains("x1"),
"deleted document 'x1' must not appear on Lite"
);
assert!(
!origin.contains("x1"),
"deleted document 'x1' must not appear on Origin"
);
}
#[tokio::test]
async fn document_update_parity() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let pg = OriginPgwire::connect().await;
let db = open_lite().await;
origin_create_schemaless(&pg, "parity_doc_update").await;
lite_register_collection(&db, "parity_doc_update").await;
origin_insert(&pg, "parity_doc_update", "u1", "Eve", 20).await;
lite_insert(&db, "parity_doc_update", "u1", "Eve", 20).await;
pg.execute("UPDATE parity_doc_update SET age = 21 WHERE id = 'u1'")
.await;
db.execute_sql("UPDATE parity_doc_update SET age = 21 WHERE id = 'u1'", &[])
.await
.unwrap_or_else(|e| panic!("Lite UPDATE failed: {e}"));
let lite = lite_ids(&db, "parity_doc_update").await;
let origin = origin_ids(&pg, "parity_doc_update").await;
assert_eq!(lite, origin, "IDs must match after UPDATE");
assert!(lite.contains("u1"), "u1 must still be present after UPDATE");
}
#[tokio::test]
async fn document_truncate_parity() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let pg = OriginPgwire::connect().await;
let db = open_lite().await;
origin_create_schemaless(&pg, "parity_doc_truncate").await;
lite_register_collection(&db, "parity_doc_truncate").await;
origin_insert(&pg, "parity_doc_truncate", "t1", "Frank", 50).await;
origin_insert(&pg, "parity_doc_truncate", "t2", "Grace", 45).await;
lite_insert(&db, "parity_doc_truncate", "t1", "Frank", 50).await;
lite_insert(&db, "parity_doc_truncate", "t2", "Grace", 45).await;
pg.execute("TRUNCATE parity_doc_truncate").await;
db.execute_sql("TRUNCATE parity_doc_truncate", &[])
.await
.unwrap_or_else(|e| panic!("Lite TRUNCATE failed: {e}"));
let lite = lite_ids(&db, "parity_doc_truncate").await;
let origin = origin_ids(&pg, "parity_doc_truncate").await;
assert!(lite.is_empty(), "Lite must be empty after TRUNCATE");
assert!(origin.is_empty(), "Origin must be empty after TRUNCATE");
assert_eq!(lite, origin, "both empty after TRUNCATE");
}
#[tokio::test]
async fn document_select_constant_parity() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let pg = OriginPgwire::connect().await;
let db = open_lite().await;
let lite_result = db
.execute_sql("SELECT 42 AS answer", &[])
.await
.expect("Lite SELECT 42");
assert_eq!(
lite_result.rows.len(),
1,
"Lite: SELECT 42 must return 1 row"
);
let origin_rows = pg.query("SELECT 42 AS answer").await;
assert_eq!(origin_rows.len(), 1, "Origin: SELECT 42 must return 1 row");
let lite_val = match &lite_result.rows[0][0] {
nodedb_types::value::Value::Integer(i) => *i,
other => panic!("expected Integer, got {other:?}"),
};
let origin_val: i64 = origin_rows[0].get::<_, i64>(0);
assert_eq!(lite_val, origin_val, "SELECT 42 value must match");
}