1use 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 pub format: Option<String>,
76 pub epoch: Option<String>,
85 pub limit: Option<usize>,
86 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 _worker: AnalyzeStreamWorkerGuard,
137 done: bool,
138}
139
140#[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#[derive(Deserialize)]
243pub struct LogicalTableDdl {
244 sql: Option<String>,
245 db: Option<String>,
246}
247
248pub 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
323fn 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 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#[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 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 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 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#[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#[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 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
805pub async fn from_output(
807 outputs: Vec<crate::error::Result<Output>>,
808) -> std::result::Result<(Vec<GreptimeQueryOutput>, HashMap<String, Value>), ErrorResponse> {
809 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 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 pub format: Option<String>,
888 pub compression: Option<String>,
890 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 alias: None,
915 }
916 }
917}
918
919#[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#[axum_macros::debug_handler]
979pub async fn metrics(
980 State(state): State<MetricsHandler>,
981 Query(_params): Query<HashMap<String, String>>,
982) -> String {
983 #[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#[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#[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#[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 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}