Skip to main content

servers/http/
handler.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 std::collections::HashMap;
16use std::future;
17use std::panic::AssertUnwindSafe;
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::time::{Duration, Instant};
21
22use axum::extract::rejection::FormRejection;
23use axum::extract::{Json, Query, State};
24use axum::response::sse::{Event, KeepAlive, Sse};
25use axum::response::{IntoResponse, Response};
26use axum::{Extension, Form};
27use common_catalog::parse_catalog_and_schema_from_db_string;
28use common_error::ext::ErrorExt;
29use common_error::status_code::StatusCode;
30use common_plugins::GREPTIME_EXEC_WRITE_COST;
31use common_query::{Output, OutputData};
32use common_recordbatch::error::Result as RecordBatchResult;
33use common_recordbatch::{RecordBatch, RecordBatchStreamWrapper, RecordBatches, util};
34use common_telemetry::tracing;
35use datafusion::physical_plan::ExecutionPlan;
36use futures::{FutureExt, StreamExt};
37use query::parser::{DEFAULT_LOOKBACK_STRING, PromQuery};
38use serde::{Deserialize, Serialize};
39use serde_json::Value;
40use session::context::{Channel, QueryContext, QueryContextRef};
41use snafu::ResultExt;
42use sql::dialect::GreptimeDbDialect;
43use sql::parser::{ParseOptions, ParserContext};
44use sql::statements::statement::Statement;
45use tokio::sync::{Notify, watch};
46
47use crate::error::{
48    CollectRecordbatchSnafu, FailedToParseQuerySnafu, InvalidQuerySnafu, Result,
49    log_error_if_necessary,
50};
51use crate::http::header::collect_plan_metrics;
52use crate::http::result::arrow_result::ArrowResponse;
53use crate::http::result::csv_result::CsvResponse;
54use crate::http::result::error_result::ErrorResponse;
55use crate::http::result::greptime_result_v1::GreptimedbV1Response;
56use crate::http::result::influxdb_result_v1::InfluxdbV1Response;
57use crate::http::result::json_result::JsonResponse;
58use crate::http::result::null_result::NullResponse;
59use crate::http::result::table_result::TableResponse;
60use crate::http::{
61    ApiState, Epoch, GreptimeOptionsConfigState, GreptimeQueryOutput, HttpRecordsOutput,
62    HttpResponse, ResponseFormat,
63};
64use crate::metrics_handler::MetricsHandler;
65use crate::query_handler::sql::ServerSqlQueryHandlerRef;
66
67#[derive(Debug, Default, Serialize, Deserialize)]
68pub struct SqlQuery {
69    pub db: Option<String>,
70    pub sql: Option<String>,
71    // (Optional) result format: [`greptimedb_v1`, `influxdb_v1`, `csv`,
72    // `arrow`],
73    // the default value is `greptimedb_v1`
74    pub format: Option<String>,
75    // Returns epoch timestamps with the specified precision.
76    // Both u and µ indicate microseconds.
77    // epoch = [ns,u,µ,ms,s],
78    //
79    // TODO(jeremy): currently, only InfluxDB result format is supported,
80    // and all columns of the `Timestamp` type will be converted to their
81    // specified time precision. Maybe greptimedb format can support this
82    // param too.
83    pub epoch: Option<String>,
84    pub limit: Option<usize>,
85    // For arrow output
86    pub compression: Option<String>,
87    pub snapshot_interval_ms: Option<u64>,
88}
89
90const DEFAULT_ANALYZE_SNAPSHOT_INTERVAL_MS: u64 = 5000;
91const MIN_ANALYZE_SNAPSHOT_INTERVAL_MS: u64 = 1000;
92const MAX_ANALYZE_SNAPSHOT_INTERVAL_MS: u64 = 60000;
93
94#[derive(Serialize)]
95struct AnalyzeStreamPayload {
96    seq: u64,
97    state: &'static str,
98    partial: bool,
99    elapsed_ms: u64,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    metrics: Option<Value>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    output: Option<GreptimeQueryOutput>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    reason: Option<String>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    code: Option<u32>,
108}
109
110#[derive(Clone, Debug)]
111#[doc(hidden)]
112pub struct AnalyzeStreamMessage {
113    pub event_name: &'static str,
114    pub payload: String,
115}
116
117struct AnalyzeStreamWorkerGuard {
118    cancel: watch::Sender<bool>,
119    handle: common_runtime::JoinHandle<()>,
120}
121
122impl Drop for AnalyzeStreamWorkerGuard {
123    fn drop(&mut self) {
124        let _ = self.cancel.send(true);
125        self.handle.abort();
126    }
127}
128
129struct AnalyzeStreamBodyState {
130    latest_metrics: watch::Receiver<Option<String>>,
131    terminal: watch::Receiver<Option<AnalyzeStreamMessage>>,
132    notify: Arc<Notify>,
133    // Keeping the guard in the body state makes dropping the response cancel and
134    // abort the worker instead of leaving an owned query stream detached.
135    _worker: AnalyzeStreamWorkerGuard,
136    done: bool,
137}
138
139/// Handler to execute sql
140#[axum_macros::debug_handler]
141#[tracing::instrument(skip_all, fields(protocol = "http", request_type = "sql"))]
142pub async fn sql(
143    State(state): State<ApiState>,
144    Query(query_params): Query<SqlQuery>,
145    Extension(mut query_ctx): Extension<QueryContext>,
146    Form(form_params): Form<SqlQuery>,
147) -> HttpResponse {
148    let start = Instant::now();
149    let sql_handler = &state.sql_handler;
150    if let Some(db) = &query_params.db.or(form_params.db) {
151        let (catalog, schema) = parse_catalog_and_schema_from_db_string(db);
152        query_ctx.set_current_catalog(&catalog);
153        query_ctx.set_current_schema(&schema);
154    }
155    let db = query_ctx.get_db_string();
156
157    query_ctx.set_channel(Channel::HttpSql);
158    let query_ctx = Arc::new(query_ctx);
159
160    let _timer = crate::metrics::METRIC_HTTP_SQL_ELAPSED
161        .with_label_values(&[db.as_str()])
162        .start_timer();
163
164    let sql = query_params.sql.or(form_params.sql);
165    let format = query_params
166        .format
167        .or(form_params.format)
168        .map(|s| s.to_lowercase())
169        .map(|s| ResponseFormat::parse(s.as_str()).unwrap_or(ResponseFormat::GreptimedbV1))
170        .unwrap_or(ResponseFormat::GreptimedbV1);
171    let epoch = query_params
172        .epoch
173        .or(form_params.epoch)
174        .map(|s| s.to_lowercase())
175        .map(|s| Epoch::parse(s.as_str()).unwrap_or(Epoch::Millisecond));
176
177    let result = if let Some(sql) = &sql {
178        if let Some((status, msg)) = validate_schema(sql_handler.clone(), query_ctx.clone()).await {
179            Err((status, msg))
180        } else {
181            Ok(sql_handler.do_query(sql, query_ctx.clone()).await)
182        }
183    } else {
184        Err((
185            StatusCode::InvalidArguments,
186            "sql parameter is required.".to_string(),
187        ))
188    };
189
190    let outputs = match result {
191        Err((status, msg)) => {
192            return HttpResponse::Error(
193                ErrorResponse::from_error_message(status, msg)
194                    .with_execution_time(start.elapsed().as_millis() as u64),
195            );
196        }
197        Ok(outputs) => outputs,
198    };
199
200    let outputs = match query_params.limit {
201        Some(limit)
202            if matches!(
203                format,
204                ResponseFormat::Csv(..)
205                    | ResponseFormat::Table
206                    | ResponseFormat::GreptimedbV1
207                    | ResponseFormat::Json
208            ) =>
209        {
210            outputs
211                .into_iter()
212                .map(|output| output.and_then(|output| limit_output_rows(output, limit)))
213                .collect()
214        }
215        _ => outputs,
216    };
217
218    let resp = match format {
219        ResponseFormat::Arrow => {
220            ArrowResponse::from_output(outputs, query_params.compression).await
221        }
222        ResponseFormat::Csv(with_names, with_types) => {
223            CsvResponse::from_output(outputs, with_names, with_types).await
224        }
225        ResponseFormat::Table => TableResponse::from_output(outputs).await,
226        ResponseFormat::GreptimedbV1 => GreptimedbV1Response::from_output(outputs).await,
227        ResponseFormat::InfluxdbV1 => InfluxdbV1Response::from_output(outputs, epoch).await,
228        ResponseFormat::Json => JsonResponse::from_output(outputs).await,
229        ResponseFormat::Null => NullResponse::from_output(outputs).await,
230    };
231
232    resp.with_execution_time(start.elapsed().as_millis() as u64)
233}
234
235/// Form parameters for the bounded logical-table DDL operation.
236#[derive(Deserialize)]
237pub struct LogicalTableDdl {
238    sql: Option<String>,
239    db: Option<String>,
240}
241
242/// Creates one batch of Metric logical tables using the normal v1 response.
243pub async fn create_logical_tables(
244    State(state): State<ApiState>,
245    Extension(mut query_ctx): Extension<QueryContext>,
246    form: std::result::Result<Form<LogicalTableDdl>, FormRejection>,
247) -> HttpResponse {
248    let start = Instant::now();
249    let form = match form {
250        Ok(Form(form)) => form,
251        Err(_) => {
252            return HttpResponse::Error(ErrorResponse::from_error_message(
253                StatusCode::InvalidArguments,
254                "invalid or oversized form body".to_string(),
255            ));
256        }
257    };
258    let Some(sql) = form.sql else {
259        return HttpResponse::Error(ErrorResponse::from_error_message(
260            StatusCode::InvalidArguments,
261            "sql parameter is required.".to_string(),
262        ));
263    };
264    if sql.len() > crate::query_handler::sql::MAX_LOGICAL_TABLE_DDL_BYTES {
265        return HttpResponse::Error(ErrorResponse::from_error_message(
266            StatusCode::InvalidArguments,
267            "logical-table DDL exceeds 1 MiB".to_string(),
268        ));
269    }
270    if let Some(db) = form.db {
271        let (catalog, schema) = parse_catalog_and_schema_from_db_string(&db);
272        query_ctx.set_current_catalog(&catalog);
273        query_ctx.set_current_schema(&schema);
274    }
275    query_ctx.set_channel(Channel::HttpSql);
276    let query_ctx = Arc::new(query_ctx);
277    if let Some((status, msg)) = validate_schema(state.sql_handler.clone(), query_ctx.clone()).await
278    {
279        return HttpResponse::Error(ErrorResponse::from_error_message(status, msg));
280    }
281    let outputs = match state
282        .sql_handler
283        .create_logical_tables(&sql, query_ctx)
284        .await
285    {
286        Ok(outputs)
287            if outputs
288                .iter()
289                .all(|output| matches!(output.data, OutputData::AffectedRows(0))) =>
290        {
291            outputs.into_iter().map(Ok).collect()
292        }
293        other => {
294            let status = match other {
295                Err(error) => {
296                    log_error_if_necessary(&error);
297                    error.status_code()
298                }
299                Ok(_) => StatusCode::Internal,
300            };
301            return HttpResponse::Error(
302                ErrorResponse::from_error_message(status, "logical-table batch failed".to_string())
303                    .with_execution_time(start.elapsed().as_millis() as u64),
304            );
305        }
306    };
307    GreptimedbV1Response::from_output(outputs)
308        .await
309        .with_execution_time(start.elapsed().as_millis() as u64)
310}
311
312/// Limits each statement's response before collecting batches and converting rows.
313fn limit_output_rows(output: Output, limit: usize) -> Result<Output> {
314    let mut remaining = limit;
315    let data = match output.data {
316        OutputData::AffectedRows(rows) => OutputData::AffectedRows(rows),
317        OutputData::RecordBatches(batches) => {
318            let schema = batches.schema();
319            let batches = batches
320                .into_iter()
321                .filter_map(|batch| take_response_rows(batch, &mut remaining).transpose())
322                .collect::<RecordBatchResult<Vec<_>>>()
323                .context(CollectRecordbatchSnafu)?;
324            OutputData::RecordBatches(
325                RecordBatches::try_new(schema, batches).context(CollectRecordbatchSnafu)?,
326            )
327        }
328        OutputData::Stream(stream) => {
329            let schema = stream.schema();
330            // This is a response limit. Drain the input even after reaching it so
331            // execution errors and terminal query metrics are still observed.
332            let stream = stream.filter_map(move |batch| {
333                future::ready(
334                    batch
335                        .and_then(|batch| take_response_rows(batch, &mut remaining))
336                        .transpose(),
337                )
338            });
339            OutputData::Stream(Box::pin(RecordBatchStreamWrapper::new(schema, stream)))
340        }
341    };
342    Ok(Output::new(data, output.meta))
343}
344
345fn take_response_rows(
346    batch: RecordBatch,
347    remaining: &mut usize,
348) -> RecordBatchResult<Option<RecordBatch>> {
349    let num_rows = batch.num_rows().min(*remaining);
350    *remaining -= num_rows;
351    if num_rows == 0 {
352        Ok(None)
353    } else if num_rows == batch.num_rows() {
354        Ok(Some(batch))
355    } else {
356        batch.slice(0, num_rows).map(Some)
357    }
358}
359
360/// Handler to stream partial `EXPLAIN ANALYZE VERBOSE` metrics as SSE.
361///
362/// This endpoint is POST-only SSE, so browser `EventSource` does
363/// not apply. Each `metrics` event carries a complete snapshot (not a delta);
364/// large snapshots are throttled but never truncated. `final`, `canceled`, and
365/// `error` are terminal events. If the client disconnects it won't receive a
366/// `canceled` event, but the production frontend stream is dropped and
367/// best-effort cancels the underlying query.
368#[axum_macros::debug_handler]
369#[tracing::instrument(
370    skip_all,
371    fields(protocol = "http", request_type = "sql_analyze_stream")
372)]
373pub async fn sql_analyze_stream(
374    State(state): State<ApiState>,
375    Query(query_params): Query<SqlQuery>,
376    Extension(mut query_ctx): Extension<QueryContext>,
377    form_params: std::result::Result<Form<SqlQuery>, FormRejection>,
378) -> Response {
379    let start = Instant::now();
380    let form_params = match form_params {
381        Ok(Form(params)) => params,
382        Err(err) => {
383            if err.status() != axum::http::StatusCode::UNSUPPORTED_MEDIA_TYPE {
384                return ErrorResponse::from_error_message(
385                    StatusCode::InvalidArguments,
386                    err.body_text(),
387                )
388                .with_execution_time(start.elapsed().as_millis() as u64)
389                .into_response();
390            }
391            SqlQuery::default()
392        }
393    };
394    let sql_handler = &state.sql_handler;
395    if let Some(db) = &query_params.db.or(form_params.db) {
396        let (catalog, schema) = parse_catalog_and_schema_from_db_string(db);
397        query_ctx.set_current_catalog(&catalog);
398        query_ctx.set_current_schema(&schema);
399    }
400    query_ctx.set_channel(Channel::HttpSql);
401    query_ctx.enable_live_analyze_metrics();
402    let query_ctx = Arc::new(query_ctx);
403
404    let Some(sql) = query_params.sql.or(form_params.sql) else {
405        return ErrorResponse::from_error_message(
406            StatusCode::InvalidArguments,
407            "sql parameter is required.".to_string(),
408        )
409        .with_execution_time(start.elapsed().as_millis() as u64)
410        .into_response();
411    };
412    if let Some((status, msg)) = validate_schema(sql_handler.clone(), query_ctx.clone()).await {
413        return ErrorResponse::from_error_message(status, msg)
414            .with_execution_time(start.elapsed().as_millis() as u64)
415            .into_response();
416    }
417
418    let interval_ms = query_params
419        .snapshot_interval_ms
420        .or(form_params.snapshot_interval_ms)
421        .unwrap_or(DEFAULT_ANALYZE_SNAPSHOT_INTERVAL_MS)
422        .clamp(
423            MIN_ANALYZE_SNAPSHOT_INTERVAL_MS,
424            MAX_ANALYZE_SNAPSHOT_INTERVAL_MS,
425        );
426
427    let output = match state
428        .sql_handler
429        .do_analyze_stream_query(&sql, query_ctx.clone())
430        .await
431    {
432        Ok(output) => output,
433        Err(err) => {
434            return ErrorResponse::from_error(err)
435                .with_execution_time(start.elapsed().as_millis() as u64)
436                .into_response();
437        }
438    };
439
440    let plan = output.meta.plan.clone();
441    let OutputData::Stream(stream) = output.data else {
442        return ErrorResponse::from_error_message(
443            StatusCode::InvalidArguments,
444            "analyze stream query must return a stream".to_string(),
445        )
446        .with_execution_time(start.elapsed().as_millis() as u64)
447        .into_response();
448    };
449    let schema = stream.schema();
450
451    let (metrics_tx, metrics_rx) = watch::channel::<Option<String>>(None);
452    let (terminal_tx, terminal_rx) = watch::channel::<Option<AnalyzeStreamMessage>>(None);
453    let notify = Arc::new(Notify::new());
454    let (cancel_tx, mut cancel_rx) = watch::channel(false);
455    let worker_notify = notify.clone();
456    let panic_notify = notify.clone();
457    let sequence = Arc::new(AtomicU64::new(0));
458    let worker_sequence = sequence.clone();
459    let worker_terminal_tx = terminal_tx.clone();
460    let worker = common_runtime::spawn_global(async move {
461        let worker_result = AssertUnwindSafe(async move {
462            let mut stream = stream;
463            let mut batches = Vec::new();
464            let mut current_interval_ms = interval_ms;
465            let tick = tokio::time::sleep(Duration::from_millis(current_interval_ms));
466            tokio::pin!(tick);
467
468            loop {
469                tokio::select! {
470                    _ = cancel_rx.changed() => return,
471                    item = stream.next() => {
472                        match item {
473                            Some(Ok(next_batch)) => batches.push(next_batch),
474                            Some(Err(err)) => {
475                                let status = err.status_code();
476                                let event_name = if status == StatusCode::Cancelled { "canceled" } else { "error" };
477                                let (payload, _) = make_analyze_payload(AnalyzePayloadArgs {
478                                    seq: worker_sequence.load(Ordering::Relaxed),
479                                    state: event_name,
480                                    partial: false,
481                                    start,
482                                    plan: plan.as_ref(),
483                                    output: None,
484                                    reason: Some(err.output_msg()),
485                                    code: Some(status as u32),
486                                });
487                                send_analyze_terminal(&terminal_tx, &worker_notify, event_name, payload);
488                                return;
489                            }
490                            None => {
491                                let output = HttpRecordsOutput::try_new(schema.clone(), batches)
492                                    .map(GreptimeQueryOutput::Records);
493                                let (event_name, payload) = make_final_analyze_event(
494                                    output.map_err(|err| (err.output_msg(), err.status_code() as u32)),
495                                    worker_sequence.load(Ordering::Relaxed),
496                                    start,
497                                    plan.as_ref(),
498                                );
499                                send_analyze_terminal(&terminal_tx, &worker_notify, event_name, payload);
500                                return;
501                            }
502                        }
503                    }
504                    _ = &mut tick, if plan.is_some() => {
505                        let (payload, payload_bytes) = make_analyze_payload(AnalyzePayloadArgs {
506                            seq: worker_sequence.load(Ordering::Relaxed),
507                            state: "metrics",
508                            partial: true,
509                            start,
510                            plan: plan.as_ref(),
511                            output: None,
512                            reason: None,
513                            code: None,
514                        });
515                        current_interval_ms = adaptive_interval_ms(payload_bytes, interval_ms);
516                        worker_sequence.fetch_add(1, Ordering::Relaxed);
517                        if metrics_tx.send(Some(payload)).is_err() {
518                            return;
519                        }
520                        worker_notify.notify_one();
521                        tick.as_mut().reset(tokio::time::Instant::now() + Duration::from_millis(current_interval_ms));
522                    }
523                }
524            }
525        })
526        .catch_unwind()
527        .await;
528
529        if worker_result.is_err() {
530            tracing::debug!("analyze stream worker panicked");
531            let (payload, _) = make_analyze_payload(AnalyzePayloadArgs {
532                seq: sequence.load(Ordering::Relaxed),
533                state: "error",
534                partial: false,
535                start,
536                plan: None,
537                output: None,
538                reason: Some("analyze stream worker panicked".to_string()),
539                code: Some(StatusCode::Internal as u32),
540            });
541            send_analyze_terminal(&worker_terminal_tx, &panic_notify, "error", payload);
542        }
543    });
544
545    let sse_stream = analyze_stream_body(metrics_rx, terminal_rx, notify, cancel_tx, worker);
546
547    Sse::new(sse_stream)
548        .keep_alive(KeepAlive::new().interval(Duration::from_secs(15)))
549        .into_response()
550}
551
552#[doc(hidden)]
553pub fn analyze_stream_body(
554    metrics: watch::Receiver<Option<String>>,
555    terminal: watch::Receiver<Option<AnalyzeStreamMessage>>,
556    notify: Arc<Notify>,
557    cancel: watch::Sender<bool>,
558    worker: common_runtime::JoinHandle<()>,
559) -> impl futures::Stream<Item = std::result::Result<Event, std::convert::Infallible>> {
560    futures::stream::unfold(
561        AnalyzeStreamBodyState {
562            latest_metrics: metrics,
563            terminal,
564            notify,
565            _worker: AnalyzeStreamWorkerGuard {
566                cancel,
567                handle: worker,
568            },
569            done: false,
570        },
571        |mut state| async move {
572            if state.done {
573                return None;
574            }
575            loop {
576                let notify = Arc::clone(&state.notify);
577                let notified = notify.notified();
578                tokio::pin!(notified);
579                // Register before checking the slots to avoid a lost wakeup.
580                notified.as_mut().enable();
581                let latest = {
582                    let latest = state.latest_metrics.borrow_and_update();
583                    latest.has_changed().then(|| latest.clone())
584                };
585                if let Some(Some(payload)) = latest {
586                    return Some((Ok(Event::default().event("metrics").data(payload)), state));
587                }
588                // Inspect the terminal slot directly because a closed watch channel
589                // can otherwise hide a value published while a worker was unwinding.
590                if state.terminal.borrow().is_some() {
591                    let terminal = { state.terminal.borrow_and_update().clone() };
592                    if let Some(AnalyzeStreamMessage {
593                        event_name,
594                        payload,
595                    }) = terminal
596                    {
597                        state.done = true;
598                        return Some((Ok(Event::default().event(event_name).data(payload)), state));
599                    }
600                }
601                notified.await;
602            }
603        },
604    )
605}
606
607#[doc(hidden)]
608pub fn send_analyze_terminal(
609    terminal_tx: &watch::Sender<Option<AnalyzeStreamMessage>>,
610    notify: &Notify,
611    event_name: &'static str,
612    payload: String,
613) {
614    if terminal_tx.send_if_modified(|terminal| {
615        if terminal.is_none() {
616            *terminal = Some(AnalyzeStreamMessage {
617                event_name,
618                payload,
619            });
620            true
621        } else {
622            false
623        }
624    }) {
625        notify.notify_one();
626    }
627}
628
629fn adaptive_interval_ms(payload_bytes: usize, requested_ms: u64) -> u64 {
630    if payload_bytes >= 10 * 1024 * 1024 {
631        requested_ms.max(30_000)
632    } else if payload_bytes >= 1024 * 1024 {
633        requested_ms.max(10_000)
634    } else {
635        requested_ms
636    }
637}
638
639fn make_final_analyze_event(
640    output: std::result::Result<GreptimeQueryOutput, (String, u32)>,
641    seq: u64,
642    start: Instant,
643    plan: Option<&Arc<dyn ExecutionPlan>>,
644) -> (&'static str, String) {
645    match output {
646        Ok(output) => (
647            "final",
648            make_analyze_payload(AnalyzePayloadArgs {
649                seq,
650                state: "final",
651                partial: false,
652                start,
653                plan,
654                output: Some(output),
655                reason: None,
656                code: None,
657            })
658            .0,
659        ),
660        Err((reason, code)) => (
661            "error",
662            make_analyze_payload(AnalyzePayloadArgs {
663                seq,
664                state: "error",
665                partial: false,
666                start,
667                plan,
668                output: None,
669                reason: Some(reason),
670                code: Some(code),
671            })
672            .0,
673        ),
674    }
675}
676
677struct AnalyzePayloadArgs<'a> {
678    seq: u64,
679    state: &'static str,
680    partial: bool,
681    start: Instant,
682    plan: Option<&'a Arc<dyn ExecutionPlan>>,
683    output: Option<GreptimeQueryOutput>,
684    reason: Option<String>,
685    code: Option<u32>,
686}
687
688fn make_analyze_payload(args: AnalyzePayloadArgs<'_>) -> (String, usize) {
689    let AnalyzePayloadArgs {
690        seq,
691        state,
692        partial,
693        start,
694        plan,
695        output,
696        reason,
697        code,
698    } = args;
699    // Periodic snapshots are compact; terminal snapshots retain verbose plan details.
700    let metrics =
701        plan.and_then(|plan| query::analyze_plan_metrics_to_json_value(plan, !partial).ok());
702    let payload = AnalyzeStreamPayload {
703        seq,
704        state,
705        partial,
706        elapsed_ms: start.elapsed().as_millis() as u64,
707        metrics,
708        output,
709        reason,
710        code,
711    };
712    let payload_string = serde_json::to_string(&payload).unwrap_or_else(|e| {
713        serde_json::json!({
714            "seq": seq,
715            "state": "error",
716            "partial": false,
717            "reason": format!("Failed to serialize SSE payload: {e}"),
718        })
719        .to_string()
720    });
721    let payload_bytes = payload_string.len();
722    (payload_string, payload_bytes)
723}
724
725/// Handler to parse sql
726#[axum_macros::debug_handler]
727#[tracing::instrument(skip_all, fields(protocol = "http", request_type = "sql"))]
728pub async fn sql_parse(
729    Query(query_params): Query<SqlQuery>,
730    Form(form_params): Form<SqlQuery>,
731) -> Result<Json<Vec<Statement>>> {
732    let Some(sql) = query_params.sql.or(form_params.sql) else {
733        return InvalidQuerySnafu {
734            reason: "sql parameter is required.",
735        }
736        .fail();
737    };
738
739    let stmts =
740        ParserContext::create_with_dialect(&sql, &GreptimeDbDialect {}, ParseOptions::default())
741            .context(FailedToParseQuerySnafu)?;
742
743    Ok(stmts.into())
744}
745
746#[derive(Debug, Serialize, Deserialize)]
747pub struct SqlFormatResponse {
748    pub formatted: String,
749}
750
751/// Handler to format sql string
752#[axum_macros::debug_handler]
753#[tracing::instrument(skip_all, fields(protocol = "http", request_type = "sql_format"))]
754pub async fn sql_format(
755    Query(query_params): Query<SqlQuery>,
756    Form(form_params): Form<SqlQuery>,
757) -> axum::response::Response {
758    let Some(sql) = query_params.sql.or(form_params.sql) else {
759        let resp = ErrorResponse::from_error_message(
760            StatusCode::InvalidArguments,
761            "sql parameter is required.".to_string(),
762        );
763        return HttpResponse::Error(resp).into_response();
764    };
765
766    // Parse using GreptimeDB dialect then reconstruct statements via Display
767    let stmts = match ParserContext::create_with_dialect(
768        &sql,
769        &GreptimeDbDialect {},
770        ParseOptions::default(),
771    ) {
772        Ok(v) => v,
773        Err(e) => return HttpResponse::Error(ErrorResponse::from_error(e)).into_response(),
774    };
775
776    let mut parts: Vec<String> = Vec::with_capacity(stmts.len());
777    for stmt in stmts {
778        let mut s = format!("{stmt}");
779        if !s.trim_end().ends_with(';') {
780            s.push(';');
781        }
782        parts.push(s);
783    }
784
785    let formatted = parts.join("\n");
786    Json(SqlFormatResponse { formatted }).into_response()
787}
788
789/// Create a response from query result
790pub async fn from_output(
791    outputs: Vec<crate::error::Result<Output>>,
792) -> std::result::Result<(Vec<GreptimeQueryOutput>, HashMap<String, Value>), ErrorResponse> {
793    // TODO(sunng87): this api response structure cannot represent error well.
794    //  It hides successful execution results from error response
795    let mut results = Vec::with_capacity(outputs.len());
796    let mut merge_map = HashMap::new();
797
798    for out in outputs {
799        match out {
800            Ok(o) => match o.data {
801                OutputData::AffectedRows(rows) => {
802                    results.push(GreptimeQueryOutput::AffectedRows(rows));
803                    if o.meta.cost > 0 {
804                        merge_map.insert(GREPTIME_EXEC_WRITE_COST.to_string(), o.meta.cost as u64);
805                    }
806                }
807                OutputData::Stream(stream) => {
808                    let schema = stream.schema().clone();
809                    // TODO(sunng87): streaming response
810                    let mut http_record_output = match util::collect(stream).await {
811                        Ok(rows) => match HttpRecordsOutput::try_new(schema, rows) {
812                            Ok(rows) => rows,
813                            Err(err) => {
814                                return Err(ErrorResponse::from_error(err));
815                            }
816                        },
817                        Err(err) => {
818                            return Err(ErrorResponse::from_error(err));
819                        }
820                    };
821                    if let Some(physical_plan) = o.meta.plan {
822                        let mut result_map = HashMap::new();
823
824                        let mut tmp = vec![&mut merge_map, &mut result_map];
825                        collect_plan_metrics(&physical_plan, &mut tmp);
826                        let re = result_map
827                            .into_iter()
828                            .map(|(k, v)| (k, Value::from(v)))
829                            .collect::<HashMap<String, Value>>();
830                        http_record_output.metrics.extend(re);
831                    }
832                    results.push(GreptimeQueryOutput::Records(http_record_output))
833                }
834                OutputData::RecordBatches(rbs) => {
835                    match HttpRecordsOutput::try_new(rbs.schema(), rbs.take()) {
836                        Ok(rows) => {
837                            results.push(GreptimeQueryOutput::Records(rows));
838                        }
839                        Err(err) => {
840                            return Err(ErrorResponse::from_error(err));
841                        }
842                    }
843                }
844            },
845
846            Err(err) => {
847                return Err(ErrorResponse::from_error(err));
848            }
849        }
850    }
851
852    let merge_map = merge_map
853        .into_iter()
854        .map(|(k, v)| (k, Value::from(v)))
855        .collect();
856
857    Ok((results, merge_map))
858}
859
860#[derive(Debug, Default, Serialize, Deserialize)]
861pub struct PromqlQuery {
862    pub query: String,
863    pub start: String,
864    pub end: String,
865    pub step: String,
866    pub lookback: Option<String>,
867    pub db: Option<String>,
868    // (Optional) result format: [`greptimedb_v1`, `influxdb_v1`, `csv`,
869    // `arrow`],
870    // the default value is `greptimedb_v1`
871    pub format: Option<String>,
872    // For arrow output
873    pub compression: Option<String>,
874    // Returns epoch timestamps with the specified precision.
875    // Both u and µ indicate microseconds.
876    // epoch = [ns,u,µ,ms,s],
877    //
878    // For influx output only
879    //
880    // TODO(jeremy): currently, only InfluxDB result format is supported,
881    // and all columns of the `Timestamp` type will be converted to their
882    // specified time precision. Maybe greptimedb format can support this
883    // param too.
884    pub epoch: Option<String>,
885}
886
887impl From<PromqlQuery> for PromQuery {
888    fn from(query: PromqlQuery) -> Self {
889        PromQuery {
890            query: query.query,
891            start: query.start,
892            end: query.end,
893            step: query.step,
894            lookback: query
895                .lookback
896                .unwrap_or_else(|| DEFAULT_LOOKBACK_STRING.to_string()),
897            // TODO(dennis): support alias from http params or parse from query.query
898            alias: None,
899        }
900    }
901}
902
903/// Handler to execute promql
904#[axum_macros::debug_handler]
905#[tracing::instrument(skip_all, fields(protocol = "http", request_type = "promql"))]
906pub async fn promql(
907    State(state): State<ApiState>,
908    Query(params): Query<PromqlQuery>,
909    Extension(mut query_ctx): Extension<QueryContext>,
910) -> Response {
911    let sql_handler = &state.sql_handler;
912    let exec_start = Instant::now();
913    let db = query_ctx.get_db_string();
914
915    query_ctx.set_channel(Channel::Promql);
916    let query_ctx = Arc::new(query_ctx);
917
918    let _timer = crate::metrics::METRIC_HTTP_PROMQL_ELAPSED
919        .with_label_values(&[db.as_str()])
920        .start_timer();
921
922    let resp = if let Some((status, msg)) =
923        validate_schema(sql_handler.clone(), query_ctx.clone()).await
924    {
925        let resp = ErrorResponse::from_error_message(status, msg);
926        HttpResponse::Error(resp)
927    } else {
928        let format = params
929            .format
930            .as_ref()
931            .map(|s| s.to_lowercase())
932            .map(|s| ResponseFormat::parse(s.as_str()).unwrap_or(ResponseFormat::GreptimedbV1))
933            .unwrap_or(ResponseFormat::GreptimedbV1);
934        let epoch = params
935            .epoch
936            .as_ref()
937            .map(|s| s.to_lowercase())
938            .map(|s| Epoch::parse(s.as_str()).unwrap_or(Epoch::Millisecond));
939        let compression = params.compression.clone();
940
941        let prom_query = params.into();
942        let outputs = sql_handler.do_promql_query(&prom_query, query_ctx).await;
943
944        match format {
945            ResponseFormat::Arrow => ArrowResponse::from_output(outputs, compression).await,
946            ResponseFormat::Csv(with_names, with_types) => {
947                CsvResponse::from_output(outputs, with_names, with_types).await
948            }
949            ResponseFormat::Table => TableResponse::from_output(outputs).await,
950            ResponseFormat::GreptimedbV1 => GreptimedbV1Response::from_output(outputs).await,
951            ResponseFormat::InfluxdbV1 => InfluxdbV1Response::from_output(outputs, epoch).await,
952            ResponseFormat::Json => JsonResponse::from_output(outputs).await,
953            ResponseFormat::Null => NullResponse::from_output(outputs).await,
954        }
955    };
956
957    resp.with_execution_time(exec_start.elapsed().as_millis() as u64)
958        .into_response()
959}
960
961/// Handler to export metrics
962#[axum_macros::debug_handler]
963pub async fn metrics(
964    State(state): State<MetricsHandler>,
965    Query(_params): Query<HashMap<String, String>>,
966) -> String {
967    // A default ProcessCollector is registered automatically in prometheus.
968    // We do not need to explicitly collect process-related data.
969    // But ProcessCollector only support on linux.
970
971    #[cfg(not(windows))]
972    if let Some(c) = crate::metrics::jemalloc::JEMALLOC_COLLECTOR.as_ref()
973        && let Err(e) = c.update()
974    {
975        common_telemetry::error!(e; "Failed to update jemalloc metrics");
976    }
977    state.render()
978}
979
980#[derive(Debug, Serialize, Deserialize)]
981pub struct HealthQuery {}
982
983#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
984pub struct HealthResponse {}
985
986/// Handler to export healthy check
987///
988/// Currently simply return status "200 OK" (default) with an empty json payload "{}"
989#[axum_macros::debug_handler]
990pub async fn health(Query(_params): Query<HealthQuery>) -> Json<HealthResponse> {
991    Json(HealthResponse {})
992}
993
994#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
995pub struct StatusResponse<'a> {
996    pub commit: &'a str,
997    pub branch: &'a str,
998    pub rustc_version: &'a str,
999    pub hostname: String,
1000    pub version: &'a str,
1001}
1002
1003/// Handler to expose information info about runtime, build, etc.
1004#[axum_macros::debug_handler]
1005pub async fn status() -> Json<StatusResponse<'static>> {
1006    let hostname = hostname::get()
1007        .map(|s| s.to_string_lossy().to_string())
1008        .unwrap_or_else(|_| "unknown".to_string());
1009    let build_info = common_version::build_info();
1010    Json(StatusResponse {
1011        commit: build_info.commit,
1012        branch: build_info.branch,
1013        rustc_version: build_info.rustc,
1014        hostname,
1015        version: build_info.version,
1016    })
1017}
1018
1019/// Handler to expose configuration information info about runtime, build, etc.
1020#[axum_macros::debug_handler]
1021pub async fn config(State(state): State<GreptimeOptionsConfigState>) -> Response {
1022    (axum::http::StatusCode::OK, state.greptime_config_options).into_response()
1023}
1024
1025async fn validate_schema(
1026    sql_handler: ServerSqlQueryHandlerRef,
1027    query_ctx: QueryContextRef,
1028) -> Option<(StatusCode, String)> {
1029    match sql_handler
1030        .is_valid_schema(query_ctx.current_catalog(), &query_ctx.current_schema())
1031        .await
1032    {
1033        Ok(true) => None,
1034        Ok(false) => Some((
1035            StatusCode::DatabaseNotFound,
1036            format!("Database not found: {}", query_ctx.get_db_string()),
1037        )),
1038        Err(e) => Some((
1039            StatusCode::Internal,
1040            format!(
1041                "Error checking database: {}, {}",
1042                query_ctx.get_db_string(),
1043                e.output_msg(),
1044            ),
1045        )),
1046    }
1047}
1048
1049pub async fn index() -> axum::response::Html<String> {
1050    let name = common_version::product_name();
1051    let version = common_version::version();
1052    axum::response::Html(format!(
1053        r#"<!DOCTYPE html>
1054<html>
1055<head><title>{name}</title></head>
1056<body>
1057<h1>{name}</h1>
1058<p>Version: {version}</p>
1059<ul>
1060<li><a href="/dashboard">Dashboard UI</a></li>
1061<li><a href="/v1/health">Health</a> (JSON)</li>
1062<li><a href="/status">Status</a> (JSON)</li>
1063<li><a href="/metrics">Metrics</a> (For Prometheus Scrape)</li>
1064<li><a href="/config">Config</a> (TXT)</li>
1065</ul>
1066</body>
1067</html>"#,
1068    ))
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073    use std::sync::Mutex;
1074    use std::sync::atomic::AtomicUsize;
1075
1076    use async_trait::async_trait;
1077    use common_error::ext::{BoxedError, PlainError};
1078    use common_query::OutputMeta;
1079    use common_telemetry::tracing::instrument::WithSubscriber;
1080    use datafusion_expr::LogicalPlan;
1081    use datatypes::prelude::ConcreteDataType;
1082    use datatypes::schema::{ColumnSchema, Schema};
1083    use datatypes::vectors::{BinaryVector, UInt32Vector, VectorRef};
1084    use futures::stream;
1085    use query::query_engine::DescribeResult;
1086    use snafu::IntoError;
1087
1088    use super::*;
1089    use crate::query_handler::sql::SqlQueryHandler;
1090
1091    struct TestSqlQueryHandler {
1092        outputs: Mutex<Vec<Result<Output>>>,
1093    }
1094
1095    #[async_trait]
1096    impl SqlQueryHandler for TestSqlQueryHandler {
1097        async fn do_query(&self, _: &str, _: QueryContextRef) -> Vec<Result<Output>> {
1098            std::mem::take(&mut *self.outputs.lock().unwrap())
1099        }
1100
1101        async fn create_logical_tables(&self, _: &str, _: QueryContextRef) -> Result<Vec<Output>> {
1102            std::mem::take(&mut *self.outputs.lock().unwrap())
1103                .into_iter()
1104                .collect()
1105        }
1106
1107        async fn do_analyze_stream_query(&self, _: &str, _: QueryContextRef) -> Result<Output> {
1108            unimplemented!()
1109        }
1110
1111        async fn do_exec_plan(
1112            &self,
1113            _: LogicalPlan,
1114            _: Option<Statement>,
1115            _: QueryContextRef,
1116        ) -> Result<Output> {
1117            unimplemented!()
1118        }
1119
1120        async fn do_promql_query(&self, _: &PromQuery, _: QueryContextRef) -> Vec<Result<Output>> {
1121            unimplemented!()
1122        }
1123
1124        async fn do_describe(
1125            &self,
1126            _: Statement,
1127            _: QueryContextRef,
1128        ) -> Result<Option<DescribeResult>> {
1129            unimplemented!()
1130        }
1131
1132        async fn is_valid_schema(&self, _: &str, _: &str) -> Result<bool> {
1133            Ok(true)
1134        }
1135    }
1136
1137    async fn sql_response(
1138        outputs: Vec<Result<Output>>,
1139        format: &str,
1140        limit: Option<usize>,
1141    ) -> HttpResponse {
1142        sql(
1143            State(ApiState {
1144                sql_handler: Arc::new(TestSqlQueryHandler {
1145                    outputs: Mutex::new(outputs),
1146                }),
1147            }),
1148            Query(SqlQuery {
1149                sql: Some("select number from numbers".to_string()),
1150                format: Some(format.to_string()),
1151                limit,
1152                ..Default::default()
1153            }),
1154            Extension(QueryContext::with_db_name(None)),
1155            Form(SqlQuery::default()),
1156        )
1157        .await
1158    }
1159
1160    #[tokio::test]
1161    async fn test_logical_ddl_logs_diagnostics_without_exposing_sql_or_response_details() {
1162        let sql = "CREATE TABLE logs (ts TIMESTAMP TIME INDEX) ENGINE=metric WITH (on_physical_table='request-only-value')";
1163        for (status, level) in [
1164            (StatusCode::InvalidArguments, "WARN"),
1165            (StatusCode::Internal, "ERROR"),
1166        ] {
1167            let logs = tempfile::NamedTempFile::new().unwrap();
1168            let writer = logs.reopen().unwrap();
1169            let subscriber = common_telemetry::tracing_subscriber::fmt()
1170                .without_time()
1171                .with_ansi(false)
1172                .with_writer(move || writer.try_clone().unwrap())
1173                .finish();
1174            let error = crate::error::ExecuteQuerySnafu.into_error(BoxedError::new(
1175                PlainError::new("sensitive-option-value".to_string(), status),
1176            ));
1177            let response = create_logical_tables(
1178                State(ApiState {
1179                    sql_handler: Arc::new(TestSqlQueryHandler {
1180                        outputs: Mutex::new(vec![Err(error)]),
1181                    }),
1182                }),
1183                Extension(QueryContext::with_db_name(None)),
1184                Ok(Form(LogicalTableDdl {
1185                    sql: Some(sql.to_string()),
1186                    db: None,
1187                })),
1188            )
1189            .with_subscriber(subscriber)
1190            .await
1191            .into_response();
1192            let diagnostics = std::fs::read_to_string(logs.path()).unwrap();
1193            assert!(diagnostics.contains(level), "{diagnostics}");
1194            assert!(
1195                diagnostics.contains("Failed to execute query"),
1196                "{diagnostics}"
1197            );
1198            assert!(
1199                diagnostics.contains("sensitive-option-value"),
1200                "{diagnostics}"
1201            );
1202            assert!(!diagnostics.contains(sql), "{diagnostics}");
1203            assert!(!diagnostics.contains("request-only-value"), "{diagnostics}");
1204            assert!(!format!("{:?}", response.headers()).contains("sensitive-option-value"));
1205            let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
1206                .await
1207                .unwrap();
1208            let body: Value = serde_json::from_slice(&bytes).unwrap();
1209            assert_eq!(body["code"], status as u32);
1210            assert_eq!(body["error"], "logical-table batch failed");
1211            assert!(body.get("output").is_none());
1212        }
1213    }
1214
1215    fn number_batches() -> RecordBatches {
1216        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1217            "number",
1218            ConcreteDataType::uint32_datatype(),
1219            false,
1220        )]));
1221        let batches = [vec![], vec![0, 1], vec![], vec![2, 3, 4], vec![]]
1222            .into_iter()
1223            .map(|values| {
1224                let columns: Vec<VectorRef> = vec![Arc::new(UInt32Vector::from_slice(values))];
1225                RecordBatch::new(schema.clone(), columns).unwrap()
1226            })
1227            .collect();
1228        RecordBatches::try_new(schema, batches).unwrap()
1229    }
1230
1231    fn number_output(streaming: bool) -> Output {
1232        let batches = number_batches();
1233        if streaming {
1234            Output::new_with_stream(batches.as_stream())
1235        } else {
1236            Output::new_with_record_batches(batches)
1237        }
1238    }
1239
1240    #[tokio::test]
1241    async fn test_sql_response_limit_across_batches_and_formats() {
1242        for format in [
1243            "greptimedb_v1",
1244            "json",
1245            "csv",
1246            "csvWithNames",
1247            "csvWithNamesAndTypes",
1248            "table",
1249        ] {
1250            for streaming in [false, true] {
1251                for limit in [
1252                    None,
1253                    Some(0),
1254                    Some(1),
1255                    Some(2),
1256                    Some(3),
1257                    Some(5),
1258                    Some(10),
1259                    Some(usize::MAX),
1260                ] {
1261                    let response =
1262                        sql_response(vec![Ok(number_output(streaming))], format, limit).await;
1263                    let output = match &response {
1264                        HttpResponse::GreptimedbV1(response) => response.output(),
1265                        HttpResponse::Json(response) => response.output(),
1266                        HttpResponse::Csv(response) => response.output(),
1267                        HttpResponse::Table(response) => response.output(),
1268                        _ => panic!("unexpected response: {response:?}"),
1269                    };
1270                    let GreptimeQueryOutput::Records(records) = &output[0] else {
1271                        panic!("expected records");
1272                    };
1273                    let expected_rows: Vec<_> = (0..limit.unwrap_or(5).min(5))
1274                        .map(|number| vec![Value::from(number)])
1275                        .collect();
1276                    assert_eq!(records.rows(), &expected_rows);
1277                    assert_eq!(records.total_rows, expected_rows.len());
1278                    assert_eq!(records.schema.column_schemas[0].name, "number");
1279                }
1280            }
1281        }
1282    }
1283
1284    #[tokio::test]
1285    async fn test_sql_response_limit_is_per_statement_and_preserves_write_cost() {
1286        let affected = Output::new(
1287            OutputData::AffectedRows(7),
1288            OutputMeta {
1289                cost: 42,
1290                ..Default::default()
1291            },
1292        );
1293        let response = sql_response(
1294            vec![
1295                Ok(number_output(true)),
1296                Ok(affected),
1297                Ok(number_output(false)),
1298            ],
1299            "greptimedb_v1",
1300            Some(1),
1301        )
1302        .await;
1303        let HttpResponse::GreptimedbV1(response) = response else {
1304            panic!("expected greptimedb response");
1305        };
1306        assert_eq!(response.output().len(), 3);
1307        for index in [0, 2] {
1308            let GreptimeQueryOutput::Records(records) = &response.output()[index] else {
1309                panic!("expected records");
1310            };
1311            assert_eq!(records.rows(), &vec![vec![Value::from(0)]]);
1312        }
1313        assert!(matches!(
1314            response.output()[1],
1315            GreptimeQueryOutput::AffectedRows(7)
1316        ));
1317        assert_eq!(
1318            response.resp_metrics[GREPTIME_EXEC_WRITE_COST],
1319            Value::from(42)
1320        );
1321    }
1322
1323    #[tokio::test]
1324    async fn test_sql_response_limit_does_not_affect_other_formats() {
1325        for format in ["arrow", "influxdb_v1", "null"] {
1326            let unlimited = sql_response(vec![Ok(number_output(true))], format, None).await;
1327            let limited = sql_response(vec![Ok(number_output(true))], format, Some(0)).await;
1328            assert_eq!(
1329                serde_json::to_value(unlimited.with_execution_time(0)).unwrap(),
1330                serde_json::to_value(limited.with_execution_time(0)).unwrap(),
1331            );
1332        }
1333    }
1334
1335    #[tokio::test]
1336    async fn test_sql_response_limit_drains_input_and_propagates_late_errors() {
1337        for limit in [0, 1, 3] {
1338            for late_error in [false, true] {
1339                let batches = number_batches();
1340                let schema = batches.schema();
1341                let mut input: Vec<_> = batches.into_iter().map(Ok).collect();
1342                if late_error {
1343                    input.push(
1344                        common_recordbatch::error::CreateRecordBatchesSnafu {
1345                            reason: "late stream error",
1346                        }
1347                        .fail(),
1348                    );
1349                }
1350                let expected_batches = input.len();
1351                let observed = Arc::new(AtomicUsize::new(0));
1352                let counter = observed.clone();
1353                let stream = stream::iter(input).inspect(move |_| {
1354                    counter.fetch_add(1, Ordering::Relaxed);
1355                });
1356                let output = Output::new_with_stream(Box::pin(RecordBatchStreamWrapper::new(
1357                    schema, stream,
1358                )));
1359                let response = sql_response(vec![Ok(output)], "greptimedb_v1", Some(limit)).await;
1360                assert_eq!(observed.load(Ordering::Relaxed), expected_batches);
1361                assert_eq!(matches!(response, HttpResponse::Error(_)), late_error);
1362            }
1363        }
1364    }
1365
1366    #[tokio::test]
1367    async fn test_sql_response_limit_skips_conversion_of_discarded_rows() {
1368        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1369            "payload",
1370            ConcreteDataType::json_datatype(),
1371            false,
1372        )]));
1373        let columns: Vec<VectorRef> = vec![Arc::new(BinaryVector::from(vec![
1374            datatypes::types::parse_string_to_jsonb(r#"{"ok":1}"#).unwrap(),
1375            b"invalid jsonb".to_vec(),
1376        ]))];
1377        let batch = RecordBatch::new(schema.clone(), columns).unwrap();
1378        for streaming in [false, true] {
1379            for limit in [0, 1, 2] {
1380                let batches = RecordBatches::try_new(schema.clone(), vec![batch.clone()]).unwrap();
1381                let output = if streaming {
1382                    Output::new_with_stream(batches.as_stream())
1383                } else {
1384                    Output::new_with_record_batches(batches)
1385                };
1386                let response = sql_response(vec![Ok(output)], "greptimedb_v1", Some(limit)).await;
1387                // Only retained rows should be decoded into HTTP response values.
1388                assert_eq!(matches!(response, HttpResponse::Error(_)), limit == 2);
1389            }
1390        }
1391    }
1392
1393    #[test]
1394    fn test_final_analyze_event_uses_error_event_for_conversion_error() {
1395        let (event_name, payload) = make_final_analyze_event(
1396            Err((
1397                "conversion failed".to_string(),
1398                StatusCode::InvalidArguments as u32,
1399            )),
1400            7,
1401            Instant::now(),
1402            None,
1403        );
1404
1405        assert_eq!(event_name, "error");
1406        let value: Value = serde_json::from_str(&payload).unwrap();
1407        assert_eq!(value["state"], "error");
1408        assert_eq!(value["reason"], "conversion failed");
1409    }
1410}