use crate::error::ConversionError;
use arrow::datatypes::{DataType, IntervalUnit, TimeUnit};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColumnNameMode {
#[default]
Quoted,
Sanitize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "UPPERCASE")]
pub enum ExasolType {
#[serde(rename = "BOOLEAN")]
Boolean,
#[serde(rename = "CHAR")]
Char { size: usize },
#[serde(rename = "VARCHAR")]
Varchar { size: usize },
#[serde(rename = "DECIMAL")]
Decimal { precision: u8, scale: i8 },
#[serde(rename = "DOUBLE")]
Double,
#[serde(rename = "DATE")]
Date,
#[serde(rename = "TIMESTAMP")]
Timestamp { with_local_time_zone: bool },
#[serde(rename = "INTERVAL YEAR TO MONTH")]
IntervalYearToMonth,
#[serde(rename = "INTERVAL DAY TO SECOND")]
IntervalDayToSecond { precision: u8 },
#[serde(rename = "GEOMETRY")]
Geometry { srid: Option<i32> },
#[serde(rename = "HASHTYPE")]
Hashtype { byte_size: usize },
}
impl ExasolType {
pub(crate) fn hashtype_from_reported_size(reported_size: Option<i64>) -> Self {
const DEFAULT_BYTE_SIZE: usize = 16;
const HEX_CHARACTERS_PER_BYTE: i64 = 2;
let byte_size = reported_size
.map(|size| (size / HEX_CHARACTERS_PER_BYTE) as usize)
.unwrap_or(DEFAULT_BYTE_SIZE);
ExasolType::Hashtype { byte_size }
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
ExasolType::Boolean => "BOOLEAN",
ExasolType::Char { .. } => "CHAR",
ExasolType::Varchar { .. } => "VARCHAR",
ExasolType::Decimal { .. } => "DECIMAL",
ExasolType::Double => "DOUBLE",
ExasolType::Date => "DATE",
ExasolType::Timestamp {
with_local_time_zone: true,
} => "TIMESTAMP WITH LOCAL TIME ZONE",
ExasolType::Timestamp {
with_local_time_zone: false,
} => "TIMESTAMP",
ExasolType::IntervalYearToMonth => "INTERVAL YEAR TO MONTH",
ExasolType::IntervalDayToSecond { .. } => "INTERVAL DAY TO SECOND",
ExasolType::Geometry { .. } => "GEOMETRY",
ExasolType::Hashtype { .. } => "HASHTYPE",
}
}
#[must_use]
pub fn to_ddl_type(&self) -> String {
match self {
ExasolType::Boolean => "BOOLEAN".to_string(),
ExasolType::Char { size } => format!("CHAR({size})"),
ExasolType::Varchar { size } => format!("VARCHAR({size})"),
ExasolType::Decimal { precision, scale } => format!("DECIMAL({precision},{scale})"),
ExasolType::Double => "DOUBLE".to_string(),
ExasolType::Date => "DATE".to_string(),
ExasolType::Timestamp {
with_local_time_zone,
} => {
if *with_local_time_zone {
"TIMESTAMP WITH LOCAL TIME ZONE".to_string()
} else {
"TIMESTAMP".to_string()
}
}
ExasolType::IntervalYearToMonth => "INTERVAL YEAR TO MONTH".to_string(),
ExasolType::IntervalDayToSecond { precision } => {
format!("INTERVAL DAY TO SECOND({precision})")
}
ExasolType::Geometry { srid } => {
if let Some(srid) = srid {
format!("GEOMETRY({srid})")
} else {
"GEOMETRY".to_string()
}
}
ExasolType::Hashtype { byte_size } => format!("HASHTYPE({} BYTE)", byte_size),
}
}
}
pub struct TypeMapper;
impl TypeMapper {
pub fn exasol_to_arrow(
exasol_type: &ExasolType,
nullable: bool,
) -> Result<DataType, ConversionError> {
let _ = nullable;
match exasol_type {
ExasolType::Boolean => Ok(DataType::Boolean),
ExasolType::Char { .. } | ExasolType::Varchar { .. } => Ok(DataType::Utf8),
ExasolType::Decimal { precision, scale } => {
Ok(DataType::Decimal128(*precision, *scale))
}
ExasolType::Double => Ok(DataType::Float64),
ExasolType::Date => Ok(DataType::Date32),
ExasolType::Timestamp {
with_local_time_zone,
} => {
if *with_local_time_zone {
Ok(DataType::Timestamp(
TimeUnit::Microsecond,
Some("UTC".into()),
))
} else {
Ok(DataType::Timestamp(TimeUnit::Microsecond, None))
}
}
ExasolType::IntervalYearToMonth => {
Ok(DataType::Interval(IntervalUnit::MonthDayNano))
}
ExasolType::IntervalDayToSecond { .. } => {
Ok(DataType::Interval(IntervalUnit::MonthDayNano))
}
ExasolType::Geometry { .. } => {
Ok(DataType::Binary)
}
ExasolType::Hashtype { .. } => {
Ok(DataType::Binary)
}
}
}
pub fn arrow_to_exasol(arrow_type: &DataType) -> Result<ExasolType, ConversionError> {
match arrow_type {
DataType::Boolean => Ok(ExasolType::Boolean),
DataType::Utf8 | DataType::LargeUtf8 => {
Ok(ExasolType::Varchar { size: 2000000 })
}
DataType::Int8 | DataType::Int16 | DataType::Int32 => Ok(ExasolType::Decimal {
precision: 18,
scale: 0,
}),
DataType::Int64 => Ok(ExasolType::Decimal {
precision: 36,
scale: 0,
}),
DataType::UInt8 | DataType::UInt16 | DataType::UInt32 => Ok(ExasolType::Decimal {
precision: 18,
scale: 0,
}),
DataType::UInt64 => Ok(ExasolType::Decimal {
precision: 36,
scale: 0,
}),
DataType::Float32 | DataType::Float64 => Ok(ExasolType::Double),
DataType::Decimal128(precision, scale) | DataType::Decimal256(precision, scale) => {
Ok(ExasolType::Decimal {
precision: *precision,
scale: *scale,
})
}
DataType::Date32 | DataType::Date64 => Ok(ExasolType::Date),
DataType::Timestamp(_, tz) => Ok(ExasolType::Timestamp {
with_local_time_zone: tz.is_some(),
}),
DataType::Interval(_) => Ok(ExasolType::IntervalDayToSecond { precision: 3 }),
DataType::Binary | DataType::LargeBinary => {
Ok(ExasolType::Varchar { size: 2000000 }) }
_ => Err(ConversionError::UnsupportedType {
exasol_type: format!("Arrow type {:?}", arrow_type),
}),
}
}
pub fn create_field_metadata(exasol_type: &ExasolType) -> HashMap<String, String> {
let mut metadata = HashMap::new();
metadata.insert(
"exasol:type".to_string(),
serde_json::to_string(exasol_type).unwrap_or_default(),
);
metadata.insert(
"exasol:type_name".to_string(),
exasol_type.type_name().to_string(),
);
match exasol_type {
ExasolType::Char { size } => {
metadata.insert("exasol:size".to_string(), size.to_string());
}
ExasolType::Varchar { size } => {
metadata.insert("exasol:size".to_string(), size.to_string());
}
ExasolType::Decimal { precision, scale } => {
metadata.insert("exasol:precision".to_string(), precision.to_string());
metadata.insert("exasol:scale".to_string(), scale.to_string());
}
ExasolType::Geometry { srid: Some(srid) } => {
metadata.insert("exasol:srid".to_string(), srid.to_string());
}
ExasolType::Geometry { srid: None } => {}
_ => {}
}
metadata
}
pub fn from_field_metadata(metadata: &HashMap<String, String>) -> Option<ExasolType> {
metadata
.get("exasol:type")
.and_then(|s| serde_json::from_str(s).ok())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_boolean_mapping() {
let exasol_type = ExasolType::Boolean;
let arrow_type = TypeMapper::exasol_to_arrow(&exasol_type, true).unwrap();
assert_eq!(arrow_type, DataType::Boolean);
let reverse = TypeMapper::arrow_to_exasol(&arrow_type).unwrap();
assert_eq!(reverse, ExasolType::Boolean);
}
#[test]
fn test_string_mapping() {
let exasol_type = ExasolType::Varchar { size: 100 };
let arrow_type = TypeMapper::exasol_to_arrow(&exasol_type, true).unwrap();
assert_eq!(arrow_type, DataType::Utf8);
}
#[test]
fn test_decimal_mapping() {
let exasol_type = ExasolType::Decimal {
precision: 18,
scale: 2,
};
let arrow_type = TypeMapper::exasol_to_arrow(&exasol_type, true).unwrap();
assert_eq!(arrow_type, DataType::Decimal128(18, 2));
let reverse = TypeMapper::arrow_to_exasol(&arrow_type).unwrap();
assert_eq!(reverse, exasol_type);
}
#[test]
fn test_arrow_decimal256_to_exasol() {
let arrow_type = DataType::Decimal256(40, 5);
let exasol_type = TypeMapper::arrow_to_exasol(&arrow_type).unwrap();
assert_eq!(
exasol_type,
ExasolType::Decimal {
precision: 40,
scale: 5
}
);
}
#[test]
fn test_timestamp_mapping() {
let exasol_type = ExasolType::Timestamp {
with_local_time_zone: false,
};
let arrow_type = TypeMapper::exasol_to_arrow(&exasol_type, true).unwrap();
assert!(matches!(
arrow_type,
DataType::Timestamp(TimeUnit::Microsecond, None)
));
}
#[test]
fn test_timestamp_with_tz_mapping() {
let exasol_type = ExasolType::Timestamp {
with_local_time_zone: true,
};
let arrow_type = TypeMapper::exasol_to_arrow(&exasol_type, true).unwrap();
assert!(matches!(
arrow_type,
DataType::Timestamp(TimeUnit::Microsecond, Some(_))
));
}
#[test]
fn test_date_mapping() {
let exasol_type = ExasolType::Date;
let arrow_type = TypeMapper::exasol_to_arrow(&exasol_type, true).unwrap();
assert_eq!(arrow_type, DataType::Date32);
}
#[test]
fn test_metadata_preservation() {
let exasol_type = ExasolType::Decimal {
precision: 18,
scale: 2,
};
let metadata = TypeMapper::create_field_metadata(&exasol_type);
assert!(metadata.contains_key("exasol:type"));
assert_eq!(metadata.get("exasol:precision"), Some(&"18".to_string()));
assert_eq!(metadata.get("exasol:scale"), Some(&"2".to_string()));
let restored = TypeMapper::from_field_metadata(&metadata).unwrap();
assert_eq!(restored, exasol_type);
}
#[test]
fn test_type_name_of_every_exasol_type_has_no_parameters() {
let cases = [
(ExasolType::Boolean, "BOOLEAN"),
(ExasolType::Char { size: 3 }, "CHAR"),
(ExasolType::Varchar { size: 50 }, "VARCHAR"),
(
ExasolType::Decimal {
precision: 10,
scale: 2,
},
"DECIMAL",
),
(ExasolType::Double, "DOUBLE"),
(ExasolType::Date, "DATE"),
(
ExasolType::Timestamp {
with_local_time_zone: false,
},
"TIMESTAMP",
),
(
ExasolType::Timestamp {
with_local_time_zone: true,
},
"TIMESTAMP WITH LOCAL TIME ZONE",
),
(ExasolType::IntervalYearToMonth, "INTERVAL YEAR TO MONTH"),
(
ExasolType::IntervalDayToSecond { precision: 3 },
"INTERVAL DAY TO SECOND",
),
(ExasolType::Geometry { srid: Some(4326) }, "GEOMETRY"),
(ExasolType::Geometry { srid: None }, "GEOMETRY"),
(ExasolType::Hashtype { byte_size: 32 }, "HASHTYPE"),
];
for (exasol_type, expected) in cases {
assert_eq!(exasol_type.type_name(), expected, "{exasol_type:?}");
}
}
fn every_exasol_type() -> Vec<ExasolType> {
vec![
ExasolType::Boolean,
ExasolType::Char { size: 3 },
ExasolType::Varchar { size: 50 },
ExasolType::Decimal {
precision: 10,
scale: 2,
},
ExasolType::Double,
ExasolType::Date,
ExasolType::Timestamp {
with_local_time_zone: false,
},
ExasolType::Timestamp {
with_local_time_zone: true,
},
ExasolType::IntervalYearToMonth,
ExasolType::IntervalDayToSecond { precision: 3 },
ExasolType::Geometry { srid: Some(4326) },
ExasolType::Hashtype { byte_size: 32 },
]
}
#[test]
fn test_create_field_metadata_writes_type_name_for_every_exasol_type() {
for exasol_type in every_exasol_type() {
let metadata = TypeMapper::create_field_metadata(&exasol_type);
assert_eq!(
metadata.get("exasol:type_name").map(String::as_str),
Some(exasol_type.type_name()),
"{exasol_type:?}"
);
}
}
#[test]
fn test_from_field_metadata_restores_every_exasol_type() {
for exasol_type in every_exasol_type() {
let metadata = TypeMapper::create_field_metadata(&exasol_type);
assert_eq!(
TypeMapper::from_field_metadata(&metadata),
Some(exasol_type)
);
}
}
#[test]
fn test_hashtype_from_reported_size_halves_the_hex_character_count() {
assert_eq!(
ExasolType::hashtype_from_reported_size(Some(64)),
ExasolType::Hashtype { byte_size: 32 }
);
assert_eq!(
ExasolType::hashtype_from_reported_size(Some(32)),
ExasolType::Hashtype { byte_size: 16 }
);
}
#[test]
fn test_hashtype_from_reported_size_defaults_to_sixteen_bytes() {
assert_eq!(
ExasolType::hashtype_from_reported_size(None),
ExasolType::Hashtype { byte_size: 16 }
);
}
#[test]
fn test_geometry_mapping() {
let exasol_type = ExasolType::Geometry { srid: Some(4326) };
let arrow_type = TypeMapper::exasol_to_arrow(&exasol_type, true).unwrap();
assert_eq!(arrow_type, DataType::Binary);
}
#[test]
fn test_interval_mapping() {
let exasol_type = ExasolType::IntervalDayToSecond { precision: 3 };
let arrow_type = TypeMapper::exasol_to_arrow(&exasol_type, true).unwrap();
assert!(matches!(
arrow_type,
DataType::Interval(IntervalUnit::MonthDayNano)
));
}
#[test]
fn test_uint_to_exasol_mapping() {
let uint8 = TypeMapper::arrow_to_exasol(&DataType::UInt8).unwrap();
assert_eq!(
uint8,
ExasolType::Decimal {
precision: 18,
scale: 0
}
);
let uint16 = TypeMapper::arrow_to_exasol(&DataType::UInt16).unwrap();
assert_eq!(
uint16,
ExasolType::Decimal {
precision: 18,
scale: 0
}
);
let uint32 = TypeMapper::arrow_to_exasol(&DataType::UInt32).unwrap();
assert_eq!(
uint32,
ExasolType::Decimal {
precision: 18,
scale: 0
}
);
let uint64 = TypeMapper::arrow_to_exasol(&DataType::UInt64).unwrap();
assert_eq!(
uint64,
ExasolType::Decimal {
precision: 36,
scale: 0
}
);
}
#[test]
fn test_to_ddl_type_boolean() {
let t = ExasolType::Boolean;
assert_eq!(t.to_ddl_type(), "BOOLEAN");
}
#[test]
fn test_to_ddl_type_char() {
let t = ExasolType::Char { size: 50 };
assert_eq!(t.to_ddl_type(), "CHAR(50)");
}
#[test]
fn test_to_ddl_type_varchar() {
let t = ExasolType::Varchar { size: 2000000 };
assert_eq!(t.to_ddl_type(), "VARCHAR(2000000)");
}
#[test]
fn test_to_ddl_type_decimal() {
let t = ExasolType::Decimal {
precision: 18,
scale: 2,
};
assert_eq!(t.to_ddl_type(), "DECIMAL(18,2)");
}
#[test]
fn test_to_ddl_type_double() {
let t = ExasolType::Double;
assert_eq!(t.to_ddl_type(), "DOUBLE");
}
#[test]
fn test_to_ddl_type_date() {
let t = ExasolType::Date;
assert_eq!(t.to_ddl_type(), "DATE");
}
#[test]
fn test_to_ddl_type_timestamp() {
let t = ExasolType::Timestamp {
with_local_time_zone: false,
};
assert_eq!(t.to_ddl_type(), "TIMESTAMP");
let t_tz = ExasolType::Timestamp {
with_local_time_zone: true,
};
assert_eq!(t_tz.to_ddl_type(), "TIMESTAMP WITH LOCAL TIME ZONE");
}
#[test]
fn test_to_ddl_type_interval() {
let t = ExasolType::IntervalYearToMonth;
assert_eq!(t.to_ddl_type(), "INTERVAL YEAR TO MONTH");
let t2 = ExasolType::IntervalDayToSecond { precision: 6 };
assert_eq!(t2.to_ddl_type(), "INTERVAL DAY TO SECOND(6)");
}
#[test]
fn test_to_ddl_type_geometry() {
let t = ExasolType::Geometry { srid: None };
assert_eq!(t.to_ddl_type(), "GEOMETRY");
let t_srid = ExasolType::Geometry { srid: Some(4326) };
assert_eq!(t_srid.to_ddl_type(), "GEOMETRY(4326)");
}
#[test]
fn test_to_ddl_type_hashtype() {
let t = ExasolType::Hashtype { byte_size: 16 };
assert_eq!(t.to_ddl_type(), "HASHTYPE(16 BYTE)");
}
#[test]
fn test_column_name_mode_default() {
let mode = ColumnNameMode::default();
assert_eq!(mode, ColumnNameMode::Quoted);
}
}