use std::time::Duration;
use franken_snowflake_core::error::{SnowflakeError, SnowflakeErrorCode};
use franken_snowflake_core::ids::StatementHandle;
use crate::response::{QueryFailureStatus, QueryStatus, ResultSet};
use crate::status::ResponseClass;
pub const MIN_POLL_INTERVAL: Duration = Duration::from_millis(50);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PollPlan {
pub max_polls: u32,
pub poll_interval: Duration,
pub partition_concurrency: usize,
pub row_cap: Option<usize>,
}
pub const DEFAULT_PARTITION_CONCURRENCY: usize = 4;
pub const MAX_PARTITION_CONCURRENCY: usize = 16;
impl Default for PollPlan {
fn default() -> Self {
Self {
max_polls: 120,
poll_interval: Duration::from_millis(1_000),
partition_concurrency: DEFAULT_PARTITION_CONCURRENCY,
row_cap: None,
}
}
}
impl PollPlan {
#[must_use]
pub fn with_max_polls(max_polls: u32) -> Self {
Self {
max_polls: max_polls.max(1),
..Self::default()
}
}
#[must_use]
pub fn with_poll_interval(mut self, poll_interval: Duration) -> Self {
self.poll_interval = poll_interval.max(MIN_POLL_INTERVAL);
self
}
#[must_use]
pub fn effective_poll_interval(&self) -> Duration {
self.poll_interval.max(MIN_POLL_INTERVAL)
}
#[must_use]
pub fn with_partition_concurrency(mut self, concurrency: usize) -> Self {
self.partition_concurrency = concurrency.clamp(1, MAX_PARTITION_CONCURRENCY);
self
}
#[must_use]
pub fn with_row_cap(mut self, row_cap: Option<usize>) -> Self {
self.row_cap = row_cap;
self
}
#[must_use]
pub fn effective_partition_concurrency(&self) -> usize {
self.partition_concurrency
.clamp(1, MAX_PARTITION_CONCURRENCY)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CompletedStatement {
pub statement_handle: StatementHandle,
pub result_set: ResultSet,
pub rows: Vec<Vec<Option<String>>>,
pub fetched_partitions: u32,
pub total_partitions: u32,
}
impl CompletedStatement {
#[must_use]
pub fn is_partial(&self) -> bool {
self.fetched_partitions < self.total_partitions
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, PartialEq)]
pub enum Progress {
PollAgain(StatementHandle),
FetchPartition {
handle: StatementHandle,
partition: u32,
},
Complete(CompletedStatement),
TimedOut(QueryFailureStatus),
Failed(QueryFailureStatus),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LifecycleError {
pub code: LifecycleErrorCode,
pub message: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LifecycleErrorCode {
DecodeFailed,
UnexpectedStatus,
PollQuotaExhausted,
PartitionRowMismatch,
}
impl LifecycleError {
fn new(code: LifecycleErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
#[must_use]
pub fn into_snowflake_error(self) -> SnowflakeError {
let code = match self.code {
LifecycleErrorCode::DecodeFailed
| LifecycleErrorCode::UnexpectedStatus
| LifecycleErrorCode::PartitionRowMismatch => SnowflakeErrorCode::UpstreamError,
LifecycleErrorCode::PollQuotaExhausted => SnowflakeErrorCode::RetryBudgetExhausted,
};
SnowflakeError::new(code, self.message)
}
}
impl std::fmt::Display for LifecycleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}: {}", self.code, self.message)
}
}
impl std::error::Error for LifecycleError {}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
enum Phase {
Pending,
Assembling {
result_set: ResultSet,
handle: StatementHandle,
total: u32,
next: u32,
rows: Vec<Vec<Option<String>>>,
},
Done,
}
#[derive(Clone, Debug)]
pub struct StatementMachine {
poll_plan: PollPlan,
polls_done: u32,
phase: Phase,
drained_rows: usize,
}
impl StatementMachine {
#[must_use]
pub fn new(poll_plan: PollPlan) -> Self {
Self {
poll_plan,
polls_done: 0,
phase: Phase::Pending,
drained_rows: 0,
}
}
pub fn drain_rows(&mut self) -> Vec<Vec<Option<String>>> {
match &mut self.phase {
Phase::Assembling { rows, .. } => {
let drained = std::mem::take(rows);
self.drained_rows = self.drained_rows.saturating_add(drained.len());
drained
}
Phase::Pending | Phase::Done => Vec::new(),
}
}
#[must_use]
pub fn result_set(&self) -> Option<&ResultSet> {
match &self.phase {
Phase::Assembling { result_set, .. } => Some(result_set),
Phase::Pending | Phase::Done => None,
}
}
#[must_use]
pub const fn polls_done(&self) -> u32 {
self.polls_done
}
#[must_use]
pub fn assembling_window(&self) -> Option<(u32, u32)> {
match &self.phase {
Phase::Assembling { next, total, .. } => Some((*next, *total)),
Phase::Pending | Phase::Done => None,
}
}
#[must_use]
pub fn rows_assembled(&self) -> usize {
match &self.phase {
Phase::Assembling { rows, .. } => self.drained_rows.saturating_add(rows.len()),
Phase::Pending | Phase::Done => 0,
}
}
pub fn complete_early(&mut self) -> Result<CompletedStatement, LifecycleError> {
let Phase::Assembling {
result_set,
handle,
total,
next,
rows,
} = std::mem::replace(&mut self.phase, Phase::Done)
else {
return Err(LifecycleError::new(
LifecycleErrorCode::UnexpectedStatus,
"early completion requested outside the assembling phase",
));
};
Ok(CompletedStatement {
statement_handle: handle,
result_set,
rows,
fetched_partitions: next,
total_partitions: total,
})
}
pub fn on_submit(
&mut self,
class: ResponseClass,
body: &[u8],
) -> Result<Progress, LifecycleError> {
self.ensure_not_terminal()?;
match class {
ResponseClass::Completed => self.enter_terminal_result(parse_result_set(body)?),
ResponseClass::Running => {
let status = parse_query_status(body)?;
Ok(Progress::PollAgain(status.statement_handle))
}
ResponseClass::StatementTimeout => self.enter_terminal_timeout(parse_failure(body)?),
ResponseClass::StatementFailed => self.enter_terminal_failure(parse_failure(body)?),
ResponseClass::RateLimited | ResponseClass::Other(_) => Err(LifecycleError::new(
LifecycleErrorCode::UnexpectedStatus,
"submit returned a non-terminal, non-running status",
)),
}
}
pub fn on_poll(
&mut self,
class: ResponseClass,
body: &[u8],
) -> Result<Progress, LifecycleError> {
self.ensure_not_terminal()?;
self.polls_done = self.polls_done.saturating_add(1);
match class {
ResponseClass::Completed => self.enter_terminal_result(parse_result_set(body)?),
ResponseClass::StatementTimeout => self.enter_terminal_timeout(parse_failure(body)?),
ResponseClass::StatementFailed => self.enter_terminal_failure(parse_failure(body)?),
ResponseClass::Running | ResponseClass::RateLimited => {
if self.polls_done > self.poll_plan.max_polls {
return Err(LifecycleError::new(
LifecycleErrorCode::PollQuotaExhausted,
format!(
"statement still running after {} polls",
self.poll_plan.max_polls
),
));
}
let status = parse_query_status(body)?;
Ok(Progress::PollAgain(status.statement_handle))
}
ResponseClass::Other(_) => Err(LifecycleError::new(
LifecycleErrorCode::UnexpectedStatus,
"poll returned an unexpected status",
)),
}
}
pub fn on_partition(
&mut self,
class: ResponseClass,
partition: u32,
body: &[u8],
) -> Result<Progress, LifecycleError> {
if !matches!(class, ResponseClass::Completed) {
self.phase = Phase::Done;
return Err(LifecycleError::new(
LifecycleErrorCode::UnexpectedStatus,
format!("partition {partition} returned a non-200 status"),
));
}
let Phase::Assembling {
result_set,
handle,
total,
next,
mut rows,
} = std::mem::replace(&mut self.phase, Phase::Done)
else {
return Err(LifecycleError::new(
LifecycleErrorCode::UnexpectedStatus,
"partition response arrived outside the assembling phase",
));
};
if partition != next {
self.phase = Phase::Assembling {
result_set,
handle,
total,
next,
rows,
};
return Err(LifecycleError::new(
LifecycleErrorCode::UnexpectedStatus,
format!("expected partition {next}, received {partition}"),
));
}
let mut partition_rows = parse_partition_rows(body)?;
validate_partition_row_count(&result_set, partition, partition_rows.len())?;
rows.append(&mut partition_rows);
let upcoming = next.saturating_add(1);
if upcoming >= total {
validate_total_row_count(
self.drained_rows.saturating_add(rows.len()),
result_set.result_set_meta_data.num_rows,
)?;
Ok(Progress::Complete(CompletedStatement {
statement_handle: handle,
result_set,
rows,
fetched_partitions: total,
total_partitions: total,
}))
} else {
let resume = handle.clone();
self.phase = Phase::Assembling {
result_set,
handle,
total,
next: upcoming,
rows,
};
Ok(Progress::FetchPartition {
handle: resume,
partition: upcoming,
})
}
}
fn ensure_not_terminal(&self) -> Result<(), LifecycleError> {
if matches!(self.phase, Phase::Done) {
return Err(LifecycleError::new(
LifecycleErrorCode::UnexpectedStatus,
"statement machine already reached a terminal state",
));
}
Ok(())
}
fn enter_terminal_timeout(
&mut self,
failure: QueryFailureStatus,
) -> Result<Progress, LifecycleError> {
self.phase = Phase::Done;
Ok(Progress::TimedOut(failure))
}
fn enter_terminal_failure(
&mut self,
failure: QueryFailureStatus,
) -> Result<Progress, LifecycleError> {
self.phase = Phase::Done;
Ok(Progress::Failed(failure))
}
fn enter_terminal_result(&mut self, result_set: ResultSet) -> Result<Progress, LifecycleError> {
let handle = result_set.statement_handle.clone();
if result_set.is_multi_statement() {
self.phase = Phase::Done;
let rows = result_set.data.clone();
return Ok(Progress::Complete(CompletedStatement {
statement_handle: handle,
result_set,
rows,
fetched_partitions: 1,
total_partitions: 1,
}));
}
let total = partition_total(&result_set);
let rows = result_set.data.clone();
validate_partition_row_count(&result_set, 0, rows.len())?;
if total <= 1 {
self.phase = Phase::Done;
validate_total_row_count(rows.len(), result_set.result_set_meta_data.num_rows)?;
Ok(Progress::Complete(CompletedStatement {
statement_handle: handle,
result_set,
rows,
fetched_partitions: 1,
total_partitions: 1,
}))
} else {
let resume = handle.clone();
self.phase = Phase::Assembling {
result_set,
handle,
total,
next: 1,
rows,
};
Ok(Progress::FetchPartition {
handle: resume,
partition: 1,
})
}
}
}
#[must_use]
fn partition_total(result_set: &ResultSet) -> u32 {
u32::try_from(result_set.result_set_meta_data.partition_info.len().max(1)).unwrap_or(u32::MAX)
}
fn parse_result_set(body: &[u8]) -> Result<ResultSet, LifecycleError> {
serde_json::from_slice(body)
.map_err(|error| LifecycleError::new(LifecycleErrorCode::DecodeFailed, error.to_string()))
}
fn parse_query_status(body: &[u8]) -> Result<QueryStatus, LifecycleError> {
serde_json::from_slice(body)
.map_err(|error| LifecycleError::new(LifecycleErrorCode::DecodeFailed, error.to_string()))
}
fn parse_failure(body: &[u8]) -> Result<QueryFailureStatus, LifecycleError> {
serde_json::from_slice(body)
.map_err(|error| LifecycleError::new(LifecycleErrorCode::DecodeFailed, error.to_string()))
}
pub fn parse_partition_rows(body: &[u8]) -> Result<Vec<Vec<Option<String>>>, LifecycleError> {
#[derive(serde::Deserialize)]
struct PartitionEnvelope {
data: Vec<Vec<Option<String>>>,
}
if let Ok(envelope) = serde_json::from_slice::<PartitionEnvelope>(body) {
return Ok(envelope.data);
}
serde_json::from_slice(body)
.map_err(|error| LifecycleError::new(LifecycleErrorCode::DecodeFailed, error.to_string()))
}
fn validate_partition_row_count(
result_set: &ResultSet,
partition: u32,
actual_rows: usize,
) -> Result<(), LifecycleError> {
let Some(expected) = usize::try_from(partition)
.ok()
.and_then(|index| result_set.result_set_meta_data.partition_info.get(index))
.map(|info| info.row_count)
else {
return Ok(());
};
if expected < 0 {
return Err(LifecycleError::new(
LifecycleErrorCode::PartitionRowMismatch,
format!("partition {partition} rowCount is negative"),
));
}
if i64::try_from(actual_rows).ok() != Some(expected) {
return Err(LifecycleError::new(
LifecycleErrorCode::PartitionRowMismatch,
format!("partition {partition} returned {actual_rows} rows but rowCount is {expected}"),
));
}
Ok(())
}
fn validate_total_row_count(actual_rows: usize, expected: i64) -> Result<(), LifecycleError> {
if expected < 0 {
return Err(LifecycleError::new(
LifecycleErrorCode::PartitionRowMismatch,
"numRows is negative",
));
}
if i64::try_from(actual_rows).ok() != Some(expected) {
return Err(LifecycleError::new(
LifecycleErrorCode::PartitionRowMismatch,
format!("assembled {actual_rows} rows but numRows is {expected}"),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn poll_plan_interval_is_always_sane() {
assert_eq!(
PollPlan::default().poll_interval,
Duration::from_millis(1_000)
);
assert!(PollPlan::default().effective_poll_interval() >= MIN_POLL_INTERVAL);
assert_eq!(
PollPlan::with_max_polls(5).poll_interval,
PollPlan::default().poll_interval
);
assert_eq!(
PollPlan::default()
.with_poll_interval(Duration::ZERO)
.poll_interval,
MIN_POLL_INTERVAL
);
let hand_set = PollPlan {
poll_interval: Duration::ZERO,
..PollPlan::default()
};
assert_eq!(hand_set.effective_poll_interval(), MIN_POLL_INTERVAL);
}
#[test]
fn partition_total_treats_absent_or_single_info_as_inline() -> Result<(), String> {
let body = br#"{"resultSetMetaData":{"numRows":0,"format":"jsonv2","rowType":[]},
"data":[],"code":"090001","statementHandle":"h"}"#;
let result_set = parse_result_set(body).map_err(|error| error.to_string())?;
assert_eq!(partition_total(&result_set), 1);
Ok(())
}
#[test]
fn single_partition_completes_immediately() -> Result<(), String> {
let body = br#"{"resultSetMetaData":{"numRows":1,"format":"jsonv2",
"rowType":[{"name":"A","type":"TEXT","nullable":false}],
"partitionInfo":[{"rowCount":1,"compressedSize":1,"uncompressedSize":1}]},
"data":[["x"]],"code":"090001","statementHandle":"h"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
match machine.on_submit(ResponseClass::Completed, body) {
Ok(Progress::Complete(done)) => {
assert_eq!(done.rows.len(), 1);
assert_eq!(done.statement_handle, StatementHandle::new("h"));
Ok(())
}
other => Err(format!("expected Complete, got {other:?}")),
}
}
#[test]
fn running_then_completed_polls_then_finishes() -> Result<(), String> {
let running = br#"{"code":"333334","statementHandle":"h2"}"#;
let completed = br#"{"resultSetMetaData":{"numRows":0,"format":"jsonv2","rowType":[]},
"data":[],"code":"090001","statementHandle":"h2"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
match machine.on_submit(ResponseClass::Running, running) {
Ok(Progress::PollAgain(h)) => assert_eq!(h, StatementHandle::new("h2")),
other => return Err(format!("expected PollAgain, got {other:?}")),
}
match machine.on_poll(ResponseClass::Completed, completed) {
Ok(Progress::Complete(_)) => Ok(()),
other => Err(format!("expected Complete, got {other:?}")),
}
}
#[test]
fn poll_quota_is_enforced() -> Result<(), String> {
let running = br#"{"code":"333334","statementHandle":"h3"}"#;
let mut machine = StatementMachine::new(PollPlan::with_max_polls(2));
machine
.on_poll(ResponseClass::Running, running)
.map_err(|e| e.to_string())?;
machine
.on_poll(ResponseClass::Running, running)
.map_err(|e| e.to_string())?;
match machine.on_poll(ResponseClass::Running, running) {
Err(error) => {
assert_eq!(error.code, LifecycleErrorCode::PollQuotaExhausted);
Ok(())
}
Ok(progress) => Err(format!("expected quota error, got {progress:?}")),
}
}
#[test]
fn timeout_and_failure_are_distinct_terminal_states() -> Result<(), String> {
let timeout = br#"{"code":"000630","message":"timed out","statementHandle":"h"}"#;
let failure = br#"{"code":"001003","message":"bad sql","statementHandle":"h"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
assert!(matches!(
machine.on_submit(ResponseClass::StatementTimeout, timeout),
Ok(Progress::TimedOut(_))
));
let mut other = StatementMachine::new(PollPlan::default());
assert!(matches!(
other.on_submit(ResponseClass::StatementFailed, failure),
Ok(Progress::Failed(_))
));
Ok(())
}
#[test]
fn timeout_and_failure_close_the_machine() -> Result<(), String> {
let timeout = br#"{"code":"000630","message":"timed out","statementHandle":"h"}"#;
let completed = br#"{"resultSetMetaData":{"numRows":0,"format":"jsonv2","rowType":[]},
"data":[],"code":"090001","statementHandle":"h"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
assert!(matches!(
machine.on_submit(ResponseClass::StatementTimeout, timeout),
Ok(Progress::TimedOut(_))
));
match machine.on_poll(ResponseClass::Completed, completed) {
Err(error) => {
assert_eq!(error.code, LifecycleErrorCode::UnexpectedStatus);
Ok(())
}
Ok(progress) => Err(format!(
"expected terminal machine refusal, got {progress:?}"
)),
}
}
#[test]
fn terminal_machine_refuses_every_reentry_path() -> Result<(), String> {
let failure = br#"{"code":"001003","message":"bad sql","statementHandle":"h"}"#;
let completed = br#"{"resultSetMetaData":{"numRows":0,"format":"jsonv2","rowType":[]},
"data":[],"code":"090001","statementHandle":"h"}"#;
let mut after_failure = StatementMachine::new(PollPlan::default());
assert!(matches!(
after_failure.on_submit(ResponseClass::StatementFailed, failure),
Ok(Progress::Failed(_))
));
assert_eq!(
after_failure
.on_poll(ResponseClass::Completed, completed)
.map(|_| ())
.unwrap_err()
.code,
LifecycleErrorCode::UnexpectedStatus
);
let mut after_success = StatementMachine::new(PollPlan::default());
assert!(matches!(
after_success.on_submit(ResponseClass::Completed, completed),
Ok(Progress::Complete(_))
));
assert_eq!(
after_success
.on_submit(ResponseClass::Completed, completed)
.map(|_| ())
.unwrap_err()
.code,
LifecycleErrorCode::UnexpectedStatus
);
assert_eq!(
after_success
.on_poll(ResponseClass::Completed, completed)
.map(|_| ())
.unwrap_err()
.code,
LifecycleErrorCode::UnexpectedStatus
);
assert_eq!(after_success.polls_done(), 0);
Ok(())
}
#[test]
fn multi_partition_assembles_rows_in_order() -> Result<(), String> {
let terminal = br#"{"resultSetMetaData":{"numRows":5,"format":"jsonv2",
"rowType":[{"name":"ID","type":"FIXED","nullable":false},
{"name":"NAME","type":"TEXT","nullable":false}],
"partitionInfo":[{"rowCount":2,"compressedSize":1,"uncompressedSize":1},
{"rowCount":2,"compressedSize":1,"uncompressedSize":1},
{"rowCount":1,"compressedSize":1,"uncompressedSize":1}]},
"data":[["1","a"],["2","b"]],"code":"090001","statementHandle":"hp"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
let first = machine.on_submit(ResponseClass::Completed, terminal);
let handle = match first {
Ok(Progress::FetchPartition {
handle,
partition: 1,
}) => handle,
other => return Err(format!("expected FetchPartition 1, got {other:?}")),
};
assert_eq!(handle, StatementHandle::new("hp"));
match machine.on_partition(ResponseClass::Completed, 1, br#"[["3","c"],["4","d"]]"#) {
Ok(Progress::FetchPartition { partition: 2, .. }) => {}
other => return Err(format!("expected FetchPartition 2, got {other:?}")),
}
match machine.on_partition(ResponseClass::Completed, 2, br#"[["5","e"]]"#) {
Ok(Progress::Complete(done)) => {
assert_eq!(done.rows.len(), 5);
assert_eq!(
done.rows[4],
vec![Some("5".to_owned()), Some("e".to_owned())]
);
Ok(())
}
other => Err(format!("expected Complete, got {other:?}")),
}
}
#[test]
fn parse_partition_rows_accepts_live_object_data_form() -> Result<(), String> {
let rows = parse_partition_rows(br#"{"data":[["3","c"],["4","d"]]}"#)
.map_err(|error| error.to_string())?;
assert_eq!(
rows,
vec![
vec![Some("3".to_owned()), Some("c".to_owned())],
vec![Some("4".to_owned()), Some("d".to_owned())],
]
);
Ok(())
}
#[test]
fn parse_partition_rows_still_accepts_bare_array_form() -> Result<(), String> {
let rows = parse_partition_rows(br#"[["5","e"]]"#).map_err(|error| error.to_string())?;
assert_eq!(rows, vec![vec![Some("5".to_owned()), Some("e".to_owned())]]);
Ok(())
}
#[test]
fn partition_info_decodes_when_compressed_size_is_omitted() -> Result<(), String> {
let body = br#"{"resultSetMetaData":{"numRows":2,"format":"jsonv2",
"rowType":[{"name":"A","type":"TEXT","nullable":false}],
"partitionInfo":[{"rowCount":2,"uncompressedSize":64}]},
"data":[["x"],["y"]],"code":"090001","statementHandle":"h"}"#;
let result_set = parse_result_set(body).map_err(|error| error.to_string())?;
let info = result_set
.result_set_meta_data
.partition_info
.first()
.ok_or("expected partition_info[0]")?;
assert_eq!(info.row_count, 2);
assert_eq!(info.compressed_size, None);
assert_eq!(info.uncompressed_size, Some(64));
Ok(())
}
#[test]
fn live_shaped_multi_partition_flow_decodes_end_to_end() -> Result<(), String> {
let terminal = br#"{"resultSetMetaData":{"numRows":3,"format":"jsonv2",
"rowType":[{"name":"ID","type":"FIXED","nullable":false}],
"partitionInfo":[{"rowCount":2,"uncompressedSize":16},
{"rowCount":1,"compressedSize":8,"uncompressedSize":16}]},
"data":[["1"],["2"]],"code":"090001","statementHandle":"hp"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
let handle = match machine.on_submit(ResponseClass::Completed, terminal) {
Ok(Progress::FetchPartition {
handle,
partition: 1,
}) => handle,
other => return Err(format!("expected FetchPartition 1, got {other:?}")),
};
assert_eq!(handle, StatementHandle::new("hp"));
match machine.on_partition(ResponseClass::Completed, 1, br#"{"data":[["3"]]}"#) {
Ok(Progress::Complete(done)) => {
assert_eq!(done.rows.len(), 3);
assert_eq!(done.rows[2], vec![Some("3".to_owned())]);
Ok(())
}
other => Err(format!("expected Complete, got {other:?}")),
}
}
#[test]
fn row_count_mismatch_is_rejected() {
let terminal = br#"{"resultSetMetaData":{"numRows":99,"format":"jsonv2",
"rowType":[{"name":"A","type":"TEXT","nullable":false}],
"partitionInfo":[{"rowCount":1,"compressedSize":1,"uncompressedSize":1},
{"rowCount":1,"compressedSize":1,"uncompressedSize":1}]},
"data":[["x"]],"code":"090001","statementHandle":"hp"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
let _ = machine.on_submit(ResponseClass::Completed, terminal);
let result = machine.on_partition(ResponseClass::Completed, 1, br#"[["y"]]"#);
assert!(matches!(
result,
Err(LifecycleError {
code: LifecycleErrorCode::PartitionRowMismatch,
..
})
));
}
#[test]
fn fetched_partition_row_count_mismatch_is_rejected_before_total_can_compensate() {
let terminal = br#"{"resultSetMetaData":{"numRows":3,"format":"jsonv2",
"rowType":[{"name":"A","type":"TEXT","nullable":false}],
"partitionInfo":[{"rowCount":1,"compressedSize":1,"uncompressedSize":1},
{"rowCount":1,"compressedSize":1,"uncompressedSize":1},
{"rowCount":1,"compressedSize":1,"uncompressedSize":1}]},
"data":[["inline"]],"code":"090001","statementHandle":"hp"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
assert!(matches!(
machine.on_submit(ResponseClass::Completed, terminal),
Ok(Progress::FetchPartition { partition: 1, .. })
));
let result = machine.on_partition(
ResponseClass::Completed,
1,
br#"[["too-many"],["would-hide-empty-next"]]"#,
);
assert!(matches!(
result,
Err(LifecycleError {
code: LifecycleErrorCode::PartitionRowMismatch,
..
})
));
}
#[test]
fn empty_fetched_partition_with_positive_row_count_is_rejected() {
let terminal = br#"{"resultSetMetaData":{"numRows":2,"format":"jsonv2",
"rowType":[{"name":"A","type":"TEXT","nullable":false}],
"partitionInfo":[{"rowCount":1,"compressedSize":1,"uncompressedSize":1},
{"rowCount":1,"compressedSize":1,"uncompressedSize":1}]},
"data":[["inline"]],"code":"090001","statementHandle":"hp"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
assert!(matches!(
machine.on_submit(ResponseClass::Completed, terminal),
Ok(Progress::FetchPartition { partition: 1, .. })
));
let result = machine.on_partition(ResponseClass::Completed, 1, br#"[]"#);
assert!(matches!(
result,
Err(LifecycleError {
code: LifecycleErrorCode::PartitionRowMismatch,
..
})
));
}
#[test]
fn inline_partition_info_row_count_mismatch_is_rejected() {
let terminal = br#"{"resultSetMetaData":{"numRows":1,"format":"jsonv2",
"rowType":[{"name":"A","type":"TEXT","nullable":false}],
"partitionInfo":[{"rowCount":2,"compressedSize":1,"uncompressedSize":1}]},
"data":[["x"]],"code":"090001","statementHandle":"h"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
assert!(matches!(
machine.on_submit(ResponseClass::Completed, terminal),
Err(LifecycleError {
code: LifecycleErrorCode::PartitionRowMismatch,
..
})
));
}
#[test]
fn single_partition_row_count_mismatch_is_rejected() {
let terminal = br#"{"resultSetMetaData":{"numRows":5,"format":"jsonv2",
"rowType":[{"name":"A","type":"TEXT","nullable":false}],
"partitionInfo":[{"rowCount":5,"compressedSize":1,"uncompressedSize":1}]},
"data":[["x"]],"code":"090001","statementHandle":"h"}"#;
let mut machine = StatementMachine::new(PollPlan::default());
assert!(matches!(
machine.on_submit(ResponseClass::Completed, terminal),
Err(LifecycleError {
code: LifecycleErrorCode::PartitionRowMismatch,
..
})
));
}
}