spiral-sdk 0.1.11

Rust SDK for Spiral resource and table APIs
Documentation
// Generated by tools/service-contracts/rust.py. Do not edit.
#![doc = "Generated reqwest client with typed errors and streaming bodies."]

use async_trait::async_trait;
use reqwest::Url;
use reqwest_middleware::ClientWithMiddleware;
use spiral_contract::auth::AuthHeader;
use spiral_contract::call::{CallError, ServiceFailure};
use spiral_contract::error::ErrorEnvelope;
use spiral_contract::stream::ByteStream;

use crate::api::sql::SqlApi;
use crate::api::sql::objects::*;
use crate::http::default_http_client;
/// HTTP client for the sql service.
#[derive(Clone)]
pub struct SqlClient {
    http: ClientWithMiddleware,
    base: Url,
}

impl std::fmt::Debug for SqlClient {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SqlClient")
            .field("base", &self.base)
            .finish_non_exhaustive()
    }
}

impl SqlClient {
    /// Create a redirect-disabled client; base may contain a deployment prefix.
    pub fn new(base: &str) -> Result<Self, CallError<SqlError>> {
        let http = default_http_client().map_err(CallError::transport)?;
        Self::from_http(base, http)
    }
    /// Use an existing connection pool with redirects disabled.
    pub fn from_http(
        base: &str,
        http: impl Into<ClientWithMiddleware>,
    ) -> Result<Self, CallError<SqlError>> {
        Self::from_transport(base, http.into())
    }
    /// Use a host-owned connection pool and instrumentation. Redirects must be disabled.
    pub fn from_transport(
        base: &str,
        http: ClientWithMiddleware,
    ) -> Result<Self, CallError<SqlError>> {
        let base =
            Url::parse(base).map_err(|_| CallError::<SqlError>::protocol("invalid base URL"))?;
        if !matches!(base.scheme(), "http" | "https")
            || base.host_str().is_none()
            || !base.username().is_empty()
            || base.password().is_some()
            || base.query().is_some()
            || base.fragment().is_some()
        {
            return Err(CallError::<SqlError>::protocol("invalid base URL"));
        }
        Ok(Self { http, base })
    }
    /// Replace the host-owned connection pool and instrumentation. Redirects must be disabled.
    pub fn with_transport(mut self, http: ClientWithMiddleware) -> Self {
        self.http = http;
        self
    }
    /// Supply transport instrumentation or custom connection policy.
    ///
    /// The caller must disable redirects to protect bearer credentials.
    pub fn with_http(mut self, http: impl Into<ClientWithMiddleware>) -> Self {
        self.http = http.into();
        self
    }
    /// Execute one query and stream its complete result. There is no implicit preview row limit;
    /// use SQL LIMIT when a bounded result is intended. The response starts with the Arrow schema,
    /// including for zero rows, followed by record batches as they become available. Consumption
    /// applies backpressure; clients can process batches without collecting the whole response.
    /// Only an explicit Arrow end-of-stream marker establishes success. Named errors use the
    /// standard contract envelope before output starts. After output starts, any failure terminates
    /// the response without that marker and the partial result must not be treated as complete.
    /// Closing the response cancels execution and releases admission and memory. Queries cannot be
    /// resumed; rerunning requires an explicit new request.
    pub async fn query_http(
        &self,
        auth_header: AuthHeader,
        request: Query,
    ) -> Result<reqwest::Response, CallError<SqlError>> {
        let mut url = self.base.clone();
        {
            let mut path = url.path_segments_mut().map_err(|_| {
                CallError::<SqlError>::protocol("base URL cannot contain path segments")
            })?;
            path.pop_if_empty();
            path.push("sql");
            path.push("query");
        }
        let builder = self
            .http
            .post(url)
            .header("accept", "application/vnd.apache.arrow.stream");
        let builder = builder.header("authorization", auth_header.as_str());
        let builder = builder
            .header("content-type", "application/json")
            .json(&request);
        let response = self
            .send(builder, "application/vnd.apache.arrow.stream")
            .await?;
        Ok(response)
    }
    async fn send(
        &self,
        builder: reqwest_middleware::RequestBuilder,
        expected_media: &str,
    ) -> Result<reqwest::Response, CallError<SqlError>> {
        let response = builder
            .timeout(std::time::Duration::from_secs(60))
            .send()
            .await
            .map_err(CallError::transport)?;
        let status = response.status();
        if !status.is_success() {
            let status = status.as_u16();
            let content_type = response
                .headers()
                .get("content-type")
                .and_then(|value| value.to_str().ok())
                .map(str::to_owned);
            let (bytes, truncated) =
                bounded_error_body::<SqlError>(response, 64 * 1024, status, content_type.clone())
                    .await?;
            if truncated {
                return Err(CallError::unknown_remote(status, content_type, bytes, true));
            }
            return match serde_json::from_slice::<ErrorEnvelope>(&bytes) {
                Ok(envelope) => Err(CallError::Service(ServiceFailure::remote(status, envelope))),
                Err(_) => Err(CallError::unknown_remote(
                    status,
                    content_type,
                    bytes,
                    false,
                )),
            };
        }
        if !media_matches(&response, expected_media) {
            return Err(CallError::protocol_response(
                status.as_u16(),
                "unexpected response media type",
            ));
        }
        Ok(response)
    }
}

fn media_matches(response: &reqwest::Response, expected: &str) -> bool {
    response
        .headers()
        .get("content-type")
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.split(';').next())
        .is_some_and(|value| value.trim().eq_ignore_ascii_case(expected))
}

async fn bounded_error_body<E>(
    mut response: reqwest::Response,
    limit: usize,
    status: u16,
    content_type: Option<String>,
) -> Result<(Vec<u8>, bool), CallError<E>> {
    let mut result = Vec::new();
    loop {
        let chunk = response.chunk().await.map_err(|error| {
            CallError::unreadable_remote(status, content_type.clone(), result.clone(), error)
        })?;
        let Some(chunk) = chunk else {
            return Ok((result, false));
        };
        let remaining = limit.saturating_sub(result.len());
        if chunk.len() > remaining {
            result.extend_from_slice(&chunk[..remaining]);
            return Ok((result, true));
        }
        result.extend_from_slice(&chunk);
    }
}
#[async_trait]
impl SqlApi for SqlClient {
    async fn query(
        &self,
        auth_header: AuthHeader,
        request: Query,
    ) -> Result<ByteStream, CallError<SqlError>> {
        let response = Self::query_http(self, auth_header, request).await?;
        Ok(ByteStream::new(response.bytes_stream()))
    }
}