use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use asupersync::Cx;
use futures_core::Stream;
use oracledb_protocol::thin::{ColumnMetadata, ExecuteOptions, QueryResult, QueryValue};
use crate::request::QueryDeadline;
use crate::rows::first_cursor_from_result;
use crate::{columns_require_define, Connection, Cursor, Error, Params, Query, Result};
type OwnedRow = Vec<Option<QueryValue>>;
type FetchOutcome = (Connection, Result<QueryResult>);
type FetchFuture = Pin<Box<dyn Future<Output = FetchOutcome>>>;
enum OwnedRowStreamState {
Buffered(VecDeque<OwnedRow>),
Fetching(FetchFuture),
Done,
Poisoned,
}
#[must_use = "an OwnedRowStream holds the connection until drained or recovered with into_connection()"]
pub struct OwnedRowStream {
connection: Option<Connection>,
cx: Cx,
columns: Arc<[ColumnMetadata]>,
cursor: Option<Cursor>,
cursor_id: u32,
arraysize: u32,
timeout: Option<Duration>,
more_rows: bool,
previous_row: Option<OwnedRow>,
state: OwnedRowStreamState,
}
impl OwnedRowStream {
fn from_first_page(
connection: Option<Connection>,
cx: Cx,
arraysize: u32,
timeout: Option<Duration>,
result: QueryResult,
) -> Self {
let cursor_id = result.cursor_id;
let more_rows = result.more_rows;
let cursor = first_cursor_from_result(&result);
let columns: Arc<[ColumnMetadata]> = Arc::from(result.columns.into_boxed_slice());
let batch: VecDeque<OwnedRow> = result.rows.into_iter().collect();
let previous_row = batch.back().cloned();
Self {
connection,
cx,
columns,
cursor,
cursor_id,
arraysize,
timeout,
more_rows,
previous_row,
state: OwnedRowStreamState::Buffered(batch),
}
}
pub fn columns(&self) -> &[ColumnMetadata] {
&self.columns
}
pub fn cursor(&self) -> Option<&Cursor> {
self.cursor.as_ref()
}
pub fn into_connection(mut self) -> Result<Connection> {
match self.connection.take() {
Some(mut connection) => {
if self.cursor_id != 0 {
connection.release_cursor(self.cursor_id);
self.cursor_id = 0;
}
Ok(connection)
}
None => Err(Error::ConnectionClosed(
"owned row stream was poisoned by a dropped in-flight fetch; \
the connection cannot be recovered"
.to_string(),
)),
}
}
fn release_cursor_if_open(&mut self) {
if self.cursor_id != 0 {
if let Some(connection) = &mut self.connection {
connection.release_cursor(self.cursor_id);
}
self.cursor_id = 0;
self.more_rows = false;
}
}
fn apply_page(&mut self, result: QueryResult) {
if self.cursor.is_none() {
if let Some(cursor) = first_cursor_from_result(&result) {
self.cursor = Some(cursor);
}
}
if result.cursor_id != 0 {
self.cursor_id = result.cursor_id;
}
self.more_rows = result.more_rows;
let columns = result.columns;
if !columns.is_empty() {
self.columns = Arc::from(columns.into_boxed_slice());
}
let batch: VecDeque<OwnedRow> = result.rows.into_iter().collect();
if let Some(last) = batch.back() {
self.previous_row = Some(last.clone());
}
self.state = OwnedRowStreamState::Buffered(batch);
}
}
fn build_fetch_future(
mut connection: Connection,
cx: Cx,
cursor_id: u32,
arraysize: u32,
timeout: Option<Duration>,
columns: Arc<[ColumnMetadata]>,
previous_row: Option<OwnedRow>,
) -> FetchFuture {
Box::pin(async move {
let deadline = QueryDeadline::new(&cx, timeout);
let outcome = match deadline
.run(connection.fetch_rows_with_columns(
&cx,
cursor_id,
arraysize,
&columns,
previous_row.as_deref(),
))
.await
{
Ok(result) => result,
Err(()) => {
connection
.recover_from_call_timeout(&cx, deadline.timeout_ms())
.await
}
};
(connection, outcome)
})
}
impl Stream for OwnedRowStream {
type Item = Result<OwnedRow>;
fn poll_next(self: Pin<&mut Self>, task_cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
match std::mem::replace(&mut this.state, OwnedRowStreamState::Poisoned) {
OwnedRowStreamState::Buffered(mut batch) => {
if let Some(row) = batch.pop_front() {
this.state = OwnedRowStreamState::Buffered(batch);
return Poll::Ready(Some(Ok(row)));
}
if !this.more_rows || this.cursor_id == 0 {
this.release_cursor_if_open();
this.state = OwnedRowStreamState::Done;
return Poll::Ready(None);
}
let Some(connection) = this.connection.take() else {
this.state = OwnedRowStreamState::Poisoned;
return Poll::Ready(Some(Err(Error::ConnectionClosed(
"owned row stream needs another page but holds no connection"
.to_string(),
))));
};
let future = build_fetch_future(
connection,
this.cx.clone(),
this.cursor_id,
this.arraysize,
this.timeout,
Arc::clone(&this.columns),
this.previous_row.clone(),
);
this.state = OwnedRowStreamState::Fetching(future);
}
OwnedRowStreamState::Fetching(mut future) => {
match future.as_mut().poll(task_cx) {
Poll::Pending => {
this.state = OwnedRowStreamState::Fetching(future);
return Poll::Pending;
}
Poll::Ready((connection, outcome)) => {
this.connection = Some(connection);
match outcome {
Ok(result) => {
this.apply_page(result);
}
Err(err) => {
this.cursor_id = 0;
this.more_rows = false;
this.state = OwnedRowStreamState::Done;
return Poll::Ready(Some(Err(err)));
}
}
}
}
}
OwnedRowStreamState::Done => {
this.state = OwnedRowStreamState::Done;
return Poll::Ready(None);
}
OwnedRowStreamState::Poisoned => {
this.state = OwnedRowStreamState::Poisoned;
return Poll::Ready(None);
}
}
}
}
}
impl Drop for OwnedRowStream {
fn drop(&mut self) {
self.release_cursor_if_open();
}
}
impl std::fmt::Debug for OwnedRowStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state = match &self.state {
OwnedRowStreamState::Buffered(batch) => {
return f
.debug_struct("OwnedRowStream")
.field("has_connection", &self.connection.is_some())
.field("cursor_id", &self.cursor_id)
.field("more_rows", &self.more_rows)
.field("buffered_rows", &batch.len())
.field("columns", &self.columns.len())
.finish();
}
OwnedRowStreamState::Fetching(_) => "Fetching",
OwnedRowStreamState::Done => "Done",
OwnedRowStreamState::Poisoned => "Poisoned",
};
f.debug_struct("OwnedRowStream")
.field("has_connection", &self.connection.is_some())
.field("cursor_id", &self.cursor_id)
.field("more_rows", &self.more_rows)
.field("state", &state)
.field("columns", &self.columns.len())
.finish()
}
}
impl Connection {
pub async fn into_row_stream<'q>(self, cx: &Cx, query: Query<'q>) -> Result<OwnedRowStream> {
let Query {
sql,
params,
arraysize,
prefetch,
prefetch_set: _,
materialize_lobs,
scrollable: _,
timeout,
} = query;
let mut connection = self;
let sql_owned = sql.into_owned();
let binds = crate::sql_convert::resolve_params(&sql_owned, params)?;
let bind_rows = if binds.is_empty() {
Vec::new()
} else {
vec![binds]
};
let deadline = QueryDeadline::new(cx, timeout);
let mut result = match deadline
.run(connection.execute_query_with_bind_rows_and_options_core(
cx,
&sql_owned,
prefetch,
&bind_rows,
ExecuteOptions::default(),
))
.await
{
Ok(result) => result?,
Err(()) => {
return connection
.recover_from_call_timeout(cx, deadline.timeout_ms())
.await
}
};
if materialize_lobs
&& columns_require_define(&result.columns)
&& result.cursor_id != 0
&& result.rows.is_empty()
{
let cursor_id = result.cursor_id;
let columns = result.columns.clone();
let fetched = match deadline
.run(connection.define_and_fetch_rows_with_columns(
cx,
cursor_id,
prefetch.max(1),
&columns,
None,
))
.await
{
Ok(result) => result?,
Err(()) => {
return connection
.recover_from_call_timeout(cx, deadline.timeout_ms())
.await
}
};
result.rows = fetched.rows;
result.more_rows = fetched.more_rows;
if !fetched.columns.is_empty() {
result.columns = fetched.columns;
}
if result.cursor_id == 0 {
result.cursor_id = cursor_id;
}
}
Ok(OwnedRowStream::from_first_page(
Some(connection),
cx.clone(),
arraysize.get(),
timeout,
result,
))
}
pub async fn into_query_stream<'p>(
self,
cx: &Cx,
sql: &str,
params: impl Into<Params<'p>>,
) -> Result<OwnedRowStream> {
self.into_row_stream(cx, Query::owned_sql(sql.to_string()).bind(params))
.await
}
}
#[cfg(test)]
mod tests {
use std::pin::pin;
use std::task::{Context, Poll, Waker};
use asupersync::Cx;
use oracledb_protocol::thin::{ColumnMetadata, QueryResult, QueryValue};
use super::*;
fn cols(names: &[&str]) -> Vec<ColumnMetadata> {
names.iter().map(|n| ColumnMetadata::new(*n, 0)).collect()
}
fn text_row(values: &[&str]) -> OwnedRow {
values
.iter()
.map(|v| Some(QueryValue::Text((*v).to_string())))
.collect()
}
fn with_cx<R>(body: impl FnOnce(Cx) -> R) -> R {
let runtime = crate::build_io_runtime().expect("io runtime builds");
runtime.block_on(async {
let cx = Cx::current().expect("block_on installs an ambient Cx");
body(cx)
})
}
fn drain_buffered(stream: &mut OwnedRowStream) -> Vec<Result<OwnedRow>> {
let mut out = Vec::new();
let mut stream = pin!(stream);
let mut task_cx = Context::from_waker(Waker::noop());
loop {
match stream.as_mut().poll_next(&mut task_cx) {
Poll::Ready(Some(item)) => out.push(item),
Poll::Ready(None) => break,
Poll::Pending => panic!("buffered-only stream must never park"),
}
}
out
}
#[test]
fn buffered_rows_yield_in_order_then_terminate() {
with_cx(|cx| {
let result = QueryResult {
columns: cols(&["A", "B"]),
rows: vec![text_row(&["1", "x"]), text_row(&["2", "y"])],
cursor_id: 7,
more_rows: false,
..QueryResult::default()
};
let mut stream = OwnedRowStream::from_first_page(None, cx, 100, None, result);
assert_eq!(stream.columns().len(), 2);
let drained = drain_buffered(&mut stream);
assert_eq!(drained.len(), 2, "both buffered rows yield");
assert_eq!(drained[0].as_ref().unwrap(), &text_row(&["1", "x"]));
assert_eq!(drained[1].as_ref().unwrap(), &text_row(&["2", "y"]));
assert_eq!(stream.cursor_id, 0, "cursor released on full drain");
assert!(matches!(stream.state, OwnedRowStreamState::Done));
});
}
#[test]
fn empty_single_page_yields_no_rows() {
with_cx(|cx| {
let result = QueryResult {
columns: cols(&["A"]),
rows: Vec::new(),
cursor_id: 0,
more_rows: false,
..QueryResult::default()
};
let mut stream = OwnedRowStream::from_first_page(None, cx, 100, None, result);
assert!(drain_buffered(&mut stream).is_empty());
assert!(matches!(stream.state, OwnedRowStreamState::Done));
});
}
#[test]
fn first_page_seeds_duplicate_column_continuation() {
with_cx(|cx| {
let result = QueryResult {
columns: cols(&["A", "B"]),
rows: vec![text_row(&["1", "x"]), text_row(&["2", "y"])],
cursor_id: 7,
more_rows: true,
..QueryResult::default()
};
let stream = OwnedRowStream::from_first_page(None, cx, 100, None, result);
assert_eq!(stream.previous_row, Some(text_row(&["2", "y"])));
});
}
#[test]
fn empty_continuation_page_keeps_previous_seed() {
with_cx(|cx| {
let first = QueryResult {
columns: cols(&["A"]),
rows: vec![text_row(&["seed"])],
cursor_id: 7,
more_rows: true,
..QueryResult::default()
};
let mut stream = OwnedRowStream::from_first_page(None, cx, 100, None, first);
assert_eq!(stream.previous_row, Some(text_row(&["seed"])));
stream.apply_page(QueryResult {
columns: Vec::new(),
rows: Vec::new(),
cursor_id: 0,
more_rows: true,
..QueryResult::default()
});
assert_eq!(
stream.previous_row,
Some(text_row(&["seed"])),
"empty page keeps the previous seed"
);
assert!(matches!(stream.state, OwnedRowStreamState::Buffered(ref b) if b.is_empty()));
stream.apply_page(QueryResult {
columns: Vec::new(),
rows: vec![text_row(&["p2a"]), text_row(&["p2b"])],
cursor_id: 0,
more_rows: false,
..QueryResult::default()
});
assert_eq!(stream.previous_row, Some(text_row(&["p2b"])));
});
}
#[test]
fn apply_page_adopts_redescribed_cursor_and_columns() {
with_cx(|cx| {
let first = QueryResult {
columns: cols(&["A"]),
rows: vec![text_row(&["1"])],
cursor_id: 7,
more_rows: true,
..QueryResult::default()
};
let mut stream = OwnedRowStream::from_first_page(None, cx, 100, None, first);
assert_eq!(stream.cursor_id, 7);
stream.apply_page(QueryResult {
columns: cols(&["A", "B"]),
rows: vec![text_row(&["2", "z"])],
cursor_id: 9,
more_rows: false,
..QueryResult::default()
});
assert_eq!(stream.cursor_id, 9, "adopts re-described cursor id");
assert_eq!(stream.columns().len(), 2, "adopts re-described columns");
assert!(!stream.more_rows);
});
}
#[test]
fn into_connection_on_poisoned_stream_errors() {
with_cx(|cx| {
let result = QueryResult {
columns: cols(&["A"]),
rows: vec![text_row(&["1"])],
cursor_id: 7,
more_rows: false,
..QueryResult::default()
};
let stream = OwnedRowStream::from_first_page(None, cx, 100, None, result);
let err = stream.into_connection().unwrap_err();
assert!(matches!(err, Error::ConnectionClosed(_)));
});
}
#[test]
fn fetch_without_connection_poisons_cleanly() {
with_cx(|cx| {
let result = QueryResult {
columns: cols(&["A"]),
rows: vec![text_row(&["only"])],
cursor_id: 7,
more_rows: true,
..QueryResult::default()
};
let mut stream = OwnedRowStream::from_first_page(None, cx, 100, None, result);
let mut stream = pin!(&mut stream);
let mut task_cx = Context::from_waker(Waker::noop());
let first = stream.as_mut().poll_next(&mut task_cx);
assert!(matches!(first, Poll::Ready(Some(Ok(_)))));
let second = stream.as_mut().poll_next(&mut task_cx);
match second {
Poll::Ready(Some(Err(Error::ConnectionClosed(_)))) => {}
other => panic!("expected clean ConnectionClosed, got {other:?}"),
}
assert!(matches!(
stream.as_mut().poll_next(&mut task_cx),
Poll::Ready(None)
));
});
}
}