LCOV - code coverage report
Current view: top level - proxy/src/serverless - sql_over_http.rs (source / functions) Coverage Total Hit
Test: 1e20c4f2b28aa592527961bb32170ebbd2c9172f.info Lines: 5.5 % 656 36
Test Date: 2025-07-16 12:29:03 Functions: 2.7 % 74 2

            Line data    Source code
       1              : use std::pin::pin;
       2              : use std::sync::Arc;
       3              : 
       4              : use bytes::Bytes;
       5              : use futures::future::{Either, select, try_join};
       6              : use futures::{StreamExt, TryFutureExt};
       7              : use http::Method;
       8              : use http::header::AUTHORIZATION;
       9              : use http_body_util::combinators::BoxBody;
      10              : use http_body_util::{BodyExt, Full};
      11              : use http_utils::error::ApiError;
      12              : use hyper::body::Incoming;
      13              : use hyper::http::{HeaderName, HeaderValue};
      14              : use hyper::{Request, Response, StatusCode, header};
      15              : use indexmap::IndexMap;
      16              : use postgres_client::error::{DbError, ErrorPosition, SqlState};
      17              : use postgres_client::{GenericClient, IsolationLevel, NoTls, ReadyForQueryStatus, Transaction};
      18              : use serde_json::Value;
      19              : use serde_json::value::RawValue;
      20              : use tokio::time::{self, Instant};
      21              : use tokio_util::sync::CancellationToken;
      22              : use tracing::{Level, debug, error, info};
      23              : use typed_json::json;
      24              : 
      25              : use super::backend::{LocalProxyConnError, PoolingBackend};
      26              : use super::conn_pool::AuthData;
      27              : use super::conn_pool_lib::{self, ConnInfo};
      28              : use super::error::{ConnInfoError, HttpCodeError, ReadPayloadError};
      29              : use super::http_util::{
      30              :     ALLOW_POOL, ARRAY_MODE, CONN_STRING, NEON_REQUEST_ID, RAW_TEXT_OUTPUT, TXN_DEFERRABLE,
      31              :     TXN_ISOLATION_LEVEL, TXN_READ_ONLY, get_conn_info, json_response, uuid_to_header_value,
      32              : };
      33              : use super::json::{JsonConversionError, json_to_pg_text, pg_text_row_to_json};
      34              : use crate::auth::backend::ComputeCredentialKeys;
      35              : use crate::config::{HttpConfig, ProxyConfig};
      36              : use crate::context::RequestContext;
      37              : use crate::error::{ErrorKind, ReportableError, UserFacingError};
      38              : use crate::http::read_body_with_limit;
      39              : use crate::metrics::{HttpDirection, Metrics};
      40              : use crate::serverless::backend::HttpConnError;
      41              : use crate::usage_metrics::{MetricCounter, MetricCounterRecorder};
      42              : use crate::util::run_until_cancelled;
      43              : 
      44              : #[derive(serde::Deserialize)]
      45              : #[serde(rename_all = "camelCase")]
      46              : struct QueryData {
      47              :     query: String,
      48              :     #[serde(deserialize_with = "bytes_to_pg_text")]
      49              :     #[serde(default)]
      50              :     params: Vec<Option<String>>,
      51              :     #[serde(default)]
      52              :     array_mode: Option<bool>,
      53              : }
      54              : 
      55            0 : #[derive(serde::Deserialize)]
      56              : struct BatchQueryData {
      57              :     queries: Vec<QueryData>,
      58              : }
      59              : 
      60              : #[derive(serde::Deserialize)]
      61              : #[serde(untagged)]
      62              : enum Payload {
      63              :     Single(QueryData),
      64              :     Batch(BatchQueryData),
      65              : }
      66              : 
      67              : static HEADER_VALUE_TRUE: HeaderValue = HeaderValue::from_static("true");
      68              : 
      69            3 : fn bytes_to_pg_text<'de, D>(deserializer: D) -> Result<Vec<Option<String>>, D::Error>
      70            3 : where
      71            3 :     D: serde::de::Deserializer<'de>,
      72              : {
      73              :     // TODO: consider avoiding the allocation here.
      74            3 :     let json: Vec<Value> = serde::de::Deserialize::deserialize(deserializer)?;
      75            3 :     Ok(json_to_pg_text(json))
      76            3 : }
      77              : 
      78            0 : pub(crate) async fn handle(
      79            0 :     config: &'static ProxyConfig,
      80            0 :     ctx: RequestContext,
      81            0 :     request: Request<Incoming>,
      82            0 :     backend: Arc<PoolingBackend>,
      83            0 :     cancel: CancellationToken,
      84            0 : ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, ApiError> {
      85            0 :     let result = handle_inner(cancel, config, &ctx, request, backend).await;
      86              : 
      87            0 :     let mut response = match result {
      88            0 :         Ok(r) => {
      89            0 :             ctx.set_success();
      90              : 
      91              :             // Handling the error response from local proxy here
      92            0 :             if config.authentication_config.is_auth_broker && r.status().is_server_error() {
      93            0 :                 let status = r.status();
      94              : 
      95            0 :                 let body_bytes = r
      96            0 :                     .collect()
      97            0 :                     .await
      98            0 :                     .map_err(|e| {
      99            0 :                         ApiError::InternalServerError(anyhow::Error::msg(format!(
     100            0 :                             "could not collect http body: {e}"
     101            0 :                         )))
     102            0 :                     })?
     103            0 :                     .to_bytes();
     104              : 
     105            0 :                 if let Ok(mut json_map) =
     106            0 :                     serde_json::from_slice::<IndexMap<&str, &RawValue>>(&body_bytes)
     107              :                 {
     108            0 :                     let message = json_map.get("message");
     109            0 :                     if let Some(message) = message {
     110            0 :                         let msg: String = match serde_json::from_str(message.get()) {
     111            0 :                             Ok(msg) => msg,
     112              :                             Err(_) => {
     113            0 :                                 "Unable to parse the response message from server".to_string()
     114              :                             }
     115              :                         };
     116              : 
     117            0 :                         error!("Error response from local_proxy: {status} {msg}");
     118              : 
     119            0 :                         json_map.retain(|key, _| !key.starts_with("neon:")); // remove all the neon-related keys
     120              : 
     121            0 :                         let resp_json = serde_json::to_string(&json_map)
     122            0 :                             .unwrap_or("failed to serialize the response message".to_string());
     123              : 
     124            0 :                         return json_response(status, resp_json);
     125            0 :                     }
     126            0 :                 }
     127              : 
     128            0 :                 error!("Unable to parse the response message from local_proxy");
     129            0 :                 return json_response(
     130            0 :                     status,
     131            0 :                     json!({ "message": "Unable to parse the response message from server".to_string() }),
     132              :                 );
     133            0 :             }
     134            0 :             r
     135              :         }
     136            0 :         Err(e @ SqlOverHttpError::Cancelled(_)) => {
     137            0 :             let error_kind = e.get_error_kind();
     138            0 :             ctx.set_error_kind(error_kind);
     139              : 
     140            0 :             let message = "Query cancelled, connection was terminated";
     141              : 
     142            0 :             tracing::info!(
     143            0 :                 kind=error_kind.to_metric_label(),
     144              :                 error=%e,
     145              :                 msg=message,
     146            0 :                 "forwarding error to user"
     147              :             );
     148              : 
     149            0 :             json_response(
     150              :                 StatusCode::BAD_REQUEST,
     151            0 :                 json!({ "message": message, "code": SqlState::PROTOCOL_VIOLATION.code() }),
     152            0 :             )?
     153              :         }
     154            0 :         Err(e) => {
     155            0 :             let error_kind = e.get_error_kind();
     156            0 :             ctx.set_error_kind(error_kind);
     157              : 
     158            0 :             let mut message = e.to_string_client();
     159            0 :             let db_error = match &e {
     160            0 :                 SqlOverHttpError::ConnectCompute(HttpConnError::PostgresConnectionError(e))
     161            0 :                 | SqlOverHttpError::Postgres(e) => e.as_db_error(),
     162            0 :                 _ => None,
     163              :             };
     164            0 :             fn get<'a, T: Default>(db: Option<&'a DbError>, x: impl FnOnce(&'a DbError) -> T) -> T {
     165            0 :                 db.map(x).unwrap_or_default()
     166            0 :             }
     167              : 
     168            0 :             if let Some(db_error) = db_error {
     169            0 :                 db_error.message().clone_into(&mut message);
     170            0 :             }
     171              : 
     172            0 :             let position = db_error.and_then(|db| db.position());
     173            0 :             let (position, internal_position, internal_query) = match position {
     174            0 :                 Some(ErrorPosition::Original(position)) => (Some(position.to_string()), None, None),
     175            0 :                 Some(ErrorPosition::Internal { position, query }) => {
     176            0 :                     (None, Some(position.to_string()), Some(query.clone()))
     177              :                 }
     178            0 :                 None => (None, None, None),
     179              :             };
     180              : 
     181            0 :             let code = get(db_error, |db| db.code().code());
     182            0 :             let severity = get(db_error, |db| db.severity());
     183            0 :             let detail = get(db_error, |db| db.detail());
     184            0 :             let hint = get(db_error, |db| db.hint());
     185            0 :             let where_ = get(db_error, |db| db.where_());
     186            0 :             let table = get(db_error, |db| db.table());
     187            0 :             let column = get(db_error, |db| db.column());
     188            0 :             let schema = get(db_error, |db| db.schema());
     189            0 :             let datatype = get(db_error, |db| db.datatype());
     190            0 :             let constraint = get(db_error, |db| db.constraint());
     191            0 :             let file = get(db_error, |db| db.file());
     192            0 :             let line = get(db_error, |db| db.line().map(|l| l.to_string()));
     193            0 :             let routine = get(db_error, |db| db.routine());
     194              : 
     195            0 :             match &e {
     196            0 :                 SqlOverHttpError::Postgres(e)
     197            0 :                     if e.as_db_error().is_some() && error_kind == ErrorKind::User =>
     198              :                 {
     199              :                     // this error contains too much info, and it's not an error we care about.
     200            0 :                     if tracing::enabled!(Level::DEBUG) {
     201            0 :                         tracing::debug!(
     202            0 :                             kind=error_kind.to_metric_label(),
     203              :                             error=%e,
     204              :                             msg=message,
     205            0 :                             "forwarding error to user"
     206              :                         );
     207              :                     } else {
     208            0 :                         tracing::info!(
     209            0 :                             kind = error_kind.to_metric_label(),
     210              :                             error = "bad query",
     211            0 :                             "forwarding error to user"
     212              :                         );
     213              :                     }
     214              :                 }
     215              :                 _ => {
     216            0 :                     tracing::info!(
     217            0 :                         kind=error_kind.to_metric_label(),
     218              :                         error=%e,
     219              :                         msg=message,
     220            0 :                         "forwarding error to user"
     221              :                     );
     222              :                 }
     223              :             }
     224              : 
     225            0 :             json_response(
     226            0 :                 e.get_http_status_code(),
     227            0 :                 json!({
     228            0 :                     "message": message,
     229            0 :                     "code": code,
     230            0 :                     "detail": detail,
     231            0 :                     "hint": hint,
     232            0 :                     "position": position,
     233            0 :                     "internalPosition": internal_position,
     234            0 :                     "internalQuery": internal_query,
     235            0 :                     "severity": severity,
     236            0 :                     "where": where_,
     237            0 :                     "table": table,
     238            0 :                     "column": column,
     239            0 :                     "schema": schema,
     240            0 :                     "dataType": datatype,
     241            0 :                     "constraint": constraint,
     242            0 :                     "file": file,
     243            0 :                     "line": line,
     244            0 :                     "routine": routine,
     245              :                 }),
     246            0 :             )?
     247              :         }
     248              :     };
     249              : 
     250            0 :     response
     251            0 :         .headers_mut()
     252            0 :         .insert("Access-Control-Allow-Origin", HeaderValue::from_static("*"));
     253            0 :     Ok(response)
     254            0 : }
     255              : 
     256              : #[derive(Debug, thiserror::Error)]
     257              : pub(crate) enum SqlOverHttpError {
     258              :     #[error("{0}")]
     259              :     ReadPayload(#[from] ReadPayloadError),
     260              :     #[error("{0}")]
     261              :     ConnectCompute(#[from] HttpConnError),
     262              :     #[error("{0}")]
     263              :     ConnInfo(#[from] ConnInfoError),
     264              :     #[error("response is too large (max is {0} bytes)")]
     265              :     ResponseTooLarge(usize),
     266              :     #[error("invalid isolation level")]
     267              :     InvalidIsolationLevel,
     268              :     /// for queries our customers choose to run
     269              :     #[error("{0}")]
     270              :     Postgres(#[source] postgres_client::Error),
     271              :     /// for queries we choose to run
     272              :     #[error("{0}")]
     273              :     InternalPostgres(#[source] postgres_client::Error),
     274              :     #[error("{0}")]
     275              :     JsonConversion(#[from] JsonConversionError),
     276              :     #[error("{0}")]
     277              :     Cancelled(SqlOverHttpCancel),
     278              : }
     279              : 
     280              : impl ReportableError for SqlOverHttpError {
     281            0 :     fn get_error_kind(&self) -> ErrorKind {
     282            0 :         match self {
     283            0 :             SqlOverHttpError::ReadPayload(e) => e.get_error_kind(),
     284            0 :             SqlOverHttpError::ConnectCompute(e) => e.get_error_kind(),
     285            0 :             SqlOverHttpError::ConnInfo(e) => e.get_error_kind(),
     286            0 :             SqlOverHttpError::ResponseTooLarge(_) => ErrorKind::User,
     287            0 :             SqlOverHttpError::InvalidIsolationLevel => ErrorKind::User,
     288              :             // customer initiated SQL errors.
     289            0 :             SqlOverHttpError::Postgres(p) => {
     290            0 :                 if p.as_db_error().is_some() {
     291            0 :                     ErrorKind::User
     292              :                 } else {
     293            0 :                     ErrorKind::Compute
     294              :                 }
     295              :             }
     296              :             // proxy initiated SQL errors.
     297            0 :             SqlOverHttpError::InternalPostgres(p) => {
     298            0 :                 if p.as_db_error().is_some() {
     299            0 :                     ErrorKind::Service
     300              :                 } else {
     301            0 :                     ErrorKind::Compute
     302              :                 }
     303              :             }
     304              :             // postgres returned a bad row format that we couldn't parse.
     305            0 :             SqlOverHttpError::JsonConversion(_) => ErrorKind::Postgres,
     306            0 :             SqlOverHttpError::Cancelled(c) => c.get_error_kind(),
     307              :         }
     308            0 :     }
     309              : }
     310              : 
     311              : impl UserFacingError for SqlOverHttpError {
     312            0 :     fn to_string_client(&self) -> String {
     313            0 :         match self {
     314            0 :             SqlOverHttpError::ReadPayload(p) => p.to_string(),
     315            0 :             SqlOverHttpError::ConnectCompute(c) => c.to_string_client(),
     316            0 :             SqlOverHttpError::ConnInfo(c) => c.to_string_client(),
     317            0 :             SqlOverHttpError::ResponseTooLarge(_) => self.to_string(),
     318            0 :             SqlOverHttpError::InvalidIsolationLevel => self.to_string(),
     319            0 :             SqlOverHttpError::Postgres(p) => p.to_string(),
     320            0 :             SqlOverHttpError::InternalPostgres(p) => p.to_string(),
     321            0 :             SqlOverHttpError::JsonConversion(_) => "could not parse postgres response".to_string(),
     322            0 :             SqlOverHttpError::Cancelled(_) => self.to_string(),
     323              :         }
     324            0 :     }
     325              : }
     326              : 
     327              : impl HttpCodeError for SqlOverHttpError {
     328            0 :     fn get_http_status_code(&self) -> StatusCode {
     329            0 :         match self {
     330            0 :             SqlOverHttpError::ReadPayload(e) => e.get_http_status_code(),
     331            0 :             SqlOverHttpError::ConnectCompute(h) => match h.get_error_kind() {
     332            0 :                 ErrorKind::User => StatusCode::BAD_REQUEST,
     333            0 :                 _ => StatusCode::INTERNAL_SERVER_ERROR,
     334              :             },
     335            0 :             SqlOverHttpError::ConnInfo(_) => StatusCode::BAD_REQUEST,
     336            0 :             SqlOverHttpError::ResponseTooLarge(_) => StatusCode::INSUFFICIENT_STORAGE,
     337            0 :             SqlOverHttpError::InvalidIsolationLevel => StatusCode::BAD_REQUEST,
     338            0 :             SqlOverHttpError::Postgres(_) => StatusCode::BAD_REQUEST,
     339            0 :             SqlOverHttpError::InternalPostgres(_) => StatusCode::INTERNAL_SERVER_ERROR,
     340            0 :             SqlOverHttpError::JsonConversion(_) => StatusCode::INTERNAL_SERVER_ERROR,
     341            0 :             SqlOverHttpError::Cancelled(_) => StatusCode::INTERNAL_SERVER_ERROR,
     342              :         }
     343            0 :     }
     344              : }
     345              : 
     346              : #[derive(Debug, thiserror::Error)]
     347              : pub(crate) enum SqlOverHttpCancel {
     348              :     #[error("query was cancelled")]
     349              :     Postgres,
     350              :     #[error("query was cancelled while stuck trying to connect to the database")]
     351              :     Connect,
     352              : }
     353              : 
     354              : impl ReportableError for SqlOverHttpCancel {
     355            0 :     fn get_error_kind(&self) -> ErrorKind {
     356            0 :         match self {
     357            0 :             SqlOverHttpCancel::Postgres => ErrorKind::ClientDisconnect,
     358            0 :             SqlOverHttpCancel::Connect => ErrorKind::ClientDisconnect,
     359              :         }
     360            0 :     }
     361              : }
     362              : 
     363              : #[derive(Clone, Copy, Debug)]
     364              : struct HttpHeaders {
     365              :     raw_output: bool,
     366              :     default_array_mode: bool,
     367              :     txn_isolation_level: Option<IsolationLevel>,
     368              :     txn_read_only: bool,
     369              :     txn_deferrable: bool,
     370              : }
     371              : 
     372              : impl HttpHeaders {
     373            0 :     fn try_parse(headers: &hyper::http::HeaderMap) -> Result<Self, SqlOverHttpError> {
     374              :         // Determine the output options. Default behaviour is 'false'. Anything that is not
     375              :         // strictly 'true' assumed to be false.
     376            0 :         let raw_output = headers.get(&RAW_TEXT_OUTPUT) == Some(&HEADER_VALUE_TRUE);
     377            0 :         let default_array_mode = headers.get(&ARRAY_MODE) == Some(&HEADER_VALUE_TRUE);
     378              : 
     379              :         // isolation level, read only and deferrable
     380            0 :         let txn_isolation_level = match headers.get(&TXN_ISOLATION_LEVEL) {
     381            0 :             Some(x) => Some(
     382            0 :                 map_header_to_isolation_level(x).ok_or(SqlOverHttpError::InvalidIsolationLevel)?,
     383              :             ),
     384            0 :             None => None,
     385              :         };
     386              : 
     387            0 :         let txn_read_only = headers.get(&TXN_READ_ONLY) == Some(&HEADER_VALUE_TRUE);
     388            0 :         let txn_deferrable = headers.get(&TXN_DEFERRABLE) == Some(&HEADER_VALUE_TRUE);
     389              : 
     390            0 :         Ok(Self {
     391            0 :             raw_output,
     392            0 :             default_array_mode,
     393            0 :             txn_isolation_level,
     394            0 :             txn_read_only,
     395            0 :             txn_deferrable,
     396            0 :         })
     397            0 :     }
     398              : }
     399              : 
     400            0 : fn map_header_to_isolation_level(level: &HeaderValue) -> Option<IsolationLevel> {
     401            0 :     match level.as_bytes() {
     402            0 :         b"Serializable" => Some(IsolationLevel::Serializable),
     403            0 :         b"ReadUncommitted" => Some(IsolationLevel::ReadUncommitted),
     404            0 :         b"ReadCommitted" => Some(IsolationLevel::ReadCommitted),
     405            0 :         b"RepeatableRead" => Some(IsolationLevel::RepeatableRead),
     406            0 :         _ => None,
     407              :     }
     408            0 : }
     409              : 
     410            0 : fn map_isolation_level_to_headers(level: IsolationLevel) -> Option<HeaderValue> {
     411            0 :     match level {
     412            0 :         IsolationLevel::ReadUncommitted => Some(HeaderValue::from_static("ReadUncommitted")),
     413            0 :         IsolationLevel::ReadCommitted => Some(HeaderValue::from_static("ReadCommitted")),
     414            0 :         IsolationLevel::RepeatableRead => Some(HeaderValue::from_static("RepeatableRead")),
     415            0 :         IsolationLevel::Serializable => Some(HeaderValue::from_static("Serializable")),
     416            0 :         _ => None,
     417              :     }
     418            0 : }
     419              : 
     420            0 : async fn handle_inner(
     421            0 :     cancel: CancellationToken,
     422            0 :     config: &'static ProxyConfig,
     423            0 :     ctx: &RequestContext,
     424            0 :     request: Request<Incoming>,
     425            0 :     backend: Arc<PoolingBackend>,
     426            0 : ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, SqlOverHttpError> {
     427            0 :     let _requeset_gauge = Metrics::get()
     428            0 :         .proxy
     429            0 :         .connection_requests
     430            0 :         .guard(ctx.protocol());
     431            0 :     info!(
     432            0 :         protocol = %ctx.protocol(),
     433            0 :         "handling interactive connection from client"
     434              :     );
     435              : 
     436            0 :     let conn_info = get_conn_info(&config.authentication_config, ctx, None, request.headers())?;
     437            0 :     info!(
     438            0 :         user = conn_info.conn_info.user_info.user.as_str(),
     439            0 :         "credentials"
     440              :     );
     441              : 
     442            0 :     match conn_info.auth {
     443            0 :         AuthData::Jwt(jwt) if config.authentication_config.is_auth_broker => {
     444            0 :             handle_auth_broker_inner(ctx, request, conn_info.conn_info, jwt, backend).await
     445              :         }
     446            0 :         auth => {
     447            0 :             handle_db_inner(
     448            0 :                 cancel,
     449            0 :                 config,
     450            0 :                 ctx,
     451            0 :                 request,
     452            0 :                 conn_info.conn_info,
     453            0 :                 auth,
     454            0 :                 backend,
     455            0 :             )
     456            0 :             .await
     457              :         }
     458              :     }
     459            0 : }
     460              : 
     461            0 : async fn handle_db_inner(
     462            0 :     cancel: CancellationToken,
     463            0 :     config: &'static ProxyConfig,
     464            0 :     ctx: &RequestContext,
     465            0 :     request: Request<Incoming>,
     466            0 :     conn_info: ConnInfo,
     467            0 :     auth: AuthData,
     468            0 :     backend: Arc<PoolingBackend>,
     469            0 : ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, SqlOverHttpError> {
     470              :     //
     471              :     // Determine the destination and connection params
     472              :     //
     473            0 :     let headers = request.headers();
     474              : 
     475              :     // Allow connection pooling only if explicitly requested
     476              :     // or if we have decided that http pool is no longer opt-in
     477            0 :     let allow_pool = !config.http_config.pool_options.opt_in
     478            0 :         || headers.get(&ALLOW_POOL) == Some(&HEADER_VALUE_TRUE);
     479              : 
     480            0 :     let parsed_headers = HttpHeaders::try_parse(headers)?;
     481              : 
     482            0 :     let mut request_len = 0;
     483            0 :     let fetch_and_process_request = Box::pin(
     484            0 :         async {
     485            0 :             let body = read_body_with_limit(
     486            0 :                 request.into_body(),
     487            0 :                 config.http_config.max_request_size_bytes,
     488            0 :             )
     489            0 :             .await?;
     490              : 
     491            0 :             request_len = body.len();
     492              : 
     493            0 :             Metrics::get()
     494            0 :                 .proxy
     495            0 :                 .http_conn_content_length_bytes
     496            0 :                 .observe(HttpDirection::Request, body.len() as f64);
     497              : 
     498            0 :             debug!(length = body.len(), "request payload read");
     499            0 :             let payload: Payload = serde_json::from_slice(&body)?;
     500            0 :             Ok::<Payload, ReadPayloadError>(payload) // Adjust error type accordingly
     501            0 :         }
     502            0 :         .map_err(SqlOverHttpError::from),
     503              :     );
     504              : 
     505            0 :     let authenticate_and_connect = Box::pin(
     506            0 :         async {
     507            0 :             let keys = match auth {
     508            0 :                 AuthData::Password(pw) => backend
     509            0 :                     .authenticate_with_password(ctx, &conn_info.user_info, &pw)
     510            0 :                     .await
     511            0 :                     .map_err(HttpConnError::AuthError)?,
     512            0 :                 AuthData::Jwt(jwt) => backend
     513            0 :                     .authenticate_with_jwt(ctx, &conn_info.user_info, jwt)
     514            0 :                     .await
     515            0 :                     .map_err(HttpConnError::AuthError)?,
     516              :             };
     517              : 
     518            0 :             let client = match keys.keys {
     519            0 :                 ComputeCredentialKeys::JwtPayload(payload)
     520            0 :                     if backend.auth_backend.is_local_proxy() =>
     521              :                 {
     522              :                     #[cfg(feature = "testing")]
     523            0 :                     let disable_pg_session_jwt = config.disable_pg_session_jwt;
     524              :                     #[cfg(not(feature = "testing"))]
     525              :                     let disable_pg_session_jwt = false;
     526            0 :                     let mut client = backend
     527            0 :                         .connect_to_local_postgres(ctx, conn_info, disable_pg_session_jwt)
     528            0 :                         .await?;
     529            0 :                     if !disable_pg_session_jwt {
     530            0 :                         let (cli_inner, _dsc) = client.client_inner();
     531            0 :                         cli_inner.set_jwt_session(&payload).await?;
     532            0 :                     }
     533            0 :                     Client::Local(client)
     534              :                 }
     535              :                 _ => {
     536            0 :                     let client = backend
     537            0 :                         .connect_to_compute(ctx, conn_info, keys, !allow_pool)
     538            0 :                         .await?;
     539            0 :                     Client::Remote(client)
     540              :                 }
     541              :             };
     542              : 
     543              :             // not strictly necessary to mark success here,
     544              :             // but it's just insurance for if we forget it somewhere else
     545            0 :             ctx.success();
     546            0 :             Ok::<_, SqlOverHttpError>(client)
     547            0 :         }
     548            0 :         .map_err(SqlOverHttpError::from),
     549              :     );
     550              : 
     551            0 :     let (payload, mut client) = match run_until_cancelled(
     552              :         // Run both operations in parallel
     553            0 :         try_join(
     554            0 :             pin!(fetch_and_process_request),
     555            0 :             pin!(authenticate_and_connect),
     556              :         ),
     557            0 :         &cancel,
     558              :     )
     559            0 :     .await
     560              :     {
     561            0 :         Some(result) => result?,
     562            0 :         None => return Err(SqlOverHttpError::Cancelled(SqlOverHttpCancel::Connect)),
     563              :     };
     564              : 
     565            0 :     let mut response = Response::builder()
     566            0 :         .status(StatusCode::OK)
     567            0 :         .header(header::CONTENT_TYPE, "application/json");
     568              : 
     569              :     // Now execute the query and return the result.
     570            0 :     let json_output = match payload {
     571            0 :         Payload::Single(stmt) => {
     572            0 :             stmt.process(&config.http_config, cancel, &mut client, parsed_headers)
     573            0 :                 .await?
     574              :         }
     575            0 :         Payload::Batch(statements) => {
     576            0 :             if parsed_headers.txn_read_only {
     577            0 :                 response = response.header(TXN_READ_ONLY.clone(), &HEADER_VALUE_TRUE);
     578            0 :             }
     579            0 :             if parsed_headers.txn_deferrable {
     580            0 :                 response = response.header(TXN_DEFERRABLE.clone(), &HEADER_VALUE_TRUE);
     581            0 :             }
     582            0 :             if let Some(txn_isolation_level) = parsed_headers
     583            0 :                 .txn_isolation_level
     584            0 :                 .and_then(map_isolation_level_to_headers)
     585            0 :             {
     586            0 :                 response = response.header(TXN_ISOLATION_LEVEL.clone(), txn_isolation_level);
     587            0 :             }
     588              : 
     589            0 :             statements
     590            0 :                 .process(&config.http_config, cancel, &mut client, parsed_headers)
     591            0 :                 .await?
     592              :         }
     593              :     };
     594              : 
     595            0 :     let metrics = client.metrics(ctx);
     596              : 
     597            0 :     let len = json_output.len();
     598            0 :     let response = response
     599            0 :         .body(
     600            0 :             Full::new(Bytes::from(json_output))
     601            0 :                 .map_err(|x| match x {})
     602            0 :                 .boxed(),
     603              :         )
     604              :         // only fails if invalid status code or invalid header/values are given.
     605              :         // these are not user configurable so it cannot fail dynamically
     606            0 :         .expect("building response payload should not fail");
     607              : 
     608              :     // count the egress bytes - we miss the TLS and header overhead but oh well...
     609              :     // moving this later in the stack is going to be a lot of effort and ehhhh
     610            0 :     metrics.record_egress(len as u64);
     611            0 :     metrics.record_ingress(request_len as u64);
     612              : 
     613            0 :     Metrics::get()
     614            0 :         .proxy
     615            0 :         .http_conn_content_length_bytes
     616            0 :         .observe(HttpDirection::Response, len as f64);
     617              : 
     618            0 :     Ok(response)
     619            0 : }
     620              : 
     621              : static HEADERS_TO_FORWARD: &[&HeaderName] = &[
     622              :     &AUTHORIZATION,
     623              :     &CONN_STRING,
     624              :     &RAW_TEXT_OUTPUT,
     625              :     &ARRAY_MODE,
     626              :     &TXN_ISOLATION_LEVEL,
     627              :     &TXN_READ_ONLY,
     628              :     &TXN_DEFERRABLE,
     629              : ];
     630              : 
     631            0 : async fn handle_auth_broker_inner(
     632            0 :     ctx: &RequestContext,
     633            0 :     request: Request<Incoming>,
     634            0 :     conn_info: ConnInfo,
     635            0 :     jwt: String,
     636            0 :     backend: Arc<PoolingBackend>,
     637            0 : ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, SqlOverHttpError> {
     638            0 :     backend
     639            0 :         .authenticate_with_jwt(ctx, &conn_info.user_info, jwt)
     640            0 :         .await
     641            0 :         .map_err(HttpConnError::from)?;
     642              : 
     643            0 :     let mut client = backend.connect_to_local_proxy(ctx, conn_info).await?;
     644              : 
     645            0 :     let local_proxy_uri = ::http::Uri::from_static("http://proxy.local/sql");
     646              : 
     647            0 :     let (mut parts, body) = request.into_parts();
     648            0 :     let mut req = Request::builder().method(Method::POST).uri(local_proxy_uri);
     649              : 
     650              :     // todo(conradludgate): maybe auth-broker should parse these and re-serialize
     651              :     // these instead just to ensure they remain normalised.
     652            0 :     for &h in HEADERS_TO_FORWARD {
     653            0 :         if let Some(hv) = parts.headers.remove(h) {
     654            0 :             req = req.header(h, hv);
     655            0 :         }
     656              :     }
     657            0 :     req = req.header(&NEON_REQUEST_ID, uuid_to_header_value(ctx.session_id()));
     658              : 
     659            0 :     let req = req
     660            0 :         .body(body.map_err(|e| e).boxed()) //TODO: is there a potential for a regression here?
     661            0 :         .expect("all headers and params received via hyper should be valid for request");
     662              : 
     663              :     // todo: map body to count egress
     664            0 :     let _metrics = client.metrics(ctx);
     665              : 
     666            0 :     Ok(client
     667            0 :         .inner
     668            0 :         .inner
     669            0 :         .send_request(req)
     670            0 :         .await
     671            0 :         .map_err(LocalProxyConnError::from)
     672            0 :         .map_err(HttpConnError::from)?
     673            0 :         .map(|b| b.boxed()))
     674            0 : }
     675              : 
     676              : impl QueryData {
     677            0 :     async fn process(
     678            0 :         self,
     679            0 :         config: &'static HttpConfig,
     680            0 :         cancel: CancellationToken,
     681            0 :         client: &mut Client,
     682            0 :         parsed_headers: HttpHeaders,
     683            0 :     ) -> Result<String, SqlOverHttpError> {
     684            0 :         let (inner, mut discard) = client.inner();
     685            0 :         let cancel_token = inner.cancel_token();
     686              : 
     687            0 :         let mut json_buf = vec![];
     688              : 
     689            0 :         let batch_result = match select(
     690            0 :             pin!(query_to_json(
     691            0 :                 config,
     692            0 :                 &mut *inner,
     693            0 :                 self,
     694            0 :                 json::ValueSer::new(&mut json_buf),
     695            0 :                 parsed_headers
     696              :             )),
     697            0 :             pin!(cancel.cancelled()),
     698              :         )
     699            0 :         .await
     700              :         {
     701            0 :             Either::Left((res, __not_yet_cancelled)) => res,
     702            0 :             Either::Right((_cancelled, query)) => {
     703            0 :                 tracing::info!("cancelling query");
     704            0 :                 if let Err(err) = cancel_token.cancel_query(NoTls).await {
     705            0 :                     tracing::warn!(?err, "could not cancel query");
     706            0 :                 }
     707              :                 // wait for the query cancellation
     708            0 :                 match time::timeout(time::Duration::from_millis(100), query).await {
     709              :                     // query successed before it was cancelled.
     710            0 :                     Ok(Ok(status)) => Ok(status),
     711              :                     // query failed or was cancelled.
     712            0 :                     Ok(Err(error)) => {
     713            0 :                         let db_error = match &error {
     714              :                             SqlOverHttpError::ConnectCompute(
     715            0 :                                 HttpConnError::PostgresConnectionError(e),
     716              :                             )
     717            0 :                             | SqlOverHttpError::Postgres(e) => e.as_db_error(),
     718            0 :                             _ => None,
     719              :                         };
     720              : 
     721              :                         // if errored for some other reason, it might not be safe to return
     722            0 :                         if !db_error.is_some_and(|e| *e.code() == SqlState::QUERY_CANCELED) {
     723            0 :                             discard.discard();
     724            0 :                         }
     725              : 
     726            0 :                         return Err(SqlOverHttpError::Cancelled(SqlOverHttpCancel::Postgres));
     727              :                     }
     728            0 :                     Err(_timeout) => {
     729            0 :                         discard.discard();
     730            0 :                         return Err(SqlOverHttpError::Cancelled(SqlOverHttpCancel::Postgres));
     731              :                     }
     732              :                 }
     733              :             }
     734              :         };
     735              : 
     736            0 :         match batch_result {
     737              :             // The query successfully completed.
     738            0 :             Ok(status) => {
     739            0 :                 discard.check_idle(status);
     740              : 
     741            0 :                 let json_output = String::from_utf8(json_buf).expect("json should be valid utf8");
     742            0 :                 Ok(json_output)
     743              :             }
     744              :             // The query failed with an error
     745            0 :             Err(e) => {
     746            0 :                 discard.discard();
     747            0 :                 Err(e)
     748              :             }
     749              :         }
     750            0 :     }
     751              : }
     752              : 
     753              : impl BatchQueryData {
     754            0 :     async fn process(
     755            0 :         self,
     756            0 :         config: &'static HttpConfig,
     757            0 :         cancel: CancellationToken,
     758            0 :         client: &mut Client,
     759            0 :         parsed_headers: HttpHeaders,
     760            0 :     ) -> Result<String, SqlOverHttpError> {
     761            0 :         info!("starting transaction");
     762            0 :         let (inner, mut discard) = client.inner();
     763            0 :         let cancel_token = inner.cancel_token();
     764            0 :         let mut builder = inner.build_transaction();
     765            0 :         if let Some(isolation_level) = parsed_headers.txn_isolation_level {
     766            0 :             builder = builder.isolation_level(isolation_level);
     767            0 :         }
     768            0 :         if parsed_headers.txn_read_only {
     769            0 :             builder = builder.read_only(true);
     770            0 :         }
     771            0 :         if parsed_headers.txn_deferrable {
     772            0 :             builder = builder.deferrable(true);
     773            0 :         }
     774              : 
     775            0 :         let mut transaction = builder
     776            0 :             .start()
     777            0 :             .await
     778            0 :             .inspect_err(|_| {
     779              :                 // if we cannot start a transaction, we should return immediately
     780              :                 // and not return to the pool. connection is clearly broken
     781            0 :                 discard.discard();
     782            0 :             })
     783            0 :             .map_err(SqlOverHttpError::Postgres)?;
     784              : 
     785            0 :         let json_output = match query_batch_to_json(
     786            0 :             config,
     787            0 :             cancel.child_token(),
     788            0 :             &mut transaction,
     789            0 :             self,
     790            0 :             parsed_headers,
     791              :         )
     792            0 :         .await
     793              :         {
     794            0 :             Ok(json_output) => {
     795            0 :                 info!("commit");
     796            0 :                 let status = transaction
     797            0 :                     .commit()
     798            0 :                     .await
     799            0 :                     .inspect_err(|_| {
     800              :                         // if we cannot commit - for now don't return connection to pool
     801              :                         // TODO: get a query status from the error
     802            0 :                         discard.discard();
     803            0 :                     })
     804            0 :                     .map_err(SqlOverHttpError::Postgres)?;
     805            0 :                 discard.check_idle(status);
     806            0 :                 json_output
     807              :             }
     808              :             Err(SqlOverHttpError::Cancelled(_)) => {
     809            0 :                 if let Err(err) = cancel_token.cancel_query(NoTls).await {
     810            0 :                     tracing::warn!(?err, "could not cancel query");
     811            0 :                 }
     812              :                 // TODO: after cancelling, wait to see if we can get a status. maybe the connection is still safe.
     813            0 :                 discard.discard();
     814              : 
     815            0 :                 return Err(SqlOverHttpError::Cancelled(SqlOverHttpCancel::Postgres));
     816              :             }
     817            0 :             Err(err) => {
     818            0 :                 info!("rollback");
     819            0 :                 let status = transaction
     820            0 :                     .rollback()
     821            0 :                     .await
     822            0 :                     .inspect_err(|_| {
     823              :                         // if we cannot rollback - for now don't return connection to pool
     824              :                         // TODO: get a query status from the error
     825            0 :                         discard.discard();
     826            0 :                     })
     827            0 :                     .map_err(SqlOverHttpError::Postgres)?;
     828            0 :                 discard.check_idle(status);
     829            0 :                 return Err(err);
     830              :             }
     831              :         };
     832              : 
     833            0 :         Ok(json_output)
     834            0 :     }
     835              : }
     836              : 
     837            0 : async fn query_batch(
     838            0 :     config: &'static HttpConfig,
     839            0 :     cancel: CancellationToken,
     840            0 :     transaction: &mut Transaction<'_>,
     841            0 :     queries: BatchQueryData,
     842            0 :     parsed_headers: HttpHeaders,
     843            0 :     results: &mut json::ListSer<'_>,
     844            0 : ) -> Result<(), SqlOverHttpError> {
     845            0 :     for stmt in queries.queries {
     846            0 :         let query = pin!(query_to_json(
     847            0 :             config,
     848            0 :             transaction,
     849            0 :             stmt,
     850            0 :             results.entry(),
     851            0 :             parsed_headers,
     852              :         ));
     853            0 :         let cancelled = pin!(cancel.cancelled());
     854            0 :         let res = select(query, cancelled).await;
     855            0 :         match res {
     856              :             // TODO: maybe we should check that the transaction bit is set here
     857            0 :             Either::Left((Ok(_), _cancelled)) => {}
     858            0 :             Either::Left((Err(e), _cancelled)) => {
     859            0 :                 return Err(e);
     860              :             }
     861            0 :             Either::Right((_cancelled, _)) => {
     862            0 :                 return Err(SqlOverHttpError::Cancelled(SqlOverHttpCancel::Postgres));
     863              :             }
     864              :         }
     865              :     }
     866              : 
     867            0 :     Ok(())
     868            0 : }
     869              : 
     870            0 : async fn query_batch_to_json(
     871            0 :     config: &'static HttpConfig,
     872            0 :     cancel: CancellationToken,
     873            0 :     tx: &mut Transaction<'_>,
     874            0 :     queries: BatchQueryData,
     875            0 :     headers: HttpHeaders,
     876            0 : ) -> Result<String, SqlOverHttpError> {
     877            0 :     let json_output = json::value_to_string!(|obj| json::value_as_object!(|obj| {
     878            0 :         let results = obj.key("results");
     879            0 :         json::value_as_list!(|results| {
     880            0 :             query_batch(config, cancel, tx, queries, headers, results).await?;
     881              :         });
     882              :     }));
     883              : 
     884            0 :     Ok(json_output)
     885            0 : }
     886              : 
     887            0 : async fn query_to_json<T: GenericClient>(
     888            0 :     config: &'static HttpConfig,
     889            0 :     client: &mut T,
     890            0 :     data: QueryData,
     891            0 :     output: json::ValueSer<'_>,
     892            0 :     parsed_headers: HttpHeaders,
     893            0 : ) -> Result<ReadyForQueryStatus, SqlOverHttpError> {
     894            0 :     let query_start = Instant::now();
     895              : 
     896            0 :     let mut output = json::ObjectSer::new(output);
     897            0 :     let mut row_stream = client
     898            0 :         .query_raw_txt(&data.query, data.params)
     899            0 :         .await
     900            0 :         .map_err(SqlOverHttpError::Postgres)?;
     901            0 :     let query_acknowledged = Instant::now();
     902              : 
     903            0 :     let mut json_fields = output.key("fields").list();
     904            0 :     for c in row_stream.statement.columns() {
     905            0 :         let json_field = json_fields.entry();
     906            0 :         json::value_as_object!(|json_field| {
     907            0 :             json_field.entry("name", c.name());
     908            0 :             json_field.entry("dataTypeID", c.type_().oid());
     909            0 :             json_field.entry("tableID", c.table_oid());
     910            0 :             json_field.entry("columnID", c.column_id());
     911            0 :             json_field.entry("dataTypeSize", c.type_size());
     912            0 :             json_field.entry("dataTypeModifier", c.type_modifier());
     913            0 :             json_field.entry("format", "text");
     914            0 :         });
     915            0 :     }
     916            0 :     json_fields.finish();
     917              : 
     918            0 :     let array_mode = data.array_mode.unwrap_or(parsed_headers.default_array_mode);
     919            0 :     let raw_output = parsed_headers.raw_output;
     920              : 
     921              :     // Manually drain the stream into a vector to leave row_stream hanging
     922              :     // around to get a command tag. Also check that the response is not too
     923              :     // big.
     924            0 :     let mut rows = 0;
     925            0 :     let mut json_rows = output.key("rows").list();
     926            0 :     while let Some(row) = row_stream.next().await {
     927            0 :         let row = row.map_err(SqlOverHttpError::Postgres)?;
     928              : 
     929              :         // we don't have a streaming response support yet so this is to prevent OOM
     930              :         // from a malicious query (eg a cross join)
     931            0 :         if json_rows.as_buffer().len() > config.max_response_size_bytes {
     932            0 :             return Err(SqlOverHttpError::ResponseTooLarge(
     933            0 :                 config.max_response_size_bytes,
     934            0 :             ));
     935            0 :         }
     936              : 
     937            0 :         pg_text_row_to_json(json_rows.entry(), &row, raw_output, array_mode)?;
     938            0 :         rows += 1;
     939              : 
     940              :         // assumption: parsing pg text and converting to json takes CPU time.
     941              :         // let's assume it is slightly expensive, so we should consume some cooperative budget.
     942              :         // Especially considering that `RowStream::next` might be pulling from a batch
     943              :         // of rows and never hit the tokio mpsc for a long time (although unlikely).
     944            0 :         tokio::task::consume_budget().await;
     945              :     }
     946            0 :     json_rows.finish();
     947              : 
     948            0 :     let query_resp_end = Instant::now();
     949              : 
     950            0 :     let ready = row_stream.status;
     951              : 
     952              :     // grab the command tag and number of rows affected
     953            0 :     let command_tag = row_stream.command_tag.unwrap_or_default();
     954            0 :     let mut command_tag_split = command_tag.split(' ');
     955            0 :     let command_tag_name = command_tag_split.next().unwrap_or_default();
     956            0 :     let command_tag_count = if command_tag_name == "INSERT" {
     957              :         // INSERT returns OID first and then number of rows
     958            0 :         command_tag_split.nth(1)
     959              :     } else {
     960              :         // other commands return number of rows (if any)
     961            0 :         command_tag_split.next()
     962              :     }
     963            0 :     .and_then(|s| s.parse::<i64>().ok());
     964              : 
     965            0 :     info!(
     966              :         rows,
     967              :         ?ready,
     968              :         command_tag,
     969            0 :         acknowledgement = ?(query_acknowledged - query_start),
     970            0 :         response = ?(query_resp_end - query_start),
     971            0 :         "finished executing query"
     972              :     );
     973              : 
     974            0 :     output.entry("command", command_tag_name);
     975            0 :     output.entry("rowCount", command_tag_count);
     976            0 :     output.entry("rowAsArray", array_mode);
     977              : 
     978            0 :     output.finish();
     979            0 :     Ok(ready)
     980            0 : }
     981              : 
     982              : enum Client {
     983              :     Remote(conn_pool_lib::Client<postgres_client::Client>),
     984              :     Local(conn_pool_lib::Client<postgres_client::Client>),
     985              : }
     986              : 
     987              : enum Discard<'a> {
     988              :     Remote(conn_pool_lib::Discard<'a, postgres_client::Client>),
     989              :     Local(conn_pool_lib::Discard<'a, postgres_client::Client>),
     990              : }
     991              : 
     992              : impl Client {
     993            0 :     fn metrics(&self, ctx: &RequestContext) -> Arc<MetricCounter> {
     994            0 :         match self {
     995            0 :             Client::Remote(client) => client.metrics(ctx),
     996            0 :             Client::Local(local_client) => local_client.metrics(ctx),
     997              :         }
     998            0 :     }
     999              : 
    1000            0 :     fn inner(&mut self) -> (&mut postgres_client::Client, Discard<'_>) {
    1001            0 :         match self {
    1002            0 :             Client::Remote(client) => {
    1003            0 :                 let (c, d) = client.inner();
    1004            0 :                 (c, Discard::Remote(d))
    1005              :             }
    1006            0 :             Client::Local(local_client) => {
    1007            0 :                 let (c, d) = local_client.inner();
    1008            0 :                 (c, Discard::Local(d))
    1009              :             }
    1010              :         }
    1011            0 :     }
    1012              : }
    1013              : 
    1014              : impl Discard<'_> {
    1015            0 :     fn check_idle(&mut self, status: ReadyForQueryStatus) {
    1016            0 :         match self {
    1017            0 :             Discard::Remote(discard) => discard.check_idle(status),
    1018            0 :             Discard::Local(discard) => discard.check_idle(status),
    1019              :         }
    1020            0 :     }
    1021            0 :     fn discard(&mut self) {
    1022            0 :         match self {
    1023            0 :             Discard::Remote(discard) => discard.discard(),
    1024            0 :             Discard::Local(discard) => discard.discard(),
    1025              :         }
    1026            0 :     }
    1027              : }
    1028              : 
    1029              : #[cfg(test)]
    1030              : mod tests {
    1031              :     use super::*;
    1032              : 
    1033              :     #[test]
    1034            1 :     fn test_payload() {
    1035            1 :         let payload = "{\"query\":\"SELECT * FROM users WHERE name = ?\",\"params\":[\"test\"],\"arrayMode\":true}";
    1036            1 :         let deserialized_payload: Payload = serde_json::from_str(payload).unwrap();
    1037              : 
    1038            1 :         match deserialized_payload {
    1039              :             Payload::Single(QueryData {
    1040            1 :                 query,
    1041            1 :                 params,
    1042            1 :                 array_mode,
    1043              :             }) => {
    1044            1 :                 assert_eq!(query, "SELECT * FROM users WHERE name = ?");
    1045            1 :                 assert_eq!(params, vec![Some(String::from("test"))]);
    1046            1 :                 assert!(array_mode.unwrap());
    1047              :             }
    1048              :             Payload::Batch(_) => {
    1049            0 :                 panic!("deserialization failed: case with single query, one param, and array mode")
    1050              :             }
    1051              :         }
    1052              : 
    1053            1 :         let payload = "{\"queries\":[{\"query\":\"SELECT * FROM users0 WHERE name = ?\",\"params\":[\"test0\"], \"arrayMode\":false},{\"query\":\"SELECT * FROM users1 WHERE name = ?\",\"params\":[\"test1\"],\"arrayMode\":true}]}";
    1054            1 :         let deserialized_payload: Payload = serde_json::from_str(payload).unwrap();
    1055              : 
    1056            1 :         match deserialized_payload {
    1057            1 :             Payload::Batch(BatchQueryData { queries }) => {
    1058            1 :                 assert_eq!(queries.len(), 2);
    1059            2 :                 for (i, query) in queries.into_iter().enumerate() {
    1060            2 :                     assert_eq!(
    1061              :                         query.query,
    1062            2 :                         format!("SELECT * FROM users{i} WHERE name = ?")
    1063              :                     );
    1064            2 :                     assert_eq!(query.params, vec![Some(format!("test{i}"))]);
    1065            2 :                     assert_eq!(query.array_mode.unwrap(), i > 0);
    1066              :                 }
    1067              :             }
    1068            0 :             Payload::Single(_) => panic!("deserialization failed: case with multiple queries"),
    1069              :         }
    1070              : 
    1071            1 :         let payload = "{\"query\":\"SELECT 1\"}";
    1072            1 :         let deserialized_payload: Payload = serde_json::from_str(payload).unwrap();
    1073              : 
    1074            1 :         match deserialized_payload {
    1075              :             Payload::Single(QueryData {
    1076            1 :                 query,
    1077            1 :                 params,
    1078            1 :                 array_mode,
    1079              :             }) => {
    1080            1 :                 assert_eq!(query, "SELECT 1");
    1081            1 :                 assert_eq!(params, vec![]);
    1082            1 :                 assert!(array_mode.is_none());
    1083              :             }
    1084            0 :             Payload::Batch(_) => panic!("deserialization failed: case with only one query"),
    1085              :         }
    1086            1 :     }
    1087              : }
        

Generated by: LCOV version 2.1-beta