use arrow::record_batch::RecordBatch;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginInitRequest {
pub command: String,
pub protocol_version: i32,
}
impl LoginInitRequest {
pub fn new() -> Self {
Self {
command: "login".to_string(),
protocol_version: 3,
}
}
}
impl Default for LoginInitRequest {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyResponse {
pub status: String,
pub response_data: Option<PublicKeyData>,
pub exception: Option<ExceptionInfo>,
pub attributes: Option<ResponseAttributes>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyData {
pub public_key_pem: String,
pub public_key_modulus: String,
pub public_key_exponent: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthRequest {
pub username: String,
pub password: String,
pub use_compression: bool,
pub client_name: String,
pub driver_name: String,
pub client_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_os_username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<HashMap<String, serde_json::Value>>,
}
impl AuthRequest {
pub fn new(username: String, encrypted_password: String, client_name: String) -> Self {
Self {
username,
password: encrypted_password,
use_compression: false,
client_name: client_name.clone(),
driver_name: client_name,
client_version: env!("CARGO_PKG_VERSION").to_string(),
client_os_username: None,
attributes: None,
}
}
pub fn with_driver_name(mut self, driver_name: String) -> Self {
self.driver_name = driver_name;
self
}
pub fn with_client_version(mut self, version: String) -> Self {
self.client_version = version;
self
}
pub fn with_os_username(mut self, username: String) -> Self {
self.client_os_username = Some(username);
self
}
pub fn with_attributes(mut self, attributes: HashMap<String, serde_json::Value>) -> Self {
self.attributes = Some(attributes);
self
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginRequest {
pub command: String,
pub protocol_version: i32,
#[serde(flatten)]
pub credentials: Credentials,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Credentials {
pub username: String,
pub password: String,
}
impl LoginRequest {
pub fn new(username: String, password: String) -> Self {
Self {
command: "login".to_string(),
protocol_version: 3,
credentials: Credentials { username, password },
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginResponse {
pub status: String,
pub response_data: Option<LoginResponseData>,
pub exception: Option<ExceptionInfo>,
pub attributes: Option<ResponseAttributes>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginResponseData {
pub session_id: i64,
pub protocol_version: i32,
pub release_version: String,
pub database_name: String,
pub product_name: String,
pub max_data_message_size: Option<i64>,
pub max_varchar_size: Option<i64>,
pub max_identifier_length: Option<i64>,
pub identifier_quote_string: Option<String>,
pub time_zone: Option<String>,
pub time_zone_behavior: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteRequest {
pub command: String,
pub sql_text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result_set_max_rows: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<HashMap<String, String>>,
}
impl ExecuteRequest {
pub fn new(sql: String) -> Self {
Self {
command: "execute".to_string(),
sql_text: sql,
result_set_max_rows: None,
attributes: None,
}
}
pub fn with_max_rows(mut self, max_rows: i64) -> Self {
self.result_set_max_rows = Some(max_rows);
self
}
pub fn with_attribute(mut self, key: String, value: String) -> Self {
self.attributes
.get_or_insert_with(HashMap::new)
.insert(key, value);
self
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteResponse {
pub status: String,
pub response_data: Option<ExecuteResponseData>,
pub exception: Option<ExceptionInfo>,
pub attributes: Option<ResponseAttributes>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteResponseData {
pub num_results: i32,
pub results: Vec<ResultSetInfo>,
pub attributes: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResultSetInfo {
pub result_type: String,
pub row_count: Option<i64>,
pub result_set: Option<ResultSetData>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResultEntryKind {
ResultSet,
RowCount,
Unknown,
}
impl ResultSetInfo {
pub fn kind(&self) -> ResultEntryKind {
match self.result_type.as_str() {
"resultSet" => ResultEntryKind::ResultSet,
"rowCount" => ResultEntryKind::RowCount,
_ => ResultEntryKind::Unknown,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResultSetData {
pub result_set_handle: Option<i32>,
pub num_columns: Option<i32>,
pub num_rows: Option<i64>,
pub num_rows_in_message: Option<i64>,
pub columns: Option<Vec<ColumnInfo>>,
#[serde(default, deserialize_with = "super::deserialize::to_row_major_option")]
pub data: Option<Vec<Vec<serde_json::Value>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ColumnInfo {
pub name: String,
pub data_type: DataType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DataType {
#[serde(rename = "type")]
pub type_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub precision: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scale: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character_set: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub with_local_time_zone: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fraction: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub srid: Option<i32>,
}
impl DataType {
pub fn decimal(precision: i32, scale: i32) -> Self {
Self {
type_name: "DECIMAL".to_string(),
precision: Some(precision),
scale: Some(scale),
size: None,
character_set: None,
with_local_time_zone: None,
fraction: None,
srid: None,
}
}
pub fn double() -> Self {
Self {
type_name: "DOUBLE".to_string(),
precision: None,
scale: None,
size: None,
character_set: None,
with_local_time_zone: None,
fraction: None,
srid: None,
}
}
pub fn varchar(size: i64) -> Self {
Self {
type_name: "VARCHAR".to_string(),
precision: None,
scale: None,
size: Some(size),
character_set: Some("UTF8".to_string()),
with_local_time_zone: None,
fraction: None,
srid: None,
}
}
pub fn boolean() -> Self {
Self {
type_name: "BOOLEAN".to_string(),
precision: None,
scale: None,
size: None,
character_set: None,
with_local_time_zone: None,
fraction: None,
srid: None,
}
}
pub fn infer_from_json(value: &serde_json::Value) -> Self {
match value {
serde_json::Value::Null => Self::varchar(2_000_000),
serde_json::Value::Bool(_) => Self::boolean(),
serde_json::Value::Number(n) => {
if n.is_f64() {
Self::double()
} else {
Self::decimal(18, 0)
}
}
serde_json::Value::String(_) => Self::varchar(2_000_000),
serde_json::Value::Array(_) | serde_json::Value::Object(_) => Self::varchar(2_000_000),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchRequest {
pub command: String,
pub result_set_handle: i32,
pub start_position: i64,
pub num_bytes: i64,
}
impl FetchRequest {
pub fn new(result_set_handle: i32, start_position: i64, num_bytes: i64) -> Self {
Self {
command: "fetch".to_string(),
result_set_handle,
start_position,
num_bytes,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchResponse {
pub status: String,
pub response_data: Option<FetchResponseData>,
pub exception: Option<ExceptionInfo>,
pub attributes: Option<ResponseAttributes>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchResponseData {
pub num_rows: i64,
#[serde(deserialize_with = "super::deserialize::to_row_major")]
pub data: Vec<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseResultSetRequest {
pub command: String,
pub result_set_handles: Vec<i32>,
}
impl CloseResultSetRequest {
pub fn new(handles: Vec<i32>) -> Self {
Self {
command: "closeResultSet".to_string(),
result_set_handles: handles,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseResultSetResponse {
pub status: String,
pub exception: Option<ExceptionInfo>,
pub attributes: Option<ResponseAttributes>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatePreparedStatementRequest {
pub command: String,
pub sql_text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<serde_json::Value>,
}
impl CreatePreparedStatementRequest {
pub fn new(sql: impl Into<String>) -> Self {
Self {
command: "createPreparedStatement".to_string(),
sql_text: sql.into(),
attributes: None,
}
}
pub fn with_attributes(mut self, attributes: serde_json::Value) -> Self {
self.attributes = Some(attributes);
self
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParameterInfo {
#[serde(default)]
pub name: Option<String>,
pub data_type: DataType,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParameterData {
pub num_columns: i32,
pub columns: Vec<ParameterInfo>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatePreparedStatementResponse {
pub status: String,
pub response_data: Option<PreparedStatementResponseData>,
pub exception: Option<ExceptionInfo>,
pub attributes: Option<ResponseAttributes>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PreparedStatementResponseData {
pub statement_handle: i32,
pub parameter_data: Option<ParameterData>,
pub num_results: Option<i32>,
pub results: Option<Vec<ResultSetInfo>>,
}
impl PreparedStatementResponseData {
pub fn result_set_columns(&self) -> Vec<ColumnInfo> {
self.results
.as_deref()
.unwrap_or_default()
.iter()
.find(|entry| entry.kind() == ResultEntryKind::ResultSet)
.and_then(|entry| entry.result_set.as_ref())
.and_then(|result_set| result_set.columns.clone())
.unwrap_or_default()
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutePreparedStatementRequest {
pub command: String,
pub statement_handle: i32,
pub num_columns: i32,
pub num_rows: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub columns: Option<Vec<ColumnInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Vec<Vec<serde_json::Value>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<serde_json::Value>,
}
impl ExecutePreparedStatementRequest {
pub fn new(statement_handle: i32) -> Self {
Self {
command: "executePreparedStatement".to_string(),
statement_handle,
num_columns: 0,
num_rows: 0,
columns: None,
data: None,
attributes: None,
}
}
pub fn with_data(
mut self,
columns: Vec<ColumnInfo>,
data: Vec<Vec<serde_json::Value>>,
) -> Self {
self.num_columns = columns.len() as i32;
self.num_rows = if data.is_empty() {
0
} else {
data[0].len() as i32
};
self.columns = Some(columns);
self.data = Some(data);
self
}
pub fn with_attributes(mut self, attributes: serde_json::Value) -> Self {
self.attributes = Some(attributes);
self
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClosePreparedStatementRequest {
pub command: String,
pub statement_handle: i32,
}
impl ClosePreparedStatementRequest {
pub fn new(statement_handle: i32) -> Self {
Self {
command: "closePreparedStatement".to_string(),
statement_handle,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClosePreparedStatementResponse {
pub status: String,
pub exception: Option<ExceptionInfo>,
pub attributes: Option<ResponseAttributes>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DisconnectRequest {
pub command: String,
}
impl DisconnectRequest {
pub fn new() -> Self {
Self {
command: "disconnect".to_string(),
}
}
}
impl Default for DisconnectRequest {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DisconnectResponse {
pub status: String,
pub exception: Option<ExceptionInfo>,
pub attributes: Option<ResponseAttributes>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetAttributesRequest {
pub command: String,
pub attributes: std::collections::HashMap<String, serde_json::Value>,
}
impl SetAttributesRequest {
pub fn new(attributes: std::collections::HashMap<String, serde_json::Value>) -> Self {
Self {
command: "setAttributes".to_string(),
attributes,
}
}
pub fn autocommit(enabled: bool) -> Self {
let mut attributes = std::collections::HashMap::new();
attributes.insert("autocommit".to_string(), serde_json::Value::Bool(enabled));
Self::new(attributes)
}
pub fn query_timeout(seconds: u64) -> Self {
let mut attributes = std::collections::HashMap::new();
attributes.insert("queryTimeout".to_string(), serde_json::Value::from(seconds));
Self::new(attributes)
}
pub fn current_schema(schema: &str) -> Self {
let mut attributes = std::collections::HashMap::new();
attributes.insert("currentSchema".to_string(), serde_json::Value::from(schema));
Self::new(attributes)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetAttributesResponse {
pub status: String,
pub exception: Option<ExceptionInfo>,
pub attributes: Option<ResponseAttributes>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetAttributesRequest {
pub command: String,
}
impl GetAttributesRequest {
pub fn new() -> Self {
Self {
command: "getAttributes".to_string(),
}
}
}
impl Default for GetAttributesRequest {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetAttributesResponse {
pub status: String,
pub exception: Option<ExceptionInfo>,
pub attributes: Option<ResponseAttributes>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResponseAttributes {
pub current_schema: Option<String>,
}
pub trait AttributedResponse {
fn attributes(&self) -> Option<&ResponseAttributes>;
}
macro_rules! impl_attributed_response {
($($response:ty),+ $(,)?) => {
$(impl AttributedResponse for $response {
fn attributes(&self) -> Option<&ResponseAttributes> {
self.attributes.as_ref()
}
})+
};
}
impl_attributed_response!(
PublicKeyResponse,
LoginResponse,
DisconnectResponse,
ExecuteResponse,
FetchResponse,
CloseResultSetResponse,
CreatePreparedStatementResponse,
ClosePreparedStatementResponse,
SetAttributesResponse,
GetAttributesResponse,
);
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExceptionInfo {
pub sql_code: Option<String>,
pub text: String,
}
#[derive(Debug, Clone)]
pub struct SessionInfo {
pub session_id: String,
pub protocol_version: i32,
pub release_version: String,
pub database_name: String,
pub product_name: String,
pub max_data_message_size: i64,
pub time_zone: Option<String>,
}
impl From<LoginResponseData> for SessionInfo {
fn from(data: LoginResponseData) -> Self {
Self {
session_id: data.session_id.to_string(),
protocol_version: data.protocol_version,
release_version: data.release_version,
database_name: data.database_name,
product_name: data.product_name,
max_data_message_size: data.max_data_message_size.unwrap_or(1024 * 1024), time_zone: data.time_zone,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ResultSetHandle(pub i32);
impl ResultSetHandle {
pub fn new(handle: i32) -> Self {
Self(handle)
}
pub fn as_i32(&self) -> i32 {
self.0
}
}
impl From<i32> for ResultSetHandle {
fn from(handle: i32) -> Self {
Self(handle)
}
}
#[derive(Debug, Clone)]
pub enum ResultPayload {
Json(Vec<Vec<serde_json::Value>>),
Arrow(RecordBatch),
}
impl ResultPayload {
pub fn is_empty(&self) -> bool {
match self {
ResultPayload::Json(rows) => rows.is_empty(),
ResultPayload::Arrow(batch) => batch.num_rows() == 0,
}
}
pub fn num_rows(&self) -> usize {
match self {
ResultPayload::Json(rows) => rows.len(),
ResultPayload::Arrow(batch) => batch.num_rows(),
}
}
pub fn as_json(&self) -> Option<&Vec<Vec<serde_json::Value>>> {
match self {
ResultPayload::Json(rows) => Some(rows),
ResultPayload::Arrow(_) => None,
}
}
pub fn as_arrow(&self) -> Option<&RecordBatch> {
match self {
ResultPayload::Json(_) => None,
ResultPayload::Arrow(batch) => Some(batch),
}
}
pub fn into_arrow(self) -> Option<RecordBatch> {
match self {
ResultPayload::Json(_) => None,
ResultPayload::Arrow(batch) => Some(batch),
}
}
}
#[derive(Debug, Clone)]
pub struct ResultData {
pub columns: Vec<ColumnInfo>,
pub data: ResultPayload,
pub total_rows: i64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_login_init_request_serialization() {
let request = LoginInitRequest::new();
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"login\""));
assert!(json.contains("\"protocolVersion\":3"));
assert!(!json.contains("username"));
assert!(!json.contains("password"));
}
#[test]
fn test_public_key_response_deserialization() {
let json = r#"{
"status": "ok",
"responseData": {
"publicKeyPem": "-----BEGIN RSA PUBLIC KEY-----\nMIIBCg...\n-----END RSA PUBLIC KEY-----",
"publicKeyModulus": "abc123",
"publicKeyExponent": "010001"
}
}"#;
let response: PublicKeyResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, "ok");
let data = response.response_data.unwrap();
assert!(data.public_key_pem.contains("BEGIN RSA PUBLIC KEY"));
assert_eq!(data.public_key_modulus, "abc123");
assert_eq!(data.public_key_exponent, "010001");
}
#[test]
fn test_auth_request_serialization() {
let request = AuthRequest::new(
"sys".to_string(),
"encrypted_password_base64".to_string(),
"exarrow-rs".to_string(),
);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"username\":\"sys\""));
assert!(json.contains("\"password\":\"encrypted_password_base64\""));
assert!(json.contains("\"useCompression\":false"));
assert!(json.contains("\"clientName\":\"exarrow-rs\""));
assert!(json.contains("\"driverName\":\"exarrow-rs\""));
assert!(json.contains("\"clientVersion\":"));
assert!(!json.contains("\"attributes\":null"));
}
#[test]
fn test_login_request_serialization() {
let request = LoginRequest::new("sys".to_string(), "exasol".to_string());
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"login\""));
assert!(json.contains("\"username\":\"sys\""));
assert!(json.contains("\"password\":\"exasol\""));
assert!(json.contains("\"protocolVersion\":3"));
}
#[test]
fn test_execute_request_serialization() {
let request = ExecuteRequest::new("SELECT * FROM test".to_string())
.with_max_rows(1000)
.with_attribute("autocommit".to_string(), "true".to_string());
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"execute\""));
assert!(json.contains("SELECT * FROM test"));
assert!(json.contains("\"resultSetMaxRows\":1000"));
assert!(json.contains("\"autocommit\""));
}
#[test]
fn test_fetch_request_serialization() {
let request = FetchRequest::new(1, 0, 1024 * 1024);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"fetch\""));
assert!(json.contains("\"resultSetHandle\":1"));
assert!(json.contains("\"startPosition\":0"));
}
#[test]
fn test_disconnect_request_serialization() {
let request = DisconnectRequest::new();
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"disconnect\""));
}
#[test]
fn test_set_attributes_query_timeout_serialization() {
let request = SetAttributesRequest::query_timeout(30);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"setAttributes\""));
assert!(json.contains("\"queryTimeout\":30"));
}
#[test]
fn test_login_response_deserialization() {
let json = r#"{
"status": "ok",
"responseData": {
"sessionId": 1234567890,
"protocolVersion": 3,
"releaseVersion": "8.0.0",
"databaseName": "MYDB",
"productName": "Exasol",
"maxDataMessageSize": 2097152,
"timeZone": "UTC"
}
}"#;
let response: LoginResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, "ok");
let data = response.response_data.unwrap();
assert_eq!(data.session_id, 1234567890);
assert_eq!(data.protocol_version, 3);
assert_eq!(data.release_version, "8.0.0");
assert_eq!(data.database_name, "MYDB");
}
#[test]
fn test_execute_response_deserialization() {
let json = r#"{
"status": "ok",
"responseData": {
"numResults": 1,
"results": [
{
"resultType": "resultSet",
"resultSet": {
"resultSetHandle": 1,
"numColumns": 2,
"numRows": 2,
"numRowsInMessage": 2,
"columns": [
{
"name": "ID",
"dataType": {
"type": "DECIMAL",
"precision": 18,
"scale": 0
}
},
{
"name": "NAME",
"dataType": {
"type": "VARCHAR",
"size": 100,
"characterSet": "UTF8"
}
}
],
"data": [
[1, 2],
["Alice", "Bob"]
]
}
}
]
}
}"#;
let response: ExecuteResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, "ok");
let data = response.response_data.unwrap();
assert_eq!(data.num_results, 1);
assert_eq!(data.results.len(), 1);
let result = &data.results[0];
assert_eq!(result.result_type, "resultSet");
let result_set = result.result_set.as_ref().unwrap();
assert_eq!(result_set.result_set_handle.unwrap(), 1);
assert_eq!(result_set.num_rows.unwrap(), 2);
let columns = result_set.columns.as_ref().unwrap();
assert_eq!(columns.len(), 2);
assert_eq!(columns[0].name, "ID");
assert_eq!(columns[0].data_type.type_name, "DECIMAL");
assert_eq!(columns[1].name, "NAME");
assert_eq!(columns[1].data_type.type_name, "VARCHAR");
let data_cols = result_set.data.as_ref().unwrap();
assert_eq!(data_cols.len(), 2);
assert_eq!(data_cols[0].len(), 2); assert_eq!(data_cols[1].len(), 2); }
#[test]
fn test_error_response_deserialization() {
let json = r#"{
"status": "error",
"exception": {
"sqlCode": "42000",
"text": "Syntax error at position 10"
}
}"#;
let response: ExecuteResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, "error");
let exception = response.exception.unwrap();
assert_eq!(exception.sql_code.unwrap(), "42000");
assert!(exception.text.contains("Syntax error"));
}
#[test]
fn test_result_set_handle() {
let handle = ResultSetHandle::new(42);
assert_eq!(handle.as_i32(), 42);
let handle2: ResultSetHandle = 42.into();
assert_eq!(handle, handle2);
}
#[test]
fn test_session_info_from_login_response() {
let response_data = LoginResponseData {
session_id: 1234567890,
protocol_version: 3,
release_version: "8.0.0".to_string(),
database_name: "TEST_DB".to_string(),
product_name: "Exasol".to_string(),
max_data_message_size: Some(5242880),
max_varchar_size: None,
max_identifier_length: None,
identifier_quote_string: None,
time_zone: Some("Europe/Berlin".to_string()),
time_zone_behavior: None,
};
let session_info: SessionInfo = response_data.into();
assert_eq!(session_info.session_id, "1234567890");
assert_eq!(session_info.protocol_version, 3);
assert_eq!(session_info.max_data_message_size, 5242880);
assert_eq!(session_info.time_zone.unwrap(), "Europe/Berlin");
}
#[test]
fn test_create_prepared_statement_request_serialization() {
let request = CreatePreparedStatementRequest::new("SELECT * FROM test WHERE id = ?");
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"createPreparedStatement\""));
assert!(json.contains("\"sqlText\":\"SELECT * FROM test WHERE id = ?\""));
assert!(!json.contains("\"attributes\":null"));
}
#[test]
fn test_create_prepared_statement_response_deserialization() {
let json = r#"{
"status": "ok",
"responseData": {
"statementHandle": 42,
"parameterData": {
"numColumns": 1,
"columns": [
{
"dataType": {
"type": "DECIMAL",
"precision": 18,
"scale": 0
}
}
]
}
}
}"#;
let response: CreatePreparedStatementResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, "ok");
let data = response.response_data.unwrap();
assert_eq!(data.statement_handle, 42);
let param_data = data.parameter_data.unwrap();
assert_eq!(param_data.num_columns, 1);
assert_eq!(param_data.columns.len(), 1);
assert_eq!(param_data.columns[0].data_type.type_name, "DECIMAL");
}
#[test]
fn prepare_response_result_set_columns_are_read() {
let json = r#"{
"status": "ok",
"responseData": {
"statementHandle": 42,
"results": [
{
"resultType": "resultSet",
"resultSet": {
"columns": [
{
"name": "ID",
"dataType": {
"type": "DECIMAL",
"precision": 18,
"scale": 0
}
},
{
"name": "NAME",
"dataType": {
"type": "VARCHAR",
"size": 50
}
}
]
}
}
]
}
}"#;
let response: CreatePreparedStatementResponse = serde_json::from_str(json).unwrap();
let columns = response.response_data.unwrap().result_set_columns();
assert_eq!(columns.len(), 2);
assert_eq!(columns[0].name, "ID");
assert_eq!(columns[0].data_type.type_name, "DECIMAL");
assert_eq!(columns[0].data_type.precision, Some(18));
assert_eq!(columns[0].data_type.scale, Some(0));
assert_eq!(columns[1].name, "NAME");
assert_eq!(columns[1].data_type.type_name, "VARCHAR");
assert_eq!(columns[1].data_type.size, Some(50));
}
#[test]
fn prepare_response_without_a_result_set_yields_no_columns() {
let json = r#"{
"status": "ok",
"responseData": {
"statementHandle": 42,
"results": [
{
"resultType": "rowCount",
"rowCount": 1
}
]
}
}"#;
let response: CreatePreparedStatementResponse = serde_json::from_str(json).unwrap();
let columns = response.response_data.unwrap().result_set_columns();
assert!(columns.is_empty());
}
#[test]
fn prepare_response_with_absent_results_yields_no_columns() {
let json = r#"{
"status": "ok",
"responseData": {
"statementHandle": 42
}
}"#;
let response: CreatePreparedStatementResponse = serde_json::from_str(json).unwrap();
let columns = response.response_data.unwrap().result_set_columns();
assert!(columns.is_empty());
}
#[test]
fn prepare_response_result_set_without_columns_yields_no_columns() {
let json = r#"{
"status": "ok",
"responseData": {
"statementHandle": 42,
"results": [
{
"resultType": "resultSet",
"resultSet": {
"numColumns": 0
}
}
]
}
}"#;
let response: CreatePreparedStatementResponse = serde_json::from_str(json).unwrap();
let columns = response.response_data.unwrap().result_set_columns();
assert!(columns.is_empty());
}
#[test]
fn prepare_response_with_two_result_sets_takes_the_first() {
let json = r#"{
"status": "ok",
"responseData": {
"statementHandle": 42,
"results": [
{
"resultType": "resultSet",
"resultSet": {
"columns": [
{
"name": "FIRST",
"dataType": {
"type": "DECIMAL",
"precision": 18,
"scale": 0
}
}
]
}
},
{
"resultType": "resultSet",
"resultSet": {
"columns": [
{
"name": "SECOND",
"dataType": {
"type": "VARCHAR",
"size": 10
}
}
]
}
}
]
}
}"#;
let response: CreatePreparedStatementResponse = serde_json::from_str(json).unwrap();
let columns = response.response_data.unwrap().result_set_columns();
assert_eq!(columns.len(), 1);
assert_eq!(columns[0].name, "FIRST");
assert_eq!(columns[0].data_type.type_name, "DECIMAL");
assert_eq!(columns[0].data_type.precision, Some(18));
assert_eq!(columns[0].data_type.scale, Some(0));
}
#[test]
fn test_execute_prepared_statement_request_serialization() {
let columns = vec![ColumnInfo {
name: "ID".to_string(),
data_type: DataType {
type_name: "DECIMAL".to_string(),
precision: Some(18),
scale: Some(0),
size: None,
character_set: None,
with_local_time_zone: None,
fraction: None,
srid: None,
},
}];
let data = vec![vec![serde_json::json!(1), serde_json::json!(2)]];
let request = ExecutePreparedStatementRequest::new(42).with_data(columns, data);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"executePreparedStatement\""));
assert!(json.contains("\"statementHandle\":42"));
assert!(json.contains("\"numColumns\":1"));
assert!(json.contains("\"numRows\":2"));
assert!(json.contains("\"columns\""));
assert!(json.contains("\"data\""));
}
#[test]
fn test_execute_prepared_statement_request_no_params() {
let request = ExecutePreparedStatementRequest::new(42);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"executePreparedStatement\""));
assert!(json.contains("\"statementHandle\":42"));
assert!(json.contains("\"numColumns\":0"));
assert!(json.contains("\"numRows\":0"));
assert!(!json.contains("\"columns\":null"));
assert!(!json.contains("\"data\":null"));
}
#[test]
fn test_close_prepared_statement_request_serialization() {
let request = ClosePreparedStatementRequest::new(42);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"closePreparedStatement\""));
assert!(json.contains("\"statementHandle\":42"));
}
#[test]
fn test_close_prepared_statement_response_deserialization() {
let json = r#"{
"status": "ok"
}"#;
let response: ClosePreparedStatementResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, "ok");
assert!(response.exception.is_none());
}
#[test]
fn test_close_prepared_statement_error_response() {
let json = r#"{
"status": "error",
"exception": {
"sqlCode": "00000",
"text": "Invalid statement handle"
}
}"#;
let response: ClosePreparedStatementResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, "error");
let exception = response.exception.unwrap();
assert!(exception.text.contains("Invalid statement handle"));
}
#[test]
fn test_data_type_decimal() {
let dt = DataType::decimal(18, 0);
assert_eq!(dt.type_name, "DECIMAL");
assert_eq!(dt.precision, Some(18));
assert_eq!(dt.scale, Some(0));
assert!(dt.size.is_none());
}
#[test]
fn test_data_type_double() {
let dt = DataType::double();
assert_eq!(dt.type_name, "DOUBLE");
assert!(dt.precision.is_none());
assert!(dt.scale.is_none());
}
#[test]
fn test_data_type_varchar() {
let dt = DataType::varchar(2_000_000);
assert_eq!(dt.type_name, "VARCHAR");
assert_eq!(dt.size, Some(2_000_000));
assert_eq!(dt.character_set, Some("UTF8".to_string()));
}
#[test]
fn test_data_type_boolean() {
let dt = DataType::boolean();
assert_eq!(dt.type_name, "BOOLEAN");
}
#[test]
fn test_data_type_infer_from_json_null() {
let dt = DataType::infer_from_json(&serde_json::Value::Null);
assert_eq!(dt.type_name, "VARCHAR");
assert_eq!(dt.size, Some(2_000_000));
}
#[test]
fn test_data_type_infer_from_json_bool() {
let dt = DataType::infer_from_json(&serde_json::json!(true));
assert_eq!(dt.type_name, "BOOLEAN");
}
#[test]
fn test_data_type_infer_from_json_integer() {
let dt = DataType::infer_from_json(&serde_json::json!(42));
assert_eq!(dt.type_name, "DECIMAL");
assert_eq!(dt.precision, Some(18));
assert_eq!(dt.scale, Some(0));
}
#[test]
fn test_data_type_infer_from_json_float() {
let dt = DataType::infer_from_json(&serde_json::json!(3.125));
assert_eq!(dt.type_name, "DOUBLE");
}
#[test]
fn test_data_type_infer_from_json_string() {
let dt = DataType::infer_from_json(&serde_json::json!("hello"));
assert_eq!(dt.type_name, "VARCHAR");
assert_eq!(dt.size, Some(2_000_000));
}
#[test]
fn test_data_type_infer_from_json_array() {
let dt = DataType::infer_from_json(&serde_json::json!([1, 2, 3]));
assert_eq!(dt.type_name, "VARCHAR");
}
#[test]
fn test_data_type_infer_from_json_object() {
let dt = DataType::infer_from_json(&serde_json::json!({"key": "value"}));
assert_eq!(dt.type_name, "VARCHAR");
}
#[test]
fn test_login_init_request_default_equals_new() {
let from_default = LoginInitRequest::default();
let from_new = LoginInitRequest::new();
assert_eq!(from_default.command, from_new.command);
assert_eq!(from_default.protocol_version, from_new.protocol_version);
}
#[test]
fn test_disconnect_request_default_equals_new() {
let from_default = DisconnectRequest::default();
assert_eq!(from_default.command, DisconnectRequest::new().command);
assert_eq!(from_default.command, "disconnect");
}
#[test]
fn test_auth_request_new_defaults_driver_name_to_client_name() {
let request = AuthRequest::new(
"sys".to_string(),
"cipher".to_string(),
"exarrow-rs".to_string(),
);
assert_eq!(request.driver_name, "exarrow-rs");
assert_eq!(request.client_version, env!("CARGO_PKG_VERSION"));
assert!(!request.use_compression);
assert!(request.client_os_username.is_none());
assert!(request.attributes.is_none());
}
#[test]
fn test_auth_request_with_driver_name_overrides_the_default() {
let request = AuthRequest::new(
"sys".to_string(),
"cipher".to_string(),
"exarrow-rs".to_string(),
)
.with_driver_name("exapump".to_string());
assert_eq!(request.driver_name, "exapump");
assert_eq!(request.client_name, "exarrow-rs");
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"driverName\":\"exapump\""));
}
#[test]
fn test_auth_request_with_client_version_overrides_the_crate_version() {
let request = AuthRequest::new(
"sys".to_string(),
"cipher".to_string(),
"exarrow-rs".to_string(),
)
.with_client_version("9.9.9".to_string());
assert_eq!(request.client_version, "9.9.9");
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"clientVersion\":\"9.9.9\""));
}
#[test]
fn test_auth_request_with_os_username_serializes_as_camel_case() {
let request = AuthRequest::new(
"sys".to_string(),
"cipher".to_string(),
"exarrow-rs".to_string(),
)
.with_os_username("alice".to_string());
assert_eq!(request.client_os_username, Some("alice".to_string()));
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"clientOsUsername\":\"alice\""));
}
#[test]
fn test_auth_request_with_attributes_serializes_the_map() {
let mut attributes = HashMap::new();
attributes.insert("autocommit".to_string(), serde_json::Value::Bool(false));
let request = AuthRequest::new(
"sys".to_string(),
"cipher".to_string(),
"exarrow-rs".to_string(),
)
.with_attributes(attributes);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"attributes\""));
assert!(json.contains("\"autocommit\":false"));
}
#[test]
fn test_auth_request_builders_chain_without_losing_earlier_settings() {
let request = AuthRequest::new(
"sys".to_string(),
"cipher".to_string(),
"exarrow-rs".to_string(),
)
.with_driver_name("exapump".to_string())
.with_client_version("1.2.3".to_string())
.with_os_username("bob".to_string());
assert_eq!(request.driver_name, "exapump");
assert_eq!(request.client_version, "1.2.3");
assert_eq!(request.client_os_username, Some("bob".to_string()));
assert_eq!(request.username, "sys");
assert_eq!(request.password, "cipher");
}
#[test]
fn test_close_result_set_request_serializes_every_handle() {
let request = CloseResultSetRequest::new(vec![1, 2, 3]);
assert_eq!(request.command, "closeResultSet");
assert_eq!(request.result_set_handles, vec![1, 2, 3]);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"command\":\"closeResultSet\""));
assert!(json.contains("\"resultSetHandles\":[1,2,3]"));
}
#[test]
fn test_close_result_set_request_accepts_an_empty_handle_list() {
let request = CloseResultSetRequest::new(vec![]);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"resultSetHandles\":[]"));
}
#[test]
fn test_create_prepared_statement_request_with_attributes_serializes_them() {
let request = CreatePreparedStatementRequest::new("SELECT 1")
.with_attributes(serde_json::json!({"autocommit": true}));
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"attributes\":{\"autocommit\":true}"));
}
#[test]
fn test_execute_prepared_statement_request_with_empty_data_reports_zero_rows() {
let request = ExecutePreparedStatementRequest::new(7).with_data(vec![], vec![]);
assert_eq!(request.num_columns, 0);
assert_eq!(request.num_rows, 0);
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"numRows\":0"));
assert!(json.contains("\"data\":[]"));
}
#[test]
fn test_execute_prepared_statement_request_with_attributes_serializes_them() {
let request = ExecutePreparedStatementRequest::new(7)
.with_attributes(serde_json::json!({"queryTimeout": 30}));
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"attributes\":{\"queryTimeout\":30}"));
}
#[test]
fn test_set_attributes_request_autocommit_serializes_the_flag() {
let enabled = SetAttributesRequest::autocommit(true);
let disabled = SetAttributesRequest::autocommit(false);
assert_eq!(enabled.command, "setAttributes");
assert_eq!(
enabled.attributes.get("autocommit"),
Some(&serde_json::Value::Bool(true))
);
let json = serde_json::to_string(&disabled).unwrap();
assert!(json.contains("\"command\":\"setAttributes\""));
assert!(json.contains("\"autocommit\":false"));
}
#[test]
fn test_set_attributes_request_current_schema_sends_the_name_as_given() {
let request = SetAttributesRequest::current_schema("zz-hyphen");
assert_eq!(
serde_json::to_value(&request).unwrap(),
serde_json::json!({
"command": "setAttributes",
"attributes": {"currentSchema": "zz-hyphen"}
})
);
}
#[test]
fn test_get_attributes_request_serialization() {
assert_eq!(
serde_json::to_value(GetAttributesRequest::new()).unwrap(),
serde_json::json!({"command": "getAttributes"})
);
}
#[test]
fn test_get_attributes_response_reads_the_current_schema_among_mixed_value_types() {
let response: GetAttributesResponse = serde_json::from_value(serde_json::json!({
"status": "ok",
"attributes": {
"autocommit": true,
"queryTimeout": 0,
"currentSchema": "SYS",
"snapshotTransactionsEnabled": false
}
}))
.unwrap();
assert_eq!(
response
.attributes()
.and_then(|attributes| attributes.current_schema.as_deref()),
Some("SYS")
);
}
#[test]
fn test_response_without_top_level_attributes_reports_none() {
let response: SetAttributesResponse =
serde_json::from_value(serde_json::json!({"status": "ok"})).unwrap();
assert!(response.attributes().is_none());
}
#[test]
fn test_execute_response_reads_top_level_attributes_apart_from_response_data() {
let response: ExecuteResponse = serde_json::from_value(serde_json::json!({
"status": "ok",
"responseData": {
"numResults": 1,
"results": [{"resultType": "rowCount", "rowCount": 0}]
},
"attributes": {"currentSchema": ""}
}))
.unwrap();
assert_eq!(
response
.attributes()
.and_then(|attributes| attributes.current_schema.as_deref()),
Some("")
);
}
fn int_record_batch(values: Vec<i32>) -> RecordBatch {
use arrow::array::Int32Array;
use arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
use std::sync::Arc;
let schema = Arc::new(Schema::new(vec![Field::new(
"ID",
ArrowDataType::Int32,
false,
)]));
RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(values))])
.expect("single Int32 column matches the schema")
}
#[test]
fn test_result_payload_json_reports_emptiness_and_row_count() {
let empty = ResultPayload::Json(vec![]);
let two_rows =
ResultPayload::Json(vec![vec![serde_json::json!(1)], vec![serde_json::json!(2)]]);
assert!(empty.is_empty());
assert_eq!(empty.num_rows(), 0);
assert!(!two_rows.is_empty());
assert_eq!(two_rows.num_rows(), 2);
}
#[test]
fn test_result_payload_arrow_reports_emptiness_and_row_count() {
let empty = ResultPayload::Arrow(int_record_batch(vec![]));
let three_rows = ResultPayload::Arrow(int_record_batch(vec![10, 20, 30]));
assert!(empty.is_empty());
assert_eq!(empty.num_rows(), 0);
assert!(!three_rows.is_empty());
assert_eq!(three_rows.num_rows(), 3);
}
#[test]
fn test_result_payload_json_exposes_rows_only_through_as_json() {
let payload = ResultPayload::Json(vec![vec![serde_json::json!("a")]]);
assert_eq!(payload.as_json(), Some(&vec![vec![serde_json::json!("a")]]));
assert!(payload.as_arrow().is_none());
assert!(payload.clone().into_arrow().is_none());
}
#[test]
fn test_result_payload_arrow_exposes_batch_only_through_as_arrow() {
let payload = ResultPayload::Arrow(int_record_batch(vec![42]));
assert!(payload.as_json().is_none());
assert_eq!(payload.as_arrow().map(RecordBatch::num_rows), Some(1));
let owned = payload
.into_arrow()
.expect("Arrow payload yields its batch");
assert_eq!(owned.num_rows(), 1);
assert_eq!(owned.num_columns(), 1);
}
#[test]
fn test_result_data_carries_columns_alongside_an_arrow_payload() {
let data = ResultData {
columns: vec![ColumnInfo {
name: "ID".to_string(),
data_type: DataType::decimal(18, 0),
}],
data: ResultPayload::Arrow(int_record_batch(vec![1, 2])),
total_rows: 2,
};
assert_eq!(data.columns.len(), 1);
assert_eq!(data.total_rows, 2);
assert_eq!(data.data.num_rows(), 2);
}
#[test]
fn get_attributes_request_default_matches_new() {
let request = GetAttributesRequest::default();
assert_eq!(request.command, "getAttributes");
assert_eq!(request.command, GetAttributesRequest::new().command);
}
}