Skip to main content

scopedb_client/
protocol.rs

1// Copyright 2024 ScopeDB, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt;
16use std::str::FromStr;
17use std::time::Duration;
18use std::time::SystemTime;
19
20use reqwest::StatusCode;
21use reqwest::header::HeaderMap;
22use serde::Deserialize;
23use serde::Serialize;
24use uuid::Uuid;
25
26use crate::Error;
27use crate::ErrorKind;
28use crate::ResultSet;
29
30/// The commit outcome used by table append responses and client-side write interruptions.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum AppendState {
34    /// Every row in the request was committed.
35    Committed,
36    /// The request was rejected before any row was committed.
37    Rejected,
38    /// The server cannot determine whether the request committed.
39    Unknown,
40}
41
42impl fmt::Display for AppendState {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Committed => f.write_str("committed"),
46            Self::Rejected => f.write_str("rejected"),
47            Self::Unknown => f.write_str("unknown"),
48        }
49    }
50}
51
52/// The result of a committed table append.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct AppendRowsResult {
55    pub status: AppendState,
56    pub num_rows_inserted: i64,
57}
58
59/// A validation error for one row in an append request.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct AppendRowError {
62    pub row_index: u64,
63    pub column: String,
64    pub message: String,
65}
66
67/// Structured failure details returned by the table append API.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct AppendErrorDetails {
70    pub status: AppendState,
71    #[serde(default)]
72    pub row_errors: Vec<AppendRowError>,
73    #[serde(default)]
74    pub row_errors_truncated: bool,
75}
76
77pub(crate) async fn decode_statement_response(
78    r: reqwest::Response,
79    retryable_transport: bool,
80) -> Result<StatementStatus, Error> {
81    let code = r.status();
82    let headers = r.headers().clone();
83    let payload = r.bytes().await.map_err(|err| {
84        let error =
85            Error::new(ErrorKind::Unexpected, "failed to read response body").set_source(err);
86        let error = if retryable_transport {
87            error.set_temporary()
88        } else {
89            error
90        };
91        apply_response_metadata(error, code, &headers)
92    })?;
93    if code.is_success() {
94        let result = serde_json::from_slice(&payload).map_err(|err| {
95            apply_response_metadata(
96                Error::new(ErrorKind::Unexpected, "failed to decode response body").set_source(err),
97                code,
98                &headers,
99            )
100        })?;
101        return Ok(result);
102    }
103
104    Err(decode_http_error(
105        code,
106        &headers,
107        &payload,
108        ErrorKind::Unexpected,
109    ))
110}
111
112pub(crate) fn decode_http_error(
113    status: StatusCode,
114    headers: &HeaderMap,
115    payload: &[u8],
116    kind: ErrorKind,
117) -> Error {
118    let parsed = parse_error_payload(payload).unwrap_or_else(|| ParsedErrorPayload {
119        code: None,
120        details: None,
121        message: String::from_utf8_lossy(payload).into_owned(),
122        request_id: None,
123        retryable: None,
124    });
125    let mut error = apply_response_metadata(
126        Error::new(kind, parsed.message)
127            .set_server_retryable(parsed.retryable)
128            .set_api_error(parsed.code, parsed.details),
129        status,
130        headers,
131    );
132    if let Some(request_id) = parsed.request_id {
133        error = error.set_request_id(request_id);
134    }
135    let retryable = parsed.retryable.unwrap_or_else(|| {
136        matches!(
137            status,
138            StatusCode::REQUEST_TIMEOUT | StatusCode::TOO_MANY_REQUESTS
139        ) || status.is_server_error()
140    });
141    if retryable {
142        error.set_temporary()
143    } else {
144        error.set_permanent()
145    }
146}
147
148#[derive(Debug)]
149struct ParsedErrorPayload {
150    code: Option<String>,
151    details: Option<serde_json::Value>,
152    message: String,
153    request_id: Option<String>,
154    retryable: Option<bool>,
155}
156
157fn parse_error_payload(payload: &[u8]) -> Option<ParsedErrorPayload> {
158    let payload = serde_json::from_slice::<serde_json::Value>(payload).ok()?;
159    let object = payload.as_object()?;
160    if let Some(message) = object.get("message").and_then(serde_json::Value::as_str) {
161        return Some(ParsedErrorPayload {
162            code: object
163                .get("code")
164                .and_then(serde_json::Value::as_str)
165                .map(str::to_string),
166            details: object.get("details").cloned(),
167            message: message.to_string(),
168            request_id: object
169                .get("request_id")
170                .and_then(serde_json::Value::as_str)
171                .map(str::to_string),
172            retryable: object.get("retryable").and_then(serde_json::Value::as_bool),
173        });
174    }
175
176    let nested = object.get("error")?.as_object()?;
177    Some(ParsedErrorPayload {
178        code: nested
179            .get("code")
180            .and_then(serde_json::Value::as_str)
181            .map(str::to_string),
182        details: nested.get("details").cloned(),
183        message: nested.get("message")?.as_str()?.to_string(),
184        request_id: object
185            .get("request_id")
186            .and_then(serde_json::Value::as_str)
187            .map(str::to_string),
188        retryable: nested.get("retryable").and_then(serde_json::Value::as_bool),
189    })
190}
191
192pub(crate) fn apply_response_metadata(
193    mut error: Error,
194    status: StatusCode,
195    headers: &HeaderMap,
196) -> Error {
197    error = error.set_http_status(status);
198    if let Some(request_id) = header_string(headers, "x-request-id") {
199        error = error.set_request_id(request_id);
200    }
201    if let Some(retry_after) = parse_retry_after(headers, SystemTime::now()) {
202        error = error.set_retry_after(retry_after);
203    }
204    error
205}
206
207fn header_string(headers: &HeaderMap, name: &'static str) -> Option<String> {
208    headers
209        .get(name)
210        .and_then(|value| value.to_str().ok())
211        .map(str::to_string)
212}
213
214fn parse_retry_after(headers: &HeaderMap, now: SystemTime) -> Option<Duration> {
215    let value = headers.get(reqwest::header::RETRY_AFTER)?.to_str().ok()?;
216    if let Ok(seconds) = value.parse::<u64>() {
217        return Some(Duration::from_secs(seconds));
218    }
219    let at = httpdate::parse_http_date(value).ok()?;
220    Some(at.duration_since(now).unwrap_or(Duration::ZERO))
221}
222
223#[derive(Debug, Serialize)]
224pub(crate) struct StatementRequest {
225    pub statement: String,
226    #[serde(default)]
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub statement_id: Option<Uuid>,
229    #[serde(default)]
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub max_elapsed_seconds: Option<f64>,
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub max_estimated_seconds: Option<f64>,
234    pub format: &'static str,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(tag = "status")]
239pub enum StatementStatus {
240    #[serde(rename = "pending")]
241    Pending(StatementStatusPending),
242    #[serde(rename = "running")]
243    Running(StatementStatusRunning),
244    #[serde(rename = "finished")]
245    Finished(StatementStatusFinished),
246    #[serde(rename = "failed")]
247    Failed(StatementStatusFailed),
248    #[serde(rename = "cancelled")]
249    Cancelled(StatementStatusCancelled),
250}
251
252#[non_exhaustive]
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct StatementStatusPending {
255    pub statement_id: Uuid,
256    pub created_at: jiff::Timestamp,
257    pub progress: StatementEstimatedProgress,
258}
259
260#[non_exhaustive]
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct StatementStatusRunning {
263    pub statement_id: Uuid,
264    pub created_at: jiff::Timestamp,
265    pub progress: StatementEstimatedProgress,
266}
267
268#[non_exhaustive]
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct StatementStatusFinished {
271    pub statement_id: Uuid,
272    pub created_at: jiff::Timestamp,
273    pub progress: StatementEstimatedProgress,
274
275    result_set: StatementResultSet,
276}
277
278impl StatementStatusFinished {
279    pub fn result_set(&self) -> ResultSet {
280        ResultSet::from_statement_result_set(self.result_set.clone())
281    }
282}
283
284#[non_exhaustive]
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct StatementStatusFailed {
287    pub statement_id: Uuid,
288    pub created_at: jiff::Timestamp,
289    pub progress: StatementEstimatedProgress,
290    pub error: StatementErrorDetails,
291}
292
293/// Structured failure returned for a failed statement.
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295pub struct StatementErrorDetails {
296    pub code: String,
297    pub message: String,
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    pub details: Option<serde_json::Value>,
300}
301
302#[non_exhaustive]
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct StatementStatusCancelled {
305    pub statement_id: Uuid,
306    pub created_at: jiff::Timestamp,
307    pub progress: StatementEstimatedProgress,
308}
309
310impl StatementStatus {
311    pub fn statement_id(&self) -> Uuid {
312        match self {
313            StatementStatus::Pending(s) => s.statement_id,
314            StatementStatus::Running(s) => s.statement_id,
315            StatementStatus::Finished(s) => s.statement_id,
316            StatementStatus::Failed(s) => s.statement_id,
317            StatementStatus::Cancelled(s) => s.statement_id,
318        }
319    }
320
321    pub fn created_at(&self) -> jiff::Timestamp {
322        match self {
323            StatementStatus::Pending(s) => s.created_at,
324            StatementStatus::Running(s) => s.created_at,
325            StatementStatus::Finished(s) => s.created_at,
326            StatementStatus::Failed(s) => s.created_at,
327            StatementStatus::Cancelled(s) => s.created_at,
328        }
329    }
330
331    pub fn progress(&self) -> &StatementEstimatedProgress {
332        match self {
333            StatementStatus::Pending(s) => &s.progress,
334            StatementStatus::Running(s) => &s.progress,
335            StatementStatus::Finished(s) => &s.progress,
336            StatementStatus::Failed(s) => &s.progress,
337            StatementStatus::Cancelled(s) => &s.progress,
338        }
339    }
340
341    pub fn is_finished(&self) -> bool {
342        matches!(self, StatementStatus::Finished(..))
343    }
344
345    pub fn is_terminated(&self) -> bool {
346        matches!(
347            self,
348            StatementStatus::Finished(..)
349                | StatementStatus::Failed(..)
350                | StatementStatus::Cancelled(..)
351        )
352    }
353}
354
355#[derive(Default, Debug, Clone, Serialize, Deserialize)]
356pub struct StatementEstimatedProgress {
357    /// Total progress in percentage: `[0.0, 100.0]`.
358    #[serde(deserialize_with = "deserialize_progress_float")]
359    pub total_percentage: f64,
360    /// Duration in nanoseconds since the statement is submitted.
361    pub nanos_from_submitted: i64,
362    /// Duration in nanoseconds since the statement is started.
363    pub nanos_from_started: i64,
364    #[serde(flatten)]
365    pub details: StatementProgress,
366}
367
368#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
369pub struct StatementProgress {
370    /// Compute consumed by the statement, in seconds.
371    #[serde(deserialize_with = "deserialize_progress_float")]
372    pub compute_seconds: f64,
373    pub total_stages: i64,
374    pub total_partitions: i64,
375    pub total_rows: i64,
376    pub total_compressed_bytes: i64,
377    pub total_uncompressed_bytes: i64,
378    pub scanned_stages: i64,
379    pub scanned_partitions: i64,
380    pub scanned_rows: i64,
381    pub scanned_compressed_bytes: i64,
382    pub scanned_uncompressed_bytes: i64,
383    pub skipped_partitions: i64,
384    pub skipped_rows: i64,
385    pub skipped_compressed_bytes: i64,
386    pub skipped_uncompressed_bytes: i64,
387}
388
389// Internally tagged enums and flattened fields buffer numbers through Serde.
390// Number understands serde_json's arbitrary-precision representation in that buffer.
391fn deserialize_progress_float<'de, D: serde::Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
392    serde_json::Number::deserialize(d)?
393        .as_f64()
394        .ok_or_else(|| serde::de::Error::custom("progress number is outside the f64 range"))
395}
396
397impl StatementProgress {
398    pub fn total_percentage(&self) -> f64 {
399        let scan_progress = if self.total_rows == 0 {
400            0.0
401        } else {
402            (self.scanned_rows + self.skipped_rows) as f64 / self.total_rows as f64 * 100.0
403        };
404
405        let stage_progress = if self.total_stages == 0 {
406            0.0
407        } else {
408            self.scanned_stages as f64 / self.total_stages as f64 * 100.0
409        };
410
411        scan_progress.max(stage_progress)
412    }
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct StatementResultSet {
417    pub metadata: ResultSetMetadata,
418    #[serde(flatten)]
419    pub data: ResultSetData,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
423#[serde(tag = "format")]
424pub enum ResultSetData {
425    #[serde(rename = "json")]
426    Json { rows: Vec<Vec<Option<String>>> },
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct ResultSetMetadata {
431    pub fields: Vec<FieldMetadata>,
432    pub num_rows: usize,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct FieldMetadata {
437    pub name: String,
438    pub data_type: DataType,
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
442pub enum DataType {
443    #[serde(rename = "int")]
444    Int,
445    #[serde(rename = "uint")]
446    UInt,
447    #[serde(rename = "float")]
448    Float,
449    #[serde(rename = "binary")]
450    Binary,
451    #[serde(rename = "string")]
452    String,
453    #[serde(rename = "boolean")]
454    Boolean,
455    #[serde(rename = "timestamp")]
456    Timestamp,
457    #[serde(rename = "interval")]
458    Interval,
459    #[serde(rename = "array")]
460    Array,
461    #[serde(rename = "object")]
462    Object,
463    #[serde(rename = "any")]
464    Any,
465    #[serde(rename = "null")]
466    Null,
467}
468
469impl FromStr for DataType {
470    type Err = Error;
471
472    fn from_str(s: &str) -> Result<Self, Self::Err> {
473        match s {
474            "int" => Ok(Self::Int),
475            "uint" => Ok(Self::UInt),
476            "float" => Ok(Self::Float),
477            "binary" => Ok(Self::Binary),
478            "string" => Ok(Self::String),
479            "boolean" => Ok(Self::Boolean),
480            "timestamp" => Ok(Self::Timestamp),
481            "interval" => Ok(Self::Interval),
482            "array" => Ok(Self::Array),
483            "object" => Ok(Self::Object),
484            "any" => Ok(Self::Any),
485            "null" => Ok(Self::Null),
486            _ => Err(Error::new(
487                ErrorKind::Unexpected,
488                format!("unrecognized data type: {s}"),
489            )),
490        }
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use std::time::Duration;
497    use std::time::SystemTime;
498
499    use reqwest::header::HeaderMap;
500    use reqwest::header::HeaderValue;
501
502    use super::StatementStatus;
503    use super::decode_http_error;
504    use super::parse_error_payload;
505    use super::parse_retry_after;
506    use crate::ErrorKind;
507
508    #[test]
509    fn failed_statement_preserves_structured_error_details() {
510        let status: StatementStatus = serde_json::from_str(
511            r#"{
512                "status":"failed",
513                "statement_id":"01992f9b-8f54-7cd0-96b5-93ff4a218468",
514                "created_at":"2026-08-26T00:00:00Z",
515                "progress":{
516                    "total_percentage":100.0,
517                    "compute_seconds":0.25,
518                    "nanos_from_submitted":1,
519                    "nanos_from_started":1,
520                    "total_stages":1,
521                    "total_partitions":1,
522                    "total_rows":101,
523                    "total_compressed_bytes":0,
524                    "total_uncompressed_bytes":0,
525                    "scanned_stages":1,
526                    "scanned_partitions":1,
527                    "scanned_rows":101,
528                    "scanned_compressed_bytes":0,
529                    "scanned_uncompressed_bytes":0,
530                    "skipped_partitions":0,
531                    "skipped_rows":0,
532                    "skipped_compressed_bytes":0,
533                    "skipped_uncompressed_bytes":0
534                },
535                "error":{
536                    "code":"row_limit_exceeded",
537                    "message":"row limit exceeded",
538                    "details":{"total_rows":101,"max_total_rows":100}
539                }
540            }"#,
541        )
542        .unwrap();
543
544        let StatementStatus::Failed(failed) = status else {
545            panic!("expected failed statement status");
546        };
547        let error = failed.error;
548        assert_eq!(error.code, "row_limit_exceeded");
549        assert_eq!(error.details.unwrap()["max_total_rows"], 100);
550    }
551
552    #[test]
553    fn http_error_message_supports_direct_and_nested_payloads() {
554        assert_eq!(
555            parse_error_payload(br#"{"message":"direct failure","error":"details"}"#)
556                .unwrap()
557                .message,
558            "direct failure"
559        );
560        assert_eq!(
561            parse_error_payload(br#"{"error":{"message":"nested failure"}}"#)
562                .unwrap()
563                .message,
564            "nested failure"
565        );
566    }
567
568    #[test]
569    fn http_error_metadata_prefers_payload_and_honors_retryable() {
570        let mut headers = HeaderMap::new();
571        headers.insert("x-request-id", HeaderValue::from_static("header-request"));
572        headers.insert(reqwest::header::RETRY_AFTER, HeaderValue::from_static("7"));
573
574        let error = decode_http_error(
575            reqwest::StatusCode::SERVICE_UNAVAILABLE,
576            &headers,
577            br#"{"message":"busy","request_id":"body-request","retryable":false}"#,
578            ErrorKind::Unexpected,
579        );
580
581        assert_eq!(error.message(), "busy");
582        assert_eq!(
583            error.http_status(),
584            Some(reqwest::StatusCode::SERVICE_UNAVAILABLE)
585        );
586        assert_eq!(error.request_id(), Some("body-request"));
587        assert_eq!(error.retry_after(), Some(Duration::from_secs(7)));
588        assert!(!error.is_retryable());
589
590        let nested = decode_http_error(
591            reqwest::StatusCode::BAD_REQUEST,
592            &HeaderMap::new(),
593            br#"{"error":{"message":"try later","retryable":true}}"#,
594            ErrorKind::Unexpected,
595        );
596        assert_eq!(nested.message(), "try later");
597        assert!(nested.is_retryable());
598    }
599
600    #[test]
601    fn retry_after_accepts_http_dates() {
602        let mut headers = HeaderMap::new();
603        headers.insert(
604            reqwest::header::RETRY_AFTER,
605            HeaderValue::from_static("Thu, 01 Jan 1970 00:00:10 GMT"),
606        );
607        assert_eq!(
608            parse_retry_after(&headers, SystemTime::UNIX_EPOCH),
609            Some(Duration::from_secs(10))
610        );
611    }
612}