Skip to main content

franken_snowflake_sqlapi/
response.rs

1//! The SQL API response bodies, one type per HTTP status class.
2//!
3//! Snowflake returns a *different JSON shape per status code*: a `200` carries a
4//! [`ResultSet`], a `202` a [`QueryStatus`] (poll again), and a `408`/`422` a
5//! [`QueryFailureStatus`]. See [`crate::status::ResponseClass`] for the routing.
6//!
7//! `data` cells stay `Option<String>` at the schema layer: every non-null cell
8//! is a `jsonv2` JSON **string** decoded later by [`crate::wire`] per its
9//! [`ColumnType`]; a SQL `NULL` is JSON `null` → `None`.
10
11use franken_snowflake_core::ids::{RequestId, StatementHandle};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15/// A `200 OK` completed result. Partition 0 arrives inline in `data`; later
16/// partitions are fetched separately (see [`PartitionInfo`]).
17#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct ResultSet {
20    /// Column types, row count, and partition layout.
21    pub result_set_meta_data: ResultSetMetaData,
22    /// Inline partition-0 rows: a row is a vector of nullable `jsonv2` strings.
23    pub data: Vec<Vec<Option<String>>>,
24    /// Snowflake response code (e.g. a success code like `090001`).
25    pub code: String,
26    /// The statement handle (also the query id for re-fetch / cancel).
27    pub statement_handle: StatementHandle,
28    /// Relative URL to re-`GET` for status/partitions.
29    #[serde(skip_serializing_if = "Option::is_none", default)]
30    pub statement_status_url: Option<String>,
31    /// Per-sub-statement handles when `MULTI_STATEMENT_COUNT` fans out.
32    #[serde(skip_serializing_if = "Option::is_none", default)]
33    pub statement_handles: Option<Vec<StatementHandle>>,
34    /// SQLSTATE, when present.
35    #[serde(skip_serializing_if = "Option::is_none", default)]
36    pub sql_state: Option<String>,
37    /// Human-readable message, when present.
38    #[serde(skip_serializing_if = "Option::is_none", default)]
39    pub message: Option<String>,
40    /// Echoed idempotency request id.
41    #[serde(skip_serializing_if = "Option::is_none", default)]
42    pub request_id: Option<RequestId>,
43    /// Server creation time (epoch millis), when present.
44    #[serde(skip_serializing_if = "Option::is_none", default)]
45    pub created_on: Option<i64>,
46    /// Opaque execution statistics, preserved verbatim.
47    #[serde(skip_serializing_if = "Option::is_none", default)]
48    pub stats: Option<Value>,
49}
50
51impl ResultSet {
52    /// Total rows across **all** partitions (not just the inline `data`).
53    #[must_use]
54    pub const fn total_rows(&self) -> i64 {
55        self.result_set_meta_data.num_rows
56    }
57
58    /// Number of result partitions (≥ 1; partition 0 is inline).
59    #[must_use]
60    pub fn partition_count(&self) -> usize {
61        self.result_set_meta_data.partition_info.len()
62    }
63
64    /// True when the response fanned out into multiple sub-statements.
65    #[must_use]
66    pub fn is_multi_statement(&self) -> bool {
67        self.statement_handles
68            .as_ref()
69            .is_some_and(|handles| !handles.is_empty())
70    }
71}
72
73/// Metadata describing the columns, total row count, and partition layout of a
74/// [`ResultSet`].
75#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct ResultSetMetaData {
78    /// Total rows across every partition.
79    pub num_rows: i64,
80    /// Result encoding; `jsonv2` for the JSON result format.
81    pub format: String,
82    /// One entry per column, in column order.
83    pub row_type: Vec<ColumnType>,
84    /// Partition sizes; index 0 corresponds to the inline `data`.
85    #[serde(default, skip_serializing_if = "Vec::is_empty")]
86    pub partition_info: Vec<PartitionInfo>,
87}
88
89/// A single column's authoritative type metadata — the source of truth for
90/// decoding (`type` + `scale` + `precision`), never row inspection.
91#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct ColumnType {
94    /// Column name.
95    pub name: String,
96    /// Snowflake logical type (`FIXED`, `REAL`, `TEXT`, `BOOLEAN`, `DATE`,
97    /// `TIME`, `TIMESTAMP_*`, `VARIANT`, `OBJECT`, `ARRAY`, `BINARY`, ...).
98    #[serde(rename = "type")]
99    pub column_type: String,
100    /// Decimal scale (digits after the point) for `FIXED`/`NUMBER`.
101    #[serde(skip_serializing_if = "Option::is_none", default)]
102    pub scale: Option<i32>,
103    /// Total precision for `FIXED`/`NUMBER`.
104    #[serde(skip_serializing_if = "Option::is_none", default)]
105    pub precision: Option<i32>,
106    /// Whether the column is nullable (distinct from the `nullable` query param).
107    pub nullable: bool,
108    /// Declared character length for `TEXT`-family columns.
109    #[serde(skip_serializing_if = "Option::is_none", default)]
110    pub length: Option<i64>,
111    /// Declared byte length.
112    #[serde(skip_serializing_if = "Option::is_none", default)]
113    pub byte_length: Option<i64>,
114    /// Source database, when reported.
115    #[serde(skip_serializing_if = "Option::is_none", default)]
116    pub database: Option<String>,
117    /// Source schema, when reported.
118    #[serde(skip_serializing_if = "Option::is_none", default)]
119    pub schema: Option<String>,
120    /// Source table, when reported.
121    #[serde(skip_serializing_if = "Option::is_none", default)]
122    pub table: Option<String>,
123    /// Collation specifier, when set.
124    #[serde(skip_serializing_if = "Option::is_none", default)]
125    pub collation: Option<String>,
126}
127
128/// The size of one result partition. `numRows` on the parent
129/// [`ResultSetMetaData`] is the total; these are per-partition.
130#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct PartitionInfo {
133    /// Rows in this partition.
134    pub row_count: i64,
135    /// Compressed (gzip) byte size. **Optional**: the live SQL API omits
136    /// `compressedSize` for inline/uncompressed partition 0 (observed against a
137    /// real account, 2026-06-25 — a `SELECT` returns `{"rowCount":N,
138    /// "uncompressedSize":B}` with no `compressedSize`). A required field here
139    /// made every live response fail to decode (`missing field compressedSize`).
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub compressed_size: Option<i64>,
142    /// Uncompressed byte size. Optional for the same reason (Snowflake omits
143    /// either size field depending on partition encoding).
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub uncompressed_size: Option<i64>,
146}
147
148/// A `202 Accepted` still-running status — the poll-again signal. Re-`GET` the
149/// handle (or [`QueryStatus::statement_status_url`]) until it returns `200`.
150#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct QueryStatus {
153    /// Snowflake status code.
154    pub code: String,
155    /// SQLSTATE, when present.
156    #[serde(skip_serializing_if = "Option::is_none", default)]
157    pub sql_state: Option<String>,
158    /// Human-readable message, when present.
159    #[serde(skip_serializing_if = "Option::is_none", default)]
160    pub message: Option<String>,
161    /// The statement handle to keep polling.
162    pub statement_handle: StatementHandle,
163    /// Relative URL to re-`GET`.
164    #[serde(skip_serializing_if = "Option::is_none", default)]
165    pub statement_status_url: Option<String>,
166}
167
168/// A `408` (statement timeout) or `422` (statement failed) body. The HTTP status
169/// distinguishes the two — `408` is a typed timeout, `422` a SQL
170/// compile/execution failure — so the same shape carries both.
171#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct QueryFailureStatus {
174    /// Snowflake error code.
175    pub code: String,
176    /// SQLSTATE, when present.
177    #[serde(skip_serializing_if = "Option::is_none", default)]
178    pub sql_state: Option<String>,
179    /// Human-readable failure message.
180    pub message: String,
181    /// The statement handle, when one was assigned before failure.
182    #[serde(skip_serializing_if = "Option::is_none", default)]
183    pub statement_handle: Option<StatementHandle>,
184}
185
186/// The body returned by `POST /api/v2/statements/{handle}/cancel`.
187#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct StatementCancelResponse {
190    /// Snowflake status code for the cancel.
191    pub code: String,
192    /// Human-readable message, when present.
193    #[serde(skip_serializing_if = "Option::is_none", default)]
194    pub message: Option<String>,
195    /// The cancelled statement's handle, when echoed.
196    #[serde(skip_serializing_if = "Option::is_none", default)]
197    pub statement_handle: Option<StatementHandle>,
198}