use crate::connection::auth::AuthResponseData;
use crate::connection::version::parse_release_version;
use crate::error::ConnectionError;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub max_retries: u32,
pub auto_commit: bool,
pub default_fetch_size: usize,
pub query_timeout: Option<Duration>,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
max_retries: 3,
auto_commit: true,
default_fetch_size: 1000,
query_timeout: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionState {
Ready,
InTransaction,
Closing,
Closed,
}
impl SessionState {
pub fn can_execute(&self) -> bool {
matches!(self, SessionState::Ready | SessionState::InTransaction)
}
}
pub struct Session {
session_id: String,
server_info: AuthResponseData,
config: SessionConfig,
state: Arc<RwLock<SessionState>>,
query_count: AtomicU64,
in_transaction: AtomicBool,
release_version: OnceLock<Option<(u32, u32, u32)>>,
}
impl Session {
pub fn new(session_id: String, server_info: AuthResponseData, config: SessionConfig) -> Self {
Self {
session_id,
server_info,
config,
state: Arc::new(RwLock::new(SessionState::Ready)),
query_count: AtomicU64::new(0),
in_transaction: AtomicBool::new(false),
release_version: OnceLock::new(),
}
}
pub fn supports_native_parquet_import(&self) -> bool {
self.parsed_release_version()
.is_some_and(crate::connection::version::supports_native_parquet_import)
}
pub fn supports_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
self.parsed_release_version()
.is_some_and(|version| version >= (major, minor, patch))
}
fn parsed_release_version(&self) -> Option<(u32, u32, u32)> {
*self
.release_version
.get_or_init(|| parse_release_version(&self.server_info.release_version))
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn server_info(&self) -> &AuthResponseData {
&self.server_info
}
pub fn config(&self) -> &SessionConfig {
&self.config
}
pub(crate) fn config_mut(&mut self) -> &mut SessionConfig {
&mut self.config
}
pub async fn state(&self) -> SessionState {
*self.state.read().await
}
pub async fn set_state(&self, new_state: SessionState) {
let mut state = self.state.write().await;
*state = new_state;
}
pub fn increment_query_count(&self) -> u64 {
self.query_count.fetch_add(1, Ordering::SeqCst) + 1
}
pub fn query_count(&self) -> u64 {
self.query_count.load(Ordering::SeqCst)
}
pub fn in_transaction(&self) -> bool {
self.in_transaction.load(Ordering::SeqCst)
}
pub(crate) async fn ensure_can_begin_transaction(&self) -> Result<(), ConnectionError> {
self.validate_ready().await?;
if self.in_transaction() {
return Err(ConnectionError::InvalidParameter {
parameter: "transaction".to_string(),
message: "Transaction already active".to_string(),
});
}
Ok(())
}
pub async fn begin_transaction(&self) -> Result<(), ConnectionError> {
self.ensure_can_begin_transaction().await?;
self.in_transaction.store(true, Ordering::SeqCst);
self.set_state(SessionState::InTransaction).await;
Ok(())
}
pub async fn commit_transaction(&self) -> Result<(), ConnectionError> {
if !self.in_transaction() {
return Ok(());
}
self.in_transaction.store(false, Ordering::SeqCst);
self.set_state(SessionState::Ready).await;
Ok(())
}
pub async fn rollback_transaction(&self) -> Result<(), ConnectionError> {
if !self.in_transaction() {
return Ok(());
}
self.in_transaction.store(false, Ordering::SeqCst);
self.set_state(SessionState::Ready).await;
Ok(())
}
pub async fn close(&self) -> Result<(), ConnectionError> {
self.set_state(SessionState::Closing).await;
if self.in_transaction() {
self.in_transaction.store(false, Ordering::SeqCst);
}
self.set_state(SessionState::Closed).await;
Ok(())
}
pub async fn is_closed(&self) -> bool {
matches!(self.state().await, SessionState::Closed)
}
pub async fn validate_ready(&self) -> Result<(), ConnectionError> {
if self.state().await.can_execute() {
Ok(())
} else {
Err(ConnectionError::ConnectionClosed)
}
}
}
impl std::fmt::Debug for Session {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Session")
.field("session_id", &self.session_id)
.field("config", &self.config)
.field("query_count", &self.query_count())
.field("in_transaction", &self.in_transaction())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::connection::auth::AuthResponseData;
fn mock_server_info() -> AuthResponseData {
mock_server_info_with_version("7.1.0")
}
fn mock_server_info_with_version(release_version: &str) -> AuthResponseData {
AuthResponseData {
session_id: "test_session".to_string(),
protocol_version: 3,
release_version: release_version.to_string(),
database_name: "EXA".to_string(),
product_name: "Exasol".to_string(),
max_data_message_size: 4_194_304,
max_identifier_length: 128,
max_varchar_length: 2_000_000,
identifier_quote_string: "\"".to_string(),
time_zone: "UTC".to_string(),
time_zone_behavior: "INVALID TIMESTAMP TO DOUBLE".to_string(),
}
}
#[tokio::test]
async fn test_session_creation() {
let session = Session::new(
"sess123".to_string(),
mock_server_info(),
SessionConfig::default(),
);
assert_eq!(session.session_id(), "sess123");
assert_eq!(session.state().await, SessionState::Ready);
assert_eq!(session.query_count(), 0);
assert!(!session.in_transaction());
}
#[tokio::test]
async fn test_session_state_transitions() {
let session = Session::new(
"sess123".to_string(),
mock_server_info(),
SessionConfig::default(),
);
assert_eq!(session.state().await, SessionState::Ready);
session.set_state(SessionState::InTransaction).await;
assert_eq!(session.state().await, SessionState::InTransaction);
session.set_state(SessionState::Closing).await;
assert_eq!(session.state().await, SessionState::Closing);
session.set_state(SessionState::Closed).await;
assert_eq!(session.state().await, SessionState::Closed);
}
#[tokio::test]
async fn test_session_query_counter() {
let session = Session::new(
"sess123".to_string(),
mock_server_info(),
SessionConfig::default(),
);
assert_eq!(session.increment_query_count(), 1);
assert_eq!(session.increment_query_count(), 2);
assert_eq!(session.query_count(), 2);
}
#[tokio::test]
async fn test_session_transaction() {
let session = Session::new(
"sess123".to_string(),
mock_server_info(),
SessionConfig::default(),
);
assert!(!session.in_transaction());
session.begin_transaction().await.unwrap();
assert!(session.in_transaction());
assert_eq!(session.state().await, SessionState::InTransaction);
match session.begin_transaction().await {
Err(ConnectionError::InvalidParameter { message, .. }) => {
assert_eq!(message, "Transaction already active")
}
other => panic!("expected an active-transaction error, got {:?}", other),
}
session.commit_transaction().await.unwrap();
assert!(!session.in_transaction());
assert_eq!(session.state().await, SessionState::Ready);
}
#[tokio::test]
async fn test_session_rollback() {
let session = Session::new(
"sess123".to_string(),
mock_server_info(),
SessionConfig::default(),
);
session.begin_transaction().await.unwrap();
assert!(session.in_transaction());
session.rollback_transaction().await.unwrap();
assert!(!session.in_transaction());
assert_eq!(session.state().await, SessionState::Ready);
}
#[tokio::test]
async fn test_commit_without_transaction_is_noop() {
let session = Session::new(
"sess123".to_string(),
mock_server_info(),
SessionConfig::default(),
);
assert!(!session.in_transaction());
session.commit_transaction().await.unwrap();
assert!(!session.in_transaction());
assert_eq!(session.state().await, SessionState::Ready);
}
#[tokio::test]
async fn test_rollback_without_transaction_is_noop() {
let session = Session::new(
"sess123".to_string(),
mock_server_info(),
SessionConfig::default(),
);
assert!(!session.in_transaction());
session.rollback_transaction().await.unwrap();
assert!(!session.in_transaction());
assert_eq!(session.state().await, SessionState::Ready);
}
#[tokio::test]
async fn test_session_close() {
let session = Session::new(
"sess123".to_string(),
mock_server_info(),
SessionConfig::default(),
);
assert!(!session.is_closed().await);
session.close().await.unwrap();
assert!(session.is_closed().await);
assert_eq!(session.state().await, SessionState::Closed);
}
#[tokio::test]
async fn test_session_validate_ready() {
let session = Session::new(
"sess123".to_string(),
mock_server_info(),
SessionConfig::default(),
);
assert!(session.validate_ready().await.is_ok());
session.set_state(SessionState::InTransaction).await;
assert!(session.validate_ready().await.is_ok());
for state in [SessionState::Closing, SessionState::Closed] {
session.set_state(state).await;
assert!(
matches!(
session.validate_ready().await,
Err(ConnectionError::ConnectionClosed)
),
"{:?} must report the connection as closed",
state
);
}
}
#[tokio::test]
async fn test_begin_transaction_reports_a_closed_session_as_closed() {
for state in [SessionState::Closing, SessionState::Closed] {
let session = Session::new(
"sess123".to_string(),
mock_server_info(),
SessionConfig::default(),
);
session.set_state(state).await;
assert!(
matches!(
session.begin_transaction().await,
Err(ConnectionError::ConnectionClosed)
),
"{:?} must report the connection as closed",
state
);
assert!(!session.in_transaction());
}
}
#[test]
fn test_session_state_checks() {
assert!(SessionState::Ready.can_execute());
assert!(SessionState::InTransaction.can_execute());
assert!(!SessionState::Closing.can_execute());
assert!(!SessionState::Closed.can_execute());
}
#[test]
fn test_supports_native_parquet_import_is_pure_and_memoized() {
let session_71 = Session::new(
"s1".to_string(),
mock_server_info_with_version("7.1.0"),
SessionConfig::default(),
);
assert!(!session_71.supports_native_parquet_import());
let session_2025_1_11 = Session::new(
"s2".to_string(),
mock_server_info_with_version("2025.1.11"),
SessionConfig::default(),
);
assert!(session_2025_1_11.supports_native_parquet_import());
let session_2025_2_1 = Session::new(
"s3".to_string(),
mock_server_info_with_version("2025.2.1"),
SessionConfig::default(),
);
assert!(session_2025_2_1.supports_native_parquet_import());
let session_garbage = Session::new(
"s4".to_string(),
mock_server_info_with_version("garbage"),
SessionConfig::default(),
);
assert!(!session_garbage.supports_native_parquet_import());
let first = session_2025_1_11.supports_native_parquet_import();
let second = session_2025_1_11.supports_native_parquet_import();
assert_eq!(first, second);
}
#[test]
fn supports_at_least_compares_major_minor_and_patch() {
let session = Session::new(
"s".to_string(),
mock_server_info_with_version("8.27.1"),
SessionConfig::default(),
);
let cases = [
((8, 27, 1), true),
((8, 27, 2), false),
((8, 28, 0), false),
((8, 26, 99), true),
((7, 99, 99), true),
((9, 0, 0), false),
];
for ((major, minor, patch), expected) in cases {
assert_eq!(
session.supports_at_least(major, minor, patch),
expected,
"8.27.1 against ({major}, {minor}, {patch})"
);
}
}
#[test]
fn supports_at_least_is_false_for_an_unparsable_release_version() {
for release in ["", "unknown", "v1"] {
let session = Session::new(
"s".to_string(),
mock_server_info_with_version(release),
SessionConfig::default(),
);
for (major, minor, patch) in [(0, 0, 0), (9, 0, 0)] {
assert!(
!session.supports_at_least(major, minor, patch),
"release {release:?} must count as below ({major}, {minor}, {patch})"
);
}
}
}
}