use std::sync::Arc;
use axum::extract::State;
use axum::http::StatusCode;
use maud::{html, Markup, PreEscaped};
use neumann_parser::ast::{EntityOp, NodeOp, SpatialOp, StatementKind};
use crate::web::templates::{format_number, layout, m_header, m_section, m_stat};
use crate::web::AdminContext;
use crate::web::NavItem;
pub mod blob;
pub mod cache;
pub mod chain;
pub mod checkpoint;
pub mod contraction;
pub mod graph;
pub mod graph_algorithms;
pub mod metrics;
pub mod relational;
pub mod storage;
pub mod vault;
pub mod vector;
pub struct DashboardStats {
pub table_count: usize,
pub total_rows: usize,
pub vector_count: usize,
pub collection_count: usize,
pub node_count: usize,
pub edge_count: usize,
pub top_tables: Vec<(String, String)>,
pub collections: Vec<(String, String)>,
pub graph_summary: Vec<(String, String)>,
pub secret_count: Option<usize>,
pub cache_hit_rate: Option<String>,
pub cache_entries: Option<usize>,
}
impl DashboardStats {
fn gather(ctx: &AdminContext) -> Self {
let tables = ctx.relational.list_tables();
let table_count = tables.len();
let mut total_rows = 0;
let mut top_tables = Vec::new();
for table in tables.iter().take(5) {
let count = ctx.relational.row_count(table).unwrap_or(0);
total_rows += count;
top_tables.push((table.clone(), format_number(count)));
}
let collections_list = ctx.vector.list_collections();
let collection_count = collections_list.len();
let mut vector_count = 0;
let mut collections = Vec::new();
for coll in collections_list.iter().take(5) {
let count = ctx.vector.collection_count(coll);
vector_count += count;
collections.push((coll.clone(), format_number(count)));
}
let default_count = ctx.vector.count();
if default_count > 0 {
vector_count += default_count;
if collections.len() < 5 {
collections.push(("(default)".to_string(), format_number(default_count)));
}
}
let node_count = ctx.graph.node_count();
let edge_count = ctx.graph.edge_count();
let graph_summary = vec![
("Nodes".to_string(), format_number(node_count)),
("Edges".to_string(), format_number(edge_count)),
];
let secret_count = ctx.vault.as_ref().map(|v| {
v.list_with_metadata("node:root", "*")
.map_or(0, |list| list.len())
});
let (cache_entries, cache_hit_rate) = ctx.cache.as_ref().map_or((None, None), |c| {
let snap = c.stats_snapshot();
let total_hits = snap.exact_hits + snap.semantic_hits + snap.embedding_hits;
let total_misses = snap.exact_misses + snap.semantic_misses + snap.embedding_misses;
let total = total_hits + total_misses;
let entries = snap.exact_size + snap.semantic_size + snap.embedding_size;
let rate = if total > 0 {
#[allow(clippy::cast_precision_loss)]
let pct = (total_hits as f64 / total as f64) * 100.0;
format!("{pct:.1}%")
} else {
"N/A".to_string()
};
(Some(entries), Some(rate))
});
Self {
table_count,
total_rows,
vector_count,
collection_count,
node_count,
edge_count,
top_tables,
collections,
graph_summary,
secret_count,
cache_hit_rate,
cache_entries,
}
}
}
#[allow(clippy::too_many_lines)]
pub async fn dashboard(State(ctx): State<Arc<AdminContext>>) -> Markup {
let stats = DashboardStats::gather(&ctx);
let content = html! {
(m_header("DASHBOARD", None))
div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-8 stagger-container" {
(m_stat("TABLES", &stats.table_count.to_string(), "relational", "relational"))
(m_stat("VECTORS", &format_number(stats.vector_count), "embeddings", "vector"))
(m_stat("NODES", &format_number(stats.node_count), "graph", "graph"))
(m_stat("ROWS", &format_number(stats.total_rows), "total records", "relational"))
(m_stat("COLLECTIONS", &stats.collection_count.to_string(), "vector", "vector"))
(m_stat("EDGES", &format_number(stats.edge_count), "relationships", "graph"))
@if let Some(count) = stats.secret_count {
(m_stat("SECRETS", &format_number(count), "vault", "vault"))
}
@if let Some(entries) = stats.cache_entries {
(m_stat("CACHED", &format_number(entries), "responses", "cache"))
}
@if let Some(ref rate) = stats.cache_hit_rate {
(m_stat("HIT RATE", rate, "cache", "cache"))
}
}
div class="grid grid-cols-1 lg:grid-cols-3 gap-6" {
(m_section("RELATIONAL", "relational", &stats.top_tables))
(m_section("VECTOR", "vector", &stats.collections))
(m_section("GRAPH", "graph", &stats.graph_summary))
}
div class="m-card mt-6" {
div class="m-card-header" { "QUERY TERMINAL" }
div class="m-card-content" {
div id="terminal-output" class="m-terminal-output mb-3" {
div class="m-terminal-output-line success" { "> System initialized" }
div class="m-terminal-output-line success" { "> All engines operational" }
div class="m-terminal-output-line" { "> Type a query (Ctrl+Enter to execute)" }
}
form id="terminal-form" class="m-terminal-input-line" {
textarea
id="terminal-input"
class="m-terminal-input-field m-terminal-textarea"
placeholder="SELECT * FROM documents LIMIT 5"
autocomplete="off"
rows="3"
spellcheck="false" {}
}
}
div class="m-card-footer flex justify-between items-center" {
span { "Ctrl+Enter to execute | Esc to clear | Up/Down for history" }
span class="text-neutral-400" { "Multi-line supported" }
}
}
script { (PreEscaped(r"
const form = document.getElementById('terminal-form');
const input = document.getElementById('terminal-input');
const output = document.getElementById('terminal-output');
// Command history
let history = [];
let historyIndex = -1;
// Auto-resize textarea
function autoResize() {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 200) + 'px';
}
input.addEventListener('input', autoResize);
// Execute query function
async function executeQuery() {
const query = input.value.trim();
if (!query) return;
// Add to history
history.push(query);
historyIndex = history.length;
// Show command (handle multi-line)
const lines = query.split('\n');
lines.forEach((line, i) => {
addLine((i === 0 ? '> ' : ' ') + line, 'command');
});
// Execute query
try {
const response = await fetch('/api/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
if (response.ok) {
const result = await response.json();
if (result.error) {
addLine('ERROR: ' + result.error, 'error');
} else if (result.message) {
addLine('OK: ' + result.message, 'success');
} else if (result.rows) {
addLine('OK: ' + result.rows.length + ' row(s)', 'success');
result.rows.slice(0, 10).forEach(row => {
addLine(' ' + JSON.stringify(row), '');
});
if (result.rows.length > 10) {
addLine(' ... and ' + (result.rows.length - 10) + ' more', '');
}
} else {
addLine('OK', 'success');
}
} else {
addLine('ERROR: ' + response.statusText, 'error');
}
} catch (err) {
addLine('ERROR: ' + err.message, 'error');
}
input.value = '';
autoResize();
}
form.addEventListener('submit', (e) => {
e.preventDefault();
executeQuery();
});
// Keyboard handling
input.addEventListener('keydown', (e) => {
// Ctrl+Enter or Cmd+Enter to execute
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
executeQuery();
}
// Up arrow at start of input for history
else if (e.key === 'ArrowUp' && input.selectionStart === 0) {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
input.value = history[historyIndex];
autoResize();
}
}
// Down arrow at end of input for history
else if (e.key === 'ArrowDown' && input.selectionStart === input.value.length) {
e.preventDefault();
if (historyIndex < history.length - 1) {
historyIndex++;
input.value = history[historyIndex];
} else {
historyIndex = history.length;
input.value = '';
}
autoResize();
}
// Escape to clear
else if (e.key === 'Escape') {
input.value = '';
autoResize();
}
});
function addLine(text, type) {
const line = document.createElement('div');
line.className = 'm-terminal-output-line ' + (type || '');
line.textContent = text;
output.appendChild(line);
output.scrollTop = output.scrollHeight;
}
")) }
};
layout("Dashboard", NavItem::Dashboard, content)
}
#[derive(Debug, serde::Deserialize)]
pub struct QueryRequest {
query: String,
}
#[derive(Debug, serde::Serialize)]
pub struct QueryResponse {
#[serde(skip_serializing_if = "Option::is_none")]
rows: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
}
#[allow(clippy::too_many_lines)]
pub async fn api_query(
State(ctx): State<Arc<AdminContext>>,
axum::Json(req): axum::Json<QueryRequest>,
) -> axum::Json<QueryResponse> {
let query = req.query.trim();
if query.is_empty() {
return axum::Json(QueryResponse {
rows: None,
error: Some("Empty query".to_string()),
message: None,
});
}
let query_upper = query.to_uppercase();
if query_upper.starts_with("SELECT") {
if let Some(from_idx) = query_upper.find("FROM") {
let after_from = &query[from_idx + 4..].trim_start();
let table_name: String = after_from
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if table_name.is_empty() {
return axum::Json(QueryResponse {
rows: None,
error: Some("Could not parse table name".to_string()),
message: None,
});
}
let tables = ctx.relational.list_tables();
if !tables.contains(&table_name) {
return axum::Json(QueryResponse {
rows: None,
error: Some(format!(
"Table '{table_name}' not found. Available: {tables:?}"
)),
message: None,
});
}
let limit = query_upper.find("LIMIT").map_or(100, |limit_idx| {
let after_limit = &query[limit_idx + 5..].trim_start();
after_limit
.chars()
.take_while(char::is_ascii_digit)
.collect::<String>()
.parse::<usize>()
.unwrap_or(100)
});
match ctx
.relational
.select(&table_name, relational_engine::Condition::True)
{
Ok(rows) => {
let json_rows: Vec<serde_json::Value> = rows
.into_iter()
.take(limit)
.map(|row| {
let mut obj = serde_json::Map::new();
obj.insert("_id".to_string(), serde_json::Value::Number(row.id.into()));
for (key, value) in row.values {
obj.insert(key, value_to_json(&value));
}
serde_json::Value::Object(obj)
})
.collect();
axum::Json(QueryResponse {
rows: Some(json_rows),
error: None,
message: None,
})
},
Err(e) => axum::Json(QueryResponse {
rows: None,
error: Some(format!("Query error: {e}")),
message: None,
}),
}
} else {
axum::Json(QueryResponse {
rows: None,
error: Some("SELECT requires FROM clause".to_string()),
message: None,
})
}
} else if query_upper.starts_with("SHOW TABLES") {
let tables = ctx.relational.list_tables();
let json_rows: Vec<serde_json::Value> = tables
.into_iter()
.map(|t| serde_json::json!({ "table_name": t }))
.collect();
axum::Json(QueryResponse {
rows: Some(json_rows),
error: None,
message: None,
})
} else if query_upper.starts_with("SHOW COLLECTIONS") {
let collections = ctx.vector.list_collections();
let json_rows: Vec<serde_json::Value> = collections
.into_iter()
.map(|c| serde_json::json!({ "collection_name": c }))
.collect();
axum::Json(QueryResponse {
rows: Some(json_rows),
error: None,
message: None,
})
} else if query_upper.starts_with("SHOW NODES") {
let count = ctx.graph.node_count();
axum::Json(QueryResponse {
rows: None,
error: None,
message: Some(format!("Graph contains {count} nodes")),
})
} else if query_upper.starts_with("SHOW EDGES") {
let count = ctx.graph.edge_count();
axum::Json(QueryResponse {
rows: None,
error: None,
message: Some(format!("Graph contains {count} edges")),
})
} else {
axum::Json(QueryResponse {
rows: None,
error: Some(
"Unsupported query. Try: SELECT * FROM <table> LIMIT n, SHOW TABLES, SHOW COLLECTIONS, SHOW NODES, SHOW EDGES".to_string()
),
message: None,
})
}
}
#[derive(Debug, serde::Serialize)]
pub struct GalaxyResponse {
#[serde(rename = "type")]
type_: String,
items: Vec<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
const fn is_read_only_statement(kind: &StatementKind) -> bool {
match kind {
StatementKind::Select(_)
| StatementKind::ShowTables
| StatementKind::ShowEmbeddings { .. }
| StatementKind::ShowVectorIndex
| StatementKind::CountEmbeddings
| StatementKind::Describe(_)
| StatementKind::Find(_)
| StatementKind::Similar(_)
| StatementKind::Neighbors(_)
| StatementKind::Path(_) => true,
StatementKind::Spatial(s) => matches!(
s.op,
SpatialOp::WithinRadius { .. } | SpatialOp::Nearest { .. } | SpatialOp::Count
),
StatementKind::Entity(e) => matches!(e.operation, EntityOp::Get { .. }),
StatementKind::Node(n) => matches!(n.operation, NodeOp::List { .. } | NodeOp::Get { .. }),
_ => false,
}
}
pub async fn api_galaxy(
State(ctx): State<Arc<AdminContext>>,
axum::Json(req): axum::Json<QueryRequest>,
) -> std::result::Result<axum::Json<GalaxyResponse>, StatusCode> {
let query = req.query.trim();
if query.is_empty() {
return Ok(axum::Json(GalaxyResponse {
type_: "message".to_string(),
items: Vec::new(),
error: Some("Empty query".to_string()),
}));
}
let stmt = match neumann_parser::parse(query) {
Ok(s) => s,
Err(e) => {
return Ok(axum::Json(GalaxyResponse {
type_: "message".to_string(),
items: Vec::new(),
error: Some(format!("Parse error: {e}")),
}));
},
};
if !is_read_only_statement(&stmt.kind) {
return Err(StatusCode::FORBIDDEN);
}
let Some(router) = ctx.query_router.as_ref() else {
return Ok(axum::Json(GalaxyResponse {
type_: "message".to_string(),
items: Vec::new(),
error: Some("Query router not configured".to_string()),
}));
};
let router = Arc::clone(router);
let result = tokio::task::spawn_blocking(move || router.read().execute_statement(&stmt))
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match result {
Ok(qr) => {
let (type_name, items) = query_result_to_json(&qr);
Ok(axum::Json(GalaxyResponse {
type_: type_name,
items,
error: None,
}))
},
Err(e) => Ok(axum::Json(GalaxyResponse {
type_: "message".to_string(),
items: Vec::new(),
error: Some(e.to_string()),
})),
}
}
pub async fn api_execute(
State(ctx): State<Arc<AdminContext>>,
axum::Json(req): axum::Json<QueryRequest>,
) -> std::result::Result<axum::Json<GalaxyResponse>, StatusCode> {
let query = req.query.trim();
if query.is_empty() {
return Ok(axum::Json(GalaxyResponse {
type_: "message".to_string(),
items: Vec::new(),
error: Some("Empty query".to_string()),
}));
}
let stmt = match neumann_parser::parse(query) {
Ok(s) => s,
Err(e) => {
return Ok(axum::Json(GalaxyResponse {
type_: "message".to_string(),
items: Vec::new(),
error: Some(format!("Parse error: {e}")),
}));
},
};
let Some(router) = ctx.query_router.as_ref() else {
return Ok(axum::Json(GalaxyResponse {
type_: "message".to_string(),
items: Vec::new(),
error: Some("Query router not configured".to_string()),
}));
};
let router = Arc::clone(router);
let result = tokio::task::spawn_blocking(move || router.write().execute_statement(&stmt))
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match result {
Ok(qr) => {
let (type_name, items) = query_result_to_json(&qr);
Ok(axum::Json(GalaxyResponse {
type_: type_name,
items,
error: None,
}))
},
Err(e) => Ok(axum::Json(GalaxyResponse {
type_: "message".to_string(),
items: Vec::new(),
error: Some(e.to_string()),
})),
}
}
fn query_result_to_json(qr: &query_router::QueryResult) -> (String, Vec<serde_json::Value>) {
use query_router::QueryResult;
match qr {
QueryResult::Empty => ("message".to_string(), vec![serde_json::json!("OK")]),
QueryResult::Value(v) => ("message".to_string(), vec![serde_json::json!(v)]),
QueryResult::Count(n) => ("message".to_string(), vec![serde_json::json!({"count": n})]),
QueryResult::Ids(ids) => (
"ids".to_string(),
ids.iter().map(|id| serde_json::json!(id)).collect(),
),
QueryResult::Rows(rows) => (
"rows".to_string(),
rows.iter()
.map(|row| {
serde_json::to_value(row).unwrap_or(serde_json::Value::Null)
})
.collect(),
),
QueryResult::Nodes(nodes) => (
"nodes".to_string(),
nodes
.iter()
.map(|n| serde_json::to_value(n).unwrap_or(serde_json::Value::Null))
.collect(),
),
QueryResult::Edges(edges) => (
"edges".to_string(),
edges
.iter()
.map(|e| serde_json::to_value(e).unwrap_or(serde_json::Value::Null))
.collect(),
),
QueryResult::Similar(sims) => (
"similar".to_string(),
sims.iter()
.map(|s| serde_json::to_value(s).unwrap_or(serde_json::Value::Null))
.collect(),
),
QueryResult::Unified(u) => (
"unified".to_string(),
vec![serde_json::to_value(u).unwrap_or(serde_json::Value::Null)],
),
QueryResult::Spatial(sp) => (
"spatial".to_string(),
sp.iter()
.map(|s| serde_json::to_value(s).unwrap_or(serde_json::Value::Null))
.collect(),
),
QueryResult::TableList(tables) => (
"tables".to_string(),
tables.iter().map(|t| serde_json::json!(t)).collect(),
),
other => (
"result".to_string(),
vec![serde_json::to_value(other).unwrap_or(serde_json::Value::Null)],
),
}
}
fn value_to_json(value: &relational_engine::Value) -> serde_json::Value {
match value {
relational_engine::Value::Null => serde_json::Value::Null,
relational_engine::Value::Bool(b) => serde_json::Value::Bool(*b),
relational_engine::Value::Int(i) => serde_json::Value::Number((*i).into()),
relational_engine::Value::Float(f) => serde_json::Number::from_f64(*f)
.map_or(serde_json::Value::Null, serde_json::Value::Number),
relational_engine::Value::String(s) => serde_json::Value::String(s.clone()),
relational_engine::Value::Bytes(b) => {
serde_json::Value::String(format!("<{} bytes>", b.len()))
},
relational_engine::Value::Json(j) => j.clone(),
_ => serde_json::Value::String("<unknown>".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use graph_engine::GraphEngine;
use relational_engine::RelationalEngine;
use vector_engine::VectorEngine;
fn create_test_context() -> Arc<AdminContext> {
Arc::new(AdminContext::new(
Arc::new(RelationalEngine::new()),
Arc::new(VectorEngine::new()),
Arc::new(GraphEngine::new()),
))
}
#[test]
fn test_value_to_json_null() {
let result = value_to_json(&relational_engine::Value::Null);
assert!(result.is_null());
}
#[test]
fn test_value_to_json_bool_true() {
let result = value_to_json(&relational_engine::Value::Bool(true));
assert_eq!(result, serde_json::Value::Bool(true));
}
#[test]
fn test_value_to_json_bool_false() {
let result = value_to_json(&relational_engine::Value::Bool(false));
assert_eq!(result, serde_json::Value::Bool(false));
}
#[test]
fn test_value_to_json_int() {
let result = value_to_json(&relational_engine::Value::Int(42));
assert_eq!(result, serde_json::json!(42));
}
#[test]
fn test_value_to_json_negative_int() {
let result = value_to_json(&relational_engine::Value::Int(-100));
assert_eq!(result, serde_json::json!(-100));
}
#[test]
fn test_value_to_json_float() {
let result = value_to_json(&relational_engine::Value::Float(3.15));
if let serde_json::Value::Number(n) = result {
assert!((n.as_f64().unwrap() - 3.15).abs() < 0.001);
} else {
panic!("Expected number");
}
}
#[test]
fn test_value_to_json_float_nan() {
let result = value_to_json(&relational_engine::Value::Float(f64::NAN));
assert!(result.is_null());
}
#[test]
fn test_value_to_json_string() {
let result = value_to_json(&relational_engine::Value::String("hello".to_string()));
assert_eq!(result, serde_json::json!("hello"));
}
#[test]
fn test_value_to_json_empty_string() {
let result = value_to_json(&relational_engine::Value::String(String::new()));
assert_eq!(result, serde_json::json!(""));
}
#[test]
fn test_value_to_json_bytes() {
let result = value_to_json(&relational_engine::Value::Bytes(vec![1, 2, 3, 4, 5]));
assert_eq!(result, serde_json::json!("<5 bytes>"));
}
#[test]
fn test_value_to_json_empty_bytes() {
let result = value_to_json(&relational_engine::Value::Bytes(vec![]));
assert_eq!(result, serde_json::json!("<0 bytes>"));
}
#[test]
fn test_value_to_json_json() {
let json_value = serde_json::json!({"key": "value", "num": 42});
let result = value_to_json(&relational_engine::Value::Json(json_value.clone()));
assert_eq!(result, json_value);
}
#[test]
fn test_dashboard_stats_gather_empty_engines() {
let ctx = create_test_context();
let stats = DashboardStats::gather(&ctx);
assert_eq!(stats.table_count, 0);
assert_eq!(stats.total_rows, 0);
assert_eq!(stats.collection_count, 0);
assert_eq!(stats.node_count, 0);
assert_eq!(stats.edge_count, 0);
assert!(stats.top_tables.is_empty());
}
#[test]
fn test_dashboard_stats_gather_with_tables() {
use relational_engine::{Column, ColumnType, Schema, Value};
let relational = Arc::new(RelationalEngine::new());
let schema = Schema::new(vec![
Column::new("id".to_string(), ColumnType::Int),
Column::new("name".to_string(), ColumnType::String),
]);
relational.create_table("users", schema).unwrap();
for i in 0..5 {
let values = vec![
("id".to_string(), Value::Int(i)),
("name".to_string(), Value::String(format!("user{}", i))),
];
relational
.insert("users", values.into_iter().collect())
.unwrap();
}
let ctx = Arc::new(AdminContext {
relational,
vector: Arc::new(VectorEngine::new()),
graph: Arc::new(GraphEngine::new()),
unified: None,
vault: None,
cache: None,
blob: None,
checkpoint: None,
store: None,
chain: None,
auth_config: None,
metrics: None,
query_router: None,
});
let stats = DashboardStats::gather(&ctx);
assert_eq!(stats.table_count, 1);
assert_eq!(stats.total_rows, 5);
assert_eq!(stats.top_tables.len(), 1);
assert_eq!(stats.top_tables[0].0, "users");
}
#[test]
fn test_dashboard_stats_gather_with_vectors() {
let vector = Arc::new(VectorEngine::new());
vector
.create_collection("embeddings", Default::default())
.unwrap();
vector
.store_in_collection("embeddings", "v1", vec![1.0, 0.0, 0.0])
.unwrap();
vector
.store_in_collection("embeddings", "v2", vec![0.0, 1.0, 0.0])
.unwrap();
let ctx = Arc::new(AdminContext {
relational: Arc::new(RelationalEngine::new()),
vector,
graph: Arc::new(GraphEngine::new()),
unified: None,
vault: None,
cache: None,
blob: None,
checkpoint: None,
store: None,
chain: None,
auth_config: None,
metrics: None,
query_router: None,
});
let stats = DashboardStats::gather(&ctx);
assert_eq!(stats.collection_count, 1);
assert_eq!(stats.vector_count, 2);
assert_eq!(stats.collections.len(), 1);
}
#[test]
fn test_dashboard_stats_gather_with_graph() {
let graph = Arc::new(GraphEngine::new());
let n1 = graph.create_node("Person", Default::default()).unwrap();
let n2 = graph.create_node("Person", Default::default()).unwrap();
graph
.create_edge(n1, n2, "KNOWS", Default::default(), true)
.unwrap();
let ctx = Arc::new(AdminContext {
relational: Arc::new(RelationalEngine::new()),
vector: Arc::new(VectorEngine::new()),
graph,
unified: None,
vault: None,
cache: None,
blob: None,
checkpoint: None,
store: None,
chain: None,
auth_config: None,
metrics: None,
query_router: None,
});
let stats = DashboardStats::gather(&ctx);
assert_eq!(stats.node_count, 2);
assert_eq!(stats.edge_count, 1);
assert_eq!(stats.graph_summary.len(), 2);
}
#[test]
fn test_query_request_deserialize() {
let json = r#"{"query": "SELECT * FROM users"}"#;
let req: QueryRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.query, "SELECT * FROM users");
}
#[test]
fn test_query_response_serialize_rows() {
let response = QueryResponse {
rows: Some(vec![
serde_json::json!({"id": 1}),
serde_json::json!({"id": 2}),
]),
error: None,
message: None,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("rows"));
assert!(!json.contains("error"));
assert!(!json.contains("message"));
}
#[test]
fn test_query_response_serialize_error() {
let response = QueryResponse {
rows: None,
error: Some("Table not found".to_string()),
message: None,
};
let json = serde_json::to_string(&response).unwrap();
assert!(!json.contains("rows"));
assert!(json.contains("Table not found"));
}
#[test]
fn test_query_response_serialize_message() {
let response = QueryResponse {
rows: None,
error: None,
message: Some("Operation completed".to_string()),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("Operation completed"));
}
#[tokio::test]
async fn test_api_query_empty() {
let ctx = create_test_context();
let req = QueryRequest {
query: "".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("Empty"));
}
#[tokio::test]
async fn test_api_query_whitespace_only() {
let ctx = create_test_context();
let req = QueryRequest {
query: " \n\t ".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.error.is_some());
}
#[tokio::test]
async fn test_api_query_show_tables_empty() {
let ctx = create_test_context();
let req = QueryRequest {
query: "SHOW TABLES".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.rows.is_some());
assert!(response.0.rows.unwrap().is_empty());
}
#[tokio::test]
async fn test_api_query_show_collections_empty() {
let ctx = create_test_context();
let req = QueryRequest {
query: "SHOW COLLECTIONS".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.rows.is_some());
assert!(response.0.rows.unwrap().is_empty());
}
#[tokio::test]
async fn test_api_query_show_nodes() {
let ctx = create_test_context();
let req = QueryRequest {
query: "SHOW NODES".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.message.is_some());
assert!(response.0.message.unwrap().contains("0 nodes"));
}
#[tokio::test]
async fn test_api_query_show_edges() {
let ctx = create_test_context();
let req = QueryRequest {
query: "SHOW EDGES".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.message.is_some());
assert!(response.0.message.unwrap().contains("0 edges"));
}
#[tokio::test]
async fn test_api_query_unsupported() {
let ctx = create_test_context();
let req = QueryRequest {
query: "DROP TABLE users".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("Unsupported"));
}
#[tokio::test]
async fn test_api_query_select_no_from() {
let ctx = create_test_context();
let req = QueryRequest {
query: "SELECT *".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("FROM"));
}
#[tokio::test]
async fn test_api_query_select_table_not_found() {
let ctx = create_test_context();
let req = QueryRequest {
query: "SELECT * FROM nonexistent".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("not found"));
}
#[tokio::test]
async fn test_api_query_select_success() {
use relational_engine::{Column, ColumnType, Schema, Value};
let relational = Arc::new(RelationalEngine::new());
let schema = Schema::new(vec![
Column::new("id".to_string(), ColumnType::Int),
Column::new("name".to_string(), ColumnType::String),
]);
relational.create_table("test_table", schema).unwrap();
relational
.insert(
"test_table",
[
("id".to_string(), Value::Int(1)),
("name".to_string(), Value::String("test".to_string())),
]
.into_iter()
.collect(),
)
.unwrap();
let ctx = Arc::new(AdminContext {
relational,
vector: Arc::new(VectorEngine::new()),
graph: Arc::new(GraphEngine::new()),
unified: None,
vault: None,
cache: None,
blob: None,
checkpoint: None,
store: None,
chain: None,
auth_config: None,
metrics: None,
query_router: None,
});
let req = QueryRequest {
query: "SELECT * FROM test_table".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.rows.is_some());
let rows = response.0.rows.unwrap();
assert_eq!(rows.len(), 1);
}
#[tokio::test]
async fn test_api_query_select_with_limit() {
use relational_engine::{Column, ColumnType, Schema, Value};
let relational = Arc::new(RelationalEngine::new());
let schema = Schema::new(vec![Column::new("id".to_string(), ColumnType::Int)]);
relational.create_table("numbers", schema).unwrap();
for i in 0..20 {
relational
.insert(
"numbers",
[("id".to_string(), Value::Int(i))].into_iter().collect(),
)
.unwrap();
}
let ctx = Arc::new(AdminContext {
relational,
vector: Arc::new(VectorEngine::new()),
graph: Arc::new(GraphEngine::new()),
unified: None,
vault: None,
cache: None,
blob: None,
checkpoint: None,
store: None,
chain: None,
auth_config: None,
metrics: None,
query_router: None,
});
let req = QueryRequest {
query: "SELECT * FROM numbers LIMIT 5".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.rows.is_some());
let rows = response.0.rows.unwrap();
assert_eq!(rows.len(), 5);
}
#[tokio::test]
async fn test_api_query_case_insensitive() {
let ctx = create_test_context();
let req = QueryRequest {
query: "show tables".to_string(),
};
let response = api_query(State(ctx.clone()), axum::Json(req)).await;
assert!(response.0.rows.is_some());
let req = QueryRequest {
query: "Show Tables".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.rows.is_some());
}
#[tokio::test]
async fn test_api_query_select_empty_table_name() {
let ctx = create_test_context();
let req = QueryRequest {
query: "SELECT * FROM ".to_string(),
};
let response = api_query(State(ctx), axum::Json(req)).await;
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("parse table name"));
}
#[tokio::test]
async fn test_dashboard_handler() {
let ctx = create_test_context();
let result = dashboard(State(ctx)).await;
let html = result.into_string();
assert!(html.contains("DASHBOARD"));
assert!(html.contains("TABLES"));
assert!(html.contains("VECTORS"));
}
#[tokio::test]
async fn test_dashboard_handler_with_data() {
use relational_engine::{Column, ColumnType, Schema, Value};
let relational = Arc::new(RelationalEngine::new());
let schema = Schema::new(vec![Column::new("id".to_string(), ColumnType::Int)]);
relational.create_table("test", schema).unwrap();
relational
.insert(
"test",
[("id".to_string(), Value::Int(1))].into_iter().collect(),
)
.unwrap();
let vector = Arc::new(VectorEngine::new());
vector.store_embedding("v1", vec![1.0, 0.0]).unwrap();
let graph = Arc::new(GraphEngine::new());
let n1 = graph.create_node("A", Default::default()).unwrap();
let n2 = graph.create_node("B", Default::default()).unwrap();
graph
.create_edge(n1, n2, "LINK", Default::default(), true)
.unwrap();
let ctx = Arc::new(AdminContext {
relational,
vector,
graph,
unified: None,
vault: None,
cache: None,
blob: None,
checkpoint: None,
store: None,
chain: None,
auth_config: None,
metrics: None,
query_router: None,
});
let result = dashboard(State(ctx)).await;
let html = result.into_string();
assert!(html.contains("DASHBOARD"));
assert!(html.contains("RELATIONAL"));
}
#[test]
fn test_dashboard_stats_gather_with_default_vectors() {
let vector = Arc::new(VectorEngine::new());
vector.store_embedding("v1", vec![1.0, 0.0]).unwrap();
vector.store_embedding("v2", vec![0.0, 1.0]).unwrap();
let ctx = Arc::new(AdminContext {
relational: Arc::new(RelationalEngine::new()),
vector,
graph: Arc::new(GraphEngine::new()),
unified: None,
vault: None,
cache: None,
blob: None,
checkpoint: None,
store: None,
chain: None,
auth_config: None,
metrics: None,
query_router: None,
});
let stats = DashboardStats::gather(&ctx);
assert_eq!(stats.vector_count, 2);
assert!(stats
.collections
.iter()
.any(|(name, _)| name == "(default)"));
}
#[test]
fn test_dashboard_stats_no_vault_no_cache() {
let ctx = create_test_context();
let stats = DashboardStats::gather(&ctx);
assert!(stats.secret_count.is_none());
assert!(stats.cache_entries.is_none());
assert!(stats.cache_hit_rate.is_none());
}
#[test]
fn test_dashboard_stats_with_vault() {
use tensor_store::TensorStore;
use tensor_vault::{Vault, VaultConfig};
let graph = Arc::new(GraphEngine::new());
let store = TensorStore::new();
let config = VaultConfig {
argon2_memory_cost: 256,
argon2_time_cost: 1,
argon2_parallelism: 1,
..VaultConfig::default()
};
let vault = Arc::new(Vault::new(b"test", Arc::clone(&graph), store, config).unwrap());
vault.set(Vault::ROOT, "key1", "val1").unwrap();
vault.set(Vault::ROOT, "key2", "val2").unwrap();
let ctx = Arc::new(AdminContext {
relational: Arc::new(RelationalEngine::new()),
vector: Arc::new(VectorEngine::new()),
graph,
unified: None,
vault: Some(vault),
cache: None,
blob: None,
checkpoint: None,
store: None,
chain: None,
auth_config: None,
metrics: None,
query_router: None,
});
let stats = DashboardStats::gather(&ctx);
assert_eq!(stats.secret_count, Some(2));
}
#[test]
fn test_dashboard_stats_with_cache() {
use tensor_cache::{Cache, CacheConfig};
let config = CacheConfig::development();
let dim = config.embedding_dim;
let cache = Arc::new(Cache::with_config(config).unwrap());
let emb = vec![0.1_f32; dim];
cache.put("q1", &emb, "a1", "m", None).unwrap();
cache.put("q2", &emb, "a2", "m", None).unwrap();
let ctx = Arc::new(AdminContext {
relational: Arc::new(RelationalEngine::new()),
vector: Arc::new(VectorEngine::new()),
graph: Arc::new(GraphEngine::new()),
unified: None,
vault: None,
cache: Some(cache),
blob: None,
checkpoint: None,
store: None,
chain: None,
auth_config: None,
metrics: None,
query_router: None,
});
let stats = DashboardStats::gather(&ctx);
assert!(stats.cache_entries.is_some());
assert!(stats.cache_entries.unwrap() >= 2);
assert!(stats.cache_hit_rate.is_some());
}
fn create_galaxy_context() -> Arc<AdminContext> {
let router = Arc::new(parking_lot::RwLock::new(query_router::QueryRouter::new()));
Arc::new(
AdminContext::new(
Arc::new(RelationalEngine::new()),
Arc::new(VectorEngine::new()),
Arc::new(GraphEngine::new()),
)
.with_query_router(Some(router)),
)
}
#[tokio::test]
async fn test_api_galaxy_find_query() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "SHOW TABLES".to_string(),
};
let response = api_galaxy(State(ctx), axum::Json(req)).await.unwrap();
assert!(response.0.error.is_none());
assert_eq!(response.0.type_, "tables");
}
#[tokio::test]
async fn test_api_galaxy_rejects_create() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "CREATE TABLE test (id INT)".to_string(),
};
let result = api_galaxy(State(ctx), axum::Json(req)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn test_api_galaxy_rejects_spatial_insert() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "SPATIAL INSERT 'k1' BOUNDS 0 0 1 1".to_string(),
};
let result = api_galaxy(State(ctx), axum::Json(req)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn test_api_galaxy_rejects_spatial_delete() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "SPATIAL DELETE 'k1' BOUNDS 0 0 1 1".to_string(),
};
let result = api_galaxy(State(ctx), axum::Json(req)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn test_api_galaxy_allows_spatial_nearest() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "SPATIAL NEAREST 0 0 LIMIT 5".to_string(),
};
let response = api_galaxy(State(ctx), axum::Json(req)).await.unwrap();
assert_eq!(response.0.type_, "spatial");
}
#[tokio::test]
async fn test_api_galaxy_rejects_entity_connect() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "ENTITY CONNECT 'a' -> 'b' : link".to_string(),
};
let result = api_galaxy(State(ctx), axum::Json(req)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn test_api_galaxy_rejects_insert() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "INSERT INTO test (id) VALUES (1)".to_string(),
};
let result = api_galaxy(State(ctx), axum::Json(req)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn test_api_galaxy_rejects_drop_table() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "DROP TABLE test".to_string(),
};
let result = api_galaxy(State(ctx), axum::Json(req)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn test_api_galaxy_empty_query() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "".to_string(),
};
let response = api_galaxy(State(ctx), axum::Json(req)).await.unwrap();
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("Empty"));
}
#[tokio::test]
async fn test_api_galaxy_no_router() {
let ctx = create_test_context();
let req = QueryRequest {
query: "SHOW TABLES".to_string(),
};
let response = api_galaxy(State(ctx), axum::Json(req)).await.unwrap();
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("not configured"));
}
#[tokio::test]
async fn test_api_galaxy_parse_error() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "INVALID GARBAGE QUERY".to_string(),
};
let response = api_galaxy(State(ctx), axum::Json(req)).await.unwrap();
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("Parse error"));
}
#[tokio::test]
async fn test_api_galaxy_select_query() {
use relational_engine::{Column, ColumnType, Schema, Value};
let relational = Arc::new(RelationalEngine::new());
let schema = Schema::new(vec![Column::new("id".to_string(), ColumnType::Int)]);
relational.create_table("papers", schema).unwrap();
relational
.insert(
"papers",
[("id".to_string(), Value::Int(1))].into_iter().collect(),
)
.unwrap();
let router = Arc::new(parking_lot::RwLock::new(
query_router::QueryRouter::with_engines(
relational.clone(),
Arc::new(GraphEngine::new()),
Arc::new(VectorEngine::new()), ),
));
let ctx = Arc::new(
AdminContext::new(
relational,
Arc::new(VectorEngine::new()),
Arc::new(GraphEngine::new()),
)
.with_query_router(Some(router)),
);
let req = QueryRequest {
query: "SELECT * FROM papers".to_string(),
};
let response = api_galaxy(State(ctx), axum::Json(req)).await.unwrap();
assert_eq!(response.0.type_, "rows");
assert!(!response.0.items.is_empty());
}
#[test]
fn test_is_read_only_statement_select() {
let stmt = neumann_parser::parse("SELECT * FROM t").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_read_only_statement_show_tables() {
let stmt = neumann_parser::parse("SHOW TABLES").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_read_only_statement_find() {
let stmt = neumann_parser::parse("FIND NODE LIMIT 10").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_read_only_spatial_nearest() {
let stmt = neumann_parser::parse("SPATIAL NEAREST 0 0 LIMIT 5").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_read_only_spatial_count() {
let stmt = neumann_parser::parse("SPATIAL COUNT").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_not_read_only_spatial_insert() {
let stmt = neumann_parser::parse("SPATIAL INSERT 'k' BOUNDS 0 0 1 1").unwrap();
assert!(!is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_not_read_only_entity_create() {
let stmt = neumann_parser::parse("ENTITY CREATE 'k' { name: 'test' }").unwrap();
assert!(!is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_not_read_only_entity_connect() {
let stmt = neumann_parser::parse("ENTITY CONNECT 'a' -> 'b' : link").unwrap();
assert!(!is_read_only_statement(&stmt.kind));
}
#[test]
fn test_galaxy_response_serialize() {
let response = GalaxyResponse {
type_: "rows".to_string(),
items: vec![serde_json::json!({"id": 1})],
error: None,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"type\":\"rows\""));
assert!(!json.contains("error"));
}
#[test]
fn test_galaxy_response_serialize_with_error() {
let response = GalaxyResponse {
type_: "message".to_string(),
items: Vec::new(),
error: Some("Something went wrong".to_string()),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("error"));
assert!(json.contains("Something went wrong"));
}
#[test]
fn test_query_result_to_json_empty() {
let (t, items) = query_result_to_json(&query_router::QueryResult::Empty);
assert_eq!(t, "message");
assert_eq!(items.len(), 1);
}
#[test]
fn test_query_result_to_json_value() {
let (t, items) =
query_result_to_json(&query_router::QueryResult::Value("hello".to_string()));
assert_eq!(t, "message");
assert_eq!(items[0], serde_json::json!("hello"));
}
#[test]
fn test_query_result_to_json_count() {
let (t, items) = query_result_to_json(&query_router::QueryResult::Count(42));
assert_eq!(t, "message");
assert!(items[0].get("count").is_some());
}
#[test]
fn test_query_result_to_json_ids() {
let (t, items) = query_result_to_json(&query_router::QueryResult::Ids(vec![1, 2, 3]));
assert_eq!(t, "ids");
assert_eq!(items.len(), 3);
}
#[test]
fn test_query_result_to_json_table_list() {
let (t, items) = query_result_to_json(&query_router::QueryResult::TableList(vec![
"a".to_string(),
"b".to_string(),
]));
assert_eq!(t, "tables");
assert_eq!(items.len(), 2);
}
#[test]
fn test_query_result_to_json_nodes() {
let (t, items) = query_result_to_json(&query_router::QueryResult::Nodes(Vec::new()));
assert_eq!(t, "nodes");
assert!(items.is_empty());
}
#[test]
fn test_query_result_to_json_edges() {
let (t, items) = query_result_to_json(&query_router::QueryResult::Edges(Vec::new()));
assert_eq!(t, "edges");
assert!(items.is_empty());
}
#[test]
fn test_query_result_to_json_similar() {
let (t, items) = query_result_to_json(&query_router::QueryResult::Similar(Vec::new()));
assert_eq!(t, "similar");
assert!(items.is_empty());
}
#[test]
fn test_query_result_to_json_spatial() {
let (t, items) = query_result_to_json(&query_router::QueryResult::Spatial(Vec::new()));
assert_eq!(t, "spatial");
assert!(items.is_empty());
}
#[test]
fn test_query_result_to_json_rows() {
let (t, _items) = query_result_to_json(&query_router::QueryResult::Rows(Vec::new()));
assert_eq!(t, "rows");
}
#[test]
fn test_query_result_to_json_unified() {
let unified = query_router::UnifiedResult {
description: "test".to_string(),
items: Vec::new(),
};
let (t, items) = query_result_to_json(&query_router::QueryResult::Unified(unified));
assert_eq!(t, "unified");
assert_eq!(items.len(), 1);
}
#[test]
fn test_query_result_to_json_other() {
let (t, items) = query_result_to_json(&query_router::QueryResult::Path(vec![1, 2]));
assert_eq!(t, "result");
assert_eq!(items.len(), 1);
}
#[tokio::test]
async fn test_api_execute_empty_query() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "".to_string(),
};
let response = api_execute(State(ctx), axum::Json(req)).await.unwrap();
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("Empty"));
}
#[tokio::test]
async fn test_api_execute_parse_error() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "TOTALLY INVALID SYNTAX".to_string(),
};
let response = api_execute(State(ctx), axum::Json(req)).await.unwrap();
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("Parse error"));
}
#[tokio::test]
async fn test_api_execute_no_router() {
let ctx = create_test_context();
let req = QueryRequest {
query: "SHOW TABLES".to_string(),
};
let response = api_execute(State(ctx), axum::Json(req)).await.unwrap();
assert!(response.0.error.is_some());
assert!(response.0.error.unwrap().contains("not configured"));
}
#[tokio::test]
async fn test_api_execute_valid_query() {
let ctx = create_galaxy_context();
let req = QueryRequest {
query: "SHOW TABLES".to_string(),
};
let response = api_execute(State(ctx), axum::Json(req)).await.unwrap();
assert!(response.0.error.is_none());
assert_eq!(response.0.type_, "tables");
}
#[tokio::test]
async fn test_api_execute_mutation() {
use relational_engine::{Column, ColumnType, Schema};
let relational = Arc::new(RelationalEngine::new());
let schema = Schema::new(vec![Column::new("id".to_string(), ColumnType::Int)]);
relational.create_table("t1", schema).unwrap();
let router = Arc::new(parking_lot::RwLock::new(
query_router::QueryRouter::with_engines(
relational.clone(),
Arc::new(GraphEngine::new()),
Arc::new(VectorEngine::new()),
),
));
let ctx = Arc::new(
AdminContext::new(
relational,
Arc::new(VectorEngine::new()),
Arc::new(GraphEngine::new()),
)
.with_query_router(Some(router)),
);
let req = QueryRequest {
query: "INSERT INTO t1 (id) VALUES (1)".to_string(),
};
let response = api_execute(State(ctx), axum::Json(req)).await.unwrap();
assert!(response.0.error.is_none());
}
#[test]
fn test_is_read_only_show_embeddings() {
let stmt = neumann_parser::parse("SHOW EMBEDDINGS").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_read_only_describe() {
let stmt = neumann_parser::parse("DESCRIBE TABLE users").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_read_only_similar() {
let stmt = neumann_parser::parse("SIMILAR [1.0, 0.0, 0.0] LIMIT 5").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_read_only_spatial_within_radius() {
let stmt = neumann_parser::parse("SPATIAL WITHIN 0 0 RADIUS 10").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_read_only_node_list() {
let stmt = neumann_parser::parse("NODE LIST LIMIT 10").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_not_read_only_node_create() {
let stmt = neumann_parser::parse("NODE CREATE Person { name: 'test' }").unwrap();
assert!(!is_read_only_statement(&stmt.kind));
}
#[test]
fn test_is_read_only_entity_get() {
let stmt = neumann_parser::parse("ENTITY GET 'some-key'").unwrap();
assert!(is_read_only_statement(&stmt.kind));
}
}