use serde::{Deserialize, Serialize};
use crate::query::viz::VizSpec;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct QueryColumn {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "kind", content = "v", rename_all = "snake_case")]
pub enum QueryValue {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
ObjRef {
index: u64,
class: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
addr: Option<u64>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct QueryResult {
pub name: String,
pub oql: String,
pub columns: Vec<QueryColumn>,
pub rows: Vec<Vec<QueryValue>>,
pub row_count: u64,
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub viz: Option<VizSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub elapsed_ms: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn result_serializes_stably() {
let r = QueryResult {
name: "q1".into(),
oql: "SELECT COUNT(*) FROM C".into(),
columns: vec![QueryColumn {
name: "COUNT(*)".into(),
}],
rows: vec![vec![QueryValue::Int(42)]],
row_count: 1,
truncated: false,
error: None,
note: None,
viz: None,
elapsed_ms: None,
};
let j = serde_json::to_string(&r).unwrap();
assert!(j.contains("\"row_count\":1"));
assert!(j.contains("\"truncated\":false"));
assert!(
!j.contains("\"error\""),
"error key should be omitted when None: {j}"
);
let back: QueryResult = serde_json::from_str(&j).unwrap();
assert_eq!(back, r);
}
#[test]
fn query_value_variants_roundtrip() {
let r = QueryResult {
name: "variants".into(),
oql: "SELECT * FROM C".into(),
columns: vec![QueryColumn { name: "v".into() }],
rows: vec![vec![
QueryValue::Null,
QueryValue::Bool(true),
QueryValue::Int(-5),
QueryValue::Float(1.5),
QueryValue::Str("hi".into()),
QueryValue::ObjRef {
index: 7,
class: "java.lang.String".into(),
addr: None,
},
]],
row_count: 6,
truncated: false,
error: None,
note: None,
viz: None,
elapsed_ms: None,
};
let j = serde_json::to_string(&r).unwrap();
let back: QueryResult = serde_json::from_str(&j).unwrap();
assert_eq!(back, r);
assert!(
j.contains("\"kind\":\"obj_ref\""),
"ObjRef must serialize as kind=obj_ref, got: {j}"
);
assert!(
j.contains("\"kind\":\"int\""),
"Int must serialize as kind=int, got: {j}"
);
assert!(
j.contains("\"kind\":\"bool\""),
"Bool must serialize as kind=bool, got: {j}"
);
assert!(
j.contains("\"java.lang.String\""),
"ObjRef class must appear in JSON"
);
}
#[test]
fn error_result_roundtrips() {
let r = QueryResult {
name: "bad_query".into(),
oql: "SELECT bad syntax".into(),
columns: vec![],
rows: vec![],
row_count: 0,
truncated: false,
error: Some("boom".into()),
note: None,
viz: None,
elapsed_ms: None,
};
let j = serde_json::to_string(&r).unwrap();
assert!(
j.contains("\"error\":\"boom\""),
"error field must appear in JSON, got: {j}"
);
let back: QueryResult = serde_json::from_str(&j).unwrap();
assert_eq!(back, r);
}
#[test]
fn schema_generates() {
let schema = schemars::schema_for!(QueryResult);
let s = serde_json::to_string(&schema).expect("schema must serialize to JSON");
assert!(!s.is_empty(), "schema JSON must be non-empty");
assert!(
s.contains("row_count") || s.contains("QueryResult"),
"schema JSON must mention row_count or QueryResult, got: {s}"
);
}
}