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}