1use franken_snowflake_core::ids::{RequestId, StatementHandle};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct ResultSet {
20 pub result_set_meta_data: ResultSetMetaData,
22 pub data: Vec<Vec<Option<String>>>,
24 pub code: String,
26 pub statement_handle: StatementHandle,
28 #[serde(skip_serializing_if = "Option::is_none", default)]
30 pub statement_status_url: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none", default)]
33 pub statement_handles: Option<Vec<StatementHandle>>,
34 #[serde(skip_serializing_if = "Option::is_none", default)]
36 pub sql_state: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none", default)]
39 pub message: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none", default)]
42 pub request_id: Option<RequestId>,
43 #[serde(skip_serializing_if = "Option::is_none", default)]
45 pub created_on: Option<i64>,
46 #[serde(skip_serializing_if = "Option::is_none", default)]
48 pub stats: Option<Value>,
49}
50
51impl ResultSet {
52 #[must_use]
54 pub const fn total_rows(&self) -> i64 {
55 self.result_set_meta_data.num_rows
56 }
57
58 #[must_use]
60 pub fn partition_count(&self) -> usize {
61 self.result_set_meta_data.partition_info.len().max(1)
62 }
63
64 #[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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct ResultSetMetaData {
78 pub num_rows: i64,
80 pub format: String,
82 pub row_type: Vec<ColumnType>,
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
86 pub partition_info: Vec<PartitionInfo>,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct ColumnType {
94 pub name: String,
96 #[serde(rename = "type")]
99 pub column_type: String,
100 #[serde(skip_serializing_if = "Option::is_none", default)]
102 pub scale: Option<i32>,
103 #[serde(skip_serializing_if = "Option::is_none", default)]
105 pub precision: Option<i32>,
106 pub nullable: bool,
108 #[serde(skip_serializing_if = "Option::is_none", default)]
110 pub length: Option<i64>,
111 #[serde(skip_serializing_if = "Option::is_none", default)]
113 pub byte_length: Option<i64>,
114 #[serde(skip_serializing_if = "Option::is_none", default)]
116 pub database: Option<String>,
117 #[serde(skip_serializing_if = "Option::is_none", default)]
119 pub schema: Option<String>,
120 #[serde(skip_serializing_if = "Option::is_none", default)]
122 pub table: Option<String>,
123 #[serde(skip_serializing_if = "Option::is_none", default)]
125 pub collation: Option<String>,
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct PartitionInfo {
133 pub row_count: i64,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub compressed_size: Option<i64>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub uncompressed_size: Option<i64>,
146}
147
148#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct QueryStatus {
153 pub code: String,
155 #[serde(skip_serializing_if = "Option::is_none", default)]
157 pub sql_state: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none", default)]
160 pub message: Option<String>,
161 pub statement_handle: StatementHandle,
163 #[serde(skip_serializing_if = "Option::is_none", default)]
165 pub statement_status_url: Option<String>,
166}
167
168#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct QueryFailureStatus {
174 pub code: String,
176 #[serde(skip_serializing_if = "Option::is_none", default)]
178 pub sql_state: Option<String>,
179 pub message: String,
181 #[serde(skip_serializing_if = "Option::is_none", default)]
183 pub statement_handle: Option<StatementHandle>,
184}
185
186#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct StatementCancelResponse {
190 pub code: String,
192 #[serde(skip_serializing_if = "Option::is_none", default)]
194 pub message: Option<String>,
195 #[serde(skip_serializing_if = "Option::is_none", default)]
197 pub statement_handle: Option<StatementHandle>,
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn partition_count_is_at_least_one() {
206 let result_empty = ResultSet {
207 result_set_meta_data: ResultSetMetaData {
208 num_rows: 0,
209 format: "jsonv2".to_owned(),
210 row_type: vec![],
211 partition_info: vec![],
212 },
213 data: vec![],
214 code: "090001".to_owned(),
215 statement_handle: StatementHandle::new("h1"),
216 statement_status_url: None,
217 statement_handles: None,
218 sql_state: None,
219 message: None,
220 request_id: None,
221 created_on: None,
222 stats: None,
223 };
224 assert_eq!(result_empty.partition_count(), 1);
225
226 let mut result_multi = result_empty;
227 result_multi.result_set_meta_data.partition_info = vec![
228 PartitionInfo {
229 row_count: 5,
230 uncompressed_size: Some(100),
231 compressed_size: Some(50),
232 },
233 PartitionInfo {
234 row_count: 5,
235 uncompressed_size: Some(100),
236 compressed_size: Some(50),
237 },
238 ];
239 assert_eq!(result_multi.partition_count(), 2);
240 }
241
242 #[test]
243 fn result_set_helpers_and_flags() {
244 let mut rs = ResultSet {
245 result_set_meta_data: ResultSetMetaData {
246 num_rows: 42,
247 format: "jsonv2".to_owned(),
248 row_type: vec![ColumnType {
249 name: "ID".to_owned(),
250 column_type: "FIXED".to_owned(),
251 scale: Some(0),
252 precision: Some(38),
253 nullable: false,
254 length: None,
255 byte_length: None,
256 database: None,
257 schema: None,
258 table: None,
259 collation: None,
260 }],
261 partition_info: vec![],
262 },
263 data: vec![],
264 code: "090001".to_owned(),
265 statement_handle: StatementHandle::new("h1"),
266 statement_status_url: None,
267 statement_handles: None,
268 sql_state: None,
269 message: None,
270 request_id: None,
271 created_on: None,
272 stats: None,
273 };
274
275 assert_eq!(rs.total_rows(), 42);
276 assert!(!rs.is_multi_statement());
277
278 rs.statement_handles = Some(vec![]);
279 assert!(!rs.is_multi_statement());
280
281 rs.statement_handles = Some(vec![
282 StatementHandle::new("sub-1"),
283 StatementHandle::new("sub-2"),
284 ]);
285 assert!(rs.is_multi_statement());
286 }
287
288 #[test]
289 fn partition_info_serde_matrix() -> Result<(), serde_json::Error> {
290 let minimal_json = r#"{"rowCount":100}"#;
292 let p1: PartitionInfo = serde_json::from_str(minimal_json)?;
293 assert_eq!(p1.row_count, 100);
294 assert_eq!(p1.compressed_size, None);
295 assert_eq!(p1.uncompressed_size, None);
296
297 let inline_json = r#"{"rowCount":50,"uncompressedSize":2048}"#;
299 let p2: PartitionInfo = serde_json::from_str(inline_json)?;
300 assert_eq!(p2.row_count, 50);
301 assert_eq!(p2.uncompressed_size, Some(2048));
302 assert_eq!(p2.compressed_size, None);
303
304 let full_json = r#"{"rowCount":50,"compressedSize":512,"uncompressedSize":2048}"#;
306 let p3: PartitionInfo = serde_json::from_str(full_json)?;
307 assert_eq!(p3.row_count, 50);
308 assert_eq!(p3.compressed_size, Some(512));
309 assert_eq!(p3.uncompressed_size, Some(2048));
310
311 let reserialized = serde_json::to_string(&p3)?;
312 let roundtrip: PartitionInfo = serde_json::from_str(&reserialized)?;
313 assert_eq!(roundtrip, p3);
314 Ok(())
315 }
316
317 #[test]
318 fn query_status_serde_roundtrip() -> Result<(), serde_json::Error> {
319 let json = r#"{
320 "code": "333334",
321 "message": "Asynchronous execution in progress.",
322 "statementHandle": "01b5a2e4-0000-0123-0000-000000000001",
323 "statementStatusUrl": "/api/v2/statements/01b5a2e4-0000-0123-0000-000000000001",
324 "sqlState": "00000"
325 }"#;
326 let qs: QueryStatus = serde_json::from_str(json)?;
327 assert_eq!(qs.code, "333334");
328 assert_eq!(
329 qs.statement_handle.as_str(),
330 "01b5a2e4-0000-0123-0000-000000000001"
331 );
332 assert_eq!(
333 qs.statement_status_url.as_deref(),
334 Some("/api/v2/statements/01b5a2e4-0000-0123-0000-000000000001")
335 );
336 assert_eq!(qs.sql_state.as_deref(), Some("00000"));
337 assert_eq!(
338 qs.message.as_deref(),
339 Some("Asynchronous execution in progress.")
340 );
341
342 let reserialized = serde_json::to_string(&qs)?;
343 let roundtrip: QueryStatus = serde_json::from_str(&reserialized)?;
344 assert_eq!(roundtrip, qs);
345 Ok(())
346 }
347
348 #[test]
349 fn query_failure_status_serde_roundtrip() -> Result<(), serde_json::Error> {
350 let json = r#"{
351 "code": "002003",
352 "sqlState": "42S02",
353 "message": "SQL compilation error: Table 'DOES_NOT_EXIST' does not exist",
354 "statementHandle": "01b5a2e4-0000-0123-0000-000000000002"
355 }"#;
356 let failure: QueryFailureStatus = serde_json::from_str(json)?;
357 assert_eq!(failure.code, "002003");
358 assert_eq!(failure.sql_state.as_deref(), Some("42S02"));
359 assert_eq!(
360 failure.statement_handle.as_ref().map(|h| h.as_str()),
361 Some("01b5a2e4-0000-0123-0000-000000000002")
362 );
363
364 let reserialized = serde_json::to_string(&failure)?;
365 let roundtrip: QueryFailureStatus = serde_json::from_str(&reserialized)?;
366 assert_eq!(roundtrip, failure);
367 Ok(())
368 }
369
370 #[test]
371 fn statement_cancel_response_serde_roundtrip() -> Result<(), serde_json::Error> {
372 let json = r#"{
373 "code": "090001",
374 "message": "Statement cancelled successfully.",
375 "statementHandle": "01b5a2e4-0000-0123-0000-000000000003"
376 }"#;
377 let cancel: StatementCancelResponse = serde_json::from_str(json)?;
378 assert_eq!(cancel.code, "090001");
379 assert_eq!(
380 cancel.message.as_deref(),
381 Some("Statement cancelled successfully.")
382 );
383 assert_eq!(
384 cancel.statement_handle.as_ref().map(|h| h.as_str()),
385 Some("01b5a2e4-0000-0123-0000-000000000003")
386 );
387
388 let reserialized = serde_json::to_string(&cancel)?;
389 let roundtrip: StatementCancelResponse = serde_json::from_str(&reserialized)?;
390 assert_eq!(roundtrip, cancel);
391 Ok(())
392 }
393
394 #[test]
395 fn full_result_set_serde_roundtrip() -> Result<(), serde_json::Error> {
396 let json = r#"{
397 "resultSetMetaData": {
398 "numRows": 2,
399 "format": "jsonv2",
400 "rowType": [
401 {
402 "name": "ID",
403 "type": "FIXED",
404 "scale": 0,
405 "precision": 38,
406 "nullable": false,
407 "database": "TEST_DB",
408 "schema": "PUBLIC",
409 "table": "USERS"
410 },
411 {
412 "name": "NAME",
413 "type": "TEXT",
414 "nullable": true,
415 "length": 16777216,
416 "byteLength": 16777216,
417 "collation": "en-ci"
418 }
419 ],
420 "partitionInfo": [
421 {"rowCount": 2, "uncompressedSize": 128}
422 ]
423 },
424 "data": [
425 ["1", "Alice"],
426 ["2", null]
427 ],
428 "code": "090001",
429 "statementHandle": "stmt-abc",
430 "statementStatusUrl": "/api/v2/statements/stmt-abc",
431 "statementHandles": ["stmt-abc"],
432 "sqlState": "00000",
433 "message": "Statement executed successfully.",
434 "requestId": "req-xyz",
435 "createdOn": 1700000000000,
436 "stats": {"scanBytes": 1024}
437 }"#;
438
439 let rs: ResultSet = serde_json::from_str(json)?;
440 assert_eq!(rs.total_rows(), 2);
441 assert_eq!(rs.partition_count(), 1);
442 assert!(rs.is_multi_statement());
443 assert_eq!(rs.data.len(), 2);
444 assert_eq!(
445 rs.data[0],
446 vec![Some("1".to_owned()), Some("Alice".to_owned())]
447 );
448 assert_eq!(rs.data[1], vec![Some("2".to_owned()), None]);
449 assert_eq!(rs.created_on, Some(1700000000000));
450 assert!(rs.stats.is_some());
451
452 let reserialized = serde_json::to_string(&rs)?;
453 let roundtrip: ResultSet = serde_json::from_str(&reserialized)?;
454 assert_eq!(roundtrip, rs);
455 Ok(())
456 }
457}