use franken_snowflake_core::ids::{RequestId, StatementHandle};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResultSet {
pub result_set_meta_data: ResultSetMetaData,
pub data: Vec<Vec<Option<String>>>,
pub code: String,
pub statement_handle: StatementHandle,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub statement_status_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub statement_handles: Option<Vec<StatementHandle>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub sql_state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub request_id: Option<RequestId>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub created_on: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub stats: Option<Value>,
}
impl ResultSet {
#[must_use]
pub const fn total_rows(&self) -> i64 {
self.result_set_meta_data.num_rows
}
#[must_use]
pub fn partition_count(&self) -> usize {
self.result_set_meta_data.partition_info.len().max(1)
}
#[must_use]
pub fn is_multi_statement(&self) -> bool {
self.statement_handles
.as_ref()
.is_some_and(|handles| !handles.is_empty())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResultSetMetaData {
pub num_rows: i64,
pub format: String,
pub row_type: Vec<ColumnType>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub partition_info: Vec<PartitionInfo>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ColumnType {
pub name: String,
#[serde(rename = "type")]
pub column_type: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub scale: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub precision: Option<i32>,
pub nullable: bool,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub length: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub byte_length: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub database: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub schema: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub table: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub collation: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PartitionInfo {
pub row_count: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compressed_size: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uncompressed_size: Option<i64>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryStatus {
pub code: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub sql_state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub message: Option<String>,
pub statement_handle: StatementHandle,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub statement_status_url: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryFailureStatus {
pub code: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub sql_state: Option<String>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub statement_handle: Option<StatementHandle>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StatementCancelResponse {
pub code: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub statement_handle: Option<StatementHandle>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partition_count_is_at_least_one() {
let result_empty = ResultSet {
result_set_meta_data: ResultSetMetaData {
num_rows: 0,
format: "jsonv2".to_owned(),
row_type: vec![],
partition_info: vec![],
},
data: vec![],
code: "090001".to_owned(),
statement_handle: StatementHandle::new("h1"),
statement_status_url: None,
statement_handles: None,
sql_state: None,
message: None,
request_id: None,
created_on: None,
stats: None,
};
assert_eq!(result_empty.partition_count(), 1);
let mut result_multi = result_empty;
result_multi.result_set_meta_data.partition_info = vec![
PartitionInfo {
row_count: 5,
uncompressed_size: Some(100),
compressed_size: Some(50),
},
PartitionInfo {
row_count: 5,
uncompressed_size: Some(100),
compressed_size: Some(50),
},
];
assert_eq!(result_multi.partition_count(), 2);
}
#[test]
fn result_set_helpers_and_flags() {
let mut rs = ResultSet {
result_set_meta_data: ResultSetMetaData {
num_rows: 42,
format: "jsonv2".to_owned(),
row_type: vec![ColumnType {
name: "ID".to_owned(),
column_type: "FIXED".to_owned(),
scale: Some(0),
precision: Some(38),
nullable: false,
length: None,
byte_length: None,
database: None,
schema: None,
table: None,
collation: None,
}],
partition_info: vec![],
},
data: vec![],
code: "090001".to_owned(),
statement_handle: StatementHandle::new("h1"),
statement_status_url: None,
statement_handles: None,
sql_state: None,
message: None,
request_id: None,
created_on: None,
stats: None,
};
assert_eq!(rs.total_rows(), 42);
assert!(!rs.is_multi_statement());
rs.statement_handles = Some(vec![]);
assert!(!rs.is_multi_statement());
rs.statement_handles = Some(vec![
StatementHandle::new("sub-1"),
StatementHandle::new("sub-2"),
]);
assert!(rs.is_multi_statement());
}
#[test]
fn partition_info_serde_matrix() -> Result<(), serde_json::Error> {
let minimal_json = r#"{"rowCount":100}"#;
let p1: PartitionInfo = serde_json::from_str(minimal_json)?;
assert_eq!(p1.row_count, 100);
assert_eq!(p1.compressed_size, None);
assert_eq!(p1.uncompressed_size, None);
let inline_json = r#"{"rowCount":50,"uncompressedSize":2048}"#;
let p2: PartitionInfo = serde_json::from_str(inline_json)?;
assert_eq!(p2.row_count, 50);
assert_eq!(p2.uncompressed_size, Some(2048));
assert_eq!(p2.compressed_size, None);
let full_json = r#"{"rowCount":50,"compressedSize":512,"uncompressedSize":2048}"#;
let p3: PartitionInfo = serde_json::from_str(full_json)?;
assert_eq!(p3.row_count, 50);
assert_eq!(p3.compressed_size, Some(512));
assert_eq!(p3.uncompressed_size, Some(2048));
let reserialized = serde_json::to_string(&p3)?;
let roundtrip: PartitionInfo = serde_json::from_str(&reserialized)?;
assert_eq!(roundtrip, p3);
Ok(())
}
#[test]
fn query_status_serde_roundtrip() -> Result<(), serde_json::Error> {
let json = r#"{
"code": "333334",
"message": "Asynchronous execution in progress.",
"statementHandle": "01b5a2e4-0000-0123-0000-000000000001",
"statementStatusUrl": "/api/v2/statements/01b5a2e4-0000-0123-0000-000000000001",
"sqlState": "00000"
}"#;
let qs: QueryStatus = serde_json::from_str(json)?;
assert_eq!(qs.code, "333334");
assert_eq!(
qs.statement_handle.as_str(),
"01b5a2e4-0000-0123-0000-000000000001"
);
assert_eq!(
qs.statement_status_url.as_deref(),
Some("/api/v2/statements/01b5a2e4-0000-0123-0000-000000000001")
);
assert_eq!(qs.sql_state.as_deref(), Some("00000"));
assert_eq!(
qs.message.as_deref(),
Some("Asynchronous execution in progress.")
);
let reserialized = serde_json::to_string(&qs)?;
let roundtrip: QueryStatus = serde_json::from_str(&reserialized)?;
assert_eq!(roundtrip, qs);
Ok(())
}
#[test]
fn query_failure_status_serde_roundtrip() -> Result<(), serde_json::Error> {
let json = r#"{
"code": "002003",
"sqlState": "42S02",
"message": "SQL compilation error: Table 'DOES_NOT_EXIST' does not exist",
"statementHandle": "01b5a2e4-0000-0123-0000-000000000002"
}"#;
let failure: QueryFailureStatus = serde_json::from_str(json)?;
assert_eq!(failure.code, "002003");
assert_eq!(failure.sql_state.as_deref(), Some("42S02"));
assert_eq!(
failure.statement_handle.as_ref().map(|h| h.as_str()),
Some("01b5a2e4-0000-0123-0000-000000000002")
);
let reserialized = serde_json::to_string(&failure)?;
let roundtrip: QueryFailureStatus = serde_json::from_str(&reserialized)?;
assert_eq!(roundtrip, failure);
Ok(())
}
#[test]
fn statement_cancel_response_serde_roundtrip() -> Result<(), serde_json::Error> {
let json = r#"{
"code": "090001",
"message": "Statement cancelled successfully.",
"statementHandle": "01b5a2e4-0000-0123-0000-000000000003"
}"#;
let cancel: StatementCancelResponse = serde_json::from_str(json)?;
assert_eq!(cancel.code, "090001");
assert_eq!(
cancel.message.as_deref(),
Some("Statement cancelled successfully.")
);
assert_eq!(
cancel.statement_handle.as_ref().map(|h| h.as_str()),
Some("01b5a2e4-0000-0123-0000-000000000003")
);
let reserialized = serde_json::to_string(&cancel)?;
let roundtrip: StatementCancelResponse = serde_json::from_str(&reserialized)?;
assert_eq!(roundtrip, cancel);
Ok(())
}
#[test]
fn full_result_set_serde_roundtrip() -> Result<(), serde_json::Error> {
let json = r#"{
"resultSetMetaData": {
"numRows": 2,
"format": "jsonv2",
"rowType": [
{
"name": "ID",
"type": "FIXED",
"scale": 0,
"precision": 38,
"nullable": false,
"database": "TEST_DB",
"schema": "PUBLIC",
"table": "USERS"
},
{
"name": "NAME",
"type": "TEXT",
"nullable": true,
"length": 16777216,
"byteLength": 16777216,
"collation": "en-ci"
}
],
"partitionInfo": [
{"rowCount": 2, "uncompressedSize": 128}
]
},
"data": [
["1", "Alice"],
["2", null]
],
"code": "090001",
"statementHandle": "stmt-abc",
"statementStatusUrl": "/api/v2/statements/stmt-abc",
"statementHandles": ["stmt-abc"],
"sqlState": "00000",
"message": "Statement executed successfully.",
"requestId": "req-xyz",
"createdOn": 1700000000000,
"stats": {"scanBytes": 1024}
}"#;
let rs: ResultSet = serde_json::from_str(json)?;
assert_eq!(rs.total_rows(), 2);
assert_eq!(rs.partition_count(), 1);
assert!(rs.is_multi_statement());
assert_eq!(rs.data.len(), 2);
assert_eq!(
rs.data[0],
vec![Some("1".to_owned()), Some("Alice".to_owned())]
);
assert_eq!(rs.data[1], vec![Some("2".to_owned()), None]);
assert_eq!(rs.created_on, Some(1700000000000));
assert!(rs.stats.is_some());
let reserialized = serde_json::to_string(&rs)?;
let roundtrip: ResultSet = serde_json::from_str(&reserialized)?;
assert_eq!(roundtrip, rs);
Ok(())
}
}