Skip to main content

servers/http/result/
json_result.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use axum::http::{HeaderValue, header};
16use axum::response::{IntoResponse, Response};
17use common_error::status_code::StatusCode;
18use common_query::Output;
19use mime_guess::mime;
20use serde::{Deserialize, Serialize};
21use serde_json::{Map, Value, json};
22
23use crate::http::header::{GREPTIME_DB_HEADER_EXECUTION_TIME, GREPTIME_DB_HEADER_FORMAT};
24use crate::http::result::error_result::ErrorResponse;
25use crate::http::{GreptimeQueryOutput, HttpResponse, ResponseFormat, handler, process_with_limit};
26
27/// The json format here is different from the default json output of `GreptimedbV1` result.
28/// `JsonResponse` is intended to make it easier for user to consume data.
29#[derive(Serialize, Deserialize, Debug)]
30pub struct JsonResponse {
31    output: Vec<GreptimeQueryOutput>,
32    execution_time_ms: u64,
33}
34
35impl JsonResponse {
36    pub async fn from_output(outputs: Vec<crate::error::Result<Output>>) -> HttpResponse {
37        match handler::from_output(outputs).await {
38            Err(err) => HttpResponse::Error(err),
39            Ok((output, _)) => {
40                if output.len() > 1 {
41                    HttpResponse::Error(ErrorResponse::from_error_message(
42                        StatusCode::InvalidArguments,
43                        "cannot output multi-statements result in json format".to_string(),
44                    ))
45                } else {
46                    HttpResponse::Json(JsonResponse {
47                        output,
48                        execution_time_ms: 0,
49                    })
50                }
51            }
52        }
53    }
54
55    pub fn output(&self) -> &[GreptimeQueryOutput] {
56        &self.output
57    }
58
59    pub fn with_execution_time(mut self, execution_time: u64) -> Self {
60        self.execution_time_ms = execution_time;
61        self
62    }
63
64    pub fn execution_time_ms(&self) -> u64 {
65        self.execution_time_ms
66    }
67
68    pub fn with_limit(mut self, limit: usize) -> Self {
69        self.output = process_with_limit(self.output, limit);
70        self
71    }
72}
73
74impl IntoResponse for JsonResponse {
75    fn into_response(mut self) -> Response {
76        debug_assert!(
77            self.output.len() <= 1,
78            "self.output has extra elements: {}",
79            self.output.len()
80        );
81
82        let execution_time = self.execution_time_ms;
83        let payload = match self.output.pop() {
84            None => String::default(),
85            Some(GreptimeQueryOutput::AffectedRows(n)) => json!({
86                "data": [],
87                "affected_rows": n,
88                "execution_time_ms": execution_time,
89            })
90            .to_string(),
91
92            Some(GreptimeQueryOutput::Records(records)) => {
93                // Borrow the schema field directly so the rows can be moved out.
94                let schema = &records.schema;
95
96                let data: Vec<Map<String, Value>> = records
97                    .rows
98                    .into_iter()
99                    .map(|mut row| {
100                        // Slicing keeps the out-of-bounds panic for short rows;
101                        // a plain zip would silently truncate them.
102                        schema
103                            .column_schemas
104                            .iter()
105                            .zip(row[..schema.column_schemas.len()].iter_mut())
106                            .map(|(col, value)| (col.name.clone(), value.take()))
107                            .collect::<Map<String, Value>>()
108                    })
109                    .collect();
110
111                let data = Value::Array(data.into_iter().map(Value::Object).collect());
112                let mut payload = Map::new();
113                payload.insert("data".to_string(), data);
114                payload.insert("execution_time_ms".to_string(), Value::from(execution_time));
115                Value::Object(payload).to_string()
116            }
117        };
118
119        (
120            [
121                (
122                    header::CONTENT_TYPE,
123                    HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
124                ),
125                (
126                    GREPTIME_DB_HEADER_FORMAT.clone(),
127                    HeaderValue::from_static(ResponseFormat::Json.as_str()),
128                ),
129                (
130                    GREPTIME_DB_HEADER_EXECUTION_TIME.clone(),
131                    HeaderValue::from(execution_time),
132                ),
133            ],
134            payload,
135        )
136            .into_response()
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use axum::body::to_bytes;
143
144    use super::*;
145
146    #[tokio::test]
147    async fn test_records_response_preserves_values_and_duplicate_columns() {
148        let response: JsonResponse = serde_json::from_value(json!({
149            "output": [{"records": {
150                "schema": {"column_schemas": [
151                    {"name": "duplicate", "data_type": "String"},
152                    {"name": "nested", "data_type": "Json"},
153                    {"name": "duplicate", "data_type": "String"},
154                    {"name": "escaped\"column", "data_type": "String"}
155                ]},
156                "rows": [
157                    ["discarded", {"array": [null, true, "中文"]}, "last", "line\n\\\""],
158                    ["discarded", [1, {"key": "value"}], null, ""]
159                ]
160            }}],
161            "execution_time_ms": 7
162        }))
163        .unwrap();
164
165        let response = response.into_response();
166        assert_eq!(response.status(), axum::http::StatusCode::OK);
167        assert_eq!(response.headers()[header::CONTENT_TYPE], "application/json");
168        assert_eq!(response.headers()[&GREPTIME_DB_HEADER_FORMAT], "json");
169        assert_eq!(response.headers()[&GREPTIME_DB_HEADER_EXECUTION_TIME], "7");
170        let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
171        assert_eq!(
172            serde_json::from_slice::<Value>(&body).unwrap(),
173            json!({
174                "data": [
175                    {"duplicate": "last", "nested": {"array": [null, true, "中文"]}, "escaped\"column": "line\n\\\""},
176                    {"duplicate": null, "nested": [1, {"key": "value"}], "escaped\"column": ""}
177                ],
178                "execution_time_ms": 7
179            })
180        );
181    }
182}