#![forbid(unsafe_code)]
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use std::num::NonZeroU32;
use std::process;
use std::sync::Arc;
use std::time::Duration;
use asupersync::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use asupersync::net::TcpStream;
use asupersync::runtime::{reactor, Runtime, RuntimeBuilder};
use asupersync::sync::Mutex as AsyncMutex;
use asupersync::{time, Cx};
use oracledb_protocol::thin::aq::{
build_aq_array_deq_payload, build_aq_array_enq_payload, build_aq_deq_payload,
build_aq_enq_payload, parse_aq_array_response_with_limits, parse_aq_deq_response_with_limits,
parse_aq_enq_response_with_limits, AqArrayResult, AqDeqOptions, AqDeqResult, AqEnqOptions,
AqMsgProps, AqQueueDesc,
};
use oracledb_protocol::thin::{
adjust_refetch_metadata, build_auth_phase_one_payload,
build_auth_phase_two_payload_with_proxy_with_seq, build_begin_pipeline_piggyback,
build_change_password_payload_with_seq, build_connect_packet_payload, build_data_types_payload,
build_define_fetch_payload_with_seq, build_end_pipeline_payload_with_seq,
build_execute_payload_with_bind_rows_and_options_with_seq, build_fast_auth_phase_one_payload,
build_fast_auth_token_payload, build_fetch_payload_with_seq, build_function_payload_with_seq,
build_function_payload_with_seq_and_token, build_lob_create_temp_payload_with_seq,
build_lob_free_temp_payload_with_seq, build_lob_read_payload_with_seq,
build_lob_trim_payload_with_seq, build_lob_write_payload_with_seq,
build_protocol_negotiation_payload, classic_connect_response_is_complete,
connect_data_fits_inline, parse_accept_payload, parse_auth_response_with_limits,
parse_define_fetch_response_borrowed_with_limits,
parse_define_fetch_response_with_context_and_limits,
parse_fetch_response_with_context_and_limits, parse_lob_create_temp_response_with_limits,
parse_lob_free_temp_response_with_limits, parse_lob_read_response_with_limits,
parse_lob_trim_response_with_limits, parse_lob_write_response_with_limits,
parse_plain_function_response_with_limits, parse_query_response_borrowed_with_limits,
parse_query_response_with_binds_options_columns_and_limits,
parse_tpc_txn_switch_response_with_limits, BindValue, BorrowedFetchResult, ClientCapabilities,
ColumnMetadata, CursorValue, ExecuteOptions, LobReadResult, QueryResult, QueryValueRef,
SessionlessTxnState, TpcChangeStateResponse, TpcSwitchResponse, TpcXid,
TNS_DATA_FLAGS_BEGIN_PIPELINE, TNS_DATA_FLAGS_END_OF_REQUEST, TNS_FUNC_COMMIT, TNS_FUNC_LOGOFF,
TNS_FUNC_PING, TNS_FUNC_ROLLBACK, TNS_MSG_TYPE_END_OF_RESPONSE, TNS_MSG_TYPE_FLUSH_OUT_BINDS,
TNS_PACKET_TYPE_ACCEPT, TNS_PACKET_TYPE_CONNECT, TNS_PACKET_TYPE_DATA,
TNS_PACKET_TYPE_REDIRECT, TNS_PACKET_TYPE_REFUSE, TNS_PACKET_TYPE_RESEND,
TNS_PIPELINE_MODE_ABORT_ON_ERROR, TNS_PIPELINE_MODE_CONTINUE_ON_ERROR, TNS_TPC_TXN_ABORT,
TNS_TPC_TXN_COMMIT, TNS_TPC_TXN_DETACH, TNS_TPC_TXN_POST_DETACH, TNS_TPC_TXN_PREPARE,
TNS_TPC_TXN_START, TNS_TPC_TXN_STATE_ABORTED, TNS_TPC_TXN_STATE_COMMITTED,
TNS_TPC_TXN_STATE_FORGOTTEN, TNS_TPC_TXN_STATE_PREPARE, TNS_TPC_TXN_STATE_READ_ONLY,
TNS_TPC_TXN_STATE_REQUIRES_COMMIT, TPC_TXN_FLAGS_NEW, TPC_TXN_FLAGS_RESUME,
TPC_TXN_FLAGS_SESSIONLESS,
};
use oracledb_protocol::thin::{
build_notify_payload_with_seq, build_subscribe_payload_with_seq,
check_notification_header_with_limits, parse_subscribe_response_with_limits,
try_parse_oac_record_with_limits, NotificationRecord, SubscribeResult, TNS_SUBSCR_OP_REGISTER,
TNS_SUBSCR_OP_UNREGISTER,
};
use oracledb_protocol::thin::{
build_sessionless_piggyback, build_tpc_change_state_payload_with_seq,
build_tpc_switch_payload_with_seq, build_tpc_txn_switch_payload_with_seq,
parse_tpc_change_state_response_with_limits, parse_tpc_switch_response_with_limits,
};
use oracledb_protocol::thin::{TNS_AQ_ARRAY_DEQ, TNS_AQ_ARRAY_ENQ};
use oracledb_protocol::wire::{encode_packet, PacketLengthWidth, ProtocolLimits};
use oracledb_protocol::{
net::{connectstring::Description, EasyConnect, Protocol as NetProtocol},
ClientIdentity,
};
const PYTHON_ORACLEDB_COMPAT_VERSION_NUM: u32 = 0x0400_1000;
const DEFAULT_SDU: usize = 8192;
const MAX_CONNECT_RESEND_ROUNDS: u8 = 8;
fn tns_packet_type_name(packet_type: u8) -> &'static str {
match packet_type {
1 => "CONNECT",
2 => "ACCEPT",
3 => "ACK",
4 => "REFUSE",
5 => "REDIRECT",
6 => "DATA",
7 => "NULL",
9 => "ABORT",
11 => "RESEND",
12 => "MARKER",
13 => "ATTENTION",
14 => "CONTROL",
_ => "unknown",
}
}
const TNS_DATA_PACKET_OVERHEAD: usize = 10;
pub use oracledb_protocol as protocol;
mod fetch_profile {
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::OnceLock;
static READ_NS: AtomicU64 = AtomicU64::new(0);
static DECODE_NS: AtomicU64 = AtomicU64::new(0);
static ENABLED: OnceLock<bool> = OnceLock::new();
static FORCE: AtomicBool = AtomicBool::new(false);
#[inline]
pub(crate) fn enabled() -> bool {
FORCE.load(Ordering::Relaxed)
|| *ENABLED.get_or_init(|| std::env::var_os("ORACLEDB_PROFILE_FETCH").is_some())
}
#[inline]
pub(crate) fn add_read(ns: u64) {
READ_NS.fetch_add(ns, Ordering::Relaxed);
}
#[inline]
pub(crate) fn add_decode(ns: u64) {
DECODE_NS.fetch_add(ns, Ordering::Relaxed);
}
pub(crate) fn snapshot() -> (u64, u64) {
(
READ_NS.load(Ordering::Relaxed),
DECODE_NS.load(Ordering::Relaxed),
)
}
pub(crate) fn reset() {
READ_NS.store(0, Ordering::Relaxed);
DECODE_NS.store(0, Ordering::Relaxed);
}
pub(crate) fn set_force(on: bool) {
FORCE.store(on, Ordering::Relaxed);
}
}
pub fn fetch_profile_read_decode_ns() -> (u64, u64) {
fetch_profile::snapshot()
}
pub fn fetch_profile_reset() {
fetch_profile::reset();
}
pub fn fetch_profile_arm(on: bool) {
fetch_profile::set_force(on);
}
#[cfg(feature = "arrow")]
#[path = "arrow/mod.rs"]
pub mod arrow;
mod cursor_logic;
#[macro_use]
mod obs;
pub mod pool;
mod recovery;
mod request;
mod rows;
#[cfg(feature = "soda")]
pub mod soda;
mod sql_convert;
pub(crate) mod tls;
pub mod transport;
#[cfg(feature = "tracing")]
#[doc(hidden)]
pub use tracing as __tracing;
#[cfg(not(feature = "tracing"))]
#[doc(hidden)]
pub use obs::ObsSpanGuard;
pub use cursor_logic::{
bind_rows_need_iterative_plsql, ExecutemanyManager, ExecutemanyManagerError,
};
use request::QueryDeadline;
pub use request::{Batch, BatchRows, Execute, Query, Registration, Scroll};
pub use rows::{
BatchError, BatchOutcome, BlockingRows, ExecuteOutcome, OutBinds, RegistrationOutcome,
ReturningRows, Row, Rows,
};
pub use sql_convert::{
BindError, ConversionError, FromRow, FromSql, IntoBinds, Params, QueryResultExt, ToSql,
TypedRow,
};
#[cfg(feature = "derive")]
pub use oracledb_derive::FromRow;
pub mod prelude {
pub use crate::protocol::thin::{BindValue, QueryValue};
pub use crate::protocol::ClientIdentity;
pub use crate::{
params, BlockingConnection, ConnectOptions, Connection, FromRow, Params, QueryResultExt,
};
}
#[cfg(test)]
use recovery::cancel_disposition;
use recovery::{
classify_recovery_result, decode_server_version_number,
observe_cancellation_between_round_trips, post_sync_protocol_error_disposition,
protocol_error_is_session_dead, protocol_error_kind, protocol_error_offset,
protocol_error_ora_code, run_recovery_without_current_cx, CancelDisposition,
PostSyncProtocolDisposition, RecoveryWireAction, SessionRecovery, SessionRecoveryPhase,
CONNECTION_LOST_ORA_CODES, SESSION_DEAD_ORA_CODES, TNS_CCAP_FIELD_VERSION_18_1_EXT_1,
TRANSIENT_ORA_CODES,
};
use transport::{Connector, WireTransport};
type DriverConnector = transport::OracleConnector;
type DriverTransport = <DriverConnector as Connector>::Transport;
type SharedWriteHalf<T = DriverTransport> = Arc<AsyncMutex<<T as WireTransport>::Write>>;
type DriverCore = ConnectionCore<DriverTransport>;
#[derive(Debug)]
struct ConnectionCore<T: WireTransport> {
read: Option<T::Read>,
write: SharedWriteHalf<T>,
recovery: Arc<SessionRecovery>,
protocol_limits: ProtocolLimits,
}
impl<T: WireTransport> ConnectionCore<T> {
fn from_halves(read: T::Read, write: T::Write, write_name: &'static str) -> Self {
Self {
read: Some(read),
write: Arc::new(AsyncMutex::with_name(write_name, write)),
recovery: Arc::new(SessionRecovery::new()),
protocol_limits: ProtocolLimits::DEFAULT,
}
}
fn set_protocol_limits(&mut self, limits: ProtocolLimits) -> Result<()> {
self.protocol_limits = limits.validate()?;
Ok(())
}
fn read_mut(&mut self) -> Result<&mut T::Read> {
self.read.as_mut().ok_or_else(|| {
Error::ConnectionClosed("connection read half unavailable during recovery".into())
})
}
fn take_read(&mut self) -> Result<T::Read> {
self.read.take().ok_or_else(|| {
Error::ConnectionClosed("connection read half unavailable during recovery".into())
})
}
fn write_handle(&self) -> SharedWriteHalf<T> {
Arc::clone(&self.write)
}
async fn write_all(&self, cx: &Cx, packet: &[u8]) -> Result<()> {
write_all_shared(cx, &self.write, packet).await
}
async fn shutdown_write(&self, cx: &Cx) -> Result<()> {
shutdown_write_shared(cx, &self.write).await
}
async fn send_data_packet(&self, cx: &Cx, payload: &[u8], sdu: usize) -> Result<()> {
send_data_packet_shared(cx, &self.write, payload, sdu).await
}
async fn send_data_packet_with_flags(
&self,
cx: &Cx,
payload: &[u8],
sdu: usize,
first_packet_flags: u16,
last_packet_flags: u16,
) -> Result<()> {
send_data_packet_shared_with_flags(
cx,
&self.write,
payload,
sdu,
first_packet_flags,
last_packet_flags,
)
.await
}
async fn read_packet(&mut self, width: PacketLengthWidth) -> Result<IncomingPacket> {
let limits = self.protocol_limits;
let result = read_packet_with_limits(self.read_mut()?, width, limits).await;
self.note_post_sync_result(result)
}
async fn read_data_response(&mut self, cx: &Cx) -> Result<Vec<u8>> {
let write = Arc::clone(&self.write);
let limits = self.protocol_limits;
let result = read_data_response_with_limits(self.read_mut()?, cx, &write, limits).await;
self.note_post_sync_result(result)
}
async fn read_classic_data_response(&mut self, cx: &Cx) -> Result<Vec<u8>> {
let write = Arc::clone(&self.write);
let limits = self.protocol_limits;
let result =
read_classic_data_response_with_limits(self.read_mut()?, cx, &write, limits).await;
self.note_post_sync_result(result)
}
async fn read_data_response_probed(
&mut self,
cx: &Cx,
classic: bool,
probe: impl Fn(&[u8]) -> bool,
) -> Result<Vec<u8>> {
if !classic {
return self.read_data_response(cx).await;
}
let write = Arc::clone(&self.write);
let limits = self.protocol_limits;
let result = read_classic_data_response_probed_with_limits(
self.read_mut()?,
cx,
&write,
&probe,
limits,
)
.await;
self.note_post_sync_result(result)
}
async fn read_data_response_flushing_out_binds_probed(
&mut self,
cx: &Cx,
sdu: usize,
classic: bool,
probe: impl Fn(&[u8]) -> bool,
) -> Result<Vec<u8>> {
if !classic {
return self.read_data_response_flushing_out_binds(cx, sdu).await;
}
let write = Arc::clone(&self.write);
let limits = self.protocol_limits;
let result = read_classic_data_response_flushing_out_binds_probed_with_limits(
self.read_mut()?,
cx,
&write,
sdu,
&probe,
limits,
)
.await;
self.note_post_sync_result(result)
}
async fn read_data_response_boundary(
&mut self,
cx: &Cx,
in_pipeline: bool,
) -> Result<DataResponse> {
let write = Arc::clone(&self.write);
let limits = self.protocol_limits;
let result = read_data_response_boundary_with_limits(
self.read_mut()?,
cx,
&write,
in_pipeline,
limits,
)
.await;
self.note_post_sync_result(result)
}
async fn read_data_response_flushing_out_binds(
&mut self,
cx: &Cx,
sdu: usize,
) -> Result<Vec<u8>> {
let write = Arc::clone(&self.write);
let limits = self.protocol_limits;
let result = read_data_response_flushing_out_binds_with_limits(
self.read_mut()?,
cx,
&write,
sdu,
limits,
)
.await;
self.note_post_sync_result(result)
}
fn note_post_sync_result<U>(&self, result: Result<U>) -> Result<U> {
if let Err(Error::Protocol(err)) = &result {
if post_sync_protocol_error_disposition(err) == PostSyncProtocolDisposition::Dead {
self.recovery.mark_dead();
}
}
result
}
fn break_and_drain_wire(&mut self, recovery_timeout: Duration) -> Result<()> {
self.run_recovery_drain(RecoveryWireAction::BreakAndDrain, recovery_timeout)
}
fn cancel_and_drain_wire(&mut self, recovery_timeout: Duration) -> Result<()> {
self.run_recovery_drain(RecoveryWireAction::BreakAndDrain, recovery_timeout)
}
fn drain_cancel_wire(&mut self, recovery_timeout: Duration) -> Result<()> {
self.run_recovery_drain(RecoveryWireAction::DrainCancel, recovery_timeout)
}
fn run_recovery_drain(
&mut self,
action: RecoveryWireAction,
recovery_timeout: Duration,
) -> Result<()> {
let read = self.take_read()?;
let write = Arc::clone(&self.write);
let limits = self.protocol_limits;
let thread = std::thread::Builder::new()
.name("oracledb-recovery-drain".to_string())
.spawn(move || {
let mut read = read;
let result = run_recovery_without_current_cx(
&mut read,
&write,
action,
recovery_timeout,
limits,
);
(read, result)
})
.map_err(|err| {
Error::ConnectionClosed(format!("failed to start recovery drain thread: {err}"))
})?;
match thread.join() {
Ok((read, result)) => {
self.read = Some(read);
result
}
Err(_) => Err(Error::ConnectionClosed(
"recovery drain thread panicked".to_string(),
)),
}
}
}
pub fn render_caret(sql: &str, offset: usize, headline: &str) -> String {
let chars: Vec<char> = sql.chars().collect();
let total = chars.len();
let target = offset.saturating_sub(1).min(total);
let mut line_start = 0usize;
let mut line_no = 1usize;
let mut col = 0usize;
for (i, &c) in chars.iter().enumerate() {
if i == target {
break;
}
if c == '\n' {
line_no += 1;
line_start = i + 1;
col = 0;
} else {
col += 1;
}
}
let mut line_end = line_start;
while line_end < total && chars[line_end] != '\n' {
line_end += 1;
}
let line: String = chars[line_start..line_end].iter().collect();
let gutter = line_no.to_string();
let pad = " ".repeat(gutter.len());
let caret_indent = " ".repeat(col);
format!("{headline}\n{pad} |\n{gutter} | {line}\n{pad} | {caret_indent}^")
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DbmsOutput {
lines: Vec<String>,
line_count: usize,
char_count: usize,
truncated: bool,
}
impl DbmsOutput {
pub fn new(lines: Vec<String>, truncated: bool) -> Self {
let line_count = lines.len();
let char_count = lines.iter().map(|line| line.chars().count()).sum();
Self {
lines,
line_count,
char_count,
truncated,
}
}
pub fn lines(&self) -> &[String] {
&self.lines
}
pub fn line_count(&self) -> usize {
self.line_count
}
pub fn char_count(&self) -> usize {
self.char_count
}
pub fn truncated(&self) -> bool {
self.truncated
}
pub fn into_lines(self) -> Vec<String> {
self.lines
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectAttribute {
pub name: String,
pub type_name: String,
pub type_owner: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CollectionElement {
pub type_name: String,
pub type_owner: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectType {
pub schema: String,
pub name: String,
pub attributes: Vec<ObjectAttribute>,
pub collection_element: Option<CollectionElement>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct DecodedObject {
type_schema: String,
type_name: String,
attributes: Vec<(String, Option<oracledb_protocol::thin::QueryValue>)>,
elements: Option<Vec<Option<oracledb_protocol::thin::QueryValue>>>,
}
impl DecodedObject {
pub fn object(
type_schema: impl Into<String>,
type_name: impl Into<String>,
attributes: Vec<(String, Option<oracledb_protocol::thin::QueryValue>)>,
) -> Self {
Self {
type_schema: type_schema.into(),
type_name: type_name.into(),
attributes,
elements: None,
}
}
pub fn collection(
type_schema: impl Into<String>,
type_name: impl Into<String>,
elements: Vec<Option<oracledb_protocol::thin::QueryValue>>,
) -> Self {
Self {
type_schema: type_schema.into(),
type_name: type_name.into(),
attributes: Vec::new(),
elements: Some(elements),
}
}
pub fn type_schema(&self) -> &str {
&self.type_schema
}
pub fn type_name(&self) -> &str {
&self.type_name
}
pub fn attributes(&self) -> &[(String, Option<oracledb_protocol::thin::QueryValue>)] {
&self.attributes
}
pub fn elements(&self) -> Option<&[Option<oracledb_protocol::thin::QueryValue>]> {
self.elements.as_deref()
}
pub fn is_collection(&self) -> bool {
self.elements.is_some()
}
pub fn into_attributes(self) -> Vec<(String, Option<oracledb_protocol::thin::QueryValue>)> {
self.attributes
}
pub fn into_elements(self) -> Option<Vec<Option<oracledb_protocol::thin::QueryValue>>> {
self.elements
}
}
pub fn decode_object(
value: &oracledb_protocol::thin::ObjectValue,
ty: &ObjectType,
) -> Result<DecodedObject> {
use oracledb_protocol::thin::DbObjectPackedReader;
use oracledb_protocol::wire::{BoundedReader, ProtocolLimits};
let mut reader = DbObjectPackedReader::new(&value.packed_data);
reader
.limits()
.check_response_bytes(value.packed_data.len())?;
reader.read_header()?;
if let Some(elem) = &ty.collection_element {
if elem.type_owner.is_some() {
return Err(oracledb_protocol::ProtocolError::UnsupportedFeature(
"collection of nested object/collection elements is not decodable yet",
)
.into());
}
let _collection_flags = reader.read_u8()?;
let num_elements = reader.read_length()?;
reader.limits().check_object_elements(num_elements)?;
let mut elements: Vec<Option<oracledb_protocol::thin::QueryValue>> =
reader.with_capacity_limited(num_elements, 1, ProtocolLimits::check_object_elements)?;
for _ in 0..num_elements {
let decoded = match reader.read_value_bytes()? {
None => None,
Some(bytes) => Some(decode_object_scalar(&elem.type_name, bytes)?),
};
elements.push(decoded);
}
return Ok(DecodedObject {
type_schema: ty.schema.clone(),
type_name: ty.name.clone(),
attributes: Vec::new(),
elements: Some(elements),
});
}
let mut attributes = Vec::with_capacity(ty.attributes.len());
for attr in &ty.attributes {
if attr.type_owner.is_some() {
return Err(oracledb_protocol::ProtocolError::UnsupportedFeature(
"nested object/collection attribute is not decodable yet",
)
.into());
}
let decoded = match reader.read_value_bytes()? {
None => None,
Some(bytes) => Some(decode_object_scalar(&attr.type_name, bytes)?),
};
attributes.push((attr.name.clone(), decoded));
}
Ok(DecodedObject {
type_schema: ty.schema.clone(),
type_name: ty.name.clone(),
attributes,
elements: None,
})
}
fn decode_object_scalar(
type_name: &str,
bytes: Vec<u8>,
) -> Result<oracledb_protocol::thin::QueryValue> {
use oracledb_protocol::thin::{
decode_datetime_value, decode_dbobject_text, decode_number_value, QueryValue,
};
let v = if type_name.starts_with("TIMESTAMP") {
decode_datetime_value(&bytes)?
} else {
match type_name {
"VARCHAR2" | "VARCHAR" | "CHAR" => {
QueryValue::Text(decode_dbobject_text(&bytes, "DB_TYPE_VARCHAR")?)
}
"NVARCHAR2" | "NCHAR" => {
QueryValue::Text(decode_dbobject_text(&bytes, "DB_TYPE_NCHAR")?)
}
"NUMBER" | "FLOAT" | "INTEGER" => decode_number_value(&bytes)?,
"DATE" => decode_datetime_value(&bytes)?,
"RAW" => QueryValue::Raw(bytes),
_ => {
return Err(oracledb_protocol::ProtocolError::UnsupportedFeature(
"object attribute/element type is not decodable yet",
)
.into())
}
}
};
Ok(v)
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ErrorKind {
Network,
Timeout,
Cancel,
Protocol,
Database,
Conversion,
Pool,
ResourceLimit,
Authentication,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ConnectionDisposition {
Reusable,
Dead,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum RetryHint {
Never,
RetrySameConnectionIfIdempotent,
ReconnectThenRetryIfIdempotent,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Protocol(#[from] oracledb_protocol::ProtocolError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("asupersync runtime error: {0}")]
Runtime(String),
#[error("listener redirected this connection; redirect handling is not implemented yet")]
RedirectUnsupported,
#[error("listener refused connection: {0}")]
ListenerRefused(String),
#[error(
"unexpected TNS packet type {ty} ({name})",
ty = .0,
name = tns_packet_type_name(*.0)
)]
UnexpectedPacket(u8),
#[error("server kept requesting CONNECT resend ({0} rounds); giving up")]
ConnectResendLoop(u8),
#[error("server did not advertise fast authentication")]
FastAuthRequired,
#[error("server response did not contain {0}")]
MissingSessionField(&'static str),
#[error("call timeout of {0} ms exceeded")]
CallTimeout(u32),
#[error("query returned no rows")]
NoRows,
#[error("query returned more than one row")]
TooManyRows,
#[error("ORA-01013: user requested cancel of current operation")]
Cancelled,
#[error("DPY-4011: the database or network closed the connection: {0}")]
ConnectionClosed(String),
#[error("TLS/TCPS error: {0}")]
Tls(String),
#[error("wallet error: {0}")]
Wallet(#[from] oracledb_protocol::tls::wallet::WalletError),
#[error("DPY-3001: access token authentication requires a TLS (TCPS) connection")]
AccessTokenRequiresTcps,
#[error("{0}")]
UnsupportedAuthMode(UnsupportedAuthMode),
#[error("{0}")]
SessionlessTransaction(SessionlessError),
#[error("DPY-5010: internal error: unknown transaction state {0}")]
UnknownTransactionState(u32),
#[error("bind validation failed: {0}")]
Bind(#[from] BindError),
#[error("type conversion failed: {0}")]
Conversion(ConversionError),
#[cfg(feature = "arrow")]
#[error(transparent)]
ArrowConversion(#[from] arrow::ArrowConversionError),
}
pub type Result<T> = std::result::Result<T, Error>;
pub(crate) fn duration_to_millis_saturating(duration: Duration) -> u32 {
duration.as_millis().min(u128::from(u32::MAX)) as u32
}
pub type Cursor = CursorValue;
pub trait ColumnIndex: column_index_private::Sealed {
#[doc(hidden)]
fn resolve(self, columns: &[ColumnMetadata]) -> std::result::Result<usize, ConversionError>;
}
mod column_index_private {
pub trait Sealed {}
}
impl column_index_private::Sealed for usize {}
impl ColumnIndex for usize {
fn resolve(self, columns: &[ColumnMetadata]) -> std::result::Result<usize, ConversionError> {
if self < columns.len() {
Ok(self)
} else {
Err(ConversionError::OutOfRange {
expected: "column index",
detail: format!("no column at index {self}"),
})
}
}
}
impl column_index_private::Sealed for &str {}
impl ColumnIndex for &str {
fn resolve(self, columns: &[ColumnMetadata]) -> std::result::Result<usize, ConversionError> {
columns
.iter()
.position(|col| col.name().eq_ignore_ascii_case(self))
.ok_or_else(|| ConversionError::OutOfRange {
expected: "column name",
detail: format!("no column named {self:?}"),
})
}
}
impl Error {
pub fn kind(&self) -> ErrorKind {
match self {
Error::Protocol(err) => protocol_error_kind(err),
Error::Io(_)
| Error::ListenerRefused(_)
| Error::ConnectionClosed(_)
| Error::Tls(_)
| Error::Wallet(_) => ErrorKind::Network,
Error::CallTimeout(_) => ErrorKind::Timeout,
Error::Cancelled => ErrorKind::Cancel,
Error::Conversion(_) | Error::Bind(_) => ErrorKind::Conversion,
#[cfg(feature = "arrow")]
Error::ArrowConversion(_) => ErrorKind::Conversion,
Error::AccessTokenRequiresTcps | Error::UnsupportedAuthMode(_) => {
ErrorKind::Authentication
}
Error::RedirectUnsupported
| Error::Runtime(_)
| Error::FastAuthRequired
| Error::UnexpectedPacket(_)
| Error::ConnectResendLoop(_)
| Error::MissingSessionField(_)
| Error::SessionlessTransaction(_)
| Error::UnknownTransactionState(_) => ErrorKind::Protocol,
Error::NoRows | Error::TooManyRows => ErrorKind::Database,
}
}
pub fn resource_limit(&self) -> Option<oracledb_protocol::ResourceLimit> {
match self {
Error::Protocol(err) => err.resource_limit(),
_ => None,
}
}
pub fn ora_code(&self) -> Option<i32> {
match self {
Error::Protocol(err) => protocol_error_ora_code(err).map(|code| code as i32),
Error::Cancelled => Some(1013),
_ => None,
}
}
pub fn oracle_code(&self) -> Option<i32> {
self.ora_code()
}
pub fn offset(&self) -> Option<i32> {
match self {
Error::Protocol(err) => protocol_error_offset(err),
_ => None,
}
}
pub fn caret(&self, sql: &str) -> Option<String> {
let offset = usize::try_from(self.offset()?).ok()?;
let full = self.to_string();
let headline = full.lines().next().unwrap_or(full.as_str());
Some(render_caret(sql, offset, headline))
}
pub fn connection_disposition(&self) -> ConnectionDisposition {
match self {
Error::Io(_) | Error::ConnectionClosed(_) => ConnectionDisposition::Dead,
_ if self
.ora_code()
.is_some_and(|code| SESSION_DEAD_ORA_CODES.contains(&(code as u32))) =>
{
ConnectionDisposition::Dead
}
_ => ConnectionDisposition::Reusable,
}
}
pub fn is_connection_lost(&self) -> bool {
match self {
Error::Io(_) | Error::ConnectionClosed(_) => true,
_ => self
.ora_code()
.is_some_and(|code| CONNECTION_LOST_ORA_CODES.contains(&(code as u32))),
}
}
pub fn is_transient(&self) -> bool {
matches!(self, Error::CallTimeout(_) | Error::Cancelled)
|| self
.ora_code()
.is_some_and(|code| TRANSIENT_ORA_CODES.contains(&(code as u32)))
}
pub fn retry_hint(&self) -> RetryHint {
if self.is_transient() {
RetryHint::RetrySameConnectionIfIdempotent
} else if self.is_connection_lost() {
RetryHint::ReconnectThenRetryIfIdempotent
} else {
RetryHint::Never
}
}
pub fn is_retryable(&self) -> bool {
!matches!(self.retry_hint(), RetryHint::Never)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SessionlessError {
DifferingMethods,
AlreadyActive,
Inactive,
}
impl SessionlessError {
pub fn full_code(self) -> &'static str {
match self {
Self::DifferingMethods => "DPY-3034",
Self::AlreadyActive => "DPY-3035",
Self::Inactive => "DPY-3036",
}
}
pub fn message(self) -> &'static str {
match self {
Self::DifferingMethods => {
"suspending or resuming a Sessionless Transaction can be done with \
DBMS_TRANSACTION or with python-oracledb, but not both"
}
Self::AlreadyActive => {
"suspend, commit, or rollback the current active sessionless \
transaction before beginning or resuming another one"
}
Self::Inactive => "no Sessionless Transaction is active",
}
}
}
impl std::fmt::Display for SessionlessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.full_code(), self.message())
}
}
fn refetch_retry_applies(err: &Error) -> bool {
let message = match err {
Error::Protocol(oracledb_protocol::ProtocolError::ServerError(message)) => message,
Error::Protocol(oracledb_protocol::ProtocolError::ServerErrorWithRowCount {
message,
..
}) => message,
Error::Protocol(oracledb_protocol::ProtocolError::ServerErrorInfo(details)) => {
return details.code == 932 || details.code == 1007;
}
_ => return false,
};
message.starts_with("ORA-00932") || message.starts_with("ORA-01007")
}
#[derive(Clone)]
pub struct AccessToken(String);
const REDACTED_SECRET: &str = "***redacted***";
impl AccessToken {
pub fn new(token: impl Into<String>) -> Self {
Self(token.into())
}
pub(crate) fn expose(&self) -> &str {
&self.0
}
}
impl std::fmt::Debug for AccessToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("AccessToken(")?;
f.write_str(REDACTED_SECRET)?;
f.write_str(")")
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum AuthModeKind {
Password,
Proxy,
External,
IamToken,
Kerberos,
Radius,
}
impl std::fmt::Display for AuthModeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::Password => "password",
Self::Proxy => "proxy",
Self::External => "external",
Self::IamToken => "iam-token",
Self::Kerberos => "kerberos",
Self::Radius => "radius",
};
f.write_str(name)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum AuthModeSupport {
Supported,
UnsupportedInThin,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct AuthCapabilities {
pub password: AuthModeSupport,
pub proxy: AuthModeSupport,
pub external: AuthModeSupport,
pub iam_token: AuthModeSupport,
pub kerberos: AuthModeSupport,
pub radius: AuthModeSupport,
}
impl AuthCapabilities {
pub const THIN: Self = Self {
password: AuthModeSupport::Supported,
proxy: AuthModeSupport::Supported,
external: AuthModeSupport::UnsupportedInThin,
iam_token: AuthModeSupport::Supported,
kerberos: AuthModeSupport::UnsupportedInThin,
radius: AuthModeSupport::UnsupportedInThin,
};
#[must_use]
pub fn support(self, mode: AuthModeKind) -> AuthModeSupport {
match mode {
AuthModeKind::Password => self.password,
AuthModeKind::Proxy => self.proxy,
AuthModeKind::External => self.external,
AuthModeKind::IamToken => self.iam_token,
AuthModeKind::Kerberos => self.kerberos,
AuthModeKind::Radius => self.radius,
}
}
}
#[derive(Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum AuthMode {
Password,
Proxy,
External,
IamToken,
Kerberos {
principal: Option<String>,
keytab: Option<String>,
},
Radius {
challenge: Option<String>,
},
}
impl AuthMode {
#[must_use]
pub fn kind(&self) -> AuthModeKind {
match self {
Self::Password => AuthModeKind::Password,
Self::Proxy => AuthModeKind::Proxy,
Self::External => AuthModeKind::External,
Self::IamToken => AuthModeKind::IamToken,
Self::Kerberos { .. } => AuthModeKind::Kerberos,
Self::Radius { .. } => AuthModeKind::Radius,
}
}
fn unsupported_in_thin(&self) -> Option<UnsupportedAuthMode> {
let mode = self.kind();
(AuthCapabilities::THIN.support(mode) == AuthModeSupport::UnsupportedInThin)
.then_some(UnsupportedAuthMode { mode })
}
}
impl std::fmt::Debug for AuthMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn redacted(value: &Option<String>) -> Option<&'static str> {
value.as_ref().map(|_| REDACTED_SECRET)
}
match self {
Self::Password => f.write_str("Password"),
Self::Proxy => f.write_str("Proxy"),
Self::External => f.write_str("External"),
Self::IamToken => f.write_str("IamToken"),
Self::Kerberos { principal, keytab } => f
.debug_struct("Kerberos")
.field("principal", &redacted(principal))
.field("keytab", &redacted(keytab))
.finish(),
Self::Radius { challenge } => f
.debug_struct("Radius")
.field("challenge", &redacted(challenge))
.finish(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
#[error("authentication mode {mode} is not supported by this thin build")]
pub struct UnsupportedAuthMode {
mode: AuthModeKind,
}
impl UnsupportedAuthMode {
#[must_use]
pub fn mode(&self) -> AuthModeKind {
self.mode
}
}
#[derive(Clone)]
pub struct ConnectOptions {
connect_string: String,
user: String,
password: String,
identity: ClientIdentity,
app_context: Vec<(String, String, String)>,
sdu: u16,
proxy_user: Option<String>,
auth_mode: AuthMode,
server_type_emon: bool,
wallet_location: Option<String>,
wallet_password: Option<String>,
edition: Option<String>,
ssl_server_dn_match: bool,
ssl_server_cert_dn: Option<String>,
use_sni: bool,
access_token: Option<AccessToken>,
statement_cache_size: usize,
protocol_limits: ProtocolLimits,
}
impl std::fmt::Debug for ConnectOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let wallet_location = self.wallet_location.as_ref().map(|_| REDACTED_SECRET);
let wallet_password = self.wallet_password.as_ref().map(|_| REDACTED_SECRET);
let server_cert_dn = self.ssl_server_cert_dn.as_ref().map(|_| REDACTED_SECRET);
f.debug_struct("ConnectOptions")
.field("connect_string", &self.connect_string)
.field("user", &self.user)
.field("password", &REDACTED_SECRET)
.field("identity", &self.identity)
.field("app_context", &self.app_context)
.field("sdu", &self.sdu)
.field("proxy_user", &self.proxy_user)
.field("auth_mode", &self.auth_mode)
.field("server_type_emon", &self.server_type_emon)
.field("wallet_location", &wallet_location)
.field("wallet_password", &wallet_password)
.field("edition", &self.edition)
.field("ssl_server_dn_match", &self.ssl_server_dn_match)
.field("ssl_server_cert_dn", &server_cert_dn)
.field("use_sni", &self.use_sni)
.field("access_token", &self.access_token)
.field("statement_cache_size", &self.statement_cache_size)
.field("protocol_limits", &self.protocol_limits)
.finish()
}
}
impl ConnectOptions {
pub fn new(
connect_string: impl Into<String>,
user: impl Into<String>,
password: impl Into<String>,
identity: ClientIdentity,
) -> Self {
Self {
connect_string: connect_string.into(),
user: user.into(),
password: password.into(),
identity,
app_context: Vec::new(),
sdu: 8192,
proxy_user: None,
auth_mode: AuthMode::Password,
server_type_emon: false,
wallet_location: None,
wallet_password: None,
ssl_server_dn_match: true,
ssl_server_cert_dn: None,
use_sni: false,
edition: None,
access_token: None,
statement_cache_size: STATEMENT_CACHE_SIZE,
protocol_limits: ProtocolLimits::DEFAULT,
}
}
pub fn external_auth(connect_string: impl Into<String>, identity: ClientIdentity) -> Self {
let mut options = Self::new(connect_string, "", "", identity);
options.auth_mode = AuthMode::External;
options
}
pub fn kerberos_auth(
connect_string: impl Into<String>,
principal: impl Into<String>,
keytab: impl Into<String>,
identity: ClientIdentity,
) -> Self {
let mut options = Self::new(connect_string, "", "", identity);
options.auth_mode = AuthMode::Kerberos {
principal: Some(principal.into()),
keytab: Some(keytab.into()),
};
options
}
pub fn radius_auth(
connect_string: impl Into<String>,
challenge: impl Into<String>,
identity: ClientIdentity,
) -> Self {
let mut options = Self::new(connect_string, "", "", identity);
options.auth_mode = AuthMode::Radius {
challenge: Some(challenge.into()),
};
options
}
#[must_use]
pub fn with_protocol_limits(mut self, limits: ProtocolLimits) -> Self {
self.protocol_limits = limits;
self
}
#[must_use]
pub fn with_statement_cache_size(mut self, size: usize) -> Self {
self.statement_cache_size = size;
self
}
#[must_use]
pub fn with_access_token(mut self, token: impl Into<String>) -> Self {
self.access_token = Some(AccessToken::new(token));
self.auth_mode = AuthMode::IamToken;
self
}
#[must_use]
pub fn with_external_auth(mut self) -> Self {
self.auth_mode = AuthMode::External;
self.user.clear();
self.password.clear();
self
}
#[must_use]
pub fn with_kerberos_auth(
mut self,
principal: impl Into<String>,
keytab: impl Into<String>,
) -> Self {
self.auth_mode = AuthMode::Kerberos {
principal: Some(principal.into()),
keytab: Some(keytab.into()),
};
self.user.clear();
self.password.clear();
self
}
#[must_use]
pub fn with_radius_auth(mut self, challenge: impl Into<String>) -> Self {
self.auth_mode = AuthMode::Radius {
challenge: Some(challenge.into()),
};
self.user.clear();
self.password.clear();
self
}
#[must_use]
pub fn with_edition(mut self, edition: impl Into<String>) -> Self {
self.edition = Some(edition.into());
self
}
#[must_use]
pub fn with_use_sni(mut self, use_sni: bool) -> Self {
self.use_sni = use_sni;
self
}
#[must_use]
pub fn with_wallet_location(mut self, location: impl Into<String>) -> Self {
self.wallet_location = Some(location.into());
self
}
#[must_use]
pub fn with_wallet_password(mut self, password: impl Into<String>) -> Self {
self.wallet_password = Some(password.into());
self
}
#[must_use]
pub fn with_ssl_server_dn_match(mut self, enabled: bool) -> Self {
self.ssl_server_dn_match = enabled;
self
}
#[must_use]
pub fn with_ssl_server_cert_dn(mut self, dn: impl Into<String>) -> Self {
self.ssl_server_cert_dn = Some(dn.into());
self
}
pub fn with_server_type_emon(mut self, emon: bool) -> Self {
self.server_type_emon = emon;
self
}
pub fn with_app_context(mut self, app_context: Vec<(String, String, String)>) -> Self {
self.app_context = app_context;
self
}
pub fn with_proxy_user(mut self, proxy_user: Option<String>) -> Self {
if proxy_user.is_some() {
self.auth_mode = AuthMode::Proxy;
} else if matches!(self.auth_mode, AuthMode::Proxy) {
self.auth_mode = AuthMode::Password;
}
self.proxy_user = proxy_user;
self
}
pub fn with_sdu(mut self, sdu: u32) -> Self {
let clamped = sdu.clamp(512, u32::from(u16::MAX));
self.sdu = u16::try_from(clamped).unwrap_or(u16::MAX);
self
}
pub fn connect_string(&self) -> &str {
&self.connect_string
}
pub fn user(&self) -> &str {
&self.user
}
pub fn password(&self) -> &str {
&self.password
}
pub fn identity(&self) -> &ClientIdentity {
&self.identity
}
pub fn app_context(&self) -> &[(String, String, String)] {
&self.app_context
}
pub fn sdu(&self) -> u16 {
self.sdu
}
pub fn proxy_user(&self) -> Option<&str> {
self.proxy_user.as_deref()
}
pub fn auth_mode(&self) -> &AuthMode {
&self.auth_mode
}
pub fn auth_capabilities(&self) -> AuthCapabilities {
AuthCapabilities::THIN
}
pub fn server_type_emon(&self) -> bool {
self.server_type_emon
}
pub fn wallet_location(&self) -> Option<&str> {
self.wallet_location.as_deref()
}
pub fn wallet_password(&self) -> Option<&str> {
self.wallet_password.as_deref()
}
pub fn edition(&self) -> Option<&str> {
self.edition.as_deref()
}
pub fn ssl_server_dn_match(&self) -> bool {
self.ssl_server_dn_match
}
pub fn ssl_server_cert_dn(&self) -> Option<&str> {
self.ssl_server_cert_dn.as_deref()
}
pub fn use_sni(&self) -> bool {
self.use_sni
}
pub fn access_token(&self) -> Option<&AccessToken> {
self.access_token.as_ref()
}
pub fn statement_cache_size(&self) -> usize {
self.statement_cache_size
}
pub fn protocol_limits(&self) -> ProtocolLimits {
self.protocol_limits
}
}
#[derive(Debug)]
pub struct Connection {
descriptor: EasyConnect,
identity: ClientIdentity,
core: DriverCore,
protocol_limits: ProtocolLimits,
session_id: u32,
serial_num: u16,
server_version: Option<String>,
server_version_tuple: Option<(u8, u8, u8, u8, u8)>,
capabilities: ClientCapabilities,
ttc_seq_num: u8,
sdu: usize,
supports_end_of_response: bool,
supports_oob: bool,
cursor_columns: BTreeMap<u32, Vec<ColumnMetadata>>,
fetch_metadata_by_sql: HashMap<String, Vec<ColumnMetadata>>,
fetch_metadata_order: VecDeque<String>,
dead: bool,
user: String,
combo_key: Vec<u8>,
statement_cache: Vec<(String, u32)>,
statement_cache_size: usize,
in_use_cursors: HashSet<u32>,
lob_prefetch_cursors: BTreeSet<u32>,
copied_cursors: HashSet<u32>,
cursors_to_close: Vec<u32>,
sessionless_data: Option<SessionlessData>,
notification_buffer: Vec<u8>,
notification_header_consumed: bool,
transaction_context: Option<Vec<u8>>,
txn_in_progress: bool,
}
#[derive(Clone, Debug)]
struct SessionlessData {
transaction_id: Vec<u8>,
timeout: u32,
operation: u32,
flags: u32,
piggyback_pending: bool,
started_on_server: bool,
}
enum PacketRead {
Appended,
TimedOut,
Closed,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum NotificationOutcome {
Record(NotificationRecord),
TimedOut,
Closed,
}
const STATEMENT_CACHE_SIZE: usize = 20;
#[derive(Clone, Debug)]
pub enum PipelineRequest {
#[non_exhaustive]
Execute {
sql: String,
bind_rows: Vec<Vec<BindValue>>,
prefetch_rows: u32,
},
Commit,
}
impl PipelineRequest {
pub fn execute(
sql: impl Into<String>,
bind_rows: Vec<Vec<BindValue>>,
prefetch_rows: u32,
) -> Self {
Self::Execute {
sql: sql.into(),
bind_rows,
prefetch_rows,
}
}
pub fn commit() -> Self {
Self::Commit
}
pub fn sql(&self) -> Option<&str> {
match self {
Self::Execute { sql, .. } => Some(sql),
Self::Commit => None,
}
}
pub fn bind_rows(&self) -> Option<&[Vec<BindValue>]> {
match self {
Self::Execute { bind_rows, .. } => Some(bind_rows),
Self::Commit => None,
}
}
pub fn prefetch_rows(&self) -> Option<u32> {
match self {
Self::Execute { prefetch_rows, .. } => Some(*prefetch_rows),
Self::Commit => None,
}
}
pub fn is_commit(&self) -> bool {
matches!(self, Self::Commit)
}
}
#[derive(Debug)]
pub struct CancelHandle {
write: SharedWriteHalf,
recovery: Arc<SessionRecovery>,
}
impl Connection {
pub async fn connect(cx: &Cx, options: ConnectOptions) -> Result<Self> {
observe_cancellation_between_round_trips(cx)?;
let protocol_limits = options.protocol_limits.validate()?;
if let Some(unsupported) = options.auth_mode.unsupported_in_thin() {
return Err(Error::UnsupportedAuthMode(unsupported));
}
let descriptor = EasyConnect::parse(&options.connect_string)?;
let full_descriptor = EasyConnect::parse_descriptor(&options.connect_string)?;
let primary_description = full_descriptor.first_description().clone();
let connect_timeout =
transport_connect_timeout_duration(primary_description.tcp_connect_timeout);
let connect_timeout_ms = duration_to_millis_saturating(connect_timeout);
let connect_result = time::timeout(time::wall_now(), connect_timeout, async {
let _span = obs_span!(
"oracledb.connect",
db.system = "oracle",
server.address = %descriptor.host,
server.port = descriptor.port as u64,
db.name = %descriptor.service_name,
);
let token_auth = options.access_token.is_some();
let descriptor_ssl_server_dn_match =
primary_description.security.ssl_server_dn_match && options.ssl_server_dn_match;
let descriptor_ssl_server_cert_dn = options
.ssl_server_cert_dn
.as_deref()
.or(primary_description.security.ssl_server_cert_dn.as_deref());
let identity = options.identity;
trace_connect_step("tcp connect");
let stream = TcpStream::connect_timeout(
(descriptor.host.clone(), descriptor.port),
connect_timeout,
)
.await?;
stream.set_nodelay(true)?;
trace_connect_step("tcp connected");
let connector = DriverConnector::default();
let (read, write) = if descriptor.protocol.is_tls() {
trace_connect_step("tls handshake");
let server_type = if options.server_type_emon {
Some("emon")
} else {
None
};
let tls_params = tls::resolve_tls_params(
&descriptor,
options.wallet_location.as_deref(),
options.wallet_password.as_deref(),
options.ssl_server_dn_match,
options.ssl_server_cert_dn.as_deref(),
options.use_sni,
)?;
let tls_stream =
tls::tls_handshake(&descriptor, server_type, &tls_params, stream).await?;
trace_connect_step("tls established");
connector.tls_split(tls_stream)
} else {
connector.plain_split(stream)
};
let mut core = ConnectionCore::from_halves(read, write, "oracle_tcp_write");
core.set_protocol_limits(protocol_limits)?;
let connect_descriptor = listener_connect_descriptor_with_server(
&descriptor,
&primary_description,
&identity,
options.server_type_emon,
token_auth,
descriptor_ssl_server_dn_match,
descriptor_ssl_server_cert_dn,
);
trace_connect_value("CONNECT descriptor", &connect_descriptor);
let connect_payload = build_connect_packet_payload(&connect_descriptor, options.sdu)?;
let packet = encode_packet(
TNS_PACKET_TYPE_CONNECT,
0,
None,
&connect_payload,
PacketLengthWidth::Legacy16,
)?;
trace_connect_bytes("CONNECT packet", &packet);
let split_connect_data = !connect_data_fits_inline(&connect_descriptor);
let mut resend_rounds = 0u8;
let accept = loop {
trace_connect_step("send CONNECT");
core.write_all(cx, &packet).await?;
if split_connect_data {
trace_connect_step("send CONNECT descriptor (data packet)");
core.send_data_packet(
cx,
connect_descriptor.as_bytes(),
usize::from(options.sdu),
)
.await?;
}
trace_connect_step("read ACCEPT");
let reply = core.read_packet(PacketLengthWidth::Legacy16).await?;
match reply.packet_type {
TNS_PACKET_TYPE_ACCEPT => break reply,
TNS_PACKET_TYPE_RESEND => {
resend_rounds += 1;
if resend_rounds > MAX_CONNECT_RESEND_ROUNDS {
return Err(Error::ConnectResendLoop(resend_rounds));
}
trace_connect_step("RESEND requested; resending CONNECT");
continue;
}
TNS_PACKET_TYPE_REDIRECT => return Err(Error::RedirectUnsupported),
TNS_PACKET_TYPE_REFUSE => {
return Err(Error::ListenerRefused(
String::from_utf8_lossy(&reply.payload).to_string(),
))
}
other => return Err(Error::UnexpectedPacket(other)),
}
};
let accept_info = parse_accept_payload(&accept.payload)?;
let sdu = usize::try_from(accept_info.sdu)
.unwrap_or(DEFAULT_SDU)
.max(TNS_DATA_PACKET_OVERHEAD + 1);
let mut ttc_seq_num = 1;
let auth_connect_string = auth_connect_descriptor(
&descriptor,
&primary_description,
token_auth,
descriptor_ssl_server_dn_match,
descriptor_ssl_server_cert_dn,
);
let (auth_two, capabilities, combo_key) = if let Some(token) = &options.access_token {
if !descriptor.protocol.is_tls() {
return Err(Error::AccessTokenRequiresTcps);
}
if !accept_info.supports_fast_auth {
return Err(Error::FastAuthRequired);
}
let auth_payload = build_fast_auth_token_payload(
&options.user,
token.expose(),
&identity.driver_name,
PYTHON_ORACLEDB_COMPAT_VERSION_NUM,
&auth_connect_string,
options.edition.as_deref(),
)?;
trace_connect_step("send AUTH token (fast-auth phase two)");
core.send_data_packet(cx, &auth_payload, sdu).await?;
trace_connect_step("read AUTH token response");
let response = core.read_data_response(cx).await?;
trace_connect_bytes("AUTH token response", &response);
let auth = parse_auth_response_with_limits(&response, protocol_limits)?;
let capabilities = auth.capabilities.unwrap_or_default();
(auth, capabilities, Vec::new())
} else {
let client_pid = process::id();
let negotiated_capabilities = if accept_info.supports_fast_auth {
None
} else {
let protocol_payload = build_protocol_negotiation_payload()?;
trace_connect_step("send protocol negotiation (classic)");
core.send_data_packet(cx, &protocol_payload, sdu).await?;
trace_connect_step("read protocol negotiation");
let response = core.read_classic_data_response(cx).await?;
trace_connect_bytes("protocol negotiation response", &response);
let negotiated = parse_auth_response_with_limits(&response, protocol_limits)?;
let data_types_payload = build_data_types_payload()?;
trace_connect_step("send data types (classic)");
core.send_data_packet(cx, &data_types_payload, sdu).await?;
trace_connect_step("read data types");
let response = core.read_classic_data_response(cx).await?;
trace_connect_bytes("data types response", &response);
parse_auth_response_with_limits(&response, protocol_limits)?;
Some(negotiated.capabilities.unwrap_or_default())
};
let auth_one = if accept_info.supports_fast_auth {
build_fast_auth_phase_one_payload(
&options.user,
&identity.program,
&identity.machine,
&identity.osuser,
&identity.terminal,
client_pid,
)?
} else {
build_auth_phase_one_payload(
&options.user,
&identity.program,
&identity.machine,
&identity.osuser,
&identity.terminal,
client_pid,
)?
};
trace_connect_bytes("AUTH phase one payload", &auth_one);
trace_connect_step("send AUTH phase one");
core.send_data_packet(cx, &auth_one, sdu).await?;
trace_connect_step("read AUTH phase one");
let auth_one_response = if accept_info.supports_fast_auth {
core.read_data_response(cx).await?
} else {
core.read_classic_data_response(cx).await?
};
trace_connect_bytes("AUTH phase one response", &auth_one_response);
let auth_one =
parse_auth_response_with_limits(&auth_one_response, protocol_limits)?;
let capabilities = negotiated_capabilities
.or(auth_one.capabilities)
.unwrap_or_default();
let verifier_type = auth_one
.verifier_type
.ok_or(Error::MissingSessionField("AUTH_VFR_DATA verifier type"))?;
let encrypted = oracledb_protocol::crypto::generate_verifier(
options.password.as_bytes(),
&auth_one.session_data,
verifier_type,
)?;
let auth_two_payload = build_auth_phase_two_payload_with_proxy_with_seq(
&options.user,
&encrypted,
&identity.driver_name,
PYTHON_ORACLEDB_COMPAT_VERSION_NUM,
&auth_connect_string,
next_ttc_sequence(&mut ttc_seq_num),
&options.app_context,
options.proxy_user.as_deref(),
options.edition.as_deref(),
capabilities.ttc_field_version,
)?;
trace_connect_bytes("AUTH phase two payload", &auth_two_payload);
trace_connect_step("send AUTH phase two");
core.send_data_packet(cx, &auth_two_payload, sdu).await?;
trace_connect_step("read AUTH phase two");
let auth_two_response = if accept_info.supports_fast_auth {
core.read_data_response(cx).await?
} else {
core.read_classic_data_response(cx).await?
};
trace_connect_bytes("AUTH phase two response", &auth_two_response);
let auth_two =
parse_auth_response_with_limits(&auth_two_response, protocol_limits)?;
oracledb_protocol::crypto::verify_server_response(
&encrypted.combo_key,
&auth_two.session_data,
)?;
(auth_two, capabilities, encrypted.combo_key)
};
let session_id = parse_session_u32(&auth_two.session_data, "AUTH_SESSION_ID")?;
let serial_num = parse_session_u16(&auth_two.session_data, "AUTH_SERIAL_NUM")?;
let server_version = auth_two.session_data.get("AUTH_VERSION_STRING").cloned();
let server_version_tuple = auth_two
.session_data
.get("AUTH_VERSION_NO")
.and_then(|value| value.trim().parse::<u32>().ok())
.map(|num| {
decode_server_version_number(
num,
capabilities.ttc_field_version >= TNS_CCAP_FIELD_VERSION_18_1_EXT_1,
)
});
Ok(Self {
descriptor,
identity,
core,
protocol_limits,
session_id,
serial_num,
server_version,
server_version_tuple,
capabilities,
ttc_seq_num,
sdu,
supports_end_of_response: accept_info.supports_end_of_response,
supports_oob: accept_info.supports_oob,
cursor_columns: BTreeMap::new(),
fetch_metadata_by_sql: HashMap::new(),
fetch_metadata_order: VecDeque::new(),
dead: false,
user: options.user,
combo_key,
statement_cache: Vec::new(),
statement_cache_size: options.statement_cache_size,
in_use_cursors: HashSet::new(),
lob_prefetch_cursors: BTreeSet::new(),
copied_cursors: HashSet::new(),
cursors_to_close: Vec::new(),
sessionless_data: None,
notification_buffer: Vec::new(),
notification_header_consumed: false,
transaction_context: None,
txn_in_progress: false,
})
})
.await;
match connect_result {
Ok(result) => result,
Err(_) => Err(Error::CallTimeout(connect_timeout_ms)),
}
}
pub fn descriptor(&self) -> &EasyConnect {
&self.descriptor
}
pub fn identity(&self) -> &ClientIdentity {
&self.identity
}
pub fn session_id(&self) -> u32 {
self.session_id
}
pub fn serial_num(&self) -> u16 {
self.serial_num
}
pub fn server_version(&self) -> Option<&str> {
self.server_version.as_deref()
}
pub fn server_version_tuple(&self) -> Option<(u8, u8, u8, u8, u8)> {
self.server_version_tuple
}
fn supports_oson_long_fnames(&self) -> bool {
self.server_version_tuple
.map(|(major, ..)| major >= 23)
.unwrap_or(false)
}
pub fn sdu(&self) -> usize {
self.sdu
}
pub fn supports_pipelining(&self) -> bool {
self.supports_end_of_response
}
pub fn cancel_handle(&self) -> Result<CancelHandle> {
Ok(CancelHandle {
write: self.core.write_handle(),
recovery: Arc::clone(&self.core.recovery),
})
}
pub fn is_dead(&self) -> bool {
self.dead || self.core.recovery.is_dead()
}
fn note_parse<T>(
&mut self,
result: std::result::Result<T, oracledb_protocol::ProtocolError>,
) -> Result<T> {
match result {
Ok(value) => Ok(value),
Err(err) => {
if protocol_error_is_session_dead(&err) {
self.dead = true;
self.core.recovery.mark_dead();
}
Err(Error::Protocol(err))
}
}
}
pub async fn ping(&mut self, cx: &Cx) -> Result<()> {
self.send_function(cx, TNS_FUNC_PING).await
}
pub async fn change_password(
&mut self,
cx: &Cx,
old_password: &str,
new_password: &str,
) -> Result<()> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let (encoded_password, encoded_newpassword) =
oracledb_protocol::crypto::encrypt_change_password_pair(
&self.combo_key,
old_password.as_bytes(),
new_password.as_bytes(),
)?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_change_password_payload_with_seq(
&self.user,
&encoded_password,
&encoded_newpassword,
seq_num,
self.capabilities.ttc_field_version,
)?;
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let limits = self.protocol_limits;
let response = self
.core
.read_data_response_probed(cx, !self.supports_end_of_response, |bytes| {
classic_connect_response_is_complete(bytes, limits).unwrap_or(true)
})
.await?;
self.note_parse(
parse_auth_response_with_limits(&response, self.protocol_limits).map(|_| ()),
)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn subscribe_register(
&mut self,
cx: &Cx,
namespace: u32,
name: Option<&str>,
public_qos: u32,
operations: u32,
timeout: u32,
grouping_class: u8,
grouping_value: u32,
grouping_type: u8,
) -> Result<SubscribeResult> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_subscribe_payload_with_seq(
seq_num,
TNS_SUBSCR_OP_REGISTER,
Some(&self.user),
None,
namespace,
name,
public_qos,
operations,
timeout,
grouping_class,
grouping_value,
grouping_type,
0,
self.capabilities.ttc_field_version,
)?;
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
self.note_parse(parse_subscribe_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
))
}
#[allow(clippy::too_many_arguments)]
pub async fn subscribe_unregister(
&mut self,
cx: &Cx,
registration_id: u64,
client_id: &[u8],
namespace: u32,
name: Option<&str>,
public_qos: u32,
operations: u32,
timeout: u32,
grouping_class: u8,
grouping_value: u32,
grouping_type: u8,
) -> Result<()> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_subscribe_payload_with_seq(
seq_num,
TNS_SUBSCR_OP_UNREGISTER,
Some(&self.user),
Some(client_id),
namespace,
name,
public_qos,
operations,
timeout,
grouping_class,
grouping_value,
grouping_type,
registration_id,
self.capabilities.ttc_field_version,
)?;
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
self.note_parse(parse_subscribe_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
))?;
Ok(())
}
pub async fn notify_register(&mut self, cx: &Cx, client_id: &[u8]) -> Result<()> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload =
build_notify_payload_with_seq(seq_num, client_id, self.capabilities.ttc_field_version)?;
self.core
.send_data_packet_with_flags(cx, &payload, self.sdu, 0, TNS_DATA_FLAGS_END_OF_REQUEST)
.await?;
Ok(())
}
pub async fn recv_notification(
&mut self,
cx: &Cx,
namespace: u32,
public_qos: u32,
read_timeout: Duration,
) -> Result<NotificationOutcome> {
observe_cancellation_between_round_trips(cx)?;
let db_name = self.descriptor.service_name.clone();
loop {
observe_cancellation_between_round_trips(cx)?;
if !self.notification_header_consumed {
if self.notification_buffer.is_empty() {
match self.read_one_notification_packet(read_timeout).await? {
PacketRead::Appended => continue,
PacketRead::TimedOut => return Ok(NotificationOutcome::TimedOut),
PacketRead::Closed => return Ok(NotificationOutcome::Closed),
}
}
let consumed = check_notification_header_with_limits(
&self.notification_buffer,
self.protocol_limits,
)?;
self.notification_buffer.drain(..consumed);
self.notification_header_consumed = true;
}
if !self.notification_buffer.is_empty() {
if let Some((record, consumed)) = try_parse_oac_record_with_limits(
&self.notification_buffer,
namespace,
public_qos,
Some(&db_name),
self.protocol_limits,
)? {
self.notification_buffer.drain(..consumed);
return Ok(NotificationOutcome::Record(record));
}
}
match self.read_one_notification_packet(read_timeout).await? {
PacketRead::Appended => {}
PacketRead::TimedOut => return Ok(NotificationOutcome::TimedOut),
PacketRead::Closed => return Ok(NotificationOutcome::Closed),
}
}
}
async fn read_one_notification_packet(&mut self, read_timeout: Duration) -> Result<PacketRead> {
let read = self.core.read_packet(PacketLengthWidth::Large32);
let packet = match time::timeout(time::wall_now(), read_timeout, read).await {
Ok(Ok(packet)) => packet,
Ok(Err(_)) => return Ok(PacketRead::Closed),
Err(_) => return Ok(PacketRead::TimedOut),
};
if packet.packet_type != TNS_PACKET_TYPE_DATA {
return Ok(PacketRead::Closed);
}
let Some((_data_flags, payload)) = packet.payload.split_at_checked(2) else {
return Ok(PacketRead::Closed);
};
self.notification_buffer.extend_from_slice(payload);
Ok(PacketRead::Appended)
}
pub async fn ping_with_timeout(&mut self, cx: &Cx, timeout_ms: u32) -> Result<()> {
if timeout_ms == 0 {
return self.ping(cx).await;
}
match time::timeout(
time::wall_now(),
Duration::from_millis(u64::from(timeout_ms)),
self.ping(cx),
)
.await
{
Ok(result) => result,
Err(_) => self.recover_from_call_timeout(cx, timeout_ms).await,
}
}
pub async fn commit(&mut self, cx: &Cx) -> Result<()> {
let _span = obs_span!("oracledb.commit");
self.send_function(cx, TNS_FUNC_COMMIT).await?;
self.sessionless_data = None;
Ok(())
}
pub async fn rollback(&mut self, cx: &Cx) -> Result<()> {
let _span = obs_span!("oracledb.rollback");
self.send_function(cx, TNS_FUNC_ROLLBACK).await?;
self.sessionless_data = None;
Ok(())
}
pub async fn enable_dbms_output(&mut self, cx: &Cx, buffer_bytes: Option<u32>) -> Result<()> {
let arg = buffer_bytes.map_or_else(|| "null".to_string(), |n| n.to_string());
self.execute_query_with_bind_rows_and_options_core(
cx,
&format!("begin dbms_output.enable({arg}); end;"),
0,
&[],
ExecuteOptions::default(),
)
.await?;
Ok(())
}
pub async fn read_dbms_output(
&mut self,
cx: &Cx,
max_lines: usize,
max_chars: usize,
) -> Result<DbmsOutput> {
const NUMBER_SIZE: u32 = 22;
let mut out = DbmsOutput::default();
loop {
observe_cancellation_between_round_trips(cx)?;
let binds = [vec![
BindValue::Output {
ora_type_num: oracledb_protocol::thin::ORA_TYPE_NUM_VARCHAR,
csfrm: oracledb_protocol::thin::CS_FORM_IMPLICIT,
buffer_size: 32767,
},
BindValue::Output {
ora_type_num: oracledb_protocol::thin::ORA_TYPE_NUM_NUMBER,
csfrm: 0,
buffer_size: NUMBER_SIZE,
},
]];
let res = self
.execute_query_with_bind_rows_and_options_core(
cx,
"begin dbms_output.get_line(:1, :2); end;",
0,
&binds,
ExecuteOptions::default(),
)
.await?;
let status = res
.out_values
.get(1)
.and_then(|(_, v)| v.as_ref())
.and_then(oracledb_protocol::thin::QueryValue::as_i64)
.unwrap_or(1);
if status != 0 {
break; }
let line = res
.out_values
.first()
.and_then(|(_, v)| v.as_ref())
.and_then(oracledb_protocol::thin::QueryValue::as_text)
.unwrap_or("")
.to_string();
let chars = line.chars().count();
if out.lines.len() >= max_lines || out.char_count + chars > max_chars {
out.truncated = true;
break;
}
out.char_count += chars;
out.lines.push(line);
out.line_count += 1;
}
Ok(out)
}
async fn start_sessionless_transaction(
&mut self,
cx: &Cx,
transaction_id: &[u8],
timeout: u32,
flags: u32,
defer_round_trip: bool,
) -> Result<()> {
if self.sessionless_data.is_some() {
return Err(Error::SessionlessTransaction(
SessionlessError::AlreadyActive,
));
}
let data = SessionlessData {
transaction_id: transaction_id.to_vec(),
timeout,
operation: TNS_TPC_TXN_START,
flags,
piggyback_pending: defer_round_trip,
started_on_server: false,
};
if defer_round_trip {
self.sessionless_data = Some(data);
return Ok(());
}
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_tpc_txn_switch_payload_with_seq(
seq_num,
0,
data.operation,
data.flags | TPC_TXN_FLAGS_SESSIONLESS,
data.timeout,
Some(transaction_id),
);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
let state = self.note_parse(parse_tpc_txn_switch_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
))?;
self.sessionless_data = Some(data);
self.apply_sessionless_state(state);
Ok(())
}
pub async fn begin_sessionless_transaction(
&mut self,
cx: &Cx,
transaction_id: &[u8],
timeout: u32,
defer_round_trip: bool,
) -> Result<()> {
self.start_sessionless_transaction(
cx,
transaction_id,
timeout,
TPC_TXN_FLAGS_NEW,
defer_round_trip,
)
.await
}
pub async fn resume_sessionless_transaction(
&mut self,
cx: &Cx,
transaction_id: &[u8],
timeout: u32,
defer_round_trip: bool,
) -> Result<()> {
self.start_sessionless_transaction(
cx,
transaction_id,
timeout,
TPC_TXN_FLAGS_RESUME,
defer_round_trip,
)
.await
}
pub async fn suspend_sessionless_transaction(&mut self, cx: &Cx) -> Result<()> {
match &self.sessionless_data {
None => return Err(Error::SessionlessTransaction(SessionlessError::Inactive)),
Some(data) if data.started_on_server => {
return Err(Error::SessionlessTransaction(
SessionlessError::DifferingMethods,
));
}
Some(_) => {}
}
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_tpc_txn_switch_payload_with_seq(
seq_num,
0,
TNS_TPC_TXN_DETACH,
TPC_TXN_FLAGS_SESSIONLESS,
0,
None,
);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
let state = self.note_parse(parse_tpc_txn_switch_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
))?;
self.sessionless_data = None;
self.apply_sessionless_state(state);
Ok(())
}
async fn tpc_switch_round_trip(
&mut self,
cx: &Cx,
operation: u32,
flags: u32,
timeout: u32,
xid: Option<&TpcXid<'_>>,
context: Option<&[u8]>,
) -> Result<TpcSwitchResponse> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload =
build_tpc_switch_payload_with_seq(seq_num, operation, flags, timeout, xid, context);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
self.note_parse(parse_tpc_switch_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
))
}
async fn tpc_change_state_round_trip(
&mut self,
cx: &Cx,
operation: u32,
requested_state: u32,
xid: Option<&TpcXid<'_>>,
context: Option<&[u8]>,
) -> Result<TpcChangeStateResponse> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_tpc_change_state_payload_with_seq(
seq_num,
operation,
requested_state,
0,
xid,
context,
);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
self.note_parse(parse_tpc_change_state_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
))
}
pub async fn tpc_begin(
&mut self,
cx: &Cx,
format_id: u32,
global_transaction_id: &[u8],
branch_qualifier: &[u8],
flags: u32,
timeout: u32,
) -> Result<()> {
let xid = TpcXid {
format_id,
global_transaction_id,
branch_qualifier,
};
let response = self
.tpc_switch_round_trip(cx, TNS_TPC_TXN_START, flags, timeout, Some(&xid), None)
.await?;
self.transaction_context = Some(response.context);
self.txn_in_progress = response.txn_in_progress;
Ok(())
}
pub async fn tpc_end(
&mut self,
cx: &Cx,
xid: Option<(u32, &[u8], &[u8])>,
flags: u32,
) -> Result<()> {
let xid = xid.map(|(format_id, gtid, bqual)| TpcXid {
format_id,
global_transaction_id: gtid,
branch_qualifier: bqual,
});
let context = self.transaction_context.clone();
let response = self
.tpc_switch_round_trip(
cx,
TNS_TPC_TXN_DETACH,
flags,
0,
xid.as_ref(),
context.as_deref(),
)
.await?;
self.txn_in_progress = response.txn_in_progress;
self.transaction_context = None;
Ok(())
}
pub async fn tpc_prepare(&mut self, cx: &Cx, xid: Option<(u32, &[u8], &[u8])>) -> Result<bool> {
let xid = xid.map(|(format_id, gtid, bqual)| TpcXid {
format_id,
global_transaction_id: gtid,
branch_qualifier: bqual,
});
let context = self.transaction_context.clone();
let response = self
.tpc_change_state_round_trip(
cx,
TNS_TPC_TXN_PREPARE,
TNS_TPC_TXN_STATE_PREPARE,
xid.as_ref(),
context.as_deref(),
)
.await?;
self.txn_in_progress = response.txn_in_progress;
match response.state {
TNS_TPC_TXN_STATE_REQUIRES_COMMIT => Ok(true),
TNS_TPC_TXN_STATE_READ_ONLY => Ok(false),
other => Err(Error::UnknownTransactionState(other)),
}
}
pub async fn tpc_commit(
&mut self,
cx: &Cx,
xid: Option<(u32, &[u8], &[u8])>,
one_phase: bool,
) -> Result<()> {
let xid = xid.map(|(format_id, gtid, bqual)| TpcXid {
format_id,
global_transaction_id: gtid,
branch_qualifier: bqual,
});
let requested_state = if one_phase {
TNS_TPC_TXN_STATE_READ_ONLY
} else {
TNS_TPC_TXN_STATE_COMMITTED
};
let context = self.transaction_context.clone();
let response = self
.tpc_change_state_round_trip(
cx,
TNS_TPC_TXN_COMMIT,
requested_state,
xid.as_ref(),
context.as_deref(),
)
.await?;
self.txn_in_progress = response.txn_in_progress;
let state = response.state;
let ok = if one_phase {
state == TNS_TPC_TXN_STATE_READ_ONLY || state == TNS_TPC_TXN_STATE_COMMITTED
} else {
state == TNS_TPC_TXN_STATE_FORGOTTEN
};
if !ok {
return Err(Error::UnknownTransactionState(state));
}
self.transaction_context = None;
Ok(())
}
pub async fn tpc_rollback(&mut self, cx: &Cx, xid: Option<(u32, &[u8], &[u8])>) -> Result<()> {
let xid = xid.map(|(format_id, gtid, bqual)| TpcXid {
format_id,
global_transaction_id: gtid,
branch_qualifier: bqual,
});
let context = self.transaction_context.clone();
let response = self
.tpc_change_state_round_trip(
cx,
TNS_TPC_TXN_ABORT,
TNS_TPC_TXN_STATE_ABORTED,
xid.as_ref(),
context.as_deref(),
)
.await?;
self.txn_in_progress = response.txn_in_progress;
if response.state != TNS_TPC_TXN_STATE_ABORTED {
return Err(Error::UnknownTransactionState(response.state));
}
Ok(())
}
pub fn transaction_in_progress(&self) -> bool {
self.txn_in_progress
}
pub fn prepare_sessionless_suspend_on_success(&mut self) -> Result<()> {
match &mut self.sessionless_data {
None => Err(Error::SessionlessTransaction(SessionlessError::Inactive)),
Some(data) if data.started_on_server => Err(Error::SessionlessTransaction(
SessionlessError::DifferingMethods,
)),
Some(data) => {
if data.piggyback_pending {
data.operation |= TNS_TPC_TXN_POST_DETACH;
} else {
data.operation = TNS_TPC_TXN_POST_DETACH;
data.flags = TPC_TXN_FLAGS_SESSIONLESS;
data.piggyback_pending = true;
}
Ok(())
}
}
}
fn take_sessionless_piggyback(&mut self) -> Option<Vec<u8>> {
let data = self.sessionless_data.as_mut()?;
if !data.piggyback_pending {
return None;
}
data.piggyback_pending = false;
let xid = if data.operation & TNS_TPC_TXN_START != 0 {
Some(data.transaction_id.clone())
} else {
None
};
let flags = data.flags | TPC_TXN_FLAGS_SESSIONLESS;
let operation = data.operation;
let timeout = data.timeout;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
Some(build_sessionless_piggyback(
seq_num,
0,
operation,
flags,
timeout,
xid.as_deref(),
))
}
fn apply_sessionless_state(&mut self, state: Option<SessionlessTxnState>) {
match state {
Some(SessionlessTxnState::Unset) => {
self.sessionless_data = None;
self.txn_in_progress = false;
}
Some(SessionlessTxnState::Set { started_on_server }) => {
self.txn_in_progress = true;
match self.sessionless_data.as_mut() {
Some(data) => {
data.started_on_server = started_on_server;
data.piggyback_pending = false;
}
None => {
self.sessionless_data = Some(SessionlessData {
transaction_id: Vec::new(),
timeout: 0,
operation: TNS_TPC_TXN_START,
flags: 0,
piggyback_pending: false,
started_on_server,
});
}
}
}
None => {}
}
}
#[allow(dead_code)]
async fn execute_query_collect_core(
&mut self,
cx: &Cx,
sql: &str,
prefetch_rows: u32,
) -> Result<QueryResult> {
let mut result = self
.execute_query_with_bind_rows_and_options_core(
cx,
sql,
prefetch_rows,
&[],
ExecuteOptions::default(),
)
.await?;
if !columns_require_define(&result.columns) || result.cursor_id == 0 {
return Ok(result);
}
if !result.rows.is_empty() {
return Ok(result);
}
let cursor_id = result.cursor_id;
let columns = result.columns.clone();
let fetched = self
.define_and_fetch_rows_with_columns(cx, cursor_id, prefetch_rows.max(1), &columns, None)
.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(result)
}
pub async fn query<'p>(
&mut self,
cx: &Cx,
sql: &str,
params: impl Into<crate::Params<'p>>,
) -> Result<Rows<'_>> {
self.query_with(cx, Query::owned_sql(sql.to_string()).bind(params))
.await
}
pub async fn query_one<'p>(
&mut self,
cx: &Cx,
sql: &str,
params: impl Into<crate::Params<'p>>,
) -> Result<Row> {
let mut rows = self
.query_with(
cx,
Query::owned_sql(sql.to_string())
.bind(params)
.arraysize(NonZeroU32::new(2).expect("two is non-zero"))
.prefetch(2),
)
.await?;
rows.materialize_for_cardinality(cx).await?;
rows.one()
}
pub async fn query_opt<'p>(
&mut self,
cx: &Cx,
sql: &str,
params: impl Into<crate::Params<'p>>,
) -> Result<Option<Row>> {
let mut rows = self
.query_with(
cx,
Query::owned_sql(sql.to_string())
.bind(params)
.arraysize(NonZeroU32::new(2).expect("two is non-zero"))
.prefetch(2),
)
.await?;
rows.materialize_for_cardinality(cx).await?;
rows.opt()
}
pub async fn query_all<'p>(
&mut self,
cx: &Cx,
sql: &str,
params: impl Into<crate::Params<'p>>,
) -> Result<Vec<Row>> {
self.query(cx, sql, params).await?.collect(cx).await
}
pub async fn query_with<'conn, 'q>(
&'conn mut self,
cx: &Cx,
query: Query<'q>,
) -> Result<Rows<'conn>> {
let Query {
sql,
params,
arraysize,
prefetch,
prefetch_set: _,
materialize_lobs,
scrollable,
timeout,
} = query;
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 exec_options = ExecuteOptions::default().with_scrollable(scrollable);
let deadline = QueryDeadline::new(cx, timeout);
let mut result = match deadline
.run(self.execute_query_with_bind_rows_and_options_core(
cx,
&sql_owned,
prefetch,
&bind_rows,
exec_options,
))
.await
{
Ok(result) => result?,
Err(()) => {
return self
.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(self.define_and_fetch_rows_with_columns(
cx,
cursor_id,
prefetch.max(1),
&columns,
None,
))
.await
{
Ok(result) => result?,
Err(()) => {
return self
.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(Rows::from_result(
self, sql_owned, arraysize, deadline, scrollable, result,
))
}
pub async fn execute<'p>(
&mut self,
cx: &Cx,
sql: &str,
params: impl Into<crate::Params<'p>>,
) -> Result<ExecuteOutcome> {
self.execute_with(cx, Execute::owned_sql(sql.to_string()).bind(params))
.await
}
pub async fn execute_with<'e>(
&mut self,
cx: &Cx,
execute: Execute<'e>,
) -> Result<ExecuteOutcome> {
let Execute {
sql,
params,
timeout,
options,
} = execute;
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 result = match deadline
.run(self.execute_query_with_bind_rows_and_options_core(
cx, &sql_owned, 0, &bind_rows, options,
))
.await
{
Ok(result) => result?,
Err(()) => {
return self
.recover_from_call_timeout(cx, deadline.timeout_ms())
.await
}
};
Ok(ExecuteOutcome::from_query_result(result))
}
pub async fn execute_many<'b>(
&mut self,
cx: &Cx,
sql: &str,
rows: impl Into<crate::BatchRows<'b>>,
) -> Result<BatchOutcome> {
self.execute_many_with(cx, Batch::owned_sql(sql.to_string(), rows))
.await
}
pub async fn execute_many_with<'b>(
&mut self,
cx: &Cx,
batch: Batch<'b>,
) -> Result<BatchOutcome> {
let Batch {
sql,
rows,
timeout,
options,
} = batch;
rows.validate_rectangular()?;
if rows.is_empty() {
return Ok(BatchOutcome::empty(options.arraydmlrowcounts()));
}
let sql_owned = sql.into_owned();
let deadline = QueryDeadline::new(cx, timeout);
let result = match deadline
.run(self.execute_query_with_bind_rows_and_options_core(
cx,
&sql_owned,
0,
rows.as_slice(),
options,
))
.await
{
Ok(result) => result?,
Err(()) => {
return self
.recover_from_call_timeout(cx, deadline.timeout_ms())
.await
}
};
Ok(BatchOutcome::from_query_result(result))
}
pub async fn register_query<'r>(
&mut self,
cx: &Cx,
registration: Registration<'r>,
) -> Result<RegistrationOutcome> {
let Registration {
sql,
params,
registration_id,
timeout,
} = registration;
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 exec_options = ExecuteOptions::default().with_registration_id(registration_id);
let deadline = QueryDeadline::new(cx, timeout);
let result = match deadline
.run(self.execute_query_with_bind_rows_and_options_core(
cx,
&sql_owned,
0,
&bind_rows,
exec_options,
))
.await
{
Ok(result) => result?,
Err(()) => {
return self
.recover_from_call_timeout(cx, deadline.timeout_ms())
.await
}
};
Ok(RegistrationOutcome::from_query_result(result))
}
pub(crate) async fn execute_query_with_binds_core(
&mut self,
cx: &Cx,
sql: &str,
prefetch_rows: u32,
binds: &[BindValue],
) -> Result<QueryResult> {
let bind_rows = if binds.is_empty() {
Vec::new()
} else {
vec![binds.to_vec()]
};
self.execute_query_with_bind_rows_and_options_core(
cx,
sql,
prefetch_rows,
&bind_rows,
ExecuteOptions::default(),
)
.await
}
pub(crate) async fn execute_query_with_bind_rows_and_options_core(
&mut self,
cx: &Cx,
sql: &str,
prefetch_rows: u32,
bind_rows: &[Vec<BindValue>],
exec_options: ExecuteOptions,
) -> Result<QueryResult> {
match self
.execute_query_with_bind_rows_options_adjusted(
cx,
sql,
prefetch_rows,
bind_rows,
exec_options,
)
.await
{
Err(err) if refetch_retry_applies(&err) && statement_is_query(sql) => {
observe_cancellation_between_round_trips(cx)?;
self.forget_fetch_metadata(sql);
self.execute_query_with_bind_rows_options_adjusted(
cx,
sql,
prefetch_rows,
bind_rows,
exec_options,
)
.await
}
other => other,
}
}
async fn execute_query_with_bind_rows_options_adjusted(
&mut self,
cx: &Cx,
sql: &str,
prefetch_rows: u32,
bind_rows: &[Vec<BindValue>],
exec_options: ExecuteOptions,
) -> Result<QueryResult> {
let _span = obs_span!(
"oracledb.execute",
db.statement = %crate::obs::sql_digest(sql),
db.bind_count = bind_rows.first().map_or(0, Vec::len) as u64,
db.bind_rows = bind_rows.len() as u64,
db.rows_fetched = tracing::field::Empty,
);
observe_cancellation_between_round_trips(cx)?;
if !exec_options.scroll_operation() && !exec_options.parse_only() {
crate::sql_convert::validate_bind_rows_shape(sql, bind_rows)?;
}
self.ensure_clean_before_request().await?;
let mut exec_options = exec_options.with_max_string_size(self.capabilities.max_string_size);
if exec_options.suspend_on_success() {
self.prepare_sessionless_suspend_on_success()?;
}
let use_cache = exec_options.cache_statement() && !exec_options.parse_only();
let mut is_copy = false;
if exec_options.cursor_id() == 0 && !exec_options.parse_only() {
if use_cache {
if self.statement_is_in_use(sql) {
is_copy = true;
} else if let Some(cursor_id) = self.statement_cache_get(sql) {
exec_options = exec_options.with_cursor_id(cursor_id);
}
} else if let Some(cursor_id) = self.statement_cache_take(sql) {
exec_options = exec_options.with_cursor_id(cursor_id);
}
}
if exec_options.cursor_id() != 0 && statement_is_query(sql) {
if let Some(columns) = self.cursor_columns.get(&exec_options.cursor_id()) {
if columns.iter().any(|column| {
column.ora_type_num() == oracledb_protocol::thin::ORA_TYPE_NUM_VECTOR
}) {
exec_options = exec_options.with_no_prefetch(true);
}
}
}
let piggyback = self.take_close_cursors_piggyback();
if piggyback.is_none() {
let has_ref_cursor_output = bind_rows.iter().any(|row| {
row.iter().any(|value| {
matches!(
value,
BindValue::Output {
ora_type_num: oracledb_protocol::thin::ORA_TYPE_NUM_CURSOR,
..
}
)
})
});
if has_ref_cursor_output {
let _ = next_ttc_sequence(&mut self.ttc_seq_num);
}
}
self.protocol_limits.check_batch_rows(bind_rows.len())?;
if let Some(first_row) = bind_rows.first() {
self.protocol_limits.check_binds(first_row.len())?;
}
let sessionless_piggyback = self.take_sessionless_piggyback();
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let mut payload = build_execute_payload_with_bind_rows_and_options_with_seq(
sql,
prefetch_rows,
seq_num,
statement_is_query(sql),
bind_rows,
exec_options,
self.capabilities.ttc_field_version,
)?;
if let Some(piggyback_bytes) = sessionless_piggyback {
let mut combined = piggyback_bytes;
combined.extend_from_slice(&payload);
payload = combined;
}
if let Some(mut piggyback_bytes) = piggyback {
piggyback_bytes.extend_from_slice(&payload);
payload = piggyback_bytes;
}
trace_query_bytes("EXECUTE query payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let known_columns = if exec_options.cursor_id() != 0 {
self.cursor_columns
.get(&exec_options.cursor_id())
.cloned()
.unwrap_or_default()
} else {
Vec::new()
};
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let first_bind_row = bind_rows.first().map(Vec::as_slice).unwrap_or(&[]);
let classic = !self.supports_end_of_response;
let response = self
.read_flushing_out_binds_cancellable(cx, classic, |bytes| {
response_complete(&parse_query_response_with_binds_options_columns_and_limits(
bytes,
capabilities,
first_bind_row,
exec_options,
&known_columns,
limits,
))
})
.await?;
trace_query_bytes("EXECUTE query response", &response);
let parsed = parse_query_response_with_binds_options_columns_and_limits(
&response,
self.capabilities,
bind_rows.first().map(Vec::as_slice).unwrap_or(&[]),
exec_options,
&known_columns,
self.protocol_limits,
);
match self.note_parse(parsed) {
Ok(result) => {
if result.cursor_id != 0
&& !result.rows.is_empty()
&& columns_have_lob_prefetch_fields(&result.columns)
{
self.lob_prefetch_cursors.insert(result.cursor_id);
}
self.apply_sessionless_state(result.sessionless_txn_state);
if let Some(txn_in_progress) = result.txn_in_progress {
self.txn_in_progress = txn_in_progress;
}
if is_copy {
if result.cursor_id != 0 {
self.copied_cursors.insert(result.cursor_id);
}
} else if use_cache {
self.statement_cache_put(sql, result.cursor_id);
}
if result.cursor_id != 0 && statement_is_query(sql) && !exec_options.parse_only() {
self.in_use_cursors.insert(result.cursor_id);
}
self.invalidate_bound_ref_cursors(bind_rows);
self.remember_cursor_columns(&result);
obs_record!(_span, db.rows_fetched = result.rows.len() as u64);
if exec_options.parse_only() {
return Ok(result);
}
self.apply_refetch_metadata(cx, sql, result, prefetch_rows.max(2))
.await
}
Err(err) => {
if use_cache {
self.statement_cache_invalidate(sql, exec_options.cursor_id());
}
Err(err)
}
}
}
pub async fn execute_raw(
&mut self,
cx: &Cx,
sql: &str,
prefetch_rows: u32,
bind_rows: &[Vec<BindValue>],
exec_options: ExecuteOptions,
timeout_ms: Option<u32>,
) -> Result<QueryResult> {
self.execute_query_with_bind_rows_options_call_timeout(
cx,
sql,
prefetch_rows,
bind_rows,
exec_options,
timeout_ms,
)
.await
}
pub(crate) async fn execute_query_with_bind_rows_options_call_timeout(
&mut self,
cx: &Cx,
sql: &str,
prefetch_rows: u32,
bind_rows: &[Vec<BindValue>],
exec_options: ExecuteOptions,
timeout_ms: Option<u32>,
) -> Result<QueryResult> {
let Some(timeout_ms) = timeout_ms.filter(|value| *value > 0) else {
return self
.execute_query_with_bind_rows_and_options_core(
cx,
sql,
prefetch_rows,
bind_rows,
exec_options,
)
.await;
};
match time::timeout(
time::wall_now(),
Duration::from_millis(u64::from(timeout_ms)),
self.execute_query_with_bind_rows_and_options_core(
cx,
sql,
prefetch_rows,
bind_rows,
exec_options,
),
)
.await
{
Ok(result) => result,
Err(_) => self.recover_from_call_timeout(cx, timeout_ms).await,
}
}
async fn ensure_clean_before_request(&mut self) -> Result<()> {
if self.core.recovery.phase() == SessionRecoveryPhase::InFlight {
self.core.recovery.mark_break_required();
}
if !self.core.recovery.begin_pending_drain()? {
return Ok(());
}
match self
.core
.cancel_and_drain_wire(BREAK_DRAIN_RECOVERY_TIMEOUT)
{
Ok(()) => {
self.core.recovery.finish_drain_ready();
Ok(())
}
Err(err) => {
self.core.recovery.mark_dead();
self.dead = true;
Err(err)
}
}
}
async fn read_response_cancellable(
&mut self,
cx: &Cx,
classic: bool,
probe: impl Fn(&[u8]) -> bool,
) -> Result<Vec<u8>> {
let recovery = Arc::clone(&self.core.recovery);
let mut guard = CancelDrainGuard::arm(recovery)?;
let response = self
.core
.read_data_response_probed(cx, classic, probe)
.await?;
guard.disarm();
Ok(response)
}
async fn read_flushing_out_binds_cancellable(
&mut self,
cx: &Cx,
classic: bool,
probe: impl Fn(&[u8]) -> bool,
) -> Result<Vec<u8>> {
let recovery = Arc::clone(&self.core.recovery);
let mut guard = CancelDrainGuard::arm(recovery)?;
let response = self
.core
.read_data_response_flushing_out_binds_probed(cx, self.sdu, classic, probe)
.await?;
guard.disarm();
Ok(response)
}
pub async fn fetch_rows(
&mut self,
cx: &Cx,
cursor_id: u32,
arraysize: u32,
previous_row: Option<&[Option<oracledb_protocol::thin::QueryValue>]>,
) -> Result<QueryResult> {
self.fetch_rows_with_columns(cx, cursor_id, arraysize, &[], previous_row)
.await
}
pub async fn fetch_rows_with_columns(
&mut self,
cx: &Cx,
cursor_id: u32,
arraysize: u32,
known_columns: &[ColumnMetadata],
previous_row: Option<&[Option<oracledb_protocol::thin::QueryValue>]>,
) -> Result<QueryResult> {
let _span = obs_span!(
"oracledb.fetch",
db.cursor_id = cursor_id as u64,
db.arraysize = arraysize as u64,
db.rows_fetched = tracing::field::Empty,
);
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let columns = self
.cursor_columns
.get(&cursor_id)
.cloned()
.unwrap_or_else(|| known_columns.to_vec());
let lob_prefetch = self.lob_prefetch_cursors.contains(&cursor_id);
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = if lob_prefetch {
build_define_fetch_payload_with_seq(
cursor_id,
arraysize,
seq_num,
&columns,
self.capabilities.ttc_field_version,
)?
} else {
build_fetch_payload_with_seq(
cursor_id,
arraysize,
seq_num,
self.capabilities.ttc_field_version,
)
};
trace_query_bytes("FETCH payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let classic = !self.supports_end_of_response;
let profile = fetch_profile::enabled();
let read_start = profile.then(time::wall_now);
let response = self
.read_response_cancellable(cx, classic, |bytes| {
response_complete(&if lob_prefetch {
parse_define_fetch_response_with_context_and_limits(
bytes,
capabilities,
&columns,
previous_row,
limits,
)
} else {
parse_fetch_response_with_context_and_limits(
bytes,
capabilities,
&columns,
previous_row,
limits,
)
})
})
.await?;
if let Some(start) = read_start {
fetch_profile::add_read(time::wall_now().duration_since(start));
}
trace_query_bytes("FETCH response", &response);
let decode_start = profile.then(time::wall_now);
let parsed = if lob_prefetch {
parse_define_fetch_response_with_context_and_limits(
&response,
self.capabilities,
&columns,
previous_row,
self.protocol_limits,
)
} else {
parse_fetch_response_with_context_and_limits(
&response,
self.capabilities,
&columns,
previous_row,
self.protocol_limits,
)
};
if let Some(start) = decode_start {
fetch_profile::add_decode(time::wall_now().duration_since(start));
}
let result = self.note_parse(parsed)?;
obs_record!(_span, db.rows_fetched = result.rows.len() as u64);
self.remember_cursor_columns(&result);
Ok(result)
}
pub async fn fetch_rows_ref(
&mut self,
cx: &Cx,
cursor_id: u32,
arraysize: u32,
previous_row: Option<&[Option<oracledb_protocol::thin::QueryValue>]>,
) -> Result<BorrowedFetchResult> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let columns = self
.cursor_columns
.get(&cursor_id)
.cloned()
.unwrap_or_default();
let lob_prefetch = self.lob_prefetch_cursors.contains(&cursor_id);
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = if lob_prefetch {
build_define_fetch_payload_with_seq(
cursor_id,
arraysize,
seq_num,
&columns,
self.capabilities.ttc_field_version,
)?
} else {
build_fetch_payload_with_seq(
cursor_id,
arraysize,
seq_num,
self.capabilities.ttc_field_version,
)
};
trace_query_bytes("FETCH payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let classic = !self.supports_end_of_response;
let profile = fetch_profile::enabled();
let read_start = profile.then(time::wall_now);
let response = self
.read_response_cancellable(cx, classic, |bytes| {
if lob_prefetch {
response_complete(&parse_define_fetch_response_borrowed_with_limits(
bytes,
capabilities,
&columns,
previous_row,
limits,
))
} else {
response_complete(&parse_query_response_borrowed_with_limits(
bytes,
capabilities,
&columns,
previous_row,
limits,
))
}
})
.await?;
if let Some(start) = read_start {
fetch_profile::add_read(time::wall_now().duration_since(start));
}
trace_query_bytes("FETCH response", &response);
let decode_start = profile.then(time::wall_now);
let parsed = if lob_prefetch {
parse_define_fetch_response_borrowed_with_limits(
&response,
self.capabilities,
&columns,
previous_row,
self.protocol_limits,
)
} else {
parse_query_response_borrowed_with_limits(
&response,
self.capabilities,
&columns,
previous_row,
self.protocol_limits,
)
};
if let Some(start) = decode_start {
fetch_profile::add_decode(time::wall_now().duration_since(start));
}
let result = self.note_parse(parsed)?;
if cursor_id != 0 && !result.batch.columns().is_empty() {
self.cursor_columns
.insert(cursor_id, result.batch.columns().to_vec());
}
Ok(result)
}
pub async fn fetch_rows_request(
&mut self,
cx: &Cx,
cursor_id: u32,
arraysize: u32,
) -> Result<()> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let columns = self
.cursor_columns
.get(&cursor_id)
.cloned()
.unwrap_or_default();
let lob_prefetch = self.lob_prefetch_cursors.contains(&cursor_id);
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = if lob_prefetch {
build_define_fetch_payload_with_seq(
cursor_id,
arraysize,
seq_num,
&columns,
self.capabilities.ttc_field_version,
)?
} else {
build_fetch_payload_with_seq(
cursor_id,
arraysize,
seq_num,
self.capabilities.ttc_field_version,
)
};
trace_query_bytes("FETCH payload (prefetch)", &payload);
self.core.recovery.begin_operation()?;
match self.core.send_data_packet(cx, &payload, self.sdu).await {
Ok(()) => Ok(()),
Err(err) => {
self.core.recovery.mark_dead();
self.dead = true;
Err(err)
}
}?;
Ok(())
}
pub async fn fetch_rows_ref_response(
&mut self,
cx: &Cx,
cursor_id: u32,
previous_row: Option<&[Option<oracledb_protocol::thin::QueryValue>]>,
) -> Result<BorrowedFetchResult> {
observe_cancellation_between_round_trips(cx)?;
let columns = self
.cursor_columns
.get(&cursor_id)
.cloned()
.unwrap_or_default();
let lob_prefetch = self.lob_prefetch_cursors.contains(&cursor_id);
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let classic = !self.supports_end_of_response;
let profile = fetch_profile::enabled();
let read_start = profile.then(time::wall_now);
let response = self
.read_response_cancellable(cx, classic, |bytes| {
if lob_prefetch {
response_complete(&parse_define_fetch_response_borrowed_with_limits(
bytes,
capabilities,
&columns,
previous_row,
limits,
))
} else {
response_complete(&parse_query_response_borrowed_with_limits(
bytes,
capabilities,
&columns,
previous_row,
limits,
))
}
})
.await?;
if let Some(start) = read_start {
fetch_profile::add_read(time::wall_now().duration_since(start));
}
trace_query_bytes("FETCH response (prefetch)", &response);
let decode_start = profile.then(time::wall_now);
let parsed = if lob_prefetch {
parse_define_fetch_response_borrowed_with_limits(
&response,
self.capabilities,
&columns,
previous_row,
self.protocol_limits,
)
} else {
parse_query_response_borrowed_with_limits(
&response,
self.capabilities,
&columns,
previous_row,
self.protocol_limits,
)
};
if let Some(start) = decode_start {
fetch_profile::add_decode(time::wall_now().duration_since(start));
}
let result = self.note_parse(parsed)?;
if cursor_id != 0 && !result.batch.columns().is_empty() {
self.cursor_columns
.insert(cursor_id, result.batch.columns().to_vec());
}
Ok(result)
}
pub async fn for_each_row_ref<F>(
&mut self,
cx: &Cx,
sql: &str,
arraysize: u32,
mut callback: F,
) -> Result<()>
where
F: FnMut(&[Option<QueryValueRef<'_>>]) -> Result<()>,
{
let first = self
.execute_query_with_bind_rows_and_options_core(
cx,
sql,
arraysize,
&[],
ExecuteOptions::default(),
)
.await?;
let cursor_id = first.cursor_id;
if cursor_id != 0 && columns_have_lob_prefetch_fields(&first.columns) {
self.lob_prefetch_cursors.insert(cursor_id);
}
for row in &first.rows {
let refs: Vec<Option<QueryValueRef<'_>>> = row
.iter()
.map(|cell| cell.as_ref().map(QueryValueRef::Owned))
.collect();
callback(&refs)?;
}
let mut more_rows = first.more_rows;
let mut previous_row: Option<Vec<Option<oracledb_protocol::thin::QueryValue>>> =
first.rows.last().cloned();
if more_rows && cursor_id != 0 {
self.fetch_rows_request(cx, cursor_id, arraysize).await?;
}
while more_rows && cursor_id != 0 {
let result = self
.fetch_rows_ref_response(cx, cursor_id, previous_row.as_deref())
.await?;
let next_more = result.more_rows;
if next_more {
self.fetch_rows_request(cx, cursor_id, arraysize).await?;
}
let row_count = result.batch.row_count();
let mut last_owned: Option<Vec<Option<oracledb_protocol::thin::QueryValue>>> = None;
let mut row_idx = 0usize;
result.batch.for_each_row_ref(|row| {
if row_idx + 1 == row_count {
last_owned = Some(
row.iter()
.map(|cell| cell.map(|v| v.to_owned_value()))
.collect(),
);
}
row_idx += 1;
callback(row)
})?;
if let Some(last) = last_owned {
previous_row = Some(last);
}
more_rows = next_more;
}
self.release_cursor(cursor_id);
Ok(())
}
pub async fn define_and_fetch_rows_with_columns(
&mut self,
cx: &Cx,
cursor_id: u32,
arraysize: u32,
define_columns: &[ColumnMetadata],
previous_row: Option<&[Option<oracledb_protocol::thin::QueryValue>]>,
) -> Result<QueryResult> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_define_fetch_payload_with_seq(
cursor_id,
arraysize,
seq_num,
define_columns,
self.capabilities.ttc_field_version,
)?;
trace_query_bytes("DEFINE FETCH payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let response = self
.core
.read_data_response_probed(cx, !self.supports_end_of_response, |bytes| {
response_complete(&parse_define_fetch_response_with_context_and_limits(
bytes,
capabilities,
define_columns,
previous_row,
limits,
))
})
.await?;
trace_query_bytes("DEFINE FETCH response", &response);
let result = parse_define_fetch_response_with_context_and_limits(
&response,
self.capabilities,
define_columns,
previous_row,
self.protocol_limits,
)
.map_err(Error::from)?;
if columns_have_lob_prefetch_fields(define_columns) {
self.lob_prefetch_cursors.insert(cursor_id);
if result.cursor_id != 0 {
self.lob_prefetch_cursors.insert(result.cursor_id);
}
} else {
self.lob_prefetch_cursors.remove(&cursor_id);
if result.cursor_id != 0 {
self.lob_prefetch_cursors.remove(&result.cursor_id);
}
}
self.cursor_columns
.insert(cursor_id, define_columns.to_vec());
self.remember_cursor_columns(&result);
Ok(result)
}
pub async fn fetch_cursor(
&mut self,
cx: &Cx,
cursor: &oracledb_protocol::thin::CursorValue,
max_rows: usize,
) -> Result<QueryResult> {
const ARRAYSIZE: usize = 100;
let fetch_size =
|fetched: usize| -> u32 { max_rows.saturating_sub(fetched).clamp(1, ARRAYSIZE) as u32 };
let mut rows: Vec<Vec<Option<oracledb_protocol::thin::QueryValue>>> = Vec::new();
let mut batch = self
.define_and_fetch_rows_with_columns(
cx,
cursor.cursor_id,
fetch_size(0),
&cursor.columns,
None,
)
.await?;
let mut more = batch.more_rows;
let mut cid = if batch.cursor_id != 0 {
batch.cursor_id
} else {
cursor.cursor_id
};
rows.append(&mut batch.rows);
while more && cid != 0 && rows.len() < max_rows {
observe_cancellation_between_round_trips(cx)?;
let previous_row = rows.last().cloned();
let mut next = self
.fetch_rows_with_columns(
cx,
cid,
fetch_size(rows.len()),
&cursor.columns,
previous_row.as_deref(),
)
.await?;
more = next.more_rows;
if next.cursor_id != 0 {
cid = next.cursor_id;
}
rows.append(&mut next.rows);
}
rows.truncate(max_rows);
self.release_cursor(cid);
Ok(QueryResult {
columns: cursor.columns.clone(),
rows,
..Default::default()
})
}
pub async fn describe_object_type(
&mut self,
cx: &Cx,
schema: &str,
type_name: &str,
) -> Result<ObjectType> {
let schema = schema.to_ascii_uppercase();
let name = type_name.to_ascii_uppercase();
let binds = || {
vec![
oracledb_protocol::thin::BindValue::Text(schema.clone()),
oracledb_protocol::thin::BindValue::Text(name.clone()),
]
};
let row_text =
|row: &[Option<oracledb_protocol::thin::QueryValue>], i: usize| -> Option<String> {
match row.get(i) {
Some(Some(v)) => {
oracledb_protocol::thin::QueryValue::as_text(v).map(str::to_string)
}
_ => None,
}
};
let coll = self
.execute_query_with_binds_core(
cx,
"select elem_type_name, elem_type_owner from all_coll_types \
where owner = :1 and type_name = :2",
10,
&binds(),
)
.await?;
if let Some(row) = coll.rows.first() {
return Ok(ObjectType {
schema,
name,
attributes: Vec::new(),
collection_element: Some(CollectionElement {
type_name: row_text(row, 0).unwrap_or_default(),
type_owner: row_text(row, 1),
}),
});
}
let res = self
.execute_query_with_binds_core(
cx,
"select attr_name, attr_type_name, attr_type_owner from all_type_attrs \
where owner = :1 and type_name = :2 order by attr_no",
1000,
&binds(),
)
.await?;
let attributes: Vec<ObjectAttribute> = res
.rows
.iter()
.map(|row| ObjectAttribute {
name: row_text(row, 0).unwrap_or_default(),
type_name: row_text(row, 1).unwrap_or_default(),
type_owner: row_text(row, 2),
})
.collect();
if attributes.is_empty() {
return Err(Error::Protocol(
oracledb_protocol::ProtocolError::UnsupportedFeature(
"object type not found or has no attributes",
),
));
}
Ok(ObjectType {
schema,
name,
attributes,
collection_element: None,
})
}
pub async fn scroll_cursor(
&mut self,
cx: &Cx,
sql: &str,
cursor_id: u32,
arraysize: u32,
fetch_orientation: u32,
fetch_pos: u32,
) -> Result<QueryResult> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let exec_options = ExecuteOptions::default()
.with_max_string_size(self.capabilities.max_string_size)
.with_cursor_id(cursor_id)
.with_scrollable(true)
.with_scroll_operation(true)
.with_fetch_orientation(fetch_orientation)
.with_fetch_pos(fetch_pos)
.with_cache_statement(false);
let piggyback = self.take_close_cursors_piggyback();
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let mut payload = build_execute_payload_with_bind_rows_and_options_with_seq(
sql,
arraysize,
seq_num,
true,
&[],
exec_options,
self.capabilities.ttc_field_version,
)?;
if let Some(mut piggyback_bytes) = piggyback {
piggyback_bytes.extend_from_slice(&payload);
payload = piggyback_bytes;
}
trace_query_bytes("SCROLL payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let known_columns = self
.cursor_columns
.get(&cursor_id)
.cloned()
.unwrap_or_default();
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let response = self
.core
.read_data_response_flushing_out_binds_probed(
cx,
self.sdu,
!self.supports_end_of_response,
|bytes| {
response_complete(&parse_query_response_with_binds_options_columns_and_limits(
bytes,
capabilities,
&[],
exec_options,
&known_columns,
limits,
))
},
)
.await?;
trace_query_bytes("SCROLL response", &response);
let parsed = parse_query_response_with_binds_options_columns_and_limits(
&response,
self.capabilities,
&[],
exec_options,
&known_columns,
self.protocol_limits,
);
let result = self.note_parse(parsed)?;
self.remember_cursor_columns(&result);
Ok(result)
}
pub async fn read_lob(
&mut self,
cx: &Cx,
locator: &[u8],
offset: u64,
amount: u64,
) -> Result<LobReadResult> {
let _span = obs_span!(
"oracledb.lob",
db.operation = "read",
db.lob_offset = offset,
db.lob_amount = amount,
);
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_lob_read_payload_with_seq(
locator,
offset,
amount,
seq_num,
self.capabilities.ttc_field_version,
)?;
trace_query_bytes("LOB READ payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let response = self
.core
.read_data_response_probed(cx, !self.supports_end_of_response, |bytes| {
response_complete(&parse_lob_read_response_with_limits(
bytes,
capabilities,
locator,
limits,
))
})
.await?;
trace_query_bytes("LOB READ response", &response);
self.note_parse(parse_lob_read_response_with_limits(
&response,
self.capabilities,
locator,
self.protocol_limits,
))
}
pub async fn read_lob_with_timeout(
&mut self,
cx: &Cx,
locator: &[u8],
offset: u64,
amount: u64,
timeout_ms: Option<u32>,
) -> Result<LobReadResult> {
self.read_lob_call_timeout(cx, locator, offset, amount, timeout_ms)
.await
}
pub async fn aq_enq_one(
&mut self,
cx: &Cx,
queue: &AqQueueDesc,
props: &AqMsgProps,
enq_options: &AqEnqOptions,
) -> Result<Option<Vec<u8>>> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
self.protocol_limits.check_frame_bytes(queue.name.len())?;
self.protocol_limits
.check_frame_bytes(queue.payload_toid.len())?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_aq_enq_payload(
queue,
props,
enq_options,
seq_num,
self.capabilities.ttc_field_version,
self.supports_oson_long_fnames(),
)?;
trace_query_bytes("AQ ENQ payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
trace_query_bytes("AQ ENQ response", &response);
self.note_parse(parse_aq_enq_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
))
}
pub async fn aq_deq_one(
&mut self,
cx: &Cx,
queue: &AqQueueDesc,
deq_options: &AqDeqOptions,
) -> Result<AqDeqResult> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
self.protocol_limits.check_frame_bytes(queue.name.len())?;
self.protocol_limits
.check_frame_bytes(queue.payload_toid.len())?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_aq_deq_payload(
queue,
deq_options,
seq_num,
self.capabilities.ttc_field_version,
)?;
trace_query_bytes("AQ DEQ payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
trace_query_bytes("AQ DEQ response", &response);
self.note_parse(parse_aq_deq_response_with_limits(
&response,
self.capabilities,
&queue.kind,
self.protocol_limits,
))
}
pub async fn aq_enq_many(
&mut self,
cx: &Cx,
queue: &AqQueueDesc,
props_list: &[AqMsgProps],
enq_options: &AqEnqOptions,
) -> Result<Vec<Vec<u8>>> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
self.protocol_limits.check_batch_rows(props_list.len())?;
self.protocol_limits.check_frame_bytes(queue.name.len())?;
self.protocol_limits
.check_frame_bytes(queue.payload_toid.len())?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_aq_array_enq_payload(
queue,
props_list,
enq_options,
seq_num,
self.capabilities.ttc_field_version,
self.supports_oson_long_fnames(),
)?;
trace_query_bytes("AQ ARRAY ENQ payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
trace_query_bytes("AQ ARRAY ENQ response", &response);
let result: AqArrayResult = self.note_parse(parse_aq_array_response_with_limits(
&response,
self.capabilities,
TNS_AQ_ARRAY_ENQ,
props_list.len() as u32,
&queue.kind,
self.protocol_limits,
))?;
Ok(result.enq_msgids)
}
pub async fn aq_deq_many(
&mut self,
cx: &Cx,
queue: &AqQueueDesc,
deq_options: &AqDeqOptions,
max_num_messages: u32,
) -> Result<Vec<oracledb_protocol::thin::aq::AqDeqMessage>> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
self.protocol_limits
.check_batch_rows(max_num_messages as usize)?;
self.protocol_limits.check_frame_bytes(queue.name.len())?;
self.protocol_limits
.check_frame_bytes(queue.payload_toid.len())?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_aq_array_deq_payload(
queue,
deq_options,
max_num_messages,
seq_num,
self.capabilities.ttc_field_version,
)?;
trace_query_bytes("AQ ARRAY DEQ payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
trace_query_bytes("AQ ARRAY DEQ response", &response);
let result: AqArrayResult = self.note_parse(parse_aq_array_response_with_limits(
&response,
self.capabilities,
TNS_AQ_ARRAY_DEQ,
max_num_messages,
&queue.kind,
self.protocol_limits,
))?;
Ok(result.deq_messages)
}
pub async fn create_temp_lob(
&mut self,
cx: &Cx,
ora_type_num: u8,
csfrm: u8,
) -> Result<LobReadResult> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_lob_create_temp_payload_with_seq(
ora_type_num,
csfrm,
seq_num,
self.capabilities.ttc_field_version,
)?;
trace_query_bytes("LOB CREATE TEMP payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let response = self
.core
.read_data_response_probed(cx, !self.supports_end_of_response, |bytes| {
response_complete(&parse_lob_create_temp_response_with_limits(
bytes,
capabilities,
limits,
))
})
.await?;
trace_query_bytes("LOB CREATE TEMP response", &response);
self.note_parse(parse_lob_create_temp_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
))
}
pub async fn write_lob(
&mut self,
cx: &Cx,
locator: &[u8],
offset: u64,
data: &[u8],
) -> Result<LobReadResult> {
let _span = obs_span!(
"oracledb.lob",
db.operation = "write",
db.lob_offset = offset,
db.lob_bytes = data.len() as u64,
);
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
self.protocol_limits.check_frame_bytes(locator.len())?;
self.protocol_limits.check_frame_bytes(data.len())?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_lob_write_payload_with_seq(
locator,
offset,
data,
seq_num,
self.capabilities.ttc_field_version,
)?;
trace_query_bytes("LOB WRITE payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let response = self
.core
.read_data_response_probed(cx, !self.supports_end_of_response, |bytes| {
response_complete(&parse_lob_write_response_with_limits(
bytes,
capabilities,
locator,
limits,
))
})
.await?;
trace_query_bytes("LOB WRITE response", &response);
self.note_parse(parse_lob_write_response_with_limits(
&response,
self.capabilities,
locator,
self.protocol_limits,
))
}
pub async fn write_lob_with_timeout(
&mut self,
cx: &Cx,
locator: &[u8],
offset: u64,
data: &[u8],
timeout_ms: Option<u32>,
) -> Result<LobReadResult> {
self.write_lob_call_timeout(cx, locator, offset, data, timeout_ms)
.await
}
pub async fn trim_lob(
&mut self,
cx: &Cx,
locator: &[u8],
new_size: u64,
) -> Result<LobReadResult> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
self.protocol_limits.check_frame_bytes(locator.len())?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_lob_trim_payload_with_seq(
locator,
new_size,
seq_num,
self.capabilities.ttc_field_version,
)?;
trace_query_bytes("LOB TRIM payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let response = self
.core
.read_data_response_probed(cx, !self.supports_end_of_response, |bytes| {
response_complete(&parse_lob_trim_response_with_limits(
bytes,
capabilities,
locator,
limits,
))
})
.await?;
trace_query_bytes("LOB TRIM response", &response);
self.note_parse(parse_lob_trim_response_with_limits(
&response,
self.capabilities,
locator,
self.protocol_limits,
))
}
pub async fn trim_lob_with_timeout(
&mut self,
cx: &Cx,
locator: &[u8],
new_size: u64,
timeout_ms: Option<u32>,
) -> Result<LobReadResult> {
self.trim_lob_call_timeout(cx, locator, new_size, timeout_ms)
.await
}
pub async fn free_temp_lobs(&mut self, cx: &Cx, locators: &[Vec<u8>]) -> Result<()> {
observe_cancellation_between_round_trips(cx)?;
if locators.is_empty() {
return Ok(());
}
self.ensure_clean_before_request().await?;
self.protocol_limits.check_lob_chunks(locators.len())?;
let returned_parameter_len = locators.iter().try_fold(0usize, |total, locator| {
self.protocol_limits.check_frame_bytes(locator.len())?;
total.checked_add(locator.len()).ok_or(
oracledb_protocol::ProtocolError::ResourceLimit {
limit: "frame_bytes",
observed: usize::MAX,
maximum: self.protocol_limits.max_frame_bytes,
},
)
})?;
self.protocol_limits
.check_frame_bytes(returned_parameter_len)?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = build_lob_free_temp_payload_with_seq(
locators,
seq_num,
self.capabilities.ttc_field_version,
)?;
trace_query_bytes("LOB FREE TEMP payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let response = self
.core
.read_data_response_probed(cx, !self.supports_end_of_response, |bytes| {
response_complete(&parse_lob_free_temp_response_with_limits(
bytes,
capabilities,
returned_parameter_len,
limits,
))
})
.await?;
trace_query_bytes("LOB FREE TEMP response", &response);
self.note_parse(parse_lob_free_temp_response_with_limits(
&response,
self.capabilities,
returned_parameter_len,
self.protocol_limits,
))
}
pub async fn free_temp_lobs_with_timeout(
&mut self,
cx: &Cx,
locators: &[Vec<u8>],
timeout_ms: Option<u32>,
) -> Result<()> {
self.free_temp_lobs_call_timeout(cx, locators, timeout_ms)
.await
}
#[allow(dead_code)]
async fn execute_query_call_timeout(
&mut self,
cx: &Cx,
sql: &str,
prefetch_rows: u32,
timeout_ms: Option<u32>,
) -> Result<QueryResult> {
let Some(timeout_ms) = timeout_ms.filter(|value| *value > 0) else {
return self
.execute_query_with_bind_rows_and_options_core(
cx,
sql,
prefetch_rows,
&[],
ExecuteOptions::default(),
)
.await;
};
match time::timeout(
time::wall_now(),
Duration::from_millis(u64::from(timeout_ms)),
self.execute_query_with_bind_rows_and_options_core(
cx,
sql,
prefetch_rows,
&[],
ExecuteOptions::default(),
),
)
.await
{
Ok(result) => result,
Err(_) => self.recover_from_call_timeout(cx, timeout_ms).await,
}
}
async fn execute_query_with_binds_call_timeout(
&mut self,
cx: &Cx,
sql: &str,
prefetch_rows: u32,
binds: &[BindValue],
timeout_ms: Option<u32>,
) -> Result<QueryResult> {
let Some(timeout_ms) = timeout_ms.filter(|value| *value > 0) else {
return self
.execute_query_with_binds_core(cx, sql, prefetch_rows, binds)
.await;
};
match time::timeout(
time::wall_now(),
Duration::from_millis(u64::from(timeout_ms)),
self.execute_query_with_binds_core(cx, sql, prefetch_rows, binds),
)
.await
{
Ok(result) => result,
Err(_) => self.recover_from_call_timeout(cx, timeout_ms).await,
}
}
#[allow(dead_code)]
async fn execute_query_with_bind_rows_call_timeout(
&mut self,
cx: &Cx,
sql: &str,
prefetch_rows: u32,
bind_rows: &[Vec<BindValue>],
timeout_ms: Option<u32>,
) -> Result<QueryResult> {
let Some(timeout_ms) = timeout_ms.filter(|value| *value > 0) else {
return self
.execute_query_with_bind_rows_and_options_core(
cx,
sql,
prefetch_rows,
bind_rows,
ExecuteOptions::default(),
)
.await;
};
match time::timeout(
time::wall_now(),
Duration::from_millis(u64::from(timeout_ms)),
self.execute_query_with_bind_rows_and_options_core(
cx,
sql,
prefetch_rows,
bind_rows,
ExecuteOptions::default(),
),
)
.await
{
Ok(result) => result,
Err(_) => self.recover_from_call_timeout(cx, timeout_ms).await,
}
}
async fn read_lob_call_timeout(
&mut self,
cx: &Cx,
locator: &[u8],
offset: u64,
amount: u64,
timeout_ms: Option<u32>,
) -> Result<LobReadResult> {
let Some(timeout_ms) = timeout_ms.filter(|value| *value > 0) else {
return self.read_lob(cx, locator, offset, amount).await;
};
match time::timeout(
time::wall_now(),
Duration::from_millis(u64::from(timeout_ms)),
self.read_lob(cx, locator, offset, amount),
)
.await
{
Ok(result) => result,
Err(_) => self.recover_from_call_timeout(cx, timeout_ms).await,
}
}
async fn write_lob_call_timeout(
&mut self,
cx: &Cx,
locator: &[u8],
offset: u64,
data: &[u8],
timeout_ms: Option<u32>,
) -> Result<LobReadResult> {
let Some(timeout_ms) = timeout_ms.filter(|value| *value > 0) else {
return self.write_lob(cx, locator, offset, data).await;
};
match time::timeout(
time::wall_now(),
Duration::from_millis(u64::from(timeout_ms)),
self.write_lob(cx, locator, offset, data),
)
.await
{
Ok(result) => result,
Err(_) => self.recover_from_call_timeout(cx, timeout_ms).await,
}
}
async fn trim_lob_call_timeout(
&mut self,
cx: &Cx,
locator: &[u8],
new_size: u64,
timeout_ms: Option<u32>,
) -> Result<LobReadResult> {
let Some(timeout_ms) = timeout_ms.filter(|value| *value > 0) else {
return self.trim_lob(cx, locator, new_size).await;
};
match time::timeout(
time::wall_now(),
Duration::from_millis(u64::from(timeout_ms)),
self.trim_lob(cx, locator, new_size),
)
.await
{
Ok(result) => result,
Err(_) => self.recover_from_call_timeout(cx, timeout_ms).await,
}
}
async fn free_temp_lobs_call_timeout(
&mut self,
cx: &Cx,
locators: &[Vec<u8>],
timeout_ms: Option<u32>,
) -> Result<()> {
let Some(timeout_ms) = timeout_ms.filter(|value| *value > 0) else {
return self.free_temp_lobs(cx, locators).await;
};
match time::timeout(
time::wall_now(),
Duration::from_millis(u64::from(timeout_ms)),
self.free_temp_lobs(cx, locators),
)
.await
{
Ok(result) => result,
Err(_) => self.recover_from_call_timeout(cx, timeout_ms).await,
}
}
pub async fn direct_path_prepare(
&mut self,
cx: &Cx,
schema_name: &str,
table_name: &str,
column_names: &[String],
) -> Result<oracledb_protocol::dpl::DirectPathPrepareResult> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
self.protocol_limits.check_columns(column_names.len())?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = oracledb_protocol::dpl::build_direct_path_prepare_payload(
schema_name,
table_name,
column_names,
seq_num,
)?;
trace_query_bytes("DIRECT PATH PREPARE payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
trace_query_bytes("DIRECT PATH PREPARE response", &response);
oracledb_protocol::dpl::parse_direct_path_prepare_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
)
.map_err(Error::from)
}
pub async fn direct_path_load_stream(
&mut self,
cx: &Cx,
cursor_id: u16,
stream: &oracledb_protocol::dpl::DirectPathStream,
) -> Result<()> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload = oracledb_protocol::dpl::build_direct_path_load_stream_payload(
cursor_id, stream, seq_num,
)?;
trace_query_bytes("DIRECT PATH LOAD STREAM payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
trace_query_bytes("DIRECT PATH LOAD STREAM response", &response);
oracledb_protocol::dpl::parse_direct_path_simple_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
)
.map_err(Error::from)
}
pub async fn direct_path_op(&mut self, cx: &Cx, cursor_id: u16, op_code: u32) -> Result<()> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let payload =
oracledb_protocol::dpl::build_direct_path_op_payload(cursor_id, op_code, seq_num);
trace_query_bytes("DIRECT PATH OP payload", &payload);
self.core.send_data_packet(cx, &payload, self.sdu).await?;
let response = self.core.read_data_response(cx).await?;
trace_query_bytes("DIRECT PATH OP response", &response);
oracledb_protocol::dpl::parse_direct_path_simple_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
)
.map_err(Error::from)
}
pub async fn direct_path_load(
&mut self,
cx: &Cx,
schema_name: &str,
table_name: &str,
column_names: &[String],
rows: &[Vec<oracledb_protocol::dpl::DirectPathColumnValue>],
batch_size: u32,
) -> Result<()> {
let prepare = self
.direct_path_prepare(cx, schema_name, table_name, column_names)
.await?;
let load_result = self
.direct_path_load_batches(cx, &prepare, rows, batch_size)
.await;
let op_code = if load_result.is_ok() {
oracledb_protocol::dpl::TNS_DP_OP_FINISH
} else {
oracledb_protocol::dpl::TNS_DP_OP_ABORT
};
let op_result = self.direct_path_op(cx, prepare.cursor_id, op_code).await;
load_result?;
op_result
}
pub async fn direct_path_load_prepared(
&mut self,
cx: &Cx,
prepare: &oracledb_protocol::dpl::DirectPathPrepareResult,
rows: &[Vec<oracledb_protocol::dpl::DirectPathColumnValue>],
batch_size: u32,
) -> Result<()> {
let load_result = self
.direct_path_load_batches(cx, prepare, rows, batch_size)
.await;
let op_code = if load_result.is_ok() {
oracledb_protocol::dpl::TNS_DP_OP_FINISH
} else {
oracledb_protocol::dpl::TNS_DP_OP_ABORT
};
let op_result = self.direct_path_op(cx, prepare.cursor_id, op_code).await;
load_result?;
op_result
}
async fn direct_path_load_batches(
&mut self,
cx: &Cx,
prepare: &oracledb_protocol::dpl::DirectPathPrepareResult,
rows: &[Vec<oracledb_protocol::dpl::DirectPathColumnValue>],
batch_size: u32,
) -> Result<()> {
for row in rows {
if row.len() != prepare.column_metadata.len() {
return Err(oracledb_protocol::ProtocolError::TtcDecode(
"direct path row width does not match column metadata",
)
.into());
}
}
let mut state =
oracledb_protocol::dpl::BatchLoadState::for_rows(rows.len() as u64, batch_size)?;
let mut row_num: u64 = 1;
while !state.is_done() {
observe_cancellation_between_round_trips(cx)?;
let start = usize::try_from(state.offset()).map_err(|_| {
oracledb_protocol::ProtocolError::TtcDecode("direct path offset overflow")
})?;
let end = start + state.num_rows() as usize;
let stream = oracledb_protocol::dpl::encode_direct_path_rows(
&prepare.column_metadata,
&rows[start..end],
row_num,
)?;
row_num += (end - start) as u64;
self.direct_path_load_stream(cx, prepare.cursor_id, &stream)
.await?;
state.next_batch();
}
Ok(())
}
async fn break_and_drain(&mut self) -> Result<()> {
self.core.recovery.begin_drain_after_break()?;
match self.core.break_and_drain_wire(BREAK_DRAIN_RECOVERY_TIMEOUT) {
Ok(()) => {
self.core.recovery.finish_drain_ready();
Ok(())
}
Err(err) => {
self.core.recovery.mark_dead();
self.dead = true;
Err(err)
}
}
}
pub(crate) async fn recover_from_call_timeout<T>(
&mut self,
cx: &Cx,
timeout_ms: u32,
) -> Result<T> {
match self.break_and_drain().await {
Ok(()) => {
let disposition = cx
.cancel_reason()
.map(|reason| CancelDisposition::from_kind(reason.kind))
.unwrap_or(CancelDisposition::Timeout);
if disposition == CancelDisposition::Close {
self.core.recovery.mark_dead();
self.dead = true;
}
Err(disposition.into_error(timeout_ms))
}
Err(closed) => Err(closed),
}
}
async fn drain_cancel_response(&mut self) -> Result<()> {
self.core.recovery.begin_drain_after_break()?;
match self.core.drain_cancel_wire(BREAK_DRAIN_RECOVERY_TIMEOUT) {
Ok(()) => {
self.core.recovery.finish_drain_ready();
Ok(())
}
Err(err) => {
self.core.recovery.mark_dead();
self.dead = true;
Err(err)
}
}
}
pub async fn cancel(&mut self, _cx: &Cx) -> Result<()> {
self.core.recovery.begin_drain_after_break()?;
match self
.core
.cancel_and_drain_wire(BREAK_DRAIN_RECOVERY_TIMEOUT)
{
Ok(()) => {
self.core.recovery.finish_drain_ready();
Ok(())
}
Err(err) => {
self.core.recovery.mark_dead();
self.dead = true;
Err(err)
}
}
}
pub fn supports_oob(&self) -> bool {
self.supports_oob
}
fn remember_cursor_columns(&mut self, result: &QueryResult) {
if result.cursor_id != 0 && !result.columns.is_empty() {
if self.cursor_columns.get(&result.cursor_id) == Some(&result.columns) {
return;
}
self.cursor_columns
.insert(result.cursor_id, result.columns.clone());
}
}
fn remember_fetch_metadata(&mut self, sql: &str, columns: &[ColumnMetadata]) {
const FETCH_METADATA_RETENTION_CAP: usize = 100;
if !self.fetch_metadata_by_sql.contains_key(sql) {
if self.fetch_metadata_order.len() >= FETCH_METADATA_RETENTION_CAP {
if let Some(oldest) = self.fetch_metadata_order.pop_front() {
self.fetch_metadata_by_sql.remove(&oldest);
}
}
self.fetch_metadata_order.push_back(sql.to_string());
}
self.fetch_metadata_by_sql
.insert(sql.to_string(), columns.to_vec());
}
fn forget_fetch_metadata(&mut self, sql: &str) -> bool {
if self.fetch_metadata_by_sql.remove(sql).is_some() {
self.fetch_metadata_order.retain(|entry| entry != sql);
return true;
}
false
}
async fn apply_refetch_metadata(
&mut self,
cx: &Cx,
sql: &str,
mut result: QueryResult,
arraysize: u32,
) -> Result<QueryResult> {
if result.columns.is_empty() {
return Ok(result);
}
if let Some(previous_columns) = self.fetch_metadata_by_sql.get(sql) {
let mut adjusted = result.columns.clone();
let mut any_adjusted = false;
for (index, column) in adjusted.iter_mut().enumerate() {
if let Some(previous) = previous_columns.get(index) {
any_adjusted |= adjust_refetch_metadata(previous, column);
}
}
if any_adjusted && result.cursor_id != 0 {
observe_cancellation_between_round_trips(cx)?;
let cursor_id = result.cursor_id;
let mut redefined = self
.define_and_fetch_rows_with_columns(
cx,
cursor_id,
arraysize.max(1),
&adjusted,
None,
)
.await?;
if redefined.columns.is_empty() {
redefined.columns = adjusted;
}
if redefined.cursor_id == 0 {
redefined.cursor_id = cursor_id;
}
result = redefined;
}
}
self.remember_fetch_metadata(sql, &result.columns);
Ok(result)
}
fn statement_cache_get(&mut self, sql: &str) -> Option<u32> {
let index = self
.statement_cache
.iter()
.position(|(cached_sql, _)| cached_sql == sql)?;
let cursor_id = self.statement_cache[index].1;
if cursor_id != 0 && self.in_use_cursors.contains(&cursor_id) {
return None;
}
let entry = self.statement_cache.remove(index);
self.statement_cache.push(entry);
Some(cursor_id)
}
fn statement_cache_take(&mut self, sql: &str) -> Option<u32> {
let index = self
.statement_cache
.iter()
.position(|(cached_sql, _)| cached_sql == sql)?;
Some(self.statement_cache.remove(index).1)
}
fn statement_cache_put(&mut self, sql: &str, cursor_id: u32) {
let to_close = statement_cache_insert(
&mut self.statement_cache,
self.statement_cache_size,
sql,
cursor_id,
);
for cursor_id in &to_close {
self.lob_prefetch_cursors.remove(cursor_id);
self.cursor_columns.remove(cursor_id);
}
self.cursors_to_close.extend(to_close);
}
fn invalidate_bound_ref_cursors(&mut self, bind_rows: &[Vec<BindValue>]) {
for row in bind_rows {
for value in row {
if let BindValue::Cursor { cursor_id } = value {
if *cursor_id == 0 {
continue;
}
self.statement_cache
.retain(|(_, cached_id)| cached_id != cursor_id);
self.cursor_columns.remove(cursor_id);
self.lob_prefetch_cursors.remove(cursor_id);
}
}
}
}
pub fn release_cursor(&mut self, cursor_id: u32) {
if cursor_id == 0 {
return;
}
self.in_use_cursors.remove(&cursor_id);
if self.copied_cursors.remove(&cursor_id) {
self.cursors_to_close.push(cursor_id);
self.cursor_columns.remove(&cursor_id);
self.lob_prefetch_cursors.remove(&cursor_id);
}
}
pub fn close_cursor(&mut self, cursor_id: u32) {
if cursor_id == 0 {
return;
}
self.in_use_cursors.remove(&cursor_id);
self.copied_cursors.remove(&cursor_id);
self.cursor_columns.remove(&cursor_id);
self.lob_prefetch_cursors.remove(&cursor_id);
if !self.cursors_to_close.contains(&cursor_id) {
self.cursors_to_close.push(cursor_id);
}
}
fn statement_is_in_use(&self, sql: &str) -> bool {
self.statement_cache
.iter()
.find(|(cached_sql, _)| cached_sql == sql)
.is_some_and(|(_, cursor_id)| {
*cursor_id != 0 && self.in_use_cursors.contains(cursor_id)
})
}
fn statement_cache_invalidate(&mut self, sql: &str, cursor_id: u32) {
if let Some(index) = self
.statement_cache
.iter()
.position(|(cached_sql, _)| cached_sql == sql)
{
self.statement_cache.remove(index);
}
if cursor_id != 0 {
self.cursors_to_close.push(cursor_id);
self.cursor_columns.remove(&cursor_id);
self.lob_prefetch_cursors.remove(&cursor_id);
self.in_use_cursors.remove(&cursor_id);
self.copied_cursors.remove(&cursor_id);
}
}
fn take_close_cursors_piggyback(&mut self) -> Option<Vec<u8>> {
if self.cursors_to_close.is_empty() {
return None;
}
let cursor_ids = std::mem::take(&mut self.cursors_to_close);
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
Some(oracledb_protocol::thin::build_close_cursors_piggyback(
&cursor_ids,
seq_num,
self.capabilities.ttc_field_version,
))
}
pub async fn close(mut self, cx: &Cx) -> Result<()> {
observe_cancellation_between_round_trips(cx)?;
match time::timeout(time::wall_now(), Duration::from_secs(5), self.rollback(cx)).await {
Ok(result) => result?,
Err(_) => {
let eof = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(oracledb_protocol::thin::TNS_DATA_FLAGS_EOF),
&[],
PacketLengthWidth::Large32,
)?;
let _ = self.core.write_all(cx, &eof).await;
let _ = self.core.shutdown_write(cx).await;
return Ok(());
}
}
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
self.core
.send_data_packet(
cx,
&build_function_payload_with_seq(
TNS_FUNC_LOGOFF,
seq_num,
self.capabilities.ttc_field_version,
),
self.sdu,
)
.await?;
let capabilities = self.capabilities;
let limits = self.protocol_limits;
if let Ok(response) = time::timeout(
time::wall_now(),
Duration::from_secs(5),
self.core
.read_data_response_probed(cx, !self.supports_end_of_response, |bytes| {
response_complete(&parse_plain_function_response_with_limits(
bytes,
capabilities,
limits,
))
}),
)
.await
{
let _ = response?;
}
let eof = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(oracledb_protocol::thin::TNS_DATA_FLAGS_EOF),
&[],
PacketLengthWidth::Large32,
)?;
self.core.write_all(cx, &eof).await?;
let _ = self.core.shutdown_write(cx).await;
Ok(())
}
pub async fn run_pipeline(
&mut self,
cx: &Cx,
requests: &[PipelineRequest],
continue_on_error: bool,
) -> Result<Vec<Vec<u8>>> {
if !self.supports_end_of_response {
return Err(Error::Protocol(
oracledb_protocol::ProtocolError::UnsupportedFeature(
"pipelining requires END_OF_RESPONSE framing, which this server \
did not negotiate (requires Oracle Database 23ai or later)",
),
));
}
observe_cancellation_between_round_trips(cx)?;
if requests.is_empty() {
return Ok(Vec::new());
}
self.ensure_clean_before_request().await?;
self.protocol_limits
.check_length_prefixed_elements(requests.len())?;
let pipeline_mode = if continue_on_error {
TNS_PIPELINE_MODE_CONTINUE_ON_ERROR
} else {
TNS_PIPELINE_MODE_ABORT_ON_ERROR
};
for (index, request) in requests.iter().enumerate() {
let token_num = index as u64 + 1;
let mut payload = Vec::new();
let mut first_packet_flags = 0u16;
if index == 0 {
if let Some(close_piggyback) = self.take_close_cursors_piggyback() {
payload.extend_from_slice(&close_piggyback);
}
let piggyback_seq = next_ttc_sequence(&mut self.ttc_seq_num);
payload.extend_from_slice(&build_begin_pipeline_piggyback(
piggyback_seq,
token_num,
pipeline_mode,
));
first_packet_flags |= TNS_DATA_FLAGS_BEGIN_PIPELINE;
}
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
match request {
PipelineRequest::Execute {
sql,
bind_rows,
prefetch_rows,
} => {
self.protocol_limits.check_batch_rows(bind_rows.len())?;
if let Some(first_row) = bind_rows.first() {
self.protocol_limits.check_binds(first_row.len())?;
}
payload.extend_from_slice(
&build_execute_payload_with_bind_rows_and_options_with_seq(
sql,
*prefetch_rows,
seq_num,
statement_is_query(sql),
bind_rows,
ExecuteOptions::default()
.with_token_num(token_num)
.with_max_string_size(self.capabilities.max_string_size),
self.capabilities.ttc_field_version,
)?,
);
}
PipelineRequest::Commit => {
payload.extend_from_slice(&build_function_payload_with_seq_and_token(
TNS_FUNC_COMMIT,
seq_num,
token_num,
self.capabilities.ttc_field_version,
));
}
}
trace_query_bytes("PIPELINE op payload", &payload);
self.core
.send_data_packet_with_flags(
cx,
&payload,
self.sdu,
first_packet_flags,
TNS_DATA_FLAGS_END_OF_REQUEST,
)
.await?;
}
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
let end_payload = build_end_pipeline_payload_with_seq(seq_num);
trace_query_bytes("PIPELINE end payload", &end_payload);
self.core
.send_data_packet(cx, &end_payload, self.sdu)
.await?;
let mut responses = Vec::with_capacity(requests.len() + 1);
for _ in 0..=requests.len() {
let response = self.core.read_data_response_boundary(cx, true).await?;
trace_query_bytes("PIPELINE response", &response.payload);
responses.push(response.payload);
}
Ok(responses)
}
pub async fn run_pipeline_decoded(
&mut self,
cx: &Cx,
requests: &[PipelineRequest],
continue_on_error: bool,
) -> Result<Vec<Result<QueryResult>>> {
let raw = self.run_pipeline(cx, requests, continue_on_error).await?;
let mut decoded = Vec::with_capacity(requests.len());
for (index, request) in requests.iter().enumerate() {
let payload = &raw[index];
let outcome = match request {
PipelineRequest::Commit => {
match parse_plain_function_response_with_limits(
payload,
self.capabilities,
self.protocol_limits,
) {
Ok(txn_in_progress) => Ok(QueryResult {
txn_in_progress: Some(txn_in_progress),
..QueryResult::default()
}),
Err(err) => Err(Error::Protocol(err)),
}
}
PipelineRequest::Execute { sql, bind_rows, .. } => {
parse_query_response_with_binds_options_columns_and_limits(
payload,
self.capabilities,
bind_rows.first().map(Vec::as_slice).unwrap_or(&[]),
ExecuteOptions::default(),
&[],
self.protocol_limits,
)
.map_err(Error::Protocol)
.inspect(|result| {
self.remember_cursor_columns(result);
if result.cursor_id != 0 && statement_is_query(sql) {
self.in_use_cursors.insert(result.cursor_id);
}
})
}
};
if let Ok(result) = &outcome {
if let Some(txn_in_progress) = result.txn_in_progress {
self.txn_in_progress = txn_in_progress;
}
}
decoded.push(outcome);
}
Ok(decoded)
}
async fn send_function(&mut self, cx: &Cx, function_code: u8) -> Result<()> {
observe_cancellation_between_round_trips(cx)?;
self.ensure_clean_before_request().await?;
let seq_num = next_ttc_sequence(&mut self.ttc_seq_num);
self.core
.send_data_packet(
cx,
&build_function_payload_with_seq(
function_code,
seq_num,
self.capabilities.ttc_field_version,
),
self.sdu,
)
.await?;
let capabilities = self.capabilities;
let limits = self.protocol_limits;
let response = self
.core
.read_data_response_probed(cx, !self.supports_end_of_response, |bytes| {
response_complete(&parse_plain_function_response_with_limits(
bytes,
capabilities,
limits,
))
})
.await?;
let txn_in_progress = self.note_parse(parse_plain_function_response_with_limits(
&response,
self.capabilities,
self.protocol_limits,
))?;
self.txn_in_progress = txn_in_progress;
Ok(())
}
pub async fn supplement_json_column_metadata(
&mut self,
cx: &Cx,
columns: &mut [ColumnMetadata],
timeout_ms: Option<u32>,
) -> Result<()> {
let candidates = json_lob_probe_candidates(columns);
if candidates.is_empty() {
return Ok(());
}
for (index, column_name) in candidates {
let result = self
.execute_query_with_binds_call_timeout(
cx,
"select 1 \
from all_json_columns \
where owner = sys_context('USERENV', 'CURRENT_SCHEMA') \
and column_name = :1",
1,
&[BindValue::Text(column_name)],
timeout_ms,
)
.await?;
if !result.rows.is_empty() {
columns[index] = columns[index].clone().with_is_json(true);
}
}
Ok(())
}
}
impl CancelHandle {
pub async fn cancel(&mut self, cx: &Cx) -> Result<()> {
observe_cancellation_between_round_trips(cx)?;
if !self.should_send_break_request()? {
return Ok(());
}
let mut write = lock_write(cx, &self.write).await?;
if !self.should_send_break_request()? {
return Ok(());
}
match send_marker(&mut *write, TNS_MARKER_TYPE_BREAK).await {
Ok(()) => self.recovery.mark_break_sent(),
Err(err) => {
self.recovery.mark_dead();
Err(err)
}
}
}
fn should_send_break_request(&self) -> Result<bool> {
match self.recovery.phase() {
SessionRecoveryPhase::Dead => {
Err(Error::ConnectionClosed("connection is closed".into()))
}
SessionRecoveryPhase::BreakSent | SessionRecoveryPhase::Draining => Ok(false),
SessionRecoveryPhase::Ready | SessionRecoveryPhase::InFlight => Ok(true),
}
}
pub fn cancel_blocking(&mut self) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
self.cancel(&cx).await
})
}
}
pub struct BlockingConnection;
impl BlockingConnection {
pub fn connect(options: ConnectOptions) -> Result<Connection> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
Connection::connect(&cx, options).await
})
}
pub fn ping(connection: &mut Connection) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.ping(&cx).await
})
}
pub fn ping_with_timeout(connection: &mut Connection, timeout_ms: u32) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.ping_with_timeout(&cx, timeout_ms).await
})
}
pub fn change_password(
connection: &mut Connection,
old_password: &str,
new_password: &str,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.change_password(&cx, old_password, new_password)
.await
})
}
pub fn cancel(connection: &mut Connection) -> Result<()> {
block_on_io(|cx| async move { connection.cancel(&cx).await })
}
pub fn commit(connection: &mut Connection) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.commit(&cx).await
})
}
#[allow(clippy::too_many_arguments)]
pub fn subscribe_register(
connection: &mut Connection,
namespace: u32,
name: Option<&str>,
public_qos: u32,
operations: u32,
timeout: u32,
grouping_class: u8,
grouping_value: u32,
grouping_type: u8,
) -> Result<SubscribeResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.subscribe_register(
&cx,
namespace,
name,
public_qos,
operations,
timeout,
grouping_class,
grouping_value,
grouping_type,
)
.await
})
}
#[allow(clippy::too_many_arguments)]
pub fn subscribe_unregister(
connection: &mut Connection,
registration_id: u64,
client_id: &[u8],
namespace: u32,
name: Option<&str>,
public_qos: u32,
operations: u32,
timeout: u32,
grouping_class: u8,
grouping_value: u32,
grouping_type: u8,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.subscribe_unregister(
&cx,
registration_id,
client_id,
namespace,
name,
public_qos,
operations,
timeout,
grouping_class,
grouping_value,
grouping_type,
)
.await
})
}
pub fn notify_register(connection: &mut Connection, client_id: &[u8]) -> Result<()> {
block_on_io(|cx| async move { connection.notify_register(&cx, client_id).await })
}
pub fn recv_notification(
connection: &mut Connection,
namespace: u32,
public_qos: u32,
read_timeout: Duration,
) -> Result<NotificationOutcome> {
block_on_io(|cx| async move {
connection
.recv_notification(&cx, namespace, public_qos, read_timeout)
.await
})
}
pub fn register_query<'r>(
connection: &mut Connection,
registration: Registration<'r>,
) -> Result<RegistrationOutcome> {
block_on_io(|cx| async move { connection.register_query(&cx, registration).await })
}
pub fn rollback(connection: &mut Connection) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.rollback(&cx).await
})
}
pub fn enable_dbms_output(
connection: &mut Connection,
buffer_bytes: Option<u32>,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.enable_dbms_output(&cx, buffer_bytes).await
})
}
pub fn read_dbms_output(
connection: &mut Connection,
max_lines: usize,
max_chars: usize,
) -> Result<DbmsOutput> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.read_dbms_output(&cx, max_lines, max_chars).await
})
}
pub fn fetch_cursor(
connection: &mut Connection,
cursor: &oracledb_protocol::thin::CursorValue,
max_rows: usize,
) -> Result<QueryResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.fetch_cursor(&cx, cursor, max_rows).await
})
}
pub fn describe_object_type(
connection: &mut Connection,
schema: &str,
type_name: &str,
) -> Result<ObjectType> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.describe_object_type(&cx, schema, type_name)
.await
})
}
pub fn begin_sessionless_transaction(
connection: &mut Connection,
transaction_id: &[u8],
timeout: u32,
defer_round_trip: bool,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.begin_sessionless_transaction(&cx, transaction_id, timeout, defer_round_trip)
.await
})
}
pub fn resume_sessionless_transaction(
connection: &mut Connection,
transaction_id: &[u8],
timeout: u32,
defer_round_trip: bool,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.resume_sessionless_transaction(&cx, transaction_id, timeout, defer_round_trip)
.await
})
}
pub fn suspend_sessionless_transaction(connection: &mut Connection) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.suspend_sessionless_transaction(&cx).await
})
}
#[allow(clippy::too_many_arguments)]
pub fn tpc_begin(
connection: &mut Connection,
format_id: u32,
global_transaction_id: &[u8],
branch_qualifier: &[u8],
flags: u32,
timeout: u32,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.tpc_begin(
&cx,
format_id,
global_transaction_id,
branch_qualifier,
flags,
timeout,
)
.await
})
}
pub fn tpc_end(
connection: &mut Connection,
xid: Option<(u32, &[u8], &[u8])>,
flags: u32,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.tpc_end(&cx, xid, flags).await
})
}
pub fn tpc_prepare(
connection: &mut Connection,
xid: Option<(u32, &[u8], &[u8])>,
) -> Result<bool> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.tpc_prepare(&cx, xid).await
})
}
pub fn tpc_commit(
connection: &mut Connection,
xid: Option<(u32, &[u8], &[u8])>,
one_phase: bool,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.tpc_commit(&cx, xid, one_phase).await
})
}
pub fn tpc_rollback(
connection: &mut Connection,
xid: Option<(u32, &[u8], &[u8])>,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.tpc_rollback(&cx, xid).await
})
}
pub fn query<'conn, 'p>(
connection: &'conn mut Connection,
sql: &str,
params: impl Into<crate::Params<'p>>,
) -> Result<BlockingRows<'conn>> {
block_on_io(|cx| async move {
connection
.query(&cx, sql, params)
.await
.map(BlockingRows::new)
})
}
pub fn query_one<'p>(
connection: &mut Connection,
sql: &str,
params: impl Into<crate::Params<'p>>,
) -> Result<Row> {
block_on_io(|cx| async move { connection.query_one(&cx, sql, params).await })
}
pub fn query_opt<'p>(
connection: &mut Connection,
sql: &str,
params: impl Into<crate::Params<'p>>,
) -> Result<Option<Row>> {
block_on_io(|cx| async move { connection.query_opt(&cx, sql, params).await })
}
pub fn query_all<'p>(
connection: &mut Connection,
sql: &str,
params: impl Into<crate::Params<'p>>,
) -> Result<Vec<Row>> {
block_on_io(|cx| async move { connection.query_all(&cx, sql, params).await })
}
pub fn query_with<'conn, 'q>(
connection: &'conn mut Connection,
query: Query<'q>,
) -> Result<BlockingRows<'conn>> {
block_on_io(|cx| async move {
connection
.query_with(&cx, query)
.await
.map(BlockingRows::new)
})
}
pub fn execute<'p>(
connection: &mut Connection,
sql: &str,
params: impl Into<crate::Params<'p>>,
) -> Result<ExecuteOutcome> {
block_on_io(|cx| async move { connection.execute(&cx, sql, params).await })
}
pub fn execute_with<'e>(
connection: &mut Connection,
execute: Execute<'e>,
) -> Result<ExecuteOutcome> {
block_on_io(|cx| async move { connection.execute_with(&cx, execute).await })
}
pub fn execute_many<'b>(
connection: &mut Connection,
sql: &str,
rows: impl Into<crate::BatchRows<'b>>,
) -> Result<BatchOutcome> {
block_on_io(|cx| async move { connection.execute_many(&cx, sql, rows).await })
}
pub fn execute_many_with<'b>(
connection: &mut Connection,
batch: Batch<'b>,
) -> Result<BatchOutcome> {
block_on_io(|cx| async move { connection.execute_many_with(&cx, batch).await })
}
pub fn execute_raw(
connection: &mut Connection,
sql: &str,
prefetch_rows: u32,
bind_rows: &[Vec<BindValue>],
exec_options: ExecuteOptions,
timeout_ms: Option<u32>,
) -> Result<QueryResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.execute_raw(&cx, sql, prefetch_rows, bind_rows, exec_options, timeout_ms)
.await
})
}
pub fn fetch_rows(
connection: &mut Connection,
cursor_id: u32,
arraysize: u32,
previous_row: Option<&[Option<oracledb_protocol::thin::QueryValue>]>,
) -> Result<QueryResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.fetch_rows(&cx, cursor_id, arraysize, previous_row)
.await
})
}
pub fn fetch_rows_with_columns(
connection: &mut Connection,
cursor_id: u32,
arraysize: u32,
known_columns: &[ColumnMetadata],
previous_row: Option<&[Option<oracledb_protocol::thin::QueryValue>]>,
) -> Result<QueryResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.fetch_rows_with_columns(&cx, cursor_id, arraysize, known_columns, previous_row)
.await
})
}
pub fn define_and_fetch_rows_with_columns(
connection: &mut Connection,
cursor_id: u32,
arraysize: u32,
define_columns: &[ColumnMetadata],
previous_row: Option<&[Option<oracledb_protocol::thin::QueryValue>]>,
) -> Result<QueryResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.define_and_fetch_rows_with_columns(
&cx,
cursor_id,
arraysize,
define_columns,
previous_row,
)
.await
})
}
pub fn scroll_cursor(
connection: &mut Connection,
sql: &str,
cursor_id: u32,
arraysize: u32,
fetch_orientation: u32,
fetch_pos: u32,
) -> Result<QueryResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.scroll_cursor(&cx, sql, cursor_id, arraysize, fetch_orientation, fetch_pos)
.await
})
}
pub fn read_lob(
connection: &mut Connection,
locator: &[u8],
offset: u64,
amount: u64,
) -> Result<LobReadResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.read_lob(&cx, locator, offset, amount).await
})
}
pub fn read_lob_with_timeout(
connection: &mut Connection,
locator: &[u8],
offset: u64,
amount: u64,
timeout_ms: Option<u32>,
) -> Result<LobReadResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.read_lob_call_timeout(&cx, locator, offset, amount, timeout_ms)
.await
})
}
pub fn aq_enq_one(
connection: &mut Connection,
queue: &AqQueueDesc,
props: &AqMsgProps,
enq_options: &AqEnqOptions,
) -> Result<Option<Vec<u8>>> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.aq_enq_one(&cx, queue, props, enq_options).await
})
}
pub fn aq_deq_one(
connection: &mut Connection,
queue: &AqQueueDesc,
deq_options: &AqDeqOptions,
) -> Result<AqDeqResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.aq_deq_one(&cx, queue, deq_options).await
})
}
pub fn aq_enq_many(
connection: &mut Connection,
queue: &AqQueueDesc,
props_list: &[AqMsgProps],
enq_options: &AqEnqOptions,
) -> Result<Vec<Vec<u8>>> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.aq_enq_many(&cx, queue, props_list, enq_options)
.await
})
}
pub fn aq_deq_many(
connection: &mut Connection,
queue: &AqQueueDesc,
deq_options: &AqDeqOptions,
max_num_messages: u32,
) -> Result<Vec<oracledb_protocol::thin::aq::AqDeqMessage>> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.aq_deq_many(&cx, queue, deq_options, max_num_messages)
.await
})
}
pub fn create_temp_lob(
connection: &mut Connection,
ora_type_num: u8,
csfrm: u8,
) -> Result<LobReadResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.create_temp_lob(&cx, ora_type_num, csfrm).await
})
}
pub fn write_lob(
connection: &mut Connection,
locator: &[u8],
offset: u64,
data: &[u8],
) -> Result<LobReadResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.write_lob(&cx, locator, offset, data).await
})
}
pub fn trim_lob(
connection: &mut Connection,
locator: &[u8],
new_size: u64,
) -> Result<LobReadResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.trim_lob(&cx, locator, new_size).await
})
}
pub fn write_lob_with_timeout(
connection: &mut Connection,
locator: &[u8],
offset: u64,
data: &[u8],
timeout_ms: Option<u32>,
) -> Result<LobReadResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.write_lob_call_timeout(&cx, locator, offset, data, timeout_ms)
.await
})
}
pub fn trim_lob_with_timeout(
connection: &mut Connection,
locator: &[u8],
new_size: u64,
timeout_ms: Option<u32>,
) -> Result<LobReadResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.trim_lob_call_timeout(&cx, locator, new_size, timeout_ms)
.await
})
}
pub fn free_temp_lobs(connection: &mut Connection, locators: &[Vec<u8>]) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.free_temp_lobs(&cx, locators).await
})
}
pub fn free_temp_lobs_with_timeout(
connection: &mut Connection,
locators: &[Vec<u8>],
timeout_ms: Option<u32>,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.free_temp_lobs_call_timeout(&cx, locators, timeout_ms)
.await
})
}
pub fn direct_path_load(
connection: &mut Connection,
schema_name: &str,
table_name: &str,
column_names: &[String],
rows: &[Vec<oracledb_protocol::dpl::DirectPathColumnValue>],
batch_size: u32,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.direct_path_load(&cx, schema_name, table_name, column_names, rows, batch_size)
.await
})
}
pub fn run_pipeline(
connection: &mut Connection,
requests: &[PipelineRequest],
continue_on_error: bool,
) -> Result<Vec<Vec<u8>>> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.run_pipeline(&cx, requests, continue_on_error)
.await
})
}
pub fn run_pipeline_decoded(
connection: &mut Connection,
requests: &[PipelineRequest],
continue_on_error: bool,
) -> Result<Vec<Result<QueryResult>>> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.run_pipeline_decoded(&cx, requests, continue_on_error)
.await
})
}
pub fn direct_path_prepare(
connection: &mut Connection,
schema_name: &str,
table_name: &str,
column_names: &[String],
) -> Result<oracledb_protocol::dpl::DirectPathPrepareResult> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.direct_path_prepare(&cx, schema_name, table_name, column_names)
.await
})
}
pub fn direct_path_load_prepared(
connection: &mut Connection,
prepare: &oracledb_protocol::dpl::DirectPathPrepareResult,
rows: &[Vec<oracledb_protocol::dpl::DirectPathColumnValue>],
batch_size: u32,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.direct_path_load_prepared(&cx, prepare, rows, batch_size)
.await
})
}
#[doc(hidden)]
pub fn __pyshim_drain_cancel_response(connection: &mut Connection) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async { connection.drain_cancel_response().await })
}
pub fn supplement_json_column_metadata(
connection: &mut Connection,
columns: &mut [ColumnMetadata],
timeout_ms: Option<u32>,
) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection
.supplement_json_column_metadata(&cx, columns, timeout_ms)
.await
})
}
pub fn close(connection: Connection) -> Result<()> {
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
connection.close(&cx).await
})
}
}
fn new_io_runtime() -> Result<Runtime> {
let reactor = reactor::create_reactor()?;
RuntimeBuilder::current_thread()
.with_reactor(reactor)
.build()
.map_err(|err| Error::Runtime(err.to_string()))
}
pub(crate) fn new_pool_runtime() -> Result<Runtime> {
let reactor = reactor::create_reactor()?;
RuntimeBuilder::current_thread()
.with_reactor(reactor)
.thread_name_prefix("oracledb-pool-bg")
.build()
.map_err(|err| Error::Runtime(err.to_string()))
}
thread_local! {
static IO_RUNTIME: std::cell::RefCell<Option<Runtime>> =
const { std::cell::RefCell::new(None) };
}
fn build_io_runtime() -> Result<Runtime> {
IO_RUNTIME.with(|slot| {
if let Some(runtime) = slot.borrow().as_ref() {
return Ok(runtime.clone());
}
let runtime = new_io_runtime()?;
*slot.borrow_mut() = Some(runtime.clone());
Ok(runtime)
})
}
pub(crate) fn block_on_io<F, Fut, T>(operation: F) -> Result<T>
where
F: FnOnce(Cx) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
operation(cx).await
})
}
#[cfg(feature = "arrow")]
pub(crate) fn block_on_connection<F, Fut, T>(operation: F) -> Result<T>
where
F: FnOnce(Cx) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
block_on_io(operation)
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct IncomingPacket {
packet_type: u8,
payload: Vec<u8>,
}
async fn lock_write<'a, W>(
cx: &Cx,
write: &'a Arc<AsyncMutex<W>>,
) -> Result<asupersync::sync::MutexGuard<'a, W>>
where
W: AsyncWrite + std::fmt::Debug + Unpin,
{
write
.lock(cx)
.await
.map_err(|err| Error::Runtime(err.to_string()))
}
async fn write_all_shared<W>(cx: &Cx, write: &Arc<AsyncMutex<W>>, packet: &[u8]) -> Result<()>
where
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let mut guard = lock_write(cx, write).await?;
guard.write_all(packet).await?;
guard.flush().await?;
Ok(())
}
async fn shutdown_write_shared<W>(cx: &Cx, write: &Arc<AsyncMutex<W>>) -> Result<()>
where
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let mut guard = lock_write(cx, write).await?;
guard.shutdown().await?;
Ok(())
}
async fn send_data_packet_shared<W>(
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
payload: &[u8],
sdu: usize,
) -> Result<()>
where
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let mut guard = lock_write(cx, write).await?;
send_data_packet(&mut *guard, payload, sdu).await
}
async fn send_data_packet_shared_with_flags<W>(
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
payload: &[u8],
sdu: usize,
first_packet_flags: u16,
last_packet_flags: u16,
) -> Result<()>
where
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let mut guard = lock_write(cx, write).await?;
send_data_packet_with_flags(
&mut *guard,
payload,
sdu,
first_packet_flags,
last_packet_flags,
)
.await
}
async fn send_marker_shared<W>(cx: &Cx, write: &Arc<AsyncMutex<W>>, marker_type: u8) -> Result<()>
where
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let mut guard = lock_write(cx, write).await?;
send_marker(&mut *guard, marker_type).await
}
fn lock_write_for_recovery<W>(
write: &Arc<AsyncMutex<W>>,
) -> Result<asupersync::sync::MutexGuard<'_, W>>
where
W: AsyncWrite + std::fmt::Debug + Unpin,
{
write.try_lock().map_err(|err| match err {
asupersync::sync::TryLockError::Locked => Error::ConnectionClosed(
"write lock unavailable while recovering from cancellation".into(),
),
asupersync::sync::TryLockError::Poisoned => {
Error::ConnectionClosed("write lock poisoned while recovering from cancellation".into())
}
})
}
async fn send_marker_recovery<W>(write: &Arc<AsyncMutex<W>>, marker_type: u8) -> Result<()>
where
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let mut guard = lock_write_for_recovery(write)?;
send_marker(&mut *guard, marker_type).await
}
async fn send_data_packet<W>(stream: &mut W, payload: &[u8], sdu: usize) -> Result<()>
where
W: AsyncWrite + Unpin,
{
send_data_packet_with_flags(stream, payload, sdu, 0, 0).await
}
async fn send_data_packet_with_flags<W>(
stream: &mut W,
payload: &[u8],
sdu: usize,
first_packet_flags: u16,
last_packet_flags: u16,
) -> Result<()>
where
W: AsyncWrite + Unpin,
{
let max_payload = sdu.saturating_sub(TNS_DATA_PACKET_OVERHEAD).max(1);
let chunk_count = payload.chunks(max_payload).len();
for (index, chunk) in payload.chunks(max_payload).enumerate() {
let mut flags = 0u16;
if index == 0 {
flags |= first_packet_flags;
}
if index + 1 == chunk_count {
flags |= last_packet_flags;
}
let packet = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(flags),
chunk,
PacketLengthWidth::Large32,
)?;
stream.write_all(&packet).await?;
}
stream.flush().await?;
Ok(())
}
struct DataResponse {
payload: Vec<u8>,
flush_out_binds: bool,
}
#[cfg(test)]
async fn read_data_response<R, W>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
) -> Result<Vec<u8>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
read_data_response_with_limits(read, cx, write, ProtocolLimits::DEFAULT).await
}
async fn read_data_response_with_limits<R, W>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
limits: ProtocolLimits,
) -> Result<Vec<u8>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
Ok(
read_data_response_boundary_with_limits(read, cx, write, false, limits)
.await?
.payload,
)
}
async fn read_classic_data_response_with_limits<R, W>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
limits: ProtocolLimits,
) -> Result<Vec<u8>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let mut response = Vec::new();
let mut pending_packet: Option<IncomingPacket> = None;
let mut after_reset = false;
loop {
let packet = match pending_packet.take() {
Some(packet) => packet,
None => read_packet_with_limits(read, PacketLengthWidth::Large32, limits).await?,
};
if packet.packet_type == TNS_PACKET_TYPE_MARKER {
pending_packet =
reset_after_marker_with_limits(read, cx, write, &packet, limits).await?;
after_reset = true;
continue;
}
if packet.packet_type != TNS_PACKET_TYPE_DATA {
return Err(Error::UnexpectedPacket(packet.packet_type));
}
let payload =
packet
.payload
.get(2..)
.ok_or(oracledb_protocol::ProtocolError::TtcDecode(
"missing data packet flags",
))?;
let combined = response.len().checked_add(payload.len()).ok_or(
oracledb_protocol::ProtocolError::ResourceLimit {
limit: "response_bytes",
observed: usize::MAX,
maximum: limits.max_response_bytes,
},
)?;
limits.check_response_bytes(combined)?;
response.extend_from_slice(payload);
if (after_reset && post_reset_packet_ends_response(payload))
|| classic_connect_response_is_complete(&response, limits)?
{
return Ok(response);
}
}
}
fn response_complete<T>(result: &oracledb_protocol::Result<T>) -> bool {
!matches!(result, Err(oracledb_protocol::ProtocolError::TtcDecode(_)))
}
async fn read_classic_data_response_probed_with_limits<R, W, P>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
probe: &P,
limits: ProtocolLimits,
) -> Result<Vec<u8>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
P: Fn(&[u8]) -> bool,
{
let mut response = Vec::new();
read_classic_data_response_probed_into(read, cx, write, probe, limits, &mut response).await?;
Ok(response)
}
async fn read_classic_data_response_probed_into<R, W, P>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
probe: &P,
limits: ProtocolLimits,
response: &mut Vec<u8>,
) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
P: Fn(&[u8]) -> bool,
{
let mut pending_packet: Option<IncomingPacket> = None;
let mut after_reset = false;
loop {
let packet = match pending_packet.take() {
Some(packet) => packet,
None => read_packet_with_limits(read, PacketLengthWidth::Large32, limits).await?,
};
if packet.packet_type == TNS_PACKET_TYPE_MARKER {
pending_packet =
reset_after_marker_with_limits(read, cx, write, &packet, limits).await?;
after_reset = true;
continue;
}
if packet.packet_type != TNS_PACKET_TYPE_DATA {
return Err(Error::UnexpectedPacket(packet.packet_type));
}
let payload =
packet
.payload
.get(2..)
.ok_or(oracledb_protocol::ProtocolError::TtcDecode(
"missing data packet flags",
))?;
let combined = response.len().checked_add(payload.len()).ok_or(
oracledb_protocol::ProtocolError::ResourceLimit {
limit: "response_bytes",
observed: usize::MAX,
maximum: limits.max_response_bytes,
},
)?;
limits.check_response_bytes(combined)?;
response.extend_from_slice(payload);
if (after_reset && post_reset_packet_ends_response(payload)) || probe(response) {
return Ok(());
}
}
}
async fn read_classic_data_response_flushing_out_binds_probed_with_limits<R, W, P>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
sdu: usize,
probe: &P,
limits: ProtocolLimits,
) -> Result<Vec<u8>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
P: Fn(&[u8]) -> bool,
{
let mut payload = Vec::new();
read_classic_data_response_probed_into(read, cx, write, probe, limits, &mut payload).await?;
while matches!(payload.last(), Some(&TNS_MSG_TYPE_FLUSH_OUT_BINDS)) {
observe_cancellation_between_round_trips(cx)?;
payload.pop();
send_data_packet_shared(cx, write, &[TNS_MSG_TYPE_FLUSH_OUT_BINDS], sdu).await?;
read_classic_data_response_probed_into(read, cx, write, probe, limits, &mut payload)
.await?;
}
Ok(payload)
}
const BREAK_DRAIN_RECOVERY_TIMEOUT: Duration = Duration::from_secs(5);
#[allow(dead_code)]
async fn break_and_drain_wire<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
recovery_timeout: Duration,
) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let result = time::timeout(
time::wall_now(),
recovery_timeout,
break_and_drain_wire_unbounded(read, write),
)
.await
.ok();
classify_recovery_result(RecoveryWireAction::BreakAndDrain, result)
}
async fn break_and_drain_wire_unbounded<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
break_and_drain_wire_unbounded_with_limits(read, write, ProtocolLimits::DEFAULT).await
}
async fn break_and_drain_wire_unbounded_with_limits<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
limits: ProtocolLimits,
) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
send_marker_recovery(write, TNS_MARKER_TYPE_BREAK)
.await
.map_err(|err| {
Error::ConnectionClosed(format!(
"failed to send break marker on call timeout: {err}"
))
})?;
drain_break_response_recovery_with_limits(read, write, limits).await
}
#[allow(dead_code)]
async fn cancel_and_drain_wire<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
recovery_timeout: Duration,
) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
break_and_drain_wire(read, write, recovery_timeout).await
}
#[allow(dead_code)]
async fn drain_cancel_wire<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
recovery_timeout: Duration,
) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let result = time::timeout(
time::wall_now(),
recovery_timeout,
drain_cancel_wire_unbounded(read, write),
)
.await
.ok();
classify_recovery_result(RecoveryWireAction::DrainCancel, result)
}
async fn drain_cancel_wire_unbounded<R, W>(read: &mut R, write: &Arc<AsyncMutex<W>>) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
drain_cancel_wire_unbounded_with_limits(read, write, ProtocolLimits::DEFAULT).await
}
async fn drain_cancel_wire_unbounded_with_limits<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
limits: ProtocolLimits,
) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
drain_break_response_recovery_with_limits(read, write, limits).await
}
struct CancelDrainGuard {
recovery: Arc<SessionRecovery>,
armed: bool,
}
impl CancelDrainGuard {
fn arm(recovery: Arc<SessionRecovery>) -> Result<Self> {
recovery.begin_or_adopt_operation()?;
Ok(Self {
recovery,
armed: true,
})
}
fn disarm(&mut self) {
self.recovery.complete_operation();
self.armed = false;
}
}
impl Drop for CancelDrainGuard {
fn drop(&mut self) {
if self.armed {
self.recovery.mark_break_required();
}
}
}
#[cfg(test)]
#[allow(dead_code)]
async fn drain_break_response_recovery<R, W>(read: &mut R, write: &Arc<AsyncMutex<W>>) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
drain_break_response_recovery_with_limits(read, write, ProtocolLimits::DEFAULT).await
}
async fn drain_break_response_recovery_with_limits<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
limits: ProtocolLimits,
) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let initial_marker = loop {
let packet = read_packet_with_limits(read, PacketLengthWidth::Large32, limits).await?;
match packet.packet_type {
TNS_PACKET_TYPE_MARKER => break packet,
TNS_PACKET_TYPE_DATA => {
trace_connect_bytes("BREAK drain: discarded in-flight packet", &packet.payload);
continue;
}
other => {
return Err(oracledb_protocol::ProtocolError::UnknownMessageType {
message_type: other,
position: 4,
}
.into())
}
}
};
let pending =
reset_after_marker_recovery_with_limits(read, write, &initial_marker, limits).await?;
let trailing =
read_data_response_boundary_from_recovery_with_limits(read, write, pending, limits).await?;
trace_connect_bytes("BREAK drain: trailing error response", &trailing.payload);
Ok(())
}
#[cfg(test)]
async fn read_data_response_flushing_out_binds<R, W>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
sdu: usize,
) -> Result<Vec<u8>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
read_data_response_flushing_out_binds_with_limits(read, cx, write, sdu, ProtocolLimits::DEFAULT)
.await
}
async fn read_data_response_flushing_out_binds_with_limits<R, W>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
sdu: usize,
limits: ProtocolLimits,
) -> Result<Vec<u8>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let mut response =
read_data_response_boundary_with_limits(read, cx, write, false, limits).await?;
let mut payload = response.payload;
while response.flush_out_binds {
observe_cancellation_between_round_trips(cx)?;
if matches!(payload.last(), Some(&TNS_MSG_TYPE_FLUSH_OUT_BINDS)) {
payload.pop();
}
send_data_packet_shared(cx, write, &[TNS_MSG_TYPE_FLUSH_OUT_BINDS], sdu).await?;
response = read_data_response_boundary_with_limits(read, cx, write, false, limits).await?;
let combined = payload.len().checked_add(response.payload.len()).ok_or(
oracledb_protocol::ProtocolError::ResourceLimit {
limit: "response_bytes",
observed: usize::MAX,
maximum: limits.max_response_bytes,
},
)?;
limits.check_response_bytes(combined)?;
payload.extend_from_slice(&response.payload);
}
Ok(payload)
}
fn data_packet_ends_response(flags: u16, payload: &[u8]) -> bool {
if flags
& (oracledb_protocol::thin::TNS_DATA_FLAGS_END_OF_RESPONSE
| oracledb_protocol::thin::TNS_DATA_FLAGS_EOF)
!= 0
{
return true;
}
payload == [TNS_MSG_TYPE_END_OF_RESPONSE] || payload == [TNS_MSG_TYPE_FLUSH_OUT_BINDS]
}
fn post_reset_packet_ends_response(payload: &[u8]) -> bool {
matches!(
payload.last(),
Some(&TNS_MSG_TYPE_FLUSH_OUT_BINDS) | Some(&TNS_MSG_TYPE_END_OF_RESPONSE)
)
}
#[cfg(test)]
#[allow(dead_code)]
async fn read_data_response_boundary<R, W>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
in_pipeline: bool,
) -> Result<DataResponse>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
read_data_response_boundary_with_limits(read, cx, write, in_pipeline, ProtocolLimits::DEFAULT)
.await
}
async fn read_data_response_boundary_with_limits<R, W>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
in_pipeline: bool,
limits: ProtocolLimits,
) -> Result<DataResponse>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
read_data_response_boundary_seeded(read, Some(cx), write, in_pipeline, None, limits).await
}
#[cfg(test)]
#[allow(dead_code)]
async fn read_data_response_boundary_from_recovery<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
seed: Option<IncomingPacket>,
) -> Result<DataResponse>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
read_data_response_boundary_from_recovery_with_limits(
read,
write,
seed,
ProtocolLimits::DEFAULT,
)
.await
}
async fn read_data_response_boundary_from_recovery_with_limits<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
seed: Option<IncomingPacket>,
limits: ProtocolLimits,
) -> Result<DataResponse>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
read_data_response_boundary_seeded(read, None, write, false, seed, limits).await
}
async fn read_data_response_boundary_seeded<R, W>(
read: &mut R,
cx: Option<&Cx>,
write: &Arc<AsyncMutex<W>>,
in_pipeline: bool,
seed: Option<IncomingPacket>,
limits: ProtocolLimits,
) -> Result<DataResponse>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
let mut response = Vec::new();
let mut pending_packet = seed;
let mut after_reset = false;
loop {
let packet = match pending_packet.take() {
Some(packet) => packet,
None => read_packet_with_limits(read, PacketLengthWidth::Large32, limits).await?,
};
if packet.packet_type == TNS_PACKET_TYPE_MARKER {
if in_pipeline {
trace_connect_bytes("MARKER packet skipped in pipeline", &packet.payload);
continue;
}
pending_packet = match cx {
Some(cx) => {
reset_after_marker_with_limits(read, cx, write, &packet, limits).await?
}
None => {
reset_after_marker_recovery_with_limits(read, write, &packet, limits).await?
}
};
after_reset = true;
continue;
}
if packet.packet_type != TNS_PACKET_TYPE_DATA {
return Err(oracledb_protocol::ProtocolError::UnknownMessageType {
message_type: packet.packet_type,
position: 4,
}
.into());
}
let (data_flags, payload) = packet.payload.split_at_checked(2).ok_or(
oracledb_protocol::ProtocolError::TtcDecode("missing data packet flags"),
)?;
let flags = u16::from_be_bytes(
data_flags
.try_into()
.map_err(|_| oracledb_protocol::ProtocolError::TtcDecode("invalid flags"))?,
);
let ends = data_packet_ends_response(flags, payload)
|| (after_reset && post_reset_packet_ends_response(payload));
if ends && response.is_empty() {
limits.check_response_bytes(payload.len())?;
response = packet.payload;
response.drain(..2);
break;
}
let combined = response.len().checked_add(payload.len()).ok_or(
oracledb_protocol::ProtocolError::ResourceLimit {
limit: "response_bytes",
observed: usize::MAX,
maximum: limits.max_response_bytes,
},
)?;
limits.check_response_bytes(combined)?;
response.extend_from_slice(payload);
if ends {
break;
}
}
let flush_out_binds = matches!(response.last(), Some(&TNS_MSG_TYPE_FLUSH_OUT_BINDS));
Ok(DataResponse {
payload: response,
flush_out_binds,
})
}
const TNS_PACKET_TYPE_MARKER: u8 = 12;
const TNS_MARKER_TYPE_BREAK: u8 = 1;
const TNS_MARKER_TYPE_RESET: u8 = 2;
#[cfg(test)]
#[allow(dead_code)]
async fn reset_after_marker_recovery<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
initial_marker: &IncomingPacket,
) -> Result<Option<IncomingPacket>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
reset_after_marker_recovery_with_limits(read, write, initial_marker, ProtocolLimits::DEFAULT)
.await
}
async fn reset_after_marker_recovery_with_limits<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
initial_marker: &IncomingPacket,
limits: ProtocolLimits,
) -> Result<Option<IncomingPacket>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
trace_connect_bytes("MARKER packet", &initial_marker.payload);
send_marker_recovery(write, TNS_MARKER_TYPE_RESET).await?;
drain_reset_markers_with_limits(read, limits).await
}
#[cfg(test)]
#[allow(dead_code)]
async fn reset_after_marker<R, W>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
initial_marker: &IncomingPacket,
) -> Result<Option<IncomingPacket>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
reset_after_marker_with_limits(read, cx, write, initial_marker, ProtocolLimits::DEFAULT).await
}
async fn reset_after_marker_with_limits<R, W>(
read: &mut R,
cx: &Cx,
write: &Arc<AsyncMutex<W>>,
initial_marker: &IncomingPacket,
limits: ProtocolLimits,
) -> Result<Option<IncomingPacket>>
where
R: AsyncRead + Unpin,
W: AsyncWrite + std::fmt::Debug + Unpin,
{
trace_connect_bytes("MARKER packet", &initial_marker.payload);
send_marker_shared(cx, write, TNS_MARKER_TYPE_RESET).await?;
drain_reset_markers_with_limits(read, limits).await
}
#[cfg(test)]
#[allow(dead_code)]
async fn drain_reset_markers<R>(read: &mut R) -> Result<Option<IncomingPacket>>
where
R: AsyncRead + Unpin,
{
drain_reset_markers_with_limits(read, ProtocolLimits::DEFAULT).await
}
async fn drain_reset_markers_with_limits<R>(
read: &mut R,
limits: ProtocolLimits,
) -> Result<Option<IncomingPacket>>
where
R: AsyncRead + Unpin,
{
loop {
let packet = read_packet_with_limits(read, PacketLengthWidth::Large32, limits).await?;
if packet.packet_type != TNS_PACKET_TYPE_MARKER {
return Ok(Some(packet));
}
trace_connect_bytes("MARKER reset response", &packet.payload);
}
}
async fn send_marker<W>(stream: &mut W, marker_type: u8) -> Result<()>
where
W: AsyncWrite + Unpin,
{
let packet = encode_packet(
TNS_PACKET_TYPE_MARKER,
0,
None,
&[1, 0, marker_type],
PacketLengthWidth::Large32,
)?;
trace_connect_bytes("send MARKER", &packet);
stream.write_all(&packet).await?;
stream.flush().await?;
Ok(())
}
#[cfg(test)]
#[allow(dead_code)]
async fn read_packet<R>(stream: &mut R, width: PacketLengthWidth) -> Result<IncomingPacket>
where
R: AsyncRead + Unpin,
{
read_packet_with_limits(stream, width, ProtocolLimits::DEFAULT).await
}
async fn read_packet_with_limits<R>(
stream: &mut R,
width: PacketLengthWidth,
limits: ProtocolLimits,
) -> Result<IncomingPacket>
where
R: AsyncRead + Unpin,
{
let mut header = [0u8; 8];
stream.read_exact(&mut header).await?;
let [len0, len1, len2, len3, packet_type, _, _, _] = header;
let declared = match width {
PacketLengthWidth::Legacy16 => usize::from(u16::from_be_bytes([len0, len1])),
PacketLengthWidth::Large32 => {
usize::try_from(u32::from_be_bytes([len0, len1, len2, len3])).unwrap_or(usize::MAX)
}
};
if declared < header.len() {
return Err(oracledb_protocol::ProtocolError::InvalidPacketLength {
length: declared,
minimum: header.len(),
}
.into());
}
limits.check_packet_bytes(declared)?;
let mut payload = vec![0u8; declared - header.len()];
stream.read_exact(&mut payload).await?;
Ok(IncomingPacket {
packet_type,
payload,
})
}
fn transport_connect_timeout_duration(seconds: f64) -> Duration {
let seconds = if seconds.is_finite() && seconds > 0.0 {
seconds
} else {
20.0
};
Duration::from_secs_f64(seconds.max(0.001))
}
fn listener_connect_descriptor_with_server(
descriptor: &EasyConnect,
description: &Description,
identity: &ClientIdentity,
server_type_emon: bool,
token_auth: bool,
ssl_server_dn_match: bool,
ssl_server_cert_dn: Option<&str>,
) -> String {
let address = descriptor_address_clause(descriptor);
let connect_data = listener_connect_data_clause(description, identity, server_type_emon);
let security = descriptor_security_clause(
descriptor.protocol,
description,
token_auth,
ssl_server_dn_match,
ssl_server_cert_dn,
);
format!("(DESCRIPTION={}{}{})", address, connect_data, security)
}
fn auth_connect_descriptor(
descriptor: &EasyConnect,
description: &Description,
token_auth: bool,
ssl_server_dn_match: bool,
ssl_server_cert_dn: Option<&str>,
) -> String {
let address = descriptor_address_clause(descriptor);
let connect_data = auth_connect_data_clause(description, descriptor);
let security = descriptor_security_clause(
descriptor.protocol,
description,
token_auth,
ssl_server_dn_match,
ssl_server_cert_dn,
);
format!("(DESCRIPTION={}{}{})", address, connect_data, security)
}
fn descriptor_address_clause(descriptor: &EasyConnect) -> String {
let protocol = match descriptor.protocol {
NetProtocol::Tcp => "tcp",
NetProtocol::Tcps => "tcps",
};
format!(
"(ADDRESS=(PROTOCOL={})(HOST={})(PORT={}))",
protocol, descriptor.host, descriptor.port
)
}
fn listener_connect_data_clause(
description: &Description,
identity: &ClientIdentity,
server_type_emon: bool,
) -> String {
let mut out = auth_connect_data_clause_from_service(description, None);
if server_type_emon {
out.push_str("(SERVER=emon)");
} else if let Some(server_type) = description.connect_data.server_type {
out.push_str("(SERVER=");
out.push_str(server_type.as_str());
out.push(')');
}
for (key, value) in &description.connect_data.extra {
out.push('(');
out.push_str(key);
out.push('=');
out.push_str(value);
out.push(')');
}
out.push_str("(CID=(PROGRAM=");
out.push_str(&identity.program);
out.push_str(")(HOST=");
out.push_str(&identity.machine);
out.push_str(")(USER=");
out.push_str(&identity.osuser);
out.push_str(")))");
out
}
fn auth_connect_data_clause(description: &Description, descriptor: &EasyConnect) -> String {
let mut out =
auth_connect_data_clause_from_service(description, Some(&descriptor.service_name));
if let Some(server_type) = description.connect_data.server_type {
out.push_str("(SERVER=");
out.push_str(server_type.as_str());
out.push(')');
}
for (key, value) in &description.connect_data.extra {
out.push('(');
out.push_str(key);
out.push('=');
out.push_str(value);
out.push(')');
}
out.push(')');
out
}
fn auth_connect_data_clause_from_service(
description: &Description,
fallback_service_name: Option<&str>,
) -> String {
let service_name = description
.connect_data
.service_name
.as_deref()
.or(fallback_service_name)
.unwrap_or("");
format!("(CONNECT_DATA=(SERVICE_NAME={service_name})")
}
fn descriptor_security_clause(
protocol: NetProtocol,
description: &Description,
token_auth: bool,
ssl_server_dn_match: bool,
ssl_server_cert_dn: Option<&str>,
) -> String {
let security = &description.security;
if !protocol.is_tls()
&& !token_auth
&& ssl_server_dn_match
&& ssl_server_cert_dn.is_none()
&& security.extra.is_empty()
{
return String::new();
}
let mut out = String::from("(SECURITY=");
if protocol.is_tls() || !ssl_server_dn_match {
out.push_str("(SSL_SERVER_DN_MATCH=");
out.push_str(if ssl_server_dn_match { "ON" } else { "OFF" });
out.push(')');
}
if let Some(cert_dn) = ssl_server_cert_dn {
out.push_str("(SSL_SERVER_CERT_DN=");
out.push_str(cert_dn);
out.push(')');
}
for (key, value) in &security.extra {
if key.eq_ignore_ascii_case("TOKEN_AUTH") {
continue;
}
out.push('(');
out.push_str(key);
out.push('=');
out.push_str(value);
out.push(')');
}
if token_auth {
out.push_str("(TOKEN_AUTH=OCI_TOKEN)");
}
out.push(')');
out
}
fn parse_session_u32(
data: &std::collections::BTreeMap<String, String>,
key: &'static str,
) -> Result<u32> {
data.get(key)
.ok_or(Error::MissingSessionField(key))?
.parse::<u32>()
.map_err(|_| Error::MissingSessionField(key))
}
fn parse_session_u16(
data: &std::collections::BTreeMap<String, String>,
key: &'static str,
) -> Result<u16> {
data.get(key)
.ok_or(Error::MissingSessionField(key))?
.parse::<u64>()
.map(|value| value as u16)
.map_err(|_| Error::MissingSessionField(key))
}
fn next_ttc_sequence(seq_num: &mut u8) -> u8 {
*seq_num = seq_num.wrapping_add(1);
if *seq_num == 0 {
*seq_num = 1;
}
*seq_num
}
fn statement_cache_insert(
cache: &mut Vec<(String, u32)>,
capacity: usize,
sql: &str,
cursor_id: u32,
) -> Vec<u32> {
let mut to_close = Vec::new();
if cursor_id == 0 {
return to_close;
}
if let Some(index) = cache.iter().position(|(cached_sql, _)| cached_sql == sql) {
let (_, cached_id) = cache.remove(index);
if cached_id != 0 && cached_id != cursor_id {
to_close.push(cached_id);
}
}
cache.push((sql.to_string(), cursor_id));
while cache.len() > capacity {
let (_, evicted_id) = cache.remove(0);
if evicted_id != 0 {
to_close.push(evicted_id);
}
}
to_close
}
fn statement_is_query(sql: &str) -> bool {
sql.trim_start()
.split(|ch: char| !ch.is_ascii_alphabetic())
.next()
.is_some_and(|keyword| keyword.eq_ignore_ascii_case("select"))
}
fn columns_require_define(columns: &[ColumnMetadata]) -> bool {
use oracledb_protocol::thin::{
ORA_TYPE_NUM_BLOB, ORA_TYPE_NUM_CLOB, ORA_TYPE_NUM_JSON, ORA_TYPE_NUM_VECTOR,
};
columns.iter().any(|column| {
matches!(
column.ora_type_num(),
ORA_TYPE_NUM_CLOB | ORA_TYPE_NUM_BLOB | ORA_TYPE_NUM_VECTOR | ORA_TYPE_NUM_JSON
)
})
}
fn columns_have_lob_prefetch_fields(columns: &[ColumnMetadata]) -> bool {
use oracledb_protocol::thin::{ORA_TYPE_NUM_BLOB, ORA_TYPE_NUM_CLOB};
columns
.iter()
.any(|column| matches!(column.ora_type_num(), ORA_TYPE_NUM_CLOB | ORA_TYPE_NUM_BLOB))
}
fn json_lob_probe_candidates(columns: &[ColumnMetadata]) -> Vec<(usize, String)> {
use oracledb_protocol::thin::{ORA_TYPE_NUM_BLOB, ORA_TYPE_NUM_CLOB};
columns
.iter()
.enumerate()
.filter(|(_, metadata)| {
!metadata.is_json()
&& matches!(
metadata.ora_type_num(),
ORA_TYPE_NUM_CLOB | ORA_TYPE_NUM_BLOB
)
&& !metadata.name().is_empty()
})
.map(|(index, metadata)| (index, metadata.name().to_ascii_uppercase()))
.collect()
}
fn trace_connect_step(step: &'static str) {
if std::env::var_os("ORACLEDB_TRACE_CONNECT").is_some() {
eprintln!("oracledb::connect: {step}");
}
}
fn trace_connect_value(label: &'static str, value: &str) {
if std::env::var_os("ORACLEDB_TRACE_CONNECT").is_some() {
eprintln!("oracledb::connect: {label}: {value}");
}
}
fn trace_connect_bytes(label: &'static str, bytes: &[u8]) {
if std::env::var_os("ORACLEDB_TRACE_CONNECT").is_some() {
let mut hex = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use std::fmt::Write as _;
let _ = write!(&mut hex, "{byte:02x}");
}
eprintln!("oracledb::connect: {label} len={} hex={hex}", bytes.len());
}
}
fn trace_query_bytes(label: &'static str, bytes: &[u8]) {
if std::env::var_os("ORACLEDB_TRACE_QUERY").is_some() {
let mut hex = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use std::fmt::Write as _;
let _ = write!(&mut hex, "{byte:02x}");
}
eprintln!("oracledb::query: {label} len={} hex={hex}", bytes.len());
}
}
#[cfg(test)]
mod tests {
use super::*;
use asupersync::lab::{DporExplorer, ExplorerConfig, LabRuntime};
use asupersync::types::{Budget, CancelKind};
use oracledb_protocol::thin::QueryValue;
use std::future::{poll_fn, Future};
use std::io::Read;
use std::net::TcpListener;
use std::pin::pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Poll, Waker};
use std::thread;
use std::time::{Duration, Instant};
#[test]
fn api_design_nothing_lost_map_covers_current_surface() {
assert_eq!(BindValue::Null.variant_name(), "Null");
assert_eq!(QueryValue::Text(String::new()).variant_name(), "Text");
let design = include_str!("../../../docs/API_DESIGN.md");
for method in [
"execute_query_for_registration",
"execute_query",
"execute_query_collect",
"execute_query_with_timeout",
"execute_query_with_binds",
"execute_query_with_binds_and_timeout",
"query",
"query_named",
"query_named_with_timeout",
"execute_query_with_bind_rows",
"execute_query_with_bind_rows_and_options",
"execute_query_with_bind_rows_and_timeout",
"execute_query_with_bind_rows_options_and_timeout",
] {
assert!(design.contains(method), "API_DESIGN.md missing {method}");
}
for group in 1..=24 {
let marker = format!("C{group:02}");
assert!(
design.contains(&marker),
"API_DESIGN.md missing capability group {marker}"
);
}
for field in [
"batcherrors",
"arraydmlrowcounts",
"parse_only",
"token_num",
"cursor_id",
"cache_statement",
"scrollable",
"fetch_orientation",
"fetch_pos",
"scroll_operation",
"suspend_on_success",
"no_prefetch",
"registration_id",
"max_string_size",
] {
assert!(
design.contains(field),
"API_DESIGN.md missing ExecuteOptions field {field}"
);
}
let bind_variants = [
"Null",
"TypedNull",
"Output",
"ReturnOutput",
"ObjectOutput",
"ObjectInput",
"Text",
"Raw",
"Lob",
"Number",
"BinaryInteger",
"BinaryDouble",
"BinaryFloat",
"Boolean",
"IntervalDS",
"IntervalYM",
"DateTime",
"Timestamp",
"TimestampTz",
"Array",
"Vector",
"Json",
"Cursor",
];
assert_eq!(bind_variants.len(), 23);
for variant in bind_variants {
assert!(
design.contains(&format!("`{variant}`")),
"API_DESIGN.md missing BindValue::{variant}"
);
}
let query_variants = [
"Text",
"TextRaw",
"Raw",
"Rowid",
"BinaryDouble",
"IntervalDS",
"IntervalYM",
"Number",
"Boolean",
"Cursor",
"DateTime",
"TimestampTz",
"Object",
"Lob",
"Vector",
"Json",
"Array",
];
assert_eq!(query_variants.len(), 17);
for variant in query_variants {
assert!(
design.contains(&format!("`{variant}`")),
"API_DESIGN.md missing QueryValue::{variant}"
);
}
}
#[test]
fn migration_guide_covers_every_deprecated_method() {
let guide = include_str!("../../../docs/MIGRATING-0.3.md");
for method in [
"execute_query",
"execute_query_collect",
"execute_query_with_timeout",
"execute_query_with_binds",
"execute_query_with_binds_and_timeout",
"query_named",
"query_named_with_timeout",
"execute_query_with_bind_rows",
"execute_query_with_bind_rows_and_options",
"execute_query_with_bind_rows_and_timeout",
"execute_query_with_bind_rows_options_and_timeout",
"execute_query_for_registration",
] {
assert!(
guide.contains(method),
"MIGRATING-0.3.md missing deprecated method {method}"
);
}
for replacement in [
"query_with",
"execute_with",
"execute_many",
"execute_many_with",
"register_query",
"query_one",
"query_opt",
"query_all",
"Query::timeout",
"Execute::raw_options",
"Batch::raw_options",
"Registration::new",
"execute_raw",
] {
assert!(
guide.contains(replacement),
"MIGRATING-0.3.md missing replacement {replacement}"
);
}
assert!(
guide.contains("0.5.0"),
"MIGRATING-0.3.md must state the shims are removed in 0.5.0"
);
}
#[test]
fn statement_cache_evicts_lru_past_capacity() {
let mut cache = Vec::new();
assert!(statement_cache_insert(&mut cache, 2, "a", 10).is_empty());
assert!(statement_cache_insert(&mut cache, 2, "b", 11).is_empty());
assert_eq!(statement_cache_insert(&mut cache, 2, "c", 12), vec![10]);
assert_eq!(
cache,
vec![("b".into(), 11), ("c".into(), 12)],
"LRU order retained"
);
assert_eq!(statement_cache_insert(&mut cache, 2, "b", 99), vec![11]);
assert_eq!(cache, vec![("c".into(), 12), ("b".into(), 99)]);
}
#[test]
fn statement_cache_size_zero_disables_caching() {
let mut cache = Vec::new();
assert_eq!(statement_cache_insert(&mut cache, 0, "a", 10), vec![10]);
assert!(cache.is_empty(), "size 0 must never retain a statement");
assert!(statement_cache_insert(&mut cache, 5, "a", 0).is_empty());
assert!(cache.is_empty());
}
#[test]
fn auth_serial_num_truncates_to_low_u16_instead_of_rejecting() {
let mut data = BTreeMap::new();
data.insert("AUTH_SERIAL_NUM".to_string(), "70000".to_string());
assert_eq!(
parse_session_u16(&data, "AUTH_SERIAL_NUM")
.expect("large AUTH_SERIAL_NUM should parse"),
70000_u64 as u16
);
}
fn char_col(line: &str, needle: char) -> usize {
line.chars()
.position(|c| c == needle)
.expect("char present")
}
#[test]
fn caret_points_at_the_flagged_char() {
let out = render_caret("select x from t", 8, "ORA-00904: invalid identifier");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines[0], "ORA-00904: invalid identifier");
assert!(lines[2].ends_with("select x from t"), "{:?}", lines[2]);
assert_eq!(
char_col(lines[3], '^'),
char_col(lines[2], 'x'),
"caret column must align under the flagged char"
);
}
#[test]
fn caret_handles_multiline_sql() {
let out = render_caret("select *\nfrom no_such", 15, "ORA-00942");
let lines: Vec<&str> = out.lines().collect();
assert!(lines[2].starts_with("2 | from no_such"), "{:?}", lines[2]);
assert_eq!(char_col(lines[3], '^'), char_col(lines[2], 'n'));
}
#[test]
fn caret_counts_unicode_scalar_values() {
let sql = "select 'café' x from t";
let x_idx = sql.chars().position(|c| c == 'x').unwrap();
let out = render_caret(sql, x_idx + 1, "h");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(char_col(lines[3], '^'), char_col(lines[2], 'x'));
}
#[test]
fn caret_clamps_and_never_panics() {
let _ = render_caret("select 1", 999, "h");
let _ = render_caret("select 1", 0, "h");
let _ = render_caret("", 5, "h");
assert!(render_caret("abc", 4, "h").ends_with('^'));
}
fn identity() -> ClientIdentity {
ClientIdentity::new("program", "machine", "osuser", "terminal", "driver")
.expect("test identity should be valid")
}
fn loopback_connection(
read: transport::OracleReadHalf,
write: transport::OracleWriteHalf,
) -> Connection {
loopback_connection_from_core(ConnectionCore::<DriverTransport>::from_halves(
read,
write,
"loopback_test_write",
))
}
fn loopback_connection_from_core(core: DriverCore) -> Connection {
Connection {
descriptor: EasyConnect::parse("127.0.0.1:1521/FREEPDB1")
.expect("test connect string should parse"),
identity: identity(),
core,
session_id: 0,
serial_num: 0,
server_version: None,
server_version_tuple: None,
capabilities: ClientCapabilities::default(),
protocol_limits: ProtocolLimits::DEFAULT,
ttc_seq_num: 0,
sdu: 8192,
supports_end_of_response: true,
supports_oob: false,
cursor_columns: BTreeMap::new(),
fetch_metadata_by_sql: HashMap::new(),
fetch_metadata_order: VecDeque::new(),
dead: false,
user: "test_user".into(),
combo_key: Vec::new(),
statement_cache: Vec::new(),
statement_cache_size: STATEMENT_CACHE_SIZE,
in_use_cursors: HashSet::new(),
lob_prefetch_cursors: BTreeSet::new(),
copied_cursors: HashSet::new(),
cursors_to_close: Vec::new(),
sessionless_data: None,
notification_buffer: Vec::new(),
notification_header_consumed: false,
transaction_context: None,
txn_in_progress: false,
}
}
#[cfg(feature = "cassette")]
fn synthetic_number_columns() -> Vec<ColumnMetadata> {
vec![
ColumnMetadata::new("INTCOL", oracledb_protocol::thin::ORA_TYPE_NUM_NUMBER)
.with_csfrm(oracledb_protocol::thin::CS_FORM_IMPLICIT)
.with_buffer_size(22)
.with_max_size(22)
.with_nulls_allowed(true),
ColumnMetadata::new("NUMBERCOL", oracledb_protocol::thin::ORA_TYPE_NUM_NUMBER)
.with_csfrm(oracledb_protocol::thin::CS_FORM_IMPLICIT)
.with_buffer_size(22)
.with_max_size(22)
.with_nulls_allowed(true),
]
}
#[cfg(feature = "cassette")]
fn synthetic_connect_packet() -> Result<Vec<u8>> {
let payload = build_connect_packet_payload(
"(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=fixture-host)(PORT=0))\
(CONNECT_DATA=(SERVICE_NAME=SYNTHETIC)(CID=(PROGRAM=rust-oracledb)\
(HOST=fixture-host)(USER=fixture-user))))",
8192,
)?;
Ok(encode_packet(
TNS_PACKET_TYPE_CONNECT,
0,
None,
&payload,
PacketLengthWidth::Legacy16,
)?)
}
#[cfg(feature = "cassette")]
fn synthetic_accept_packet() -> Result<Vec<u8>> {
Ok(encode_packet(
TNS_PACKET_TYPE_ACCEPT,
0,
None,
b"SYNTHETIC-ACCEPT",
PacketLengthWidth::Legacy16,
)?)
}
#[cfg(feature = "cassette")]
fn synthetic_execute_packet() -> Result<Vec<u8>> {
let payload = build_execute_payload_with_bind_rows_and_options_with_seq(
"select value from synthetic_fixture",
2,
1,
true,
&[],
ExecuteOptions::default(),
ClientCapabilities::default().ttc_field_version,
)?;
Ok(encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
&payload,
PacketLengthWidth::Large32,
)?)
}
#[cfg(feature = "cassette")]
fn synthetic_fetch_packet() -> Result<Vec<u8>> {
let payload =
build_fetch_payload_with_seq(42, 2, 2, ClientCapabilities::default().ttc_field_version);
Ok(encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
&payload,
PacketLengthWidth::Large32,
)?)
}
#[cfg(feature = "cassette")]
fn synthetic_function_packet(function_code: u8, seq_num: u8) -> Result<Vec<u8>> {
let payload = build_function_payload_with_seq(
function_code,
seq_num,
ClientCapabilities::default().ttc_field_version,
);
Ok(encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
&payload,
PacketLengthWidth::Large32,
)?)
}
#[cfg(feature = "cassette")]
fn hex_value(byte: u8) -> Result<u8> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
_ => Err(Error::Runtime(format!(
"invalid synthetic fixture hex byte {byte:#04x}"
))),
}
}
#[cfg(feature = "cassette")]
fn decode_hex_fixture(hex: &str) -> Result<Vec<u8>> {
let clean = hex
.bytes()
.filter(|byte| !byte.is_ascii_whitespace())
.collect::<Vec<_>>();
if clean.len() % 2 != 0 {
return Err(Error::Runtime(
"synthetic fixture hex must contain an even number of digits".into(),
));
}
let mut out = Vec::with_capacity(clean.len() / 2);
for pair in clean.chunks_exact(2) {
out.push((hex_value(pair[0])? << 4) | hex_value(pair[1])?);
}
Ok(out)
}
#[cfg(feature = "cassette")]
fn synthetic_execute_response_payload() -> Result<Vec<u8>> {
decode_hex_fixture(concat!(
"101710740fb986350b6010fbcb6e06a74ed0787e060a110328014001018201800000",
"014000000000020369010140023ffe010501050556414c554500000000000000000000",
"010707787e060a110b1000021fe8010a010a00062201010001020000000708414c33",
"32555446380801060323a4d500010100000000000004010102013b010102057b0000",
"01010003000000000000000000000000030001010000000002057b0101010300194f",
"52412d30313430333a206e6f206461746120666f756e640a1d",
))
}
#[cfg(feature = "cassette")]
fn synthetic_fetch_response_payload() -> Result<Vec<u8>> {
decode_hex_fixture("06020101000205dc0001010101000702c1041d")
}
#[cfg(feature = "cassette")]
fn synthetic_plain_function_response_payload() -> [u8; 1] {
[TNS_MSG_TYPE_END_OF_RESPONSE]
}
#[cfg(feature = "cassette")]
#[test]
fn synthetic_cassette_replays_connect_execute_fetch_close_offline() -> Result<()> {
let cassette = include_bytes!("../tests/fixtures/cassettes/select_7_plus_5.tns-cassette");
let (read, write, audit) =
transport::replay_split_with_audit(cassette, transport::ReplayWriteMode::Check)
.map_err(|err| Error::Runtime(format!("invalid replay cassette: {err}")))?;
let mut core = ConnectionCore::<DriverTransport>::from_halves(
read,
write,
"synthetic_fixture_replay_write",
);
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = test_cx()?;
let connect_packet = synthetic_connect_packet()?;
core.write_all(&cx, &connect_packet).await?;
let accept = core.read_packet(PacketLengthWidth::Legacy16).await?;
assert_eq!(accept.packet_type, TNS_PACKET_TYPE_ACCEPT);
assert_eq!(accept.payload, b"SYNTHETIC-ACCEPT");
let mut conn = loopback_connection_from_core(core);
let execute = conn
.execute_raw(
&cx,
"select value from synthetic_fixture",
2,
&[],
ExecuteOptions::default(),
None,
)
.await?;
assert_eq!(execute.columns.len(), 1);
assert_eq!(execute.rows.len(), 1);
assert_eq!(
execute.cell(0, 0).and_then(QueryValue::as_text),
Some("AL32UTF8")
);
let previous_row = vec![
Some(QueryValue::number_from_text("2", true)),
Some(QueryValue::number_from_text("0.5", false)),
];
let columns = synthetic_number_columns();
let fetched = conn
.fetch_rows_with_columns(&cx, 42, 2, &columns, Some(&previous_row))
.await?;
assert_eq!(fetched.rows.len(), 1);
assert_eq!(
fetched
.cell(0, 0)
.and_then(QueryValue::as_number_text)
.as_deref(),
Some("3")
);
assert_eq!(
fetched
.cell(0, 1)
.and_then(QueryValue::as_number_text)
.as_deref(),
Some("0.5")
);
conn.close(&cx).await?;
Ok::<_, Error>(())
})?;
audit
.assert_finished()
.map_err(|err| Error::Runtime(err.to_string()))?;
let _ = (
synthetic_accept_packet()?,
synthetic_execute_packet()?,
synthetic_fetch_packet()?,
synthetic_function_packet(TNS_FUNC_ROLLBACK, 3)?,
synthetic_function_packet(TNS_FUNC_LOGOFF, 4)?,
synthetic_execute_response_payload()?,
synthetic_fetch_response_payload()?,
synthetic_plain_function_response_payload(),
);
Ok(())
}
fn server_error(message: &str) -> Error {
Error::Protocol(oracledb_protocol::ProtocolError::ServerError(
message.to_string(),
))
}
fn structured_error(code: u32, pos: i32) -> Error {
Error::Protocol(oracledb_protocol::ProtocolError::ServerErrorInfo(Box::new(
oracledb_protocol::ServerErrorDetails {
message: format!("ORA-{code:05}: synthetic"),
code,
pos,
..Default::default()
},
)))
}
fn column(name: &str, ora_type_num: u8, is_json: bool) -> ColumnMetadata {
ColumnMetadata::new(name, ora_type_num).with_is_json(is_json)
}
#[test]
fn json_lob_probe_candidates_selects_named_non_json_lobs() {
use oracledb_protocol::thin::{ORA_TYPE_NUM_BLOB, ORA_TYPE_NUM_CLOB, ORA_TYPE_NUM_VARCHAR};
let columns = vec![
column("doc", ORA_TYPE_NUM_CLOB, false), column("blob_doc", ORA_TYPE_NUM_BLOB, false), column("already_json", ORA_TYPE_NUM_CLOB, true), column("name", ORA_TYPE_NUM_VARCHAR, false), column("", ORA_TYPE_NUM_CLOB, false), ];
assert_eq!(
json_lob_probe_candidates(&columns),
vec![(0, "DOC".to_string()), (1, "BLOB_DOC".to_string())]
);
}
#[test]
fn json_lob_probe_candidates_empty_when_no_lobs() {
use oracledb_protocol::thin::ORA_TYPE_NUM_VARCHAR;
let columns = vec![column("name", ORA_TYPE_NUM_VARCHAR, false)];
assert!(json_lob_probe_candidates(&columns).is_empty());
}
#[test]
fn ora_code_parses_from_message_and_struct() {
assert_eq!(
server_error("ORA-00060: deadlock detected").ora_code(),
Some(60)
);
assert_eq!(structured_error(942, 0).ora_code(), Some(942));
assert_eq!(server_error("listener problem").ora_code(), None);
assert_eq!(Error::CallTimeout(500).ora_code(), None);
}
#[test]
fn stable_error_methods_classify_without_display_parsing() {
let transient = server_error("ORA-00060: deadlock detected");
assert_eq!(transient.kind(), ErrorKind::Database);
assert_eq!(transient.oracle_code(), Some(60));
assert_eq!(
transient.connection_disposition(),
ConnectionDisposition::Reusable
);
assert_eq!(
transient.retry_hint(),
RetryHint::RetrySameConnectionIfIdempotent
);
let lost = server_error("ORA-03113: end-of-file on communication channel");
assert_eq!(lost.kind(), ErrorKind::Database);
assert_eq!(lost.connection_disposition(), ConnectionDisposition::Dead);
assert_eq!(lost.retry_hint(), RetryHint::ReconnectThenRetryIfIdempotent);
let timeout = Error::CallTimeout(500);
assert_eq!(timeout.kind(), ErrorKind::Timeout);
assert_eq!(
timeout.connection_disposition(),
ConnectionDisposition::Reusable
);
assert_eq!(
timeout.retry_hint(),
RetryHint::RetrySameConnectionIfIdempotent
);
let cancelled = Error::Cancelled;
assert_eq!(cancelled.kind(), ErrorKind::Cancel);
assert_eq!(cancelled.oracle_code(), Some(1013));
assert_eq!(
cancelled.retry_hint(),
RetryHint::RetrySameConnectionIfIdempotent
);
let bind = Error::Bind(BindError::PositionalCountMismatch {
expected: 2,
actual: 1,
});
assert_eq!(bind.kind(), ErrorKind::Conversion);
assert_eq!(bind.retry_hint(), RetryHint::Never);
let resource = Error::Protocol(oracledb_protocol::ProtocolError::ResourceLimit {
limit: "binds",
observed: 4,
maximum: 3,
});
assert_eq!(resource.kind(), ErrorKind::ResourceLimit);
assert_eq!(
resource.connection_disposition(),
ConnectionDisposition::Reusable
);
assert_eq!(resource.retry_hint(), RetryHint::Never);
let session_dead = server_error("ORA-00600: internal error code, arguments: []");
assert_eq!(
session_dead.connection_disposition(),
ConnectionDisposition::Dead
);
assert!(
!session_dead.is_connection_lost(),
"ORA-00600 kills the session but is not a connection-lost retry code"
);
assert_eq!(session_dead.retry_hint(), RetryHint::Never);
}
#[test]
fn cancel_kind_maps_to_disposition() {
for kind in [
CancelKind::Timeout,
CancelKind::Deadline,
CancelKind::PollQuota,
CancelKind::CostBudget,
] {
assert_eq!(
CancelDisposition::from_kind(kind),
CancelDisposition::Timeout,
"{kind:?} must drain + stay reusable (timeout disposition)"
);
}
for kind in [
CancelKind::Shutdown,
CancelKind::ResourceUnavailable,
CancelKind::LinkedExit,
] {
assert_eq!(
CancelDisposition::from_kind(kind),
CancelDisposition::Close,
"{kind:?} must close the connection"
);
}
for kind in [
CancelKind::User,
CancelKind::RaceLost,
CancelKind::FailFast,
CancelKind::ParentCancelled,
] {
assert_eq!(
CancelDisposition::from_kind(kind),
CancelDisposition::Cancel,
"{kind:?} must drain quietly + stay reusable (cancel disposition)"
);
}
}
#[test]
fn cancel_disposition_flattens_to_distinct_error_variants() {
let timeout = CancelDisposition::Timeout.into_error(750);
assert!(matches!(timeout, Error::CallTimeout(750)));
assert_eq!(timeout.kind(), ErrorKind::Timeout);
assert_eq!(
timeout.connection_disposition(),
ConnectionDisposition::Reusable
);
assert_eq!(
timeout.retry_hint(),
RetryHint::RetrySameConnectionIfIdempotent,
"a drained timeout may be retried on the same connection"
);
let cancelled = CancelDisposition::Cancel.into_error(750);
assert!(matches!(cancelled, Error::Cancelled));
assert!(
!matches!(cancelled, Error::Runtime(_) | Error::Io(_)),
"a cancel must be a distinct variant, never a generic runtime/io error"
);
assert_eq!(cancelled.kind(), ErrorKind::Cancel);
assert_eq!(cancelled.oracle_code(), Some(1013));
assert_eq!(
cancelled.connection_disposition(),
ConnectionDisposition::Reusable
);
assert_eq!(
cancelled.retry_hint(),
RetryHint::RetrySameConnectionIfIdempotent
);
let closed = CancelDisposition::Close.into_error(750);
assert!(matches!(closed, Error::ConnectionClosed(_)));
assert_eq!(closed.kind(), ErrorKind::Network);
assert_eq!(closed.connection_disposition(), ConnectionDisposition::Dead);
assert_eq!(
closed.retry_hint(),
RetryHint::ReconnectThenRetryIfIdempotent
);
}
#[test]
fn missing_cancel_reason_is_a_plain_cancel_at_checkpoint() {
assert_eq!(cancel_disposition(None), CancelDisposition::Cancel);
}
#[test]
fn cancelled_cx_resolves_to_the_kind_mapped_distinct_error() {
fn err_for(kind: CancelKind) -> Error {
let cx = Cx::detached_cancel_context();
cx.cancel_with(kind, Some("test cancel"));
assert!(
cx.checkpoint().is_err(),
"{kind:?}: a cancelled context must fail its checkpoint"
);
let reason = cx.cancel_reason().unwrap_or_else(|| {
panic!("{kind:?}: cancel_reason must carry the structured kind")
});
assert_eq!(reason.kind, kind, "cancel_reason must round-trip the kind");
cancel_disposition(Some(reason)).into_error(750)
}
for kind in [CancelKind::Timeout, CancelKind::Deadline] {
let err = err_for(kind);
assert!(
matches!(err, Error::CallTimeout(_)),
"{kind:?} should surface CallTimeout, got {err:?}"
);
assert_eq!(err.kind(), ErrorKind::Timeout);
}
let shutdown = err_for(CancelKind::Shutdown);
assert!(
matches!(shutdown, Error::ConnectionClosed(_)),
"Shutdown should surface ConnectionClosed, got {shutdown:?}"
);
assert_eq!(
shutdown.connection_disposition(),
ConnectionDisposition::Dead
);
for kind in [CancelKind::User, CancelKind::RaceLost] {
let err = err_for(kind);
assert!(
matches!(err, Error::Cancelled),
"{kind:?} should surface Cancelled, got {err:?}"
);
assert!(
!matches!(err, Error::Runtime(_)),
"{kind:?} must NOT be flattened to a generic Error::Runtime"
);
assert_eq!(err.kind(), ErrorKind::Cancel);
}
}
#[test]
fn uncancelled_checkpoint_is_ok() {
let cx = Cx::detached_cancel_context();
assert!(cx.checkpoint().is_ok());
assert!(cx.cancel_reason().is_none());
}
#[test]
fn offset_only_from_structured_nonzero() {
assert_eq!(structured_error(942, 14).offset(), Some(14));
assert_eq!(structured_error(942, 0).offset(), None);
assert_eq!(
server_error("ORA-00942: table or view does not exist").offset(),
None
);
}
#[test]
fn transient_classification() {
for &code in TRANSIENT_ORA_CODES {
let err = server_error(&format!("ORA-{code:05}: transient"));
assert!(err.is_transient(), "ORA-{code:05} should be transient");
assert!(err.is_retryable(), "transient implies retryable");
assert!(
!err.is_connection_lost(),
"ORA-{code:05} is not connection-lost"
);
}
let perm = server_error("ORA-00942: table or view does not exist");
assert!(!perm.is_transient());
assert!(!perm.is_connection_lost());
assert!(!perm.is_retryable());
}
#[test]
fn connection_lost_classification() {
for &code in CONNECTION_LOST_ORA_CODES {
let err = server_error(&format!("ORA-{code:05}: lost"));
assert!(
err.is_connection_lost(),
"ORA-{code:05} should be connection-lost"
);
assert!(err.is_retryable(), "connection-lost implies retryable");
assert!(
!err.is_transient(),
"ORA-{code:05} is not a transient (retry-in-place) code"
);
}
let io = Error::Io(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"reset",
));
assert!(io.is_connection_lost());
assert!(io.is_retryable());
let timeout = Error::CallTimeout(1000);
assert!(
!timeout.is_connection_lost(),
"a call timeout leaves the connection usable after the drain"
);
assert!(
timeout.is_transient(),
"a call timeout is a retry-in-place (transient) condition"
);
assert!(
timeout.is_retryable(),
"transient implies retryable on the same connection"
);
let recovery_failed =
Error::ConnectionClosed("socket timed out while recovering".to_string());
assert!(
recovery_failed.is_connection_lost(),
"a failed timeout-recovery drain marks the connection lost"
);
assert!(recovery_failed.is_retryable(), "reconnect, then retry");
assert!(
!recovery_failed.is_transient(),
"ConnectionClosed needs a reconnect first, so it is not retry-in-place"
);
}
#[test]
fn resource_limit_error_defines_pre_sync_and_post_sync_disposition() {
let err = oracledb_protocol::ProtocolError::ResourceLimit {
limit: "columns",
observed: 3,
maximum: 2,
};
assert_eq!(
post_sync_protocol_error_disposition(&err),
PostSyncProtocolDisposition::Dead,
"a post-sync resource-limit decode failure leaves unread response bytes"
);
let pre_sync = Error::Protocol(oracledb_protocol::ProtocolError::ResourceLimit {
limit: "columns",
observed: 3,
maximum: 2,
});
assert_eq!(
pre_sync.resource_limit(),
Some(oracledb_protocol::ResourceLimit {
limit: "columns",
observed: 3,
maximum: 2,
})
);
assert!(
!pre_sync.is_connection_lost(),
"client-side/pre-sync resource-limit validation does not imply a lost connection"
);
assert!(
!pre_sync.is_transient(),
"raising the configured limit, not retrying in-place, is the remedy"
);
assert!(
!pre_sync.is_retryable(),
"resource limits are deterministic for the same input and policy"
);
assert!(matches!(
pre_sync,
Error::Protocol(oracledb_protocol::ProtocolError::ResourceLimit {
limit: "columns",
observed: 3,
maximum: 2,
})
));
}
#[test]
fn post_sync_resource_limit_marks_live_connection_dead() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (_socket, _) = listener.accept().expect("accept test client");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let mut conn = runtime.block_on(async {
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (read, write) = transport::plain_split(stream);
loopback_connection(read, write)
});
let err = conn
.note_parse::<()>(Err(oracledb_protocol::ProtocolError::ResourceLimit {
limit: "response_bytes",
observed: 33,
maximum: 32,
}))
.expect_err("post-sync resource-limit violation must be surfaced");
assert!(matches!(
err,
Error::Protocol(oracledb_protocol::ProtocolError::ResourceLimit {
limit: "response_bytes",
observed: 33,
maximum: 32,
})
));
assert!(
conn.is_dead(),
"post-sync resource-limit violation stops consuming a response and kills the session"
);
drop(conn);
server.join().expect("server thread joins");
Ok(())
}
#[test]
fn in_flight_packet_resource_limit_marks_connection_dead() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
use std::io::Write as _;
socket
.write_all(&data_packet(&[0x01, 0x02, 0x03], true))
.expect("write oversized packet");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let conn = runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (read, write) = transport::plain_split(stream);
let mut conn = loopback_connection(read, write);
let limits = ProtocolLimits {
max_packet_bytes: 12,
max_frame_bytes: 16,
max_response_bytes: 32,
..ProtocolLimits::DEFAULT
}
.validate()?;
conn.protocol_limits = limits;
conn.core.set_protocol_limits(limits)?;
let err = conn
.core
.read_data_response(&cx)
.await
.expect_err("oversized in-flight packet must fail closed");
assert!(matches!(
err,
Error::Protocol(oracledb_protocol::ProtocolError::ResourceLimit {
limit: "packet_bytes",
observed: 13,
maximum: 12,
})
));
Ok::<Connection, Error>(conn)
})?;
assert!(
conn.is_dead(),
"in-flight resource-limit violations leave unread wire bytes"
);
assert_eq!(conn.core.recovery.phase(), SessionRecoveryPhase::Dead);
drop(conn);
server.join().expect("server thread joins");
Ok(())
}
#[test]
fn classic_probed_read_reassembles_until_probe_reports_complete() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
use std::io::Write as _;
socket
.write_all(&data_packet(&[0x01, 0x02], false))
.expect("write first classic packet");
socket
.write_all(&data_packet(&[0x03, 0x04], false))
.expect("write second classic packet");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let probed = Arc::new(std::sync::Mutex::new(Vec::<Vec<u8>>::new()));
let probed_in = Arc::clone(&probed);
let response = runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (read, write) = transport::plain_split(stream);
let mut conn = loopback_connection(read, write);
conn.core
.read_data_response_probed(&cx, true, move |bytes| {
probed_in
.lock()
.expect("probe snapshot lock")
.push(bytes.to_vec());
bytes.len() >= 4
})
.await
})?;
assert_eq!(
response,
[0x01, 0x02, 0x03, 0x04],
"classic read must reassemble both flag-stripped payloads"
);
let probed = probed.lock().expect("probe snapshot lock");
assert_eq!(
probed.as_slice(),
&[vec![0x01, 0x02], vec![0x01, 0x02, 0x03, 0x04]],
"the probe must run over the accumulated payload after every packet"
);
server.join().expect("server thread joins");
Ok(())
}
#[test]
fn classic_probed_read_single_packet_happy_path() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
use std::io::Write as _;
socket
.write_all(&data_packet(
&[0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06],
false,
))
.expect("write classic packet");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let response = runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (read, write) = transport::plain_split(stream);
let mut conn = loopback_connection(read, write);
conn.core
.read_data_response_probed(&cx, true, |bytes| !bytes.is_empty())
.await
})?;
assert_eq!(response, [0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
server.join().expect("server thread joins");
Ok(())
}
#[test]
fn probed_read_ignores_probe_when_not_classic() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
use std::io::Write as _;
socket
.write_all(&data_packet(&[0x01, 0x02, 0x03], true))
.expect("write flag-framed packet");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let response = runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (read, write) = transport::plain_split(stream);
let mut conn = loopback_connection(read, write);
conn.core
.read_data_response_probed(&cx, false, |_| false)
.await
})?;
assert_eq!(response, [0x01, 0x02, 0x03]);
server.join().expect("server thread joins");
Ok(())
}
#[test]
fn classic_probed_read_rejects_non_data_packet() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
use std::io::Write as _;
let packet = encode_packet(
TNS_PACKET_TYPE_ACCEPT,
0,
None,
&[0x00],
PacketLengthWidth::Large32,
)
.expect("encode unexpected packet");
socket.write_all(&packet).expect("write unexpected packet");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let err = runtime.block_on(async {
let cx = Cx::current().expect("ambient Cx");
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (read, write) = transport::plain_split(stream);
let mut conn = loopback_connection(read, write);
conn.core
.read_data_response_probed(&cx, true, |_| true)
.await
.expect_err("non-DATA packet must fail closed")
});
assert!(matches!(
err,
Error::UnexpectedPacket(TNS_PACKET_TYPE_ACCEPT)
));
assert_eq!(err.kind(), ErrorKind::Protocol);
server.join().expect("server thread joins");
Ok(())
}
#[test]
fn data_packet_ends_response_requires_flag_or_lone_marker_byte() {
const EOR: u8 = TNS_MSG_TYPE_END_OF_RESPONSE; const FOB: u8 = TNS_MSG_TYPE_FLUSH_OUT_BINDS; let eor_flag = oracledb_protocol::thin::TNS_DATA_FLAGS_END_OF_RESPONSE;
let eof_flag = oracledb_protocol::thin::TNS_DATA_FLAGS_EOF;
assert!(data_packet_ends_response(eor_flag, &[0x01, 0x02, EOR]));
assert!(data_packet_ends_response(eor_flag, &[]));
assert!(data_packet_ends_response(eof_flag, &[0x01, 0x02, 0x03]));
assert!(data_packet_ends_response(0x0000, &[EOR]));
assert!(data_packet_ends_response(0x0000, &[FOB]));
assert!(!data_packet_ends_response(0x0000, &[0xc1, 0x02, EOR]));
assert!(!data_packet_ends_response(0x0000, &[0x00, EOR]));
assert!(!data_packet_ends_response(0x0000, &[EOR, 0x05, 0x06, EOR]));
assert!(!data_packet_ends_response(0x0000, &[0xc1, 0x02, FOB]));
assert!(!data_packet_ends_response(0x0000, &[0x00, FOB]));
assert!(!data_packet_ends_response(0x0000, &[0x01, 0x02, 0x03]));
assert!(!data_packet_ends_response(0x0000, &[]));
}
fn replay_boundary(packets: &[(u16, Vec<u8>)]) -> (Vec<u8>, Option<usize>, bool) {
let mut reassembled = Vec::new();
let mut stopped_at = None;
for (index, (flags, payload)) in packets.iter().enumerate() {
let ends = data_packet_ends_response(*flags, payload);
if ends && reassembled.is_empty() {
reassembled = payload.clone();
stopped_at = Some(index);
break;
}
reassembled.extend_from_slice(payload);
if ends {
stopped_at = Some(index);
break;
}
}
let flush_out_binds = matches!(reassembled.last(), Some(&TNS_MSG_TYPE_FLUSH_OUT_BINDS));
(reassembled, stopped_at, flush_out_binds)
}
#[test]
fn boundary_loop_reassembles_packets_ending_in_marker_byte() {
const EOR: u8 = TNS_MSG_TYPE_END_OF_RESPONSE;
const FOB: u8 = TNS_MSG_TYPE_FLUSH_OUT_BINDS;
let packets: [(u16, Vec<u8>); 5] = [
(0x0000, vec![0x10, 0x11, EOR]), (0x0000, vec![0x20, 0x21, 0x22, FOB]), (0x0000, vec![0x30, 0x31, 0x32, EOR]), (0x0000, vec![0x33, 0x34, 0x35]), (
oracledb_protocol::thin::TNS_DATA_FLAGS_END_OF_RESPONSE,
vec![0x40, 0x41, EOR], ),
];
let (reassembled, stopped_at, flush_out_binds) = replay_boundary(&packets);
assert_eq!(
stopped_at,
Some(4),
"reassembly must stop only on the flagged final packet, not on a body packet ending in a marker byte"
);
assert!(
!flush_out_binds,
"the response ended in END_OF_RESPONSE, not FLUSH_OUT_BINDS"
);
assert_eq!(
reassembled,
vec![
0x10, 0x11, EOR, 0x20, 0x21, 0x22, FOB, 0x30, 0x31, 0x32, EOR, 0x33, 0x34, 0x35, 0x40, 0x41, EOR, ],
"every body packet's bytes must be concatenated in order with none dropped"
);
}
#[test]
fn boundary_loop_detects_flush_out_binds_only_at_true_boundary() {
const FOB: u8 = TNS_MSG_TYPE_FLUSH_OUT_BINDS;
let packets: [(u16, Vec<u8>); 2] = [
(0x0000, vec![0x01, 0x02, FOB]), (
oracledb_protocol::thin::TNS_DATA_FLAGS_END_OF_RESPONSE,
vec![0x03, FOB], ),
];
let (reassembled, stopped_at, flush_out_binds) = replay_boundary(&packets);
assert_eq!(stopped_at, Some(1), "stop on the EOR-flagged tail");
assert!(
flush_out_binds,
"flush-out-binds must be detected from the terminal FLUSH_OUT_BINDS message byte"
);
assert_eq!(reassembled, vec![0x01, 0x02, FOB, 0x03, FOB]);
}
#[test]
fn single_packet_passthrough_is_byte_identical_to_extend() {
const EOR: u8 = TNS_MSG_TYPE_END_OF_RESPONSE;
const FOB: u8 = TNS_MSG_TYPE_FLUSH_OUT_BINDS;
let eor_flag = oracledb_protocol::thin::TNS_DATA_FLAGS_END_OF_RESPONSE;
fn replay_extend_only(packets: &[(u16, Vec<u8>)]) -> (Vec<u8>, Option<usize>, bool) {
let mut reassembled = Vec::new();
let mut stopped_at = None;
for (index, (flags, payload)) in packets.iter().enumerate() {
reassembled.extend_from_slice(payload);
if data_packet_ends_response(*flags, payload) {
stopped_at = Some(index);
break;
}
}
let flush = matches!(reassembled.last(), Some(&TNS_MSG_TYPE_FLUSH_OUT_BINDS));
(reassembled, stopped_at, flush)
}
let cases: &[(u16, Vec<u8>)] = &[
(eor_flag, vec![0x40, 0x41, EOR]),
(eor_flag, vec![0x03, FOB]), (eor_flag, vec![0xde, 0xad, 0xbe, 0xef]),
(eor_flag, vec![0x00]),
];
for (flags, payload) in cases {
let one = [(*flags, payload.clone())];
let passthrough = replay_boundary(&one);
let extend = replay_extend_only(&one);
assert_eq!(
passthrough, extend,
"passthrough must equal extend for single packet {payload:02x?}"
);
assert_eq!(&passthrough.0, payload);
}
}
#[test]
fn descriptor_builder_uses_identity_in_listener_cid() {
let options = ConnectOptions::new("localhost/FREEPDB1", "user", "password", identity());
let descriptor =
EasyConnect::parse(&options.connect_string).expect("test connect string should parse");
let full_descriptor = EasyConnect::parse_descriptor(&options.connect_string)
.expect("test connect string should parse as full descriptor");
let description = full_descriptor.first_description();
let built = listener_connect_descriptor_with_server(
&descriptor,
description,
&options.identity,
false,
false,
true,
None,
);
assert!(built.contains("(PROGRAM=program)"));
assert!(built.contains("(HOST=machine)"));
assert!(built.contains("(USER=osuser)"));
assert!(!built.contains("(SERVER=emon)"));
let emon = listener_connect_descriptor_with_server(
&descriptor,
description,
&options.identity,
true,
false,
true,
None,
);
assert!(emon.contains("(SERVICE_NAME=FREEPDB1)(SERVER=emon)(CID="));
}
#[test]
fn token_auth_descriptor_uses_tcps_security_and_passthrough() {
let connect_string = concat!(
"(DESCRIPTION=(ADDRESS=(PROTOCOL=tcps)(HOST=adb.example.test)(PORT=2484))",
"(CONNECT_DATA=(SERVICE_NAME=adbsvc))",
"(SECURITY=(SSL_SERVER_DN_MATCH=off)",
"(SSL_SERVER_CERT_DN=CN=adb.example.test)",
"(OCI_IAM_HOST=private-endpoint)))"
);
let descriptor = EasyConnect::parse(connect_string).expect("parse tcps descriptor");
let full_descriptor =
EasyConnect::parse_descriptor(connect_string).expect("parse full descriptor");
let description = full_descriptor.first_description();
let built = auth_connect_descriptor(
&descriptor,
description,
true,
false,
description.security.ssl_server_cert_dn.as_deref(),
);
assert!(built.contains("(PROTOCOL=tcps)"));
assert!(built.contains("(SECURITY="));
assert!(built.contains("(SSL_SERVER_DN_MATCH=OFF)"));
assert!(built.contains("(SSL_SERVER_CERT_DN=CN=adb.example.test)"));
assert!(built.contains("(OCI_IAM_HOST=private-endpoint)"));
assert!(built.contains("(TOKEN_AUTH=OCI_TOKEN)"));
assert!(!built.contains("WALLET"));
}
#[test]
fn transport_connect_timeout_bounds_post_dial_accept_read() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || -> std::io::Result<()> {
let (mut socket, _) = listener.accept()?;
socket.set_read_timeout(Some(Duration::from_secs(5)))?;
let mut header = [0u8; 8];
socket.read_exact(&mut header)?;
let declared = usize::from(u16::from_be_bytes([header[0], header[1]]));
let mut payload = vec![0u8; declared.saturating_sub(header.len())];
socket.read_exact(&mut payload)?;
thread::sleep(Duration::from_millis(300));
Ok(())
});
let options = ConnectOptions::new(
format!(
"127.0.0.1:{}/FREEPDB1?transport_connect_timeout=100ms",
addr.port()
),
"user",
"password",
identity(),
);
let runtime = build_io_runtime().expect("asupersync runtime");
let started = Instant::now();
let err = runtime
.block_on(async {
let cx = Cx::current().expect("ambient Cx");
Connection::connect(&cx, options).await
})
.expect_err("stalling listener should hit transport connect timeout");
assert!(
matches!(err, Error::CallTimeout(ms) if ms == 100),
"expected 100ms CallTimeout, got {err:?}"
);
assert!(
started.elapsed() < Duration::from_secs(2),
"post-dial ACCEPT read should be bounded"
);
server.join().expect("server thread joins")?;
Ok(())
}
#[test]
fn async_cancel_handle_requests_break_and_reconciles_ready() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
let mut packet = [0u8; 11];
socket.read_exact(&mut packet).expect("read marker packet");
packet
});
let runtime = build_io_runtime().expect("asupersync runtime");
let recovery = Arc::new(SessionRecovery::new());
let mut handle = runtime.block_on(async {
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (_read, write) = transport::plain_split(stream);
CancelHandle {
write: Arc::new(AsyncMutex::with_name("oracle_tcp_write_test", write)),
recovery: Arc::clone(&recovery),
}
});
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
handle.cancel(&cx).await
})?;
let packet = server.join().expect("server thread joins");
assert_eq!(
packet,
[
0,
0,
0,
11,
TNS_PACKET_TYPE_MARKER,
0,
0,
0,
1,
0,
TNS_MARKER_TYPE_BREAK
]
);
assert_eq!(
recovery.phase(),
SessionRecoveryPhase::BreakSent,
"CancelHandle::cancel(&Cx) requests cancellation without draining"
);
assert!(
recovery.begin_pending_drain()?,
"the connection owner must be able to adopt the pending cancel response"
);
recovery.finish_drain_ready();
assert_eq!(
recovery.phase(),
SessionRecoveryPhase::Ready,
"successful cancel-response drain reconciles the session to Ready"
);
Ok(())
}
#[test]
fn async_cancel_handle_owner_drain_reconciles_ready() -> Result<()> {
const INFLIGHT_BODY: &[u8] = &[0xCA, 0xFE];
const ERROR_BODY: &[u8] = &[0x04, 0x01, 0x0d];
const FRESH_BODY: &[u8] = &[0x07, 0x05, 0x0c];
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
use std::io::Write as _;
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_BREAK,
"CancelHandle must send exactly one BREAK request"
);
socket
.write_all(&data_packet(INFLIGHT_BODY, true))
.expect("write in-flight response");
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_BREAK))
.expect("write break-ack marker");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_RESET,
"owner drain must answer the break marker with RESET"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write reset-confirm marker");
socket
.write_all(&data_packet(ERROR_BODY, true))
.expect("write trailing cancel error packet");
socket
.write_all(&data_packet(FRESH_BODY, true))
.expect("write fresh response");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let next = runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (read, write) = transport::plain_split(stream);
let mut conn = loopback_connection(read, write);
let mut handle = conn.cancel_handle()?;
handle.cancel(&cx).await?;
assert_eq!(
conn.core.recovery.phase(),
SessionRecoveryPhase::BreakSent,
"handle cancel only requests recovery"
);
conn.drain_cancel_response().await?;
assert_eq!(
conn.core.recovery.phase(),
SessionRecoveryPhase::Ready,
"owner drain reconciles a successful cancel response to Ready"
);
conn.core.read_data_response(&cx).await
})?;
assert_eq!(
next, FRESH_BODY,
"after owner drain the next read must consume the fresh response"
);
server.join().expect("server thread joins");
Ok(())
}
#[test]
fn async_cancel_handle_owner_drain_failure_marks_dead() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_BREAK,
"CancelHandle must send the BREAK before the failed drain"
);
});
let runtime = build_io_runtime().expect("asupersync runtime");
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (read, write) = transport::plain_split(stream);
let mut conn = loopback_connection(read, write);
let mut handle = conn.cancel_handle()?;
handle.cancel(&cx).await?;
assert_eq!(conn.core.recovery.phase(), SessionRecoveryPhase::BreakSent);
assert!(
conn.drain_cancel_response().await.is_err(),
"incomplete cancel response must fail owner drain"
);
assert!(
conn.is_dead(),
"failed owner drain marks the connection dead"
);
assert_eq!(conn.core.recovery.phase(), SessionRecoveryPhase::Dead);
Ok::<(), Error>(())
})?;
server.join().expect("server thread joins");
Ok(())
}
#[test]
fn async_cancel_handle_does_not_send_duplicate_break_when_recovery_pending() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
let mut packet = [0u8; 11];
socket.read_exact(&mut packet).expect("read first break");
socket
.set_read_timeout(Some(Duration::from_millis(200)))
.expect("set short read timeout");
let mut extra = [0u8; 1];
let extra_read = socket.read_exact(&mut extra);
assert!(
matches!(
extra_read.as_ref().map_err(std::io::Error::kind),
Err(std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut)
),
"duplicate BREAK check expected a read timeout, got {extra_read:?} with byte {:02x}",
extra[0]
);
packet
});
let runtime = build_io_runtime().expect("asupersync runtime");
let recovery = Arc::new(SessionRecovery::new());
let mut handle = runtime.block_on(async {
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (_read, write) = transport::plain_split(stream);
CancelHandle {
write: Arc::new(AsyncMutex::with_name("oracle_tcp_write_test", write)),
recovery: Arc::clone(&recovery),
}
});
runtime.block_on(async {
let cx = Cx::current()
.ok_or_else(|| Error::Runtime("asupersync did not install an ambient Cx".into()))?;
handle.cancel(&cx).await?;
assert_eq!(recovery.phase(), SessionRecoveryPhase::BreakSent);
handle.cancel(&cx).await?;
assert!(
recovery.begin_pending_drain()?,
"test moves the pending cancel into Draining"
);
handle.cancel(&cx).await
})?;
let packet = server.join().expect("server thread joins");
assert_eq!(
packet,
[
0,
0,
0,
11,
TNS_PACKET_TYPE_MARKER,
0,
0,
0,
1,
0,
TNS_MARKER_TYPE_BREAK
]
);
assert_eq!(recovery.phase(), SessionRecoveryPhase::Draining);
Ok(())
}
#[test]
fn blocking_cancel_handle_sends_tns_break_marker() {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
let mut packet = [0u8; 11];
socket.read_exact(&mut packet).expect("read marker packet");
packet
});
let runtime = build_io_runtime().expect("asupersync runtime");
let recovery = Arc::new(SessionRecovery::new());
let mut handle = runtime.block_on(async {
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (_read, write) = transport::plain_split(stream);
CancelHandle {
write: Arc::new(AsyncMutex::with_name("oracle_tcp_write_test", write)),
recovery: Arc::clone(&recovery),
}
});
handle.cancel_blocking().expect("cancel marker write");
let packet = server.join().expect("server thread joins");
assert_eq!(
packet,
[
0,
0,
0,
11,
TNS_PACKET_TYPE_MARKER,
0,
0,
0,
1,
0,
TNS_MARKER_TYPE_BREAK
]
);
assert_eq!(recovery.phase(), SessionRecoveryPhase::BreakSent);
}
const EOR_FLAG: u16 = oracledb_protocol::thin::TNS_DATA_FLAGS_END_OF_RESPONSE;
fn data_packet(message: &[u8], end_of_response: bool) -> Vec<u8> {
let flags = if end_of_response { EOR_FLAG } else { 0 };
encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(flags),
message,
PacketLengthWidth::Large32,
)
.expect("encode data packet")
}
fn marker_packet(marker_type: u8) -> Vec<u8> {
encode_packet(
TNS_PACKET_TYPE_MARKER,
0,
None,
&[1, 0, marker_type],
PacketLengthWidth::Large32,
)
.expect("encode marker packet")
}
#[derive(Debug)]
struct ScriptedTransport;
impl WireTransport for ScriptedTransport {
type Read = ScriptedRead;
type Write = ScriptedWrite;
}
#[derive(Debug)]
struct ScriptedRead {
state: Arc<std::sync::Mutex<ScriptedIoState>>,
}
impl ScriptedRead {
fn from_state(state: Arc<std::sync::Mutex<ScriptedIoState>>) -> Self {
Self { state }
}
}
impl asupersync::io::AsyncRead for ScriptedRead {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut asupersync::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let Ok(mut state) = self.state.lock() else {
return std::task::Poll::Ready(Err(std::io::Error::other(
"scripted transport: poisoned read state",
)));
};
loop {
match state.read.front_mut() {
Some(ReadAction::Bytes {
bytes,
offset,
max_chunk,
cancel_current_on_completion,
}) => {
if *offset >= bytes.len() {
state.read.pop_front();
continue;
}
let cap = max_chunk.unwrap_or(usize::MAX);
let available = bytes.len() - *offset;
let take = available.min(buf.remaining()).min(cap);
if take == 0 {
return std::task::Poll::Ready(Ok(()));
}
buf.put_slice(&bytes[*offset..*offset + take]);
*offset += take;
if *offset >= bytes.len() {
let cancel_current_on_completion = *cancel_current_on_completion;
state.read.pop_front();
if cancel_current_on_completion {
if let Some(cx) = Cx::current() {
cx.cancel_fast(asupersync::CancelKind::User);
}
}
}
return std::task::Poll::Ready(Ok(()));
}
Some(ReadAction::Pending) => {
state.read.pop_front();
cx.waker().wake_by_ref();
return std::task::Poll::Pending;
}
Some(ReadAction::PendingUntil(gate)) => {
if gate.is_open() {
state.read.pop_front();
continue;
}
gate.register(cx.waker());
return std::task::Poll::Pending;
}
Some(ReadAction::Eof) | None => return std::task::Poll::Ready(Ok(())),
Some(ReadAction::Error(message)) => {
let message = *message;
state.read.pop_front();
return std::task::Poll::Ready(Err(std::io::Error::other(message)));
}
Some(ReadAction::AdvanceTime(duration)) => {
let duration = *duration;
state.read.pop_front();
state.clock.advance(duration);
}
}
}
}
}
#[derive(Debug)]
struct ScriptedWrite {
state: Arc<std::sync::Mutex<ScriptedIoState>>,
}
impl ScriptedWrite {
fn from_state(state: Arc<std::sync::Mutex<ScriptedIoState>>) -> Self {
Self { state }
}
}
impl asupersync::io::AsyncWrite for ScriptedWrite {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
let Ok(mut state) = self.state.lock() else {
return std::task::Poll::Ready(Err(std::io::Error::other(
"scripted transport: poisoned write state",
)));
};
loop {
match state.write.front_mut() {
Some(WriteAction::Expect { .. }) => {
let mut completed = None;
let take = {
let Some(WriteAction::Expect {
bytes,
offset,
max_chunk,
}) = state.write.front_mut()
else {
unreachable!("front action already matched Expect");
};
if *offset >= bytes.len() {
state.write.pop_front();
continue;
}
let cap = max_chunk.unwrap_or(usize::MAX);
let available = bytes.len() - *offset;
let take = available.min(buf.len()).min(cap);
if take == 0 {
return std::task::Poll::Ready(Ok(0));
}
if bytes[*offset..*offset + take] != buf[..take] {
return std::task::Poll::Ready(Err(std::io::Error::other(
"scripted transport: write mismatch",
)));
}
*offset += take;
if *offset >= bytes.len() {
completed = Some(bytes.clone());
}
take
};
if let Some(bytes) = completed {
state.note_write(&bytes);
state.write.pop_front();
}
return std::task::Poll::Ready(Ok(take));
}
Some(WriteAction::Pending) => {
state.write.pop_front();
cx.waker().wake_by_ref();
return std::task::Poll::Pending;
}
Some(WriteAction::Eof) => {
state.write.pop_front();
return std::task::Poll::Ready(Ok(0));
}
Some(WriteAction::Error(message)) => {
let message = *message;
state.write.pop_front();
return std::task::Poll::Ready(Err(std::io::Error::other(message)));
}
Some(WriteAction::AdvanceTime(duration)) => {
let duration = *duration;
state.write.pop_front();
state.clock.advance(duration);
}
None => {
return std::task::Poll::Ready(Err(std::io::Error::other(
"scripted transport: unexpected write",
)));
}
}
}
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
}
#[derive(Clone, Debug, Default)]
struct ScriptedClock {
nanos: Arc<std::sync::atomic::AtomicU64>,
}
impl ScriptedClock {
fn advance(&self, duration: Duration) {
let nanos = match u64::try_from(duration.as_nanos()) {
Ok(nanos) => nanos,
Err(_) => u64::MAX,
};
self.nanos.fetch_add(nanos, Ordering::Relaxed);
}
fn elapsed(&self) -> Duration {
Duration::from_nanos(self.nanos.load(Ordering::Relaxed))
}
}
#[derive(Clone, Default)]
struct ScriptedGate {
ready: Arc<AtomicBool>,
waker: Arc<std::sync::Mutex<Option<Waker>>>,
}
impl std::fmt::Debug for ScriptedGate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScriptedGate")
.field("ready", &self.ready.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
impl ScriptedGate {
fn open(&self) {
self.ready.store(true, Ordering::Release);
if let Ok(mut waker) = self.waker.lock() {
if let Some(waker) = waker.take() {
waker.wake();
}
}
}
fn is_open(&self) -> bool {
self.ready.load(Ordering::Acquire)
}
fn register(&self, waker: &Waker) {
if let Ok(mut current) = self.waker.lock() {
let replace = current
.as_ref()
.is_none_or(|registered| !registered.will_wake(waker));
if replace {
*current = Some(waker.clone());
}
}
}
}
#[derive(Debug)]
struct ScriptedIoState {
read: VecDeque<ReadAction>,
write: VecDeque<WriteAction>,
clock: ScriptedClock,
break_writes: usize,
reset_writes: usize,
}
impl ScriptedIoState {
fn new(read: Vec<ReadAction>, write: Vec<WriteAction>, clock: ScriptedClock) -> Self {
Self {
read: read.into(),
write: write.into(),
clock,
break_writes: 0,
reset_writes: 0,
}
}
fn is_consumed(&self) -> bool {
self.read.is_empty() && self.write.is_empty()
}
fn note_write(&mut self, bytes: &[u8]) {
let break_marker = marker_packet(TNS_MARKER_TYPE_BREAK);
let reset_marker = marker_packet(TNS_MARKER_TYPE_RESET);
if bytes == break_marker.as_slice() {
self.break_writes += 1;
} else if bytes == reset_marker.as_slice() {
self.reset_writes += 1;
}
}
}
#[derive(Debug)]
enum ReadAction {
Bytes {
bytes: Vec<u8>,
offset: usize,
max_chunk: Option<usize>,
cancel_current_on_completion: bool,
},
Pending,
PendingUntil(ScriptedGate),
Eof,
Error(&'static str),
AdvanceTime(Duration),
}
impl ReadAction {
fn bytes(bytes: Vec<u8>, max_chunk: Option<usize>) -> Self {
Self::Bytes {
bytes,
offset: 0,
max_chunk,
cancel_current_on_completion: false,
}
}
fn bytes_then_cancel_current(bytes: Vec<u8>, max_chunk: Option<usize>) -> Self {
Self::Bytes {
bytes,
offset: 0,
max_chunk,
cancel_current_on_completion: true,
}
}
}
#[derive(Debug)]
enum WriteAction {
Expect {
bytes: Vec<u8>,
offset: usize,
max_chunk: Option<usize>,
},
Pending,
Eof,
Error(&'static str),
AdvanceTime(Duration),
}
impl WriteAction {
fn expect_bytes(bytes: Vec<u8>, max_chunk: Option<usize>) -> Self {
Self::Expect {
bytes,
offset: 0,
max_chunk,
}
}
}
fn test_cx() -> Result<Cx> {
Cx::current().ok_or_else(|| Error::Runtime("missing ambient Cx in test runtime".into()))
}
#[test]
fn connection_core_routes_connect_execute_fetch_over_scripted_transport() -> Result<()> {
const EXECUTE_BODY: &[u8] = b"scripted execute payload";
const FETCH_BODY: &[u8] = b"scripted fetch payload";
const EXECUTE_RESPONSE: &[u8] = b"scripted execute response";
const FETCH_RESPONSE: &[u8] = b"scripted fetch response";
let connect_packet = encode_packet(
TNS_PACKET_TYPE_CONNECT,
0,
None,
b"SCRIPTED-CONNECT",
PacketLengthWidth::Legacy16,
)?;
let accept_packet = encode_packet(
TNS_PACKET_TYPE_ACCEPT,
0,
None,
b"SCRIPTED-ACCEPT",
PacketLengthWidth::Legacy16,
)?;
let execute_packet = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
EXECUTE_BODY,
PacketLengthWidth::Large32,
)?;
let fetch_packet = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
FETCH_BODY,
PacketLengthWidth::Large32,
)?;
let script = Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
vec![
ReadAction::bytes(accept_packet, None),
ReadAction::bytes(data_packet(EXECUTE_RESPONSE, true), None),
ReadAction::bytes(data_packet(FETCH_RESPONSE, true), None),
],
vec![
WriteAction::expect_bytes(connect_packet.clone(), None),
WriteAction::expect_bytes(execute_packet, None),
WriteAction::expect_bytes(fetch_packet, None),
],
ScriptedClock::default(),
)));
let mut core = ConnectionCore::<ScriptedTransport>::from_halves(
ScriptedRead::from_state(Arc::clone(&script)),
ScriptedWrite::from_state(Arc::clone(&script)),
"scripted_core_write",
);
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = test_cx()?;
core.write_all(&cx, &connect_packet).await?;
let accept = core.read_packet(PacketLengthWidth::Legacy16).await?;
assert_eq!(accept.packet_type, TNS_PACKET_TYPE_ACCEPT);
assert_eq!(accept.payload, b"SCRIPTED-ACCEPT");
core.send_data_packet(&cx, EXECUTE_BODY, 8192).await?;
let execute_response = core.read_data_response(&cx).await?;
assert_eq!(execute_response, EXECUTE_RESPONSE);
core.send_data_packet(&cx, FETCH_BODY, 8192).await?;
let fetch_response = core.read_data_response(&cx).await?;
assert_eq!(fetch_response, FETCH_RESPONSE);
Ok::<_, Error>(())
})?;
let state = script
.lock()
.map_err(|_| Error::Runtime("scripted I/O state lock poisoned".into()))?;
assert!(
state.is_consumed(),
"scripted core must perform exactly the expected connect/execute/fetch I/O"
);
Ok(())
}
#[test]
fn scripted_transport_replays_short_pending_and_virtual_time() -> Result<()> {
const EXECUTE_BODY: &[u8] = b"fault-matrix execute payload";
const EXECUTE_RESPONSE: &[u8] = b"fault-matrix execute response";
let connect_packet = encode_packet(
TNS_PACKET_TYPE_CONNECT,
0,
None,
b"FAULT-MATRIX-CONNECT",
PacketLengthWidth::Legacy16,
)?;
let accept_packet = encode_packet(
TNS_PACKET_TYPE_ACCEPT,
0,
None,
b"FAULT-MATRIX-ACCEPT",
PacketLengthWidth::Legacy16,
)?;
let execute_packet = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
EXECUTE_BODY,
PacketLengthWidth::Large32,
)?;
let execute_response_packet = data_packet(EXECUTE_RESPONSE, true);
let clock = ScriptedClock::default();
let script = Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
vec![
ReadAction::Pending,
ReadAction::AdvanceTime(Duration::from_millis(7)),
ReadAction::bytes(accept_packet, Some(3)),
ReadAction::Pending,
ReadAction::AdvanceTime(Duration::from_millis(11)),
ReadAction::bytes(execute_response_packet, Some(2)),
],
vec![
WriteAction::Pending,
WriteAction::AdvanceTime(Duration::from_millis(5)),
WriteAction::expect_bytes(connect_packet.clone(), Some(2)),
WriteAction::Pending,
WriteAction::AdvanceTime(Duration::from_millis(13)),
WriteAction::expect_bytes(execute_packet, Some(3)),
],
clock.clone(),
)));
let mut core = ConnectionCore::<ScriptedTransport>::from_halves(
ScriptedRead::from_state(Arc::clone(&script)),
ScriptedWrite::from_state(Arc::clone(&script)),
"scripted_fault_matrix_write",
);
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = test_cx()?;
core.write_all(&cx, &connect_packet).await?;
let accept = core.read_packet(PacketLengthWidth::Legacy16).await?;
assert_eq!(accept.packet_type, TNS_PACKET_TYPE_ACCEPT);
assert_eq!(accept.payload, b"FAULT-MATRIX-ACCEPT");
core.send_data_packet(&cx, EXECUTE_BODY, 8192).await?;
let execute_response = core.read_data_response(&cx).await?;
assert_eq!(execute_response, EXECUTE_RESPONSE);
Ok::<_, Error>(())
})?;
assert_eq!(
clock.elapsed(),
Duration::from_millis(36),
"virtual time advances are deterministic and do not require wall-clock sleeps"
);
let state = script
.lock()
.map_err(|_| Error::Runtime("scripted I/O state lock poisoned".into()))?;
assert!(
state.is_consumed(),
"scripted fault matrix must consume every read/write step"
);
Ok(())
}
const DPOR_SATURATION_WINDOW: usize = 1;
const DPOR_WIRE_SEED: u64 = 0xE3_E4_D0_00;
const DPOR_WIRE_MAX_ITERS: usize = 96;
const DPOR_WIRE_TIMEOUT_MS: u32 = 1;
fn dpor_wire_recovery_timeout() -> Duration {
Duration::from_secs(1)
}
#[derive(Clone, Copy, Debug)]
enum DporWireMode {
UserCancel,
Timeout,
}
fn dpor_wire_seed(mode: DporWireMode) -> u64 {
DPOR_WIRE_SEED
+ match mode {
DporWireMode::UserCancel => 0,
DporWireMode::Timeout => 1,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DporWireResultKind {
Cancelled,
CallTimeout,
}
#[derive(Debug)]
struct DporWireObservation {
result: DporWireResultKind,
phase: SessionRecoveryPhase,
break_writes: usize,
reset_writes: usize,
script_consumed: bool,
}
async fn dpor_read_until_cancel_gate(
core: &mut ConnectionCore<ScriptedTransport>,
cx: &Cx,
cancel_gate: &ScriptedGate,
) -> Result<Vec<u8>> {
let recovery = Arc::clone(&core.recovery);
let read = async {
let mut guard = CancelDrainGuard::arm(recovery)?;
let response = core.read_data_response(cx).await;
if response.is_ok() {
guard.disarm();
}
response
};
let cancel = poll_fn(|task_cx| {
if cancel_gate.is_open() {
Poll::Ready(())
} else {
cancel_gate.register(task_cx.waker());
Poll::Pending
}
});
let mut read = pin!(read);
let mut cancel = pin!(cancel);
poll_fn(|task_cx| {
if cancel_gate.is_open() {
return Poll::Ready(Err(Error::Cancelled));
}
if let Poll::Ready(result) = read.as_mut().poll(task_cx) {
return Poll::Ready(result);
}
if let Poll::Ready(()) = cancel.as_mut().poll(task_cx) {
return Poll::Ready(Err(Error::Cancelled));
}
Poll::Pending
})
.await
}
async fn dpor_read_until_timeout_gate(
core: &mut ConnectionCore<ScriptedTransport>,
cx: &Cx,
timeout_gate: &ScriptedGate,
) -> Result<Vec<u8>> {
let recovery = Arc::clone(&core.recovery);
let read = async {
let mut guard = CancelDrainGuard::arm(recovery)?;
let response = core.read_data_response(cx).await;
if response.is_ok() {
guard.disarm();
}
response
};
let timeout = poll_fn(|task_cx| {
if timeout_gate.is_open() {
Poll::Ready(())
} else {
timeout_gate.register(task_cx.waker());
Poll::Pending
}
});
let mut read = pin!(read);
let mut timeout = pin!(timeout);
poll_fn(|task_cx| {
if timeout_gate.is_open() {
return Poll::Ready(Err(Error::CallTimeout(DPOR_WIRE_TIMEOUT_MS)));
}
if let Poll::Ready(result) = read.as_mut().poll(task_cx) {
return Poll::Ready(result);
}
if let Poll::Ready(()) = timeout.as_mut().poll(task_cx) {
return Poll::Ready(Err(Error::CallTimeout(DPOR_WIRE_TIMEOUT_MS)));
}
Poll::Pending
})
.await
}
fn dpor_wire_script(
gate: ScriptedGate,
execute_packet: Vec<u8>,
) -> Arc<std::sync::Mutex<ScriptedIoState>> {
const INFLIGHT_BODY: &[u8] = b"dpor in-flight response";
const ERROR_BODY: &[u8] = &[0x04, 0x01, 0x0d];
Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
vec![
ReadAction::PendingUntil(gate),
ReadAction::bytes(data_packet(INFLIGHT_BODY, true), Some(3)),
ReadAction::Pending,
ReadAction::bytes(marker_packet(TNS_MARKER_TYPE_BREAK), Some(2)),
ReadAction::Pending,
ReadAction::bytes(marker_packet(TNS_MARKER_TYPE_RESET), Some(2)),
ReadAction::Pending,
ReadAction::bytes(data_packet(ERROR_BODY, true), Some(2)),
],
vec![
WriteAction::Pending,
WriteAction::expect_bytes(execute_packet, Some(3)),
WriteAction::Pending,
WriteAction::expect_bytes(marker_packet(TNS_MARKER_TYPE_BREAK), Some(2)),
WriteAction::Pending,
WriteAction::expect_bytes(marker_packet(TNS_MARKER_TYPE_RESET), Some(2)),
],
ScriptedClock::default(),
)))
}
async fn run_dpor_wire_operation(
mode: DporWireMode,
gate: ScriptedGate,
script: Arc<std::sync::Mutex<ScriptedIoState>>,
execute_body: &'static [u8],
) -> Result<DporWireObservation> {
let cx = Cx::current().expect("LabRuntime task should install an ambient Cx");
let mut core = ConnectionCore::<ScriptedTransport>::from_halves(
ScriptedRead::from_state(Arc::clone(&script)),
ScriptedWrite::from_state(Arc::clone(&script)),
"dpor_wire_core_write",
);
core.send_data_packet(&cx, execute_body, 8192).await?;
let result = match mode {
DporWireMode::UserCancel => dpor_read_until_cancel_gate(&mut core, &cx, &gate).await,
DporWireMode::Timeout => dpor_read_until_timeout_gate(&mut core, &cx, &gate).await,
};
let result = match result {
Ok(payload) => {
return Err(Error::Runtime(format!(
"DPOR wire race unexpectedly completed normally with payload {payload:?}"
)));
}
Err(Error::Cancelled) => {
core.recovery.begin_drain_after_break()?;
core.cancel_and_drain_wire(dpor_wire_recovery_timeout())?;
core.recovery.finish_drain_ready();
DporWireResultKind::Cancelled
}
Err(Error::CallTimeout(_)) => {
core.recovery.begin_drain_after_break()?;
core.break_and_drain_wire(dpor_wire_recovery_timeout())?;
core.recovery.finish_drain_ready();
DporWireResultKind::CallTimeout
}
Err(err) => return Err(err),
};
let state = script
.lock()
.map_err(|_| Error::Runtime("scripted DPOR wire state lock poisoned".into()))?;
Ok(DporWireObservation {
result,
phase: core.recovery.phase(),
break_writes: state.break_writes,
reset_writes: state.reset_writes,
script_consumed: state.is_consumed(),
})
}
fn explore_dpor_wire_mode(mode: DporWireMode) -> asupersync::lab::ExplorationReport {
const EXECUTE_BODY: &[u8] = b"dpor execute payload";
let execute_packet = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
EXECUTE_BODY,
PacketLengthWidth::Large32,
)
.expect("encode DPOR execute packet");
let mut explorer = DporExplorer::new(
ExplorerConfig::new(dpor_wire_seed(mode), DPOR_WIRE_MAX_ITERS).max_steps(100_000),
);
explorer.explore(|runtime: &mut LabRuntime| {
let order = Arc::new(std::sync::Mutex::new(Vec::new()));
let root = runtime.state.create_root_region(Budget::INFINITE);
let operation_order = Arc::clone(&order);
let (operation, _operation_handle) = runtime
.state
.create_task(root, Budget::INFINITE, async move {
operation_order
.lock()
.expect("record DPOR wire operation ordering")
.push("operation");
})
.expect("create DPOR wire operation task");
runtime.scheduler.lock().schedule(operation, 0);
let interrupt_order = Arc::clone(&order);
let (interrupt, _interrupt_handle) = runtime
.state
.create_task(root, Budget::INFINITE, async move {
interrupt_order
.lock()
.expect("record DPOR wire interrupt ordering")
.push("interrupt");
})
.expect("create DPOR wire interrupt task");
runtime.scheduler.lock().schedule(interrupt, 0);
runtime.run_until_quiescent();
assert!(
runtime.is_quiescent(),
"DPOR wire ordering model did not quiesce"
);
let observed_order = order.lock().expect("read DPOR wire ordering").clone();
assert_eq!(
observed_order.len(),
2,
"DPOR wire ordering should include operation and interrupt"
);
let replay_gate = ScriptedGate::default();
replay_gate.open();
let script = dpor_wire_script(replay_gate.clone(), execute_packet.clone());
let io_runtime = build_io_runtime().expect("asupersync runtime for DPOR wire replay");
let observed = io_runtime
.block_on(run_dpor_wire_operation(
mode,
replay_gate,
script,
EXECUTE_BODY,
))
.expect("DPOR wire operation should not fail");
let expected = match mode {
DporWireMode::UserCancel => DporWireResultKind::Cancelled,
DporWireMode::Timeout => DporWireResultKind::CallTimeout,
};
assert_eq!(
observed.result, expected,
"delivered cancel/timeout mapped to the wrong public error"
);
assert_eq!(
observed.phase,
SessionRecoveryPhase::Ready,
"wire recovery must finish at a clean Ready boundary"
);
assert_eq!(observed.break_writes, 1, "exactly one BREAK is required");
assert_eq!(observed.reset_writes, 1, "exactly one RESET is required");
assert!(
observed.script_consumed,
"wire recovery must consume the whole scripted break response"
);
})
}
#[test]
fn dpor_wire_cancel_and_timeout_recovery_saturates() {
for mode in [DporWireMode::UserCancel, DporWireMode::Timeout] {
let report = explore_dpor_wire_mode(mode);
eprintln!(
"[dpor-wire] mode={mode:?} seed={} max_iters={} runs={} classes={} saturated={}",
dpor_wire_seed(mode),
DPOR_WIRE_MAX_ITERS,
report.total_runs,
report.unique_classes,
report.coverage.is_saturated(DPOR_SATURATION_WINDOW)
);
assert!(
!report.has_violations(),
"DPOR wire {mode:?} found violations at seeds {:?}",
report.violation_seeds()
);
assert!(
report.total_runs == DPOR_WIRE_MAX_ITERS,
"DPOR wire fallback seed space did not complete for {mode:?}: runs={}, classes={}, new={}",
report.total_runs,
report.unique_classes,
report.coverage.new_class_discoveries
);
}
}
#[test]
fn flush_out_binds_observes_cancel_before_follow_up_round_trip() -> Result<()> {
let first_response = data_packet(&[TNS_MSG_TYPE_FLUSH_OUT_BINDS], true);
let unread_response = data_packet(b"must-not-read-after-cancel", true);
let script = Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
vec![
ReadAction::bytes_then_cancel_current(first_response, None),
ReadAction::bytes(unread_response, None),
],
Vec::new(),
ScriptedClock::default(),
)));
let runtime = build_io_runtime()?;
let err = runtime.block_on(async {
let cx = test_cx()?;
let mut read = ScriptedRead::from_state(Arc::clone(&script));
let write = Arc::new(AsyncMutex::with_name(
"flush_cancel_checkpoint_test_write",
ScriptedWrite::from_state(Arc::clone(&script)),
));
let err = read_data_response_flushing_out_binds(&mut read, &cx, &write, 8192)
.await
.expect_err("cancel checkpoint must stop before FLUSH_OUT_BINDS follow-up");
Ok::<_, Error>(err)
})?;
assert!(
matches!(&err, Error::Cancelled),
"flush continuation should stop on the cancellation checkpoint with a distinct cancel error, got {err:?}"
);
assert_eq!(err.kind(), ErrorKind::Cancel);
let state = script
.lock()
.map_err(|_| Error::Runtime("scripted I/O state lock poisoned".into()))?;
assert_eq!(
state.read.len(),
1,
"the next response packet must not be read after cancellation"
);
assert!(
state.write.is_empty(),
"the FLUSH_OUT_BINDS follow-up must not be sent after cancellation"
);
Ok(())
}
#[test]
fn pending_cx_cancel_is_observed_before_fetch_continuation_write() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
let server = thread::spawn(move || -> std::io::Result<Option<u8>> {
let (mut socket, _) = listener.accept()?;
socket.set_read_timeout(Some(Duration::from_millis(300)))?;
let mut first_byte = [0u8; 1];
match socket.read(&mut first_byte) {
Ok(0) => Ok(None),
Ok(_) => Ok(Some(first_byte[0])),
Err(err)
if matches!(
err.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) =>
{
Ok(None)
}
Err(err) => Err(err),
}
});
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = test_cx()?;
let stream = TcpStream::connect(addr).await?;
let (read, write) = transport::plain_split(stream);
let mut connection = loopback_connection(read, write);
let before_seq = connection.ttc_seq_num;
cx.cancel_fast(asupersync::CancelKind::User);
let err = connection
.fetch_rows_request(&cx, 42, 10)
.await
.expect_err("fetch continuation must checkpoint before writing");
assert!(
matches!(&err, Error::Cancelled),
"fetch continuation should stop on the cancellation checkpoint with a distinct cancel error, got {err:?}"
);
assert_eq!(err.kind(), ErrorKind::Cancel);
assert_eq!(
connection.ttc_seq_num, before_seq,
"checkpoint must run before allocating the next TTC sequence number"
);
assert_eq!(
connection.core.recovery.phase(),
SessionRecoveryPhase::Ready,
"checkpoint failure must not arm the recovery state machine"
);
Ok::<_, Error>(())
})?;
let received = server.join().expect("server thread joins")?;
assert_eq!(
received, None,
"cancelled fetch continuation must not write a FETCH packet"
);
Ok(())
}
#[test]
fn fetch_rows_ref_drop_mid_read_arms_break_recovery() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
let (packet_tx, packet_rx) = std::sync::mpsc::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
let server = thread::spawn(move || -> std::io::Result<()> {
let (mut socket, _) = listener.accept()?;
socket.set_read_timeout(Some(Duration::from_secs(2)))?;
let mut buf = [0u8; 1024];
let read = socket.read(&mut buf)?;
packet_tx
.send(read)
.expect("test packet notification receiver is alive");
let _ = release_rx.recv_timeout(Duration::from_secs(2));
Ok(())
});
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = test_cx()?;
let stream = TcpStream::connect(addr).await?;
let (read, write) = transport::plain_split(stream);
let mut connection = loopback_connection(read, write);
{
let mut fetch = pin!(connection.fetch_rows_ref(&cx, 42, 10, None));
let first_poll = poll_fn(|task_cx| Poll::Ready(fetch.as_mut().poll(task_cx))).await;
assert!(
matches!(first_poll, Poll::Pending),
"fetch_rows_ref must wait for the server response"
);
let packet_len = packet_rx
.recv_timeout(Duration::from_secs(2))
.expect("fetch request should be sent before the response read waits");
assert!(packet_len > 0, "fetch request packet must not be empty");
let second_poll =
poll_fn(|task_cx| Poll::Ready(fetch.as_mut().poll(task_cx))).await;
assert!(
matches!(second_poll, Poll::Pending),
"fetch_rows_ref response read should still be pending"
);
}
assert_eq!(
connection.core.recovery.phase(),
SessionRecoveryPhase::BreakSent,
"dropping fetch_rows_ref mid-read must arm break/drain recovery"
);
release_tx
.send(())
.expect("test server release receiver is alive");
Ok::<_, Error>(())
})?;
server
.join()
.expect("server thread joins")
.map_err(Error::Io)?;
Ok(())
}
#[test]
fn dropped_cancellable_read_is_drained_before_commit_request() -> Result<()> {
const STRANDED_BODY: &[u8] = b"stranded response";
const TRAILING_CANCEL_ERROR: &[u8] = &[0x04, 0x01, 0x0d];
let commit_packet = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
&build_function_payload_with_seq(
TNS_FUNC_COMMIT,
1,
ClientCapabilities::default().ttc_field_version,
),
PacketLengthWidth::Large32,
)?;
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
use std::io::Write as _;
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_BREAK,
"commit must send BREAK before its own request when recovery is pending"
);
socket
.write_all(&data_packet(STRANDED_BODY, true))
.expect("write stranded response");
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_BREAK))
.expect("write break-ack marker");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_RESET,
"commit recovery drain must answer the server break marker with RESET"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write reset-confirm marker");
socket
.write_all(&data_packet(TRAILING_CANCEL_ERROR, true))
.expect("write trailing cancel error packet");
let mut header = [0u8; 8];
socket
.read_exact(&mut header)
.expect("read fresh commit request header");
let len = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize;
let mut commit_request = header.to_vec();
let mut body = vec![0u8; len - header.len()];
socket
.read_exact(&mut body)
.expect("read fresh commit request body");
commit_request.extend_from_slice(&body);
assert_eq!(
commit_request, commit_packet,
"fresh COMMIT request must be written after BREAK/RESET drain"
);
socket
.write_all(&data_packet(&[TNS_MSG_TYPE_END_OF_RESPONSE], true))
.expect("write commit response");
});
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = test_cx()?;
let stream = TcpStream::connect(addr).await?;
let (read, write) = transport::plain_split(stream);
let mut connection = loopback_connection(read, write);
{
let _guard = CancelDrainGuard::arm(Arc::clone(&connection.core.recovery))?;
}
assert_eq!(
connection.core.recovery.phase(),
SessionRecoveryPhase::BreakSent
);
connection.commit(&cx).await?;
assert_eq!(
connection.core.recovery.phase(),
SessionRecoveryPhase::Ready
);
Ok::<_, Error>(())
})?;
server.join().expect("server thread joins");
Ok(())
}
#[test]
fn scripted_transport_injects_errors_and_eof() -> Result<()> {
let payload = encode_packet(
TNS_PACKET_TYPE_CONNECT,
0,
None,
b"FAULT",
PacketLengthWidth::Legacy16,
)?;
let runtime = build_io_runtime()?;
let read_error = Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
vec![ReadAction::Error("scripted read fault")],
Vec::new(),
ScriptedClock::default(),
)));
let mut core = ConnectionCore::<ScriptedTransport>::from_halves(
ScriptedRead::from_state(read_error),
ScriptedWrite::from_state(Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
Vec::new(),
Vec::new(),
ScriptedClock::default(),
)))),
"scripted_read_error_write",
);
let read_err = runtime.block_on(async {
match core.read_packet(PacketLengthWidth::Legacy16).await {
Ok(_) => Err(Error::Runtime(
"scripted read fault unexpectedly succeeded".into(),
)),
Err(err) => Ok(err),
}
})?;
assert!(
read_err.to_string().contains("scripted read fault"),
"scripted read error should keep its diagnostic"
);
let read_eof = Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
vec![ReadAction::Eof],
Vec::new(),
ScriptedClock::default(),
)));
let mut core = ConnectionCore::<ScriptedTransport>::from_halves(
ScriptedRead::from_state(read_eof),
ScriptedWrite::from_state(Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
Vec::new(),
Vec::new(),
ScriptedClock::default(),
)))),
"scripted_read_eof_write",
);
let eof_err = runtime.block_on(async {
match core.read_packet(PacketLengthWidth::Legacy16).await {
Ok(_) => Err(Error::Runtime(
"scripted read EOF unexpectedly succeeded".into(),
)),
Err(err) => Ok(err),
}
})?;
let eof_message = eof_err.to_string().to_ascii_lowercase();
assert!(
matches!(&eof_err, Error::Io(_))
&& (eof_message.contains("failed to fill whole buffer")
|| eof_message.contains("early eof")
|| eof_message.contains("unexpected eof")
|| eof_message.contains("end of file")),
"scripted EOF should surface as an incomplete read, got {eof_err:?}"
);
let write_error = Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
Vec::new(),
vec![WriteAction::Error("scripted write fault")],
ScriptedClock::default(),
)));
let core = ConnectionCore::<ScriptedTransport>::from_halves(
ScriptedRead::from_state(Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
Vec::new(),
Vec::new(),
ScriptedClock::default(),
)))),
ScriptedWrite::from_state(write_error),
"scripted_write_error_write",
);
let write_err = runtime.block_on(async {
let cx = test_cx()?;
match core.write_all(&cx, &payload).await {
Ok(()) => Err(Error::Runtime(
"scripted write error unexpectedly succeeded".into(),
)),
Err(err) => Ok(err),
}
})?;
assert!(
write_err.to_string().contains("scripted write fault"),
"scripted write error should keep its diagnostic"
);
let write_eof = Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
Vec::new(),
vec![WriteAction::Eof],
ScriptedClock::default(),
)));
let core = ConnectionCore::<ScriptedTransport>::from_halves(
ScriptedRead::from_state(Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
Vec::new(),
Vec::new(),
ScriptedClock::default(),
)))),
ScriptedWrite::from_state(write_eof),
"scripted_write_eof_write",
);
let write_eof_err = runtime.block_on(async {
let cx = test_cx()?;
match core.write_all(&cx, &payload).await {
Ok(()) => Err(Error::Runtime(
"scripted write EOF unexpectedly succeeded".into(),
)),
Err(err) => Ok(err),
}
})?;
assert!(
write_eof_err
.to_string()
.contains("failed to write whole buffer")
|| write_eof_err.to_string().contains("write zero"),
"scripted write EOF should surface as an incomplete write"
);
Ok(())
}
#[test]
fn scripted_transport_rejects_mismatched_and_extra_writes() -> Result<()> {
let expected = encode_packet(
TNS_PACKET_TYPE_CONNECT,
0,
None,
b"EXPECTED",
PacketLengthWidth::Legacy16,
)?;
let actual = encode_packet(
TNS_PACKET_TYPE_CONNECT,
0,
None,
b"ACTUAL",
PacketLengthWidth::Legacy16,
)?;
let runtime = build_io_runtime()?;
let mismatch = Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
Vec::new(),
vec![WriteAction::expect_bytes(expected, None)],
ScriptedClock::default(),
)));
let core = ConnectionCore::<ScriptedTransport>::from_halves(
ScriptedRead::from_state(Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
Vec::new(),
Vec::new(),
ScriptedClock::default(),
)))),
ScriptedWrite::from_state(mismatch),
"scripted_mismatch_write",
);
let mismatch_err = runtime.block_on(async {
let cx = test_cx()?;
match core.write_all(&cx, &actual).await {
Ok(()) => Err(Error::Runtime(
"scripted mismatched write unexpectedly succeeded".into(),
)),
Err(err) => Ok(err),
}
})?;
assert!(
mismatch_err.to_string().contains("write mismatch"),
"scripted write mismatch should be explicit"
);
let extra = Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
Vec::new(),
Vec::new(),
ScriptedClock::default(),
)));
let core = ConnectionCore::<ScriptedTransport>::from_halves(
ScriptedRead::from_state(Arc::new(std::sync::Mutex::new(ScriptedIoState::new(
Vec::new(),
Vec::new(),
ScriptedClock::default(),
)))),
ScriptedWrite::from_state(extra),
"scripted_extra_write",
);
let extra_err = runtime.block_on(async {
let cx = test_cx()?;
match core.write_all(&cx, &actual).await {
Ok(()) => Err(Error::Runtime(
"scripted extra write unexpectedly succeeded".into(),
)),
Err(err) => Ok(err),
}
})?;
assert!(
extra_err.to_string().contains("unexpected write"),
"scripted extra write should be rejected"
);
Ok(())
}
#[cfg(feature = "cassette")]
#[test]
fn connection_core_routes_connect_execute_fetch_over_replay_transport() -> Result<()> {
use oracledb_protocol::net::cassette::{self, Direction};
const EXECUTE_BODY: &[u8] = b"replay execute payload";
const FETCH_BODY: &[u8] = b"replay fetch payload";
const EXECUTE_RESPONSE: &[u8] = b"replay execute response";
const FETCH_RESPONSE: &[u8] = b"replay fetch response";
let connect_packet = encode_packet(
TNS_PACKET_TYPE_CONNECT,
0,
None,
b"REPLAY-CONNECT",
PacketLengthWidth::Legacy16,
)?;
let accept_packet = encode_packet(
TNS_PACKET_TYPE_ACCEPT,
0,
None,
b"REPLAY-ACCEPT",
PacketLengthWidth::Legacy16,
)?;
let execute_packet = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
EXECUTE_BODY,
PacketLengthWidth::Large32,
)?;
let fetch_packet = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
FETCH_BODY,
PacketLengthWidth::Large32,
)?;
let mut cassette_bytes = Vec::new();
cassette::write_header(&mut cassette_bytes);
cassette::write_frame(
&mut cassette_bytes,
Direction::ClientToServer,
0,
&connect_packet,
);
cassette::write_frame(
&mut cassette_bytes,
Direction::ServerToClient,
1,
&accept_packet,
);
cassette::write_frame(
&mut cassette_bytes,
Direction::ClientToServer,
2,
&execute_packet,
);
cassette::write_frame(
&mut cassette_bytes,
Direction::ServerToClient,
3,
&data_packet(EXECUTE_RESPONSE, true),
);
cassette::write_frame(
&mut cassette_bytes,
Direction::ClientToServer,
4,
&fetch_packet,
);
cassette::write_frame(
&mut cassette_bytes,
Direction::ServerToClient,
5,
&data_packet(FETCH_RESPONSE, true),
);
let (read, write) =
transport::replay_split(&cassette_bytes, transport::ReplayWriteMode::Check)
.map_err(|err| Error::Runtime(format!("invalid replay cassette: {err}")))?;
let mut core =
ConnectionCore::<DriverTransport>::from_halves(read, write, "replay_core_write");
let runtime = build_io_runtime()?;
runtime.block_on(async {
let cx = test_cx()?;
core.write_all(&cx, &connect_packet).await?;
let accept = core.read_packet(PacketLengthWidth::Legacy16).await?;
assert_eq!(accept.packet_type, TNS_PACKET_TYPE_ACCEPT);
assert_eq!(accept.payload, b"REPLAY-ACCEPT");
core.send_data_packet(&cx, EXECUTE_BODY, 8192).await?;
let execute_response = core.read_data_response(&cx).await?;
assert_eq!(execute_response, EXECUTE_RESPONSE);
core.send_data_packet(&cx, FETCH_BODY, 8192).await?;
let fetch_response = core.read_data_response(&cx).await?;
assert_eq!(fetch_response, FETCH_RESPONSE);
Ok::<_, Error>(())
})?;
Ok(())
}
#[cfg(feature = "cassette")]
#[test]
fn connection_core_routes_connect_execute_fetch_over_recording_transport() -> Result<()> {
use oracledb_protocol::net::cassette::{self, Direction};
use std::io::Write as _;
const EXECUTE_BODY: &[u8] = b"recording execute payload";
const FETCH_BODY: &[u8] = b"recording fetch payload";
const EXECUTE_RESPONSE: &[u8] = b"recording execute response";
const FETCH_RESPONSE: &[u8] = b"recording fetch response";
let connect_packet = encode_packet(
TNS_PACKET_TYPE_CONNECT,
0,
None,
b"RECORDING-CONNECT",
PacketLengthWidth::Legacy16,
)?;
let accept_packet = encode_packet(
TNS_PACKET_TYPE_ACCEPT,
0,
None,
b"RECORDING-ACCEPT",
PacketLengthWidth::Legacy16,
)?;
let execute_packet = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
EXECUTE_BODY,
PacketLengthWidth::Large32,
)?;
let fetch_packet = encode_packet(
TNS_PACKET_TYPE_DATA,
0,
Some(0),
FETCH_BODY,
PacketLengthWidth::Large32,
)?;
let execute_response_packet = data_packet(EXECUTE_RESPONSE, true);
let fetch_response_packet = data_packet(FETCH_RESPONSE, true);
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
let server_connect = connect_packet.clone();
let server_accept = accept_packet.clone();
let server_execute = execute_packet.clone();
let server_fetch = fetch_packet.clone();
let server_execute_response = execute_response_packet.clone();
let server_fetch_response = fetch_response_packet.clone();
let server = thread::spawn(move || -> std::io::Result<()> {
let (mut socket, _) = listener.accept()?;
socket.set_read_timeout(Some(Duration::from_secs(5)))?;
let mut got = vec![0u8; server_connect.len()];
socket.read_exact(&mut got)?;
assert_eq!(got, server_connect);
socket.write_all(&server_accept)?;
let mut got = vec![0u8; server_execute.len()];
socket.read_exact(&mut got)?;
assert_eq!(got, server_execute);
socket.write_all(&server_execute_response)?;
let mut got = vec![0u8; server_fetch.len()];
socket.read_exact(&mut got)?;
assert_eq!(got, server_fetch);
socket.write_all(&server_fetch_response)?;
Ok(())
});
let runtime = build_io_runtime()?;
let cassette_bytes = runtime.block_on(async {
let cx = test_cx()?;
let scope = transport::capture_scope();
let stream = TcpStream::connect(addr).await?;
let (read, write) = transport::plain_split(stream);
let mut core =
ConnectionCore::<DriverTransport>::from_halves(read, write, "recording_core_write");
core.write_all(&cx, &connect_packet).await?;
let accept = core.read_packet(PacketLengthWidth::Legacy16).await?;
assert_eq!(accept.packet_type, TNS_PACKET_TYPE_ACCEPT);
assert_eq!(accept.payload, b"RECORDING-ACCEPT");
core.send_data_packet(&cx, EXECUTE_BODY, 8192).await?;
let execute_response = core.read_data_response(&cx).await?;
assert_eq!(execute_response, EXECUTE_RESPONSE);
core.send_data_packet(&cx, FETCH_BODY, 8192).await?;
let fetch_response = core.read_data_response(&cx).await?;
assert_eq!(fetch_response, FETCH_RESPONSE);
Ok::<_, Error>(scope.to_cassette_bytes())
})?;
server
.join()
.map_err(|_| Error::Runtime("recording test server thread panicked".into()))??;
let frames = cassette::decode_all(&cassette_bytes)
.map_err(|err| Error::Runtime(format!("invalid recorded cassette: {err}")))?;
let (accept_header, accept_payload) = accept_packet.split_at(8);
let (execute_response_header, execute_response_payload) =
execute_response_packet.split_at(8);
let (fetch_response_header, fetch_response_payload) = fetch_response_packet.split_at(8);
assert_eq!(frames.len(), 9);
assert_eq!(frames[0].direction, Direction::ClientToServer);
assert_eq!(frames[0].bytes, connect_packet);
assert_eq!(frames[1].direction, Direction::ServerToClient);
assert_eq!(frames[1].bytes, accept_header);
assert_eq!(frames[2].direction, Direction::ServerToClient);
assert_eq!(frames[2].bytes, accept_payload);
assert_eq!(frames[3].direction, Direction::ClientToServer);
assert_eq!(frames[3].bytes, execute_packet);
assert_eq!(frames[4].direction, Direction::ServerToClient);
assert_eq!(frames[4].bytes, execute_response_header);
assert_eq!(frames[5].direction, Direction::ServerToClient);
assert_eq!(frames[5].bytes, execute_response_payload);
assert_eq!(frames[6].direction, Direction::ClientToServer);
assert_eq!(frames[6].bytes, fetch_packet);
assert_eq!(frames[7].direction, Direction::ServerToClient);
assert_eq!(frames[7].bytes, fetch_response_header);
assert_eq!(frames[8].direction, Direction::ServerToClient);
assert_eq!(frames[8].bytes, fetch_response_payload);
Ok(())
}
fn read_marker_type(socket: &mut std::net::TcpStream) -> u8 {
let mut packet = [0u8; 11];
socket.read_exact(&mut packet).expect("read marker packet");
assert_eq!(
packet[4], TNS_PACKET_TYPE_MARKER,
"expected a MARKER packet"
);
packet[10]
}
#[test]
fn break_and_drain_consumes_inflight_response_and_reset_then_next_read_is_fresh() {
const INFLIGHT_BODY: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF];
const ERROR_BODY: &[u8] = &[0x04, 0x01, 0x02];
const FRESH_BODY: &[u8] = &[0x11, 0x22, 0x33, 0x44, 0x55];
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
use std::io::Write as _;
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_BREAK,
"client must send a BREAK marker first"
);
socket
.write_all(&data_packet(INFLIGHT_BODY, true))
.expect("write in-flight response");
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_BREAK))
.expect("write break-ack marker");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_RESET,
"client must answer the marker with a RESET"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write reset-confirm marker");
socket
.write_all(&data_packet(ERROR_BODY, true))
.expect("write trailing error packet");
socket
.write_all(&data_packet(FRESH_BODY, true))
.expect("write fresh response");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let next = runtime.block_on(async {
let cx = Cx::current().expect("ambient Cx");
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (mut read, write) = transport::plain_split(stream);
let write: SharedWriteHalf = Arc::new(AsyncMutex::with_name("drain_test_write", write));
break_and_drain_wire(&mut read, &write, Duration::from_secs(5))
.await
.expect("drain must succeed and leave the stream clean");
read_data_response(&mut read, &cx, &write)
.await
.expect("next read after drain must decode cleanly")
});
assert_eq!(
next, FRESH_BODY,
"after break_and_drain the reused connection must read the FRESH response, \
not the stale in-flight response ({INFLIGHT_BODY:?}) or error body ({ERROR_BODY:?})"
);
server.join().expect("server thread joins");
}
#[test]
fn core_break_and_drain_runs_after_caller_context_timeout() {
const INFLIGHT_BODY: &[u8] = &[0xD1, 0xA1, 0xB1];
const ERROR_BODY: &[u8] = &[0x04, 0x01, 0x0d];
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
use std::io::Write as _;
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_BREAK,
"recovery must send BREAK even after caller context timeout"
);
socket
.write_all(&data_packet(INFLIGHT_BODY, true))
.expect("write in-flight response");
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_BREAK))
.expect("write break-ack marker");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_RESET,
"recovery must answer break marker with RESET"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write reset-confirm marker");
socket
.write_all(&data_packet(ERROR_BODY, true))
.expect("write trailing error packet");
});
let runtime = build_io_runtime().expect("asupersync runtime");
runtime.block_on(async {
let cx = Cx::current().expect("ambient Cx");
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (read, write) = transport::plain_split(stream);
let mut core = ConnectionCore::<DriverTransport>::from_halves(
read,
write,
"timeout_drain_test_write",
);
cx.cancel_fast(asupersync::CancelKind::Timeout);
assert!(
cx.checkpoint().is_err(),
"test must start from an expired caller context"
);
core.break_and_drain_wire(Duration::from_secs(5))
.expect("recovery drain must ignore the expired caller context");
});
server.join().expect("server thread joins");
}
#[test]
fn reset_after_marker_drains_multiple_trailing_markers_no_duplicate_reset() {
const INFLIGHT_BODY: &[u8] = &[0xDE, 0xAD];
const ERROR_BODY: &[u8] = &[0x04, 0x01, 0x02];
const FRESH_BODY: &[u8] = &[0x11, 0x22, 0x33];
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
use std::io::Write as _;
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_BREAK,
"client must send a BREAK marker first"
);
socket
.write_all(&data_packet(INFLIGHT_BODY, true))
.expect("write in-flight response");
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_BREAK))
.expect("write break-ack marker");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_RESET,
"client must answer with a RESET"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write reset marker #1");
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write reset marker #2");
socket
.write_all(&data_packet(ERROR_BODY, true))
.expect("write trailing error packet");
socket
.write_all(&data_packet(FRESH_BODY, true))
.expect("write fresh response");
socket
.set_read_timeout(Some(Duration::from_millis(750)))
.expect("set short read timeout");
let mut extra = [0u8; 11];
if socket.read_exact(&mut extra).is_ok() {
panic!(
"client sent a DUPLICATE marker (type {}): the drain did not \
consume all trailing RESET markers (bead rust-oracledb-yhz)",
extra[10]
);
}
});
let runtime = build_io_runtime().expect("asupersync runtime");
let next = runtime.block_on(async {
let cx = Cx::current().expect("ambient Cx");
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (mut read, write) = transport::plain_split(stream);
let write: SharedWriteHalf = Arc::new(AsyncMutex::with_name("yhz_test_write", write));
break_and_drain_wire(&mut read, &write, Duration::from_secs(5))
.await
.expect("drain must succeed even with multiple RESET markers");
read_data_response(&mut read, &cx, &write)
.await
.expect("next read after drain must decode cleanly")
});
assert_eq!(
next, FRESH_BODY,
"after draining multiple RESET markers the reused connection must read \
the FRESH response"
);
server.join().expect("server thread joins");
}
#[test]
fn break_without_drain_leaves_stale_bytes_for_next_read() {
const STALE_BODY: &[u8] = &[0x53, 0x54, 0x41, 0x4c, 0x45]; const FRESH_BODY: &[u8] = &[0x11, 0x22, 0x33];
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
use std::io::Write as _;
assert_eq!(read_marker_type(&mut socket), TNS_MARKER_TYPE_BREAK);
socket
.write_all(&data_packet(STALE_BODY, true))
.expect("write stale in-flight response");
socket
.write_all(&data_packet(FRESH_BODY, true))
.expect("write fresh response");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let first_read = runtime.block_on(async {
let cx = Cx::current().expect("ambient Cx");
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (mut read, write) = transport::plain_split(stream);
let write: SharedWriteHalf =
Arc::new(AsyncMutex::with_name("nodrain_test_write", write));
send_marker_shared(&cx, &write, TNS_MARKER_TYPE_BREAK)
.await
.expect("send break");
read_data_response(&mut read, &cx, &write)
.await
.expect("read after bare break")
});
assert_eq!(
first_read, STALE_BODY,
"without the drain, the next read misframes onto the stale in-flight bytes — \
this is the bug break_and_drain fixes"
);
server.join().expect("server thread joins");
}
#[test]
fn dml_returning_error_flush_out_binds_after_reset_completes_without_hang() {
const ERROR_BODY: &[u8] = &[0x04, 0x01, 0x02, 0x37];
const FLUSH_REQUEST_BODY: &[u8] = &[0x07, 0x00, 0x00, TNS_MSG_TYPE_FLUSH_OUT_BINDS];
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
use std::io::Write as _;
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_BREAK))
.expect("write break marker");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_RESET,
"client must answer the BREAK with a RESET"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write reset-confirm marker");
socket
.write_all(&data_packet(FLUSH_REQUEST_BODY, false))
.expect("write flush-out-binds request");
let mut header = [0u8; 8];
socket
.read_exact(&mut header)
.expect("read flush-out-binds reply header");
assert_eq!(
header[4], TNS_PACKET_TYPE_DATA,
"client's flush-out-binds reply must be a DATA packet"
);
let len = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize;
let mut body = vec![0u8; len - 8];
socket
.read_exact(&mut body)
.expect("read flush-out-binds reply body");
assert_eq!(
body.last().copied(),
Some(TNS_MSG_TYPE_FLUSH_OUT_BINDS),
"client must reply with a FLUSH_OUT_BINDS message"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_BREAK))
.expect("write second break marker");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_RESET,
"client must answer the second BREAK with a RESET"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write second reset-confirm marker");
socket
.write_all(&data_packet(ERROR_BODY, true))
.expect("write trailing ORA-12899 error packet");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let payload = runtime.block_on(async {
let cx = Cx::current().expect("ambient Cx");
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (mut read, write) = transport::plain_split(stream);
let write: SharedWriteHalf =
Arc::new(AsyncMutex::with_name("returning_err_test_write", write));
time::timeout(
time::wall_now(),
Duration::from_secs(10),
read_data_response_flushing_out_binds(&mut read, &cx, &write, 8192),
)
.await
.expect("must NOT hang on the DML-RETURNING error path (flush-out-binds after reset)")
.expect("read must complete and yield the trailing error payload")
});
assert!(
payload.ends_with(ERROR_BODY),
"the reassembled response must end with the ORA-12899 error payload, got {payload:?}"
);
server.join().expect("server thread joins");
}
#[test]
fn cancel_and_drain_wire_leaves_connection_reusable() {
const INFLIGHT_BODY: &[u8] = &[0xCA, 0xFE];
const ERROR_BODY: &[u8] = &[0x04, 0x01, 0x0d];
const FRESH_BODY: &[u8] = &[0x07, 0x05, 0x0c];
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
use std::io::Write as _;
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_BREAK,
"cancel must send a BREAK marker first"
);
socket
.write_all(&data_packet(INFLIGHT_BODY, true))
.expect("write in-flight response");
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_BREAK))
.expect("write break-ack marker");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_RESET,
"cancel must answer the marker with a RESET"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write reset-confirm marker");
socket
.write_all(&data_packet(ERROR_BODY, true))
.expect("write trailing error packet");
socket
.write_all(&data_packet(FRESH_BODY, true))
.expect("write fresh response");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let next = runtime.block_on(async {
let cx = Cx::current().expect("ambient Cx");
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (mut read, write) = transport::plain_split(stream);
let write: SharedWriteHalf =
Arc::new(AsyncMutex::with_name("cancel_test_write", write));
cancel_and_drain_wire(&mut read, &write, Duration::from_secs(5))
.await
.expect("cancel drain must succeed and leave the stream clean");
read_data_response(&mut read, &cx, &write)
.await
.expect("next read after cancel must decode cleanly")
});
assert_eq!(
next, FRESH_BODY,
"after cancel the reused connection must read the FRESH response, not the \
stale in-flight response ({INFLIGHT_BODY:?}) or error body ({ERROR_BODY:?})"
);
server.join().expect("server thread joins");
}
#[test]
fn ensure_clean_drains_inflight_bare_request_before_reuse() {
const INFLIGHT_BODY: &[u8] = &[0xCA, 0xFE];
const ERROR_BODY: &[u8] = &[0x04, 0x01, 0x0d];
const FRESH_BODY: &[u8] = &[0x07, 0x05, 0x0c];
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
use std::io::Write as _;
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_BREAK,
"reuse after a bare request must send a BREAK to drain the stranded page"
);
socket
.write_all(&data_packet(INFLIGHT_BODY, true))
.expect("write stranded page");
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_BREAK))
.expect("write break-ack marker");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_RESET,
"drain must answer the break-ack marker with a RESET"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write reset-confirm marker");
socket
.write_all(&data_packet(ERROR_BODY, true))
.expect("write trailing error packet");
socket
.write_all(&data_packet(FRESH_BODY, true))
.expect("write fresh response");
});
let runtime = build_io_runtime().expect("asupersync runtime");
let next = runtime
.block_on(async {
let cx = Cx::current().expect("ambient Cx");
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (read, write) = transport::plain_split(stream);
let mut conn = loopback_connection(read, write);
conn.core
.recovery
.begin_operation()
.expect("enter InFlight");
assert_eq!(conn.core.recovery.phase(), SessionRecoveryPhase::InFlight);
conn.ensure_clean_before_request()
.await
.expect("a stranded bare request must be drained, not wedge the connection");
assert_eq!(
conn.core.recovery.phase(),
SessionRecoveryPhase::Ready,
"after draining the stranded page the session is Ready for reuse"
);
conn.core.read_data_response(&cx).await
})
.expect("the reused connection must read its fresh response");
assert_eq!(
next, FRESH_BODY,
"after draining the stranded bare request the reused connection must read \
the FRESH response, not the stranded page ({INFLIGHT_BODY:?})"
);
server.join().expect("server thread joins");
}
#[test]
fn drain_cancel_wire_drains_without_sending_a_break() {
const INFLIGHT_BODY: &[u8] = &[0xCA, 0xFE];
const ERROR_BODY: &[u8] = &[0x04, 0x01, 0x0d];
const FRESH_BODY: &[u8] = &[0x07, 0x05, 0x0c];
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener");
let addr = listener.local_addr().expect("listener address");
let server = thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept test client");
socket
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
use std::io::Write as _;
socket
.write_all(&data_packet(INFLIGHT_BODY, true))
.expect("write in-flight response");
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_BREAK))
.expect("write break-ack marker");
assert_eq!(
read_marker_type(&mut socket),
TNS_MARKER_TYPE_RESET,
"drain must answer the break-ack marker with a RESET"
);
socket
.write_all(&marker_packet(TNS_MARKER_TYPE_RESET))
.expect("write reset-confirm marker");
socket
.write_all(&data_packet(ERROR_BODY, true))
.expect("write trailing error packet");
socket
.write_all(&data_packet(FRESH_BODY, true))
.expect("write fresh response");
socket
.set_read_timeout(Some(Duration::from_millis(500)))
.expect("set short read timeout");
let mut extra = [0u8; 11];
if let Ok(()) = socket.read_exact(&mut extra) {
assert_ne!(
extra[10], TNS_MARKER_TYPE_BREAK,
"drain-only cancel must NOT send a BREAK marker"
);
}
});
let runtime = build_io_runtime().expect("asupersync runtime");
let next = runtime.block_on(async {
let cx = Cx::current().expect("ambient Cx");
let stream = TcpStream::connect(addr).await.expect("connect to listener");
let (mut read, write) = transport::plain_split(stream);
let write: SharedWriteHalf =
Arc::new(AsyncMutex::with_name("drain_cancel_test_write", write));
drain_cancel_wire(&mut read, &write, Duration::from_secs(5))
.await
.expect("drain-only cancel must succeed and leave the stream clean");
read_data_response(&mut read, &cx, &write)
.await
.expect("next read after drain must decode cleanly")
});
assert_eq!(
next, FRESH_BODY,
"after drain-only cancel the reused connection must read the FRESH response"
);
server.join().expect("server thread joins");
}
#[test]
fn cancel_drain_guard_transitions_recovery_phase_only_when_dropped_in_flight() -> Result<()> {
let recovery = Arc::new(SessionRecovery::new());
{
let _guard = CancelDrainGuard::arm(Arc::clone(&recovery))?;
assert_eq!(recovery.phase(), SessionRecoveryPhase::InFlight);
}
assert_eq!(
recovery.phase(),
SessionRecoveryPhase::BreakSent,
"dropping an armed guard (cancelled in flight) must require recovery"
);
assert!(recovery.begin_pending_drain()?);
assert_eq!(recovery.phase(), SessionRecoveryPhase::Draining);
assert!(
recovery.begin_pending_drain().is_err(),
"a drain that is already running must not start a second drain"
);
recovery.finish_drain_ready();
assert_eq!(recovery.phase(), SessionRecoveryPhase::Ready);
recovery.begin_operation()?;
match recovery.begin_operation() {
Err(Error::ConnectionClosed(message)) => {
assert!(message.contains("still in flight"));
}
other => {
return Err(Error::Runtime(format!(
"second operation start should fail while InFlight, got {other:?}"
)));
}
}
recovery.complete_operation();
assert_eq!(recovery.phase(), SessionRecoveryPhase::Ready);
recovery.begin_operation()?;
{
let mut guard = CancelDrainGuard::arm(Arc::clone(&recovery))?;
assert_eq!(recovery.phase(), SessionRecoveryPhase::InFlight);
guard.disarm();
}
assert_eq!(recovery.phase(), SessionRecoveryPhase::Ready);
{
let mut guard = CancelDrainGuard::arm(Arc::clone(&recovery))?;
assert_eq!(recovery.phase(), SessionRecoveryPhase::InFlight);
guard.disarm();
}
assert_eq!(
recovery.phase(),
SessionRecoveryPhase::Ready,
"a disarmed guard (clean completion) must NOT require recovery"
);
Ok(())
}
#[test]
fn cancelled_error_is_not_connection_lost_but_is_transient() {
let cancelled = Error::Cancelled;
assert!(
!cancelled.is_connection_lost(),
"a user cancel leaves the session alive (ORA-01013 / DPY-4024 semantics)"
);
assert!(
cancelled.is_transient(),
"a cancelled operation may be retried on the same clean connection"
);
assert_eq!(cancelled.ora_code(), Some(1013));
}
}