perf(servers): avoid rebuilding JSON records payload

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
This commit is contained in:
discord9
2026-09-14 20:13:42 +08:00
parent dcbb639a56
commit a123ec053d
+124 -5
View File
@@ -105,11 +105,11 @@ impl IntoResponse for JsonResponse {
})
.collect();
json!({
"data": data,
"execution_time_ms": execution_time,
})
.to_string()
let data = Value::Array(data.into_iter().map(Value::Object).collect());
let mut payload = Map::new();
payload.insert("data".to_string(), data);
payload.insert("execution_time_ms".to_string(), Value::from(execution_time));
Value::Object(payload).to_string()
}
};
@@ -133,3 +133,122 @@ impl IntoResponse for JsonResponse {
.into_response()
}
}
#[cfg(test)]
mod tests {
use axum::body::to_bytes;
use axum::http::{StatusCode, header};
use serde_json::{Map, Value, json};
use super::*;
use crate::http::{ColumnSchema, HttpRecordsOutput, OutputSchema};
fn records(column_names: &[&str], rows: Vec<Vec<Value>>) -> HttpRecordsOutput {
let total_rows = rows.len();
HttpRecordsOutput {
schema: OutputSchema::new(
column_names
.iter()
.map(|name| ColumnSchema::new((*name).to_string(), "Json".to_string()))
.collect(),
),
rows,
total_rows,
metrics: Default::default(),
}
}
fn old_records_payload(records: &HttpRecordsOutput, execution_time: u64) -> String {
let schema = records.schema();
let data: Vec<Map<String, Value>> = records
.rows()
.iter()
.map(|row| {
schema
.column_schemas
.iter()
.enumerate()
.map(|(i, col)| (col.name.clone(), row[i].clone()))
.collect()
})
.collect();
json!({
"data": data,
"execution_time_ms": execution_time,
})
.to_string()
}
#[tokio::test]
async fn test_records_response_matches_old_json_representation() {
let records = records(
&["z", "a", "z", "nested", "escaped", "nullable"],
vec![
vec![
json!(1),
json!("first"),
json!(2),
json!({"outer": {"items": [null, "value"]}}),
json!("line\nwith \"quotes\" and \\slashes"),
Value::Null,
],
vec![
json!(3),
json!("second"),
json!(4),
json!({"array": [true, {"key": "value"}]}),
json!("\tescaped"),
Value::Null,
],
],
);
let execution_time = 42;
let expected = old_records_payload(&records, execution_time);
let response = JsonResponse {
output: vec![GreptimeQueryOutput::Records(records)],
execution_time_ms: execution_time,
}
.into_response();
assert_eq!(StatusCode::OK, response.status());
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert_eq!(expected.as_bytes(), body.as_ref());
}
#[tokio::test]
async fn test_empty_records_response_matches_old_json_representation_and_headers() {
let records = records(&[], vec![]);
let execution_time = 42;
let expected = old_records_payload(&records, execution_time);
let response = JsonResponse {
output: vec![GreptimeQueryOutput::Records(records)],
execution_time_ms: execution_time,
}
.into_response();
assert_eq!(StatusCode::OK, response.status());
assert_eq!(
mime::APPLICATION_JSON.as_ref(),
response.headers().get(header::CONTENT_TYPE).unwrap()
);
assert_eq!(
ResponseFormat::Json.as_str(),
response.headers().get(&GREPTIME_DB_HEADER_FORMAT).unwrap()
);
assert_eq!(
execution_time.to_string(),
response
.headers()
.get(&GREPTIME_DB_HEADER_EXECUTION_TIME)
.unwrap()
.to_str()
.unwrap()
);
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert_eq!(expected.as_bytes(), body.as_ref());
}
}