mod common;
use common::native_harness::{do_handshake, send_sql};
use common::pgwire_harness::TestServer;
use nodedb_types::protocol::HelloFrame;
use nodedb_types::protocol::opcodes::ResponseStatus;
use nodedb_types::value::Value;
use tokio::net::TcpStream;
async fn native_session(srv: &TestServer) -> TcpStream {
let addr = format!("127.0.0.1:{}", srv.native_port)
.parse()
.expect("native addr");
let (stream, _ack) = do_handshake(addr, &HelloFrame::current())
.await
.expect("native handshake");
stream
}
fn looks_like_document_blob(cell: &Value) -> bool {
matches!(cell, Value::String(s) if s.trim_start().starts_with('{'))
}
#[tokio::test]
async fn doc_strict_full_scan_projects_declared_columns() {
let srv = TestServer::start().await;
srv.exec("CREATE TABLE nb_doc (id TEXT PRIMARY KEY, title TEXT)")
.await
.expect("create");
srv.exec("INSERT INTO nb_doc (id, title) VALUES ('x', 'foo')")
.await
.expect("insert");
let mut stream = native_session(&srv).await;
let resp = send_sql(&mut stream, 1, "SELECT id, title FROM nb_doc").await;
srv.graceful_shutdown().await;
assert_ne!(
resp.status,
ResponseStatus::Error,
"full scan must succeed: {resp:?}"
);
let columns = resp.columns.expect("columns present");
assert_eq!(
columns,
vec!["id".to_string(), "title".to_string()],
"full scan must project declared columns, not storage tuple; got {columns:?}"
);
assert!(
!columns.contains(&"data".to_string()),
"full scan must not expose the internal `data` blob column: {columns:?}"
);
let rows = resp.rows.expect("rows present");
assert_eq!(rows.len(), 1, "one row expected: {rows:?}");
assert_eq!(rows[0][0], Value::String("x".into()), "id cell");
assert_eq!(rows[0][1], Value::String("foo".into()), "title cell");
assert!(
!looks_like_document_blob(&rows[0][0]),
"id cell must be the scalar, not a JSON document blob: {:?}",
rows[0][0]
);
}
#[tokio::test]
async fn doc_strict_point_lookup_projects_declared_columns() {
let srv = TestServer::start().await;
srv.exec("CREATE TABLE nb_doc_pt (id TEXT PRIMARY KEY, title TEXT)")
.await
.expect("create");
srv.exec("INSERT INTO nb_doc_pt (id, title) VALUES ('x', 'foo')")
.await
.expect("insert");
let mut stream = native_session(&srv).await;
let resp = send_sql(
&mut stream,
1,
"SELECT id, title FROM nb_doc_pt WHERE id = 'x'",
)
.await;
srv.graceful_shutdown().await;
assert_ne!(
resp.status,
ResponseStatus::Error,
"point lookup must succeed: {resp:?}"
);
let columns = resp.columns.expect("columns present");
assert_eq!(
columns,
vec!["id".to_string(), "title".to_string()],
"point lookup must project declared columns; got {columns:?}"
);
let rows = resp.rows.expect("rows present");
assert_eq!(rows[0][1], Value::String("foo".into()), "title cell");
}
#[tokio::test]
async fn doc_schemaless_full_scan_projects_declared_columns() {
let srv = TestServer::start().await;
srv.exec("CREATE COLLECTION nb_docs WITH (engine='document_schemaless')")
.await
.expect("create");
srv.exec("INSERT INTO nb_docs { id: 'x', title: 'foo' }")
.await
.expect("insert");
let mut stream = native_session(&srv).await;
let resp = send_sql(&mut stream, 1, "SELECT id, title FROM nb_docs").await;
srv.graceful_shutdown().await;
assert_ne!(
resp.status,
ResponseStatus::Error,
"full scan must succeed: {resp:?}"
);
let columns = resp.columns.expect("columns present");
assert_eq!(
columns,
vec!["id".to_string(), "title".to_string()],
"schemaless full scan must project declared columns, not storage tuple; got {columns:?}"
);
assert!(
!columns.contains(&"data".to_string()),
"full scan must not expose the internal `data` blob column: {columns:?}"
);
let rows = resp.rows.expect("rows present");
assert_eq!(rows[0][0], Value::String("x".into()), "id cell");
assert_eq!(rows[0][1], Value::String("foo".into()), "title cell");
}
#[tokio::test]
async fn kv_full_scan_includes_value_column() {
let srv = TestServer::start().await;
srv.exec("CREATE COLLECTION nb_kv (key TEXT PRIMARY KEY, value TEXT) WITH (engine='kv')")
.await
.expect("create");
srv.exec("INSERT INTO nb_kv (key, value) VALUES ('k1', 'v1')")
.await
.expect("insert");
let mut stream = native_session(&srv).await;
let resp = send_sql(&mut stream, 1, "SELECT key, value FROM nb_kv").await;
srv.graceful_shutdown().await;
assert_ne!(
resp.status,
ResponseStatus::Error,
"full scan must succeed: {resp:?}"
);
let columns = resp.columns.expect("columns present");
assert_eq!(
columns,
vec!["key".to_string(), "value".to_string()],
"KV full scan must project key AND value; got {columns:?}"
);
assert!(
columns.contains(&"value".to_string()),
"KV full scan must include the `value` column: {columns:?}"
);
let rows = resp.rows.expect("rows present");
assert_eq!(rows[0][0], Value::String("k1".into()), "key cell");
assert_eq!(
rows[0][1],
Value::String("v1".into()),
"value cell must carry the payload"
);
}
#[tokio::test]
async fn kv_point_lookup_includes_value_column() {
let srv = TestServer::start().await;
srv.exec("CREATE COLLECTION nb_kv_pt (key TEXT PRIMARY KEY, value TEXT) WITH (engine='kv')")
.await
.expect("create");
srv.exec("INSERT INTO nb_kv_pt (key, value) VALUES ('k1', 'v1')")
.await
.expect("insert");
let mut stream = native_session(&srv).await;
let resp = send_sql(
&mut stream,
1,
"SELECT key, value FROM nb_kv_pt WHERE key = 'k1'",
)
.await;
srv.graceful_shutdown().await;
assert_ne!(
resp.status,
ResponseStatus::Error,
"point lookup must succeed: {resp:?}"
);
let columns = resp.columns.expect("columns present");
assert!(
columns.contains(&"value".to_string()),
"KV point lookup must include the `value` column: {columns:?}"
);
let rows = resp.rows.expect("rows present");
let value_idx = columns
.iter()
.position(|c| c == "value")
.expect("value column");
assert_eq!(
rows[0][value_idx],
Value::String("v1".into()),
"value cell must carry the payload"
);
}
#[tokio::test]
async fn vector_search_serializes_distance() {
let srv = TestServer::start().await;
srv.exec("CREATE COLLECTION nb_vec WITH (engine='vector')")
.await
.expect("create");
srv.exec("CREATE INDEX ON nb_vec (embedding)")
.await
.expect("index");
srv.exec("INSERT INTO nb_vec { id: 'a', embedding: [0.1, 0.2, 0.3] }")
.await
.expect("insert");
let mut stream = native_session(&srv).await;
let resp = send_sql(
&mut stream,
1,
"SEARCH nb_vec USING VECTOR(embedding, ARRAY[0.1, 0.2, 0.3], 1)",
)
.await;
srv.graceful_shutdown().await;
assert_ne!(
resp.status,
ResponseStatus::Error,
"vector search must succeed: {resp:?}"
);
let columns = resp.columns.expect("columns present");
let dist_idx = columns
.iter()
.position(|c| c == "distance")
.unwrap_or_else(|| panic!("a `distance` column must be present: {columns:?}"));
let rows = resp.rows.expect("rows present");
assert_eq!(rows.len(), 1, "one match expected: {rows:?}");
assert_ne!(
rows[0][dist_idx],
Value::Null,
"distance must be serialized, not collapsed to NULL: row={:?}",
rows[0]
);
assert!(
matches!(rows[0][dist_idx], Value::Float(_) | Value::Integer(_)),
"distance must be numeric, got {:?}",
rows[0][dist_idx]
);
}