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