use arrow::array::{RecordBatch, RecordBatchReader};
use arrow::datatypes::SchemaRef;
use arrow::error::ArrowError;
use crate::egress::Cursor;
use crate::egress::arrow::convert::external_arrow_error;
use crate::error::{Error, ErrorCode};
pub struct CursorRecordBatchReader<'r, 'c> {
cursor: &'c mut Cursor<'r>,
schema: SchemaRef,
pending: Option<RecordBatch>,
poisoned: bool,
resets_at_pin: u32,
}
impl<'r, 'c> CursorRecordBatchReader<'r, 'c> {
pub(crate) fn new(cursor: &'c mut Cursor<'r>) -> Result<Self, Error> {
let first = cursor.next_arrow_batch_inner(None, false)?.ok_or_else(|| {
Error::new(
ErrorCode::NoSchema,
"no batch produced; nothing to snapshot",
)
})?;
let schema = first.schema();
let resets_at_pin = cursor.failover_resets();
Ok(Self {
cursor,
schema,
pending: Some(first),
poisoned: false,
resets_at_pin,
})
}
pub fn schema(&self) -> SchemaRef {
self.schema.clone()
}
pub(crate) fn failover_resets(&self) -> u32 {
self.cursor.failover_resets()
}
}
impl Iterator for CursorRecordBatchReader<'_, '_> {
type Item = Result<RecordBatch, ArrowError>;
fn next(&mut self) -> Option<Self::Item> {
if self.poisoned {
return None;
}
if let Some(rb) = self.pending.take() {
return Some(Ok(rb));
}
let drift_check = if self.cursor.failover_resets() == self.resets_at_pin {
Some(&self.schema)
} else {
None
};
match self.cursor.next_arrow_batch_inner(drift_check, false) {
Ok(Some(rb)) => {
if self.cursor.failover_resets() != self.resets_at_pin {
if rb.schema() != self.schema {
self.poisoned = true;
return Some(Err(external_arrow_error(Error::new(
ErrorCode::SchemaDrift,
"post-failover replay returned a different schema; \
a RecordBatchReader schema must be stable for the \
reader's lifetime; use Cursor::next_arrow_batch to \
handle drift explicitly",
))));
}
self.resets_at_pin = self.cursor.failover_resets();
} else if has_tentative_array(&self.schema) && rb.schema() != self.schema {
self.poisoned = true;
return Some(Err(external_arrow_error(Error::new(
ErrorCode::SchemaDrift,
"tentative→firm ndim upgrade is not representable in \
RecordBatchReader (schema must be stable for the \
reader's lifetime); use Cursor::next_arrow_batch \
to handle drift explicitly",
))));
}
Some(Ok(rb))
}
Ok(None) => {
self.poisoned = true;
None
}
Err(e) => {
self.poisoned = true;
Some(Err(external_arrow_error(e)))
}
}
}
}
pub fn has_tentative_array(schema: &SchemaRef) -> bool {
schema.fields().iter().any(|f| {
f.metadata()
.get(crate::arrow_metadata::ARRAY_DIM_TENTATIVE)
.is_some_and(|v| v == "true")
})
}
impl RecordBatchReader for CursorRecordBatchReader<'_, '_> {
fn schema(&self) -> SchemaRef {
self.schema.clone()
}
}
pub fn try_downcast_questdb(err: &ArrowError) -> Option<&Error> {
match err {
ArrowError::ExternalError(boxed) => boxed.downcast_ref::<Error>(),
_ => None,
}
}