use std::{
cmp::Ordering,
fmt::{Arguments, Display},
};
use arrayvec::ArrayString;
use serde::{Deserialize, Serialize};
use sqlx_core::type_info::TypeInfo;
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "ExaDataType")]
pub struct ExaTypeInfo {
name: DataTypeName,
datatype: ExaDataType,
}
impl ExaTypeInfo {
pub fn compatible(&self, other: &Self) -> bool {
self.datatype.compatible(&other.datatype)
}
}
impl From<ExaDataType> for ExaTypeInfo {
fn from(datatype: ExaDataType) -> Self {
let name = datatype.full_name();
Self { name, datatype }
}
}
impl Serialize for ExaTypeInfo {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.datatype.serialize(serializer)
}
}
impl PartialEq for ExaTypeInfo {
fn eq(&self, other: &Self) -> bool {
self.datatype == other.datatype
}
}
impl Display for ExaTypeInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
impl TypeInfo for ExaTypeInfo {
fn is_null(&self) -> bool {
false
}
fn name(&self) -> &str {
self.name.as_ref()
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
#[serde(tag = "type")]
pub enum ExaDataType {
Null,
Boolean,
Char(StringLike),
Date,
Decimal(Decimal),
Double,
Geometry(Geometry),
#[serde(rename = "INTERVAL DAY TO SECOND")]
IntervalDayToSecond(IntervalDayToSecond),
#[serde(rename = "INTERVAL YEAR TO MONTH")]
IntervalYearToMonth(IntervalYearToMonth),
Timestamp,
#[serde(rename = "TIMESTAMP WITH LOCAL TIME ZONE")]
TimestampWithLocalTimeZone,
Varchar(StringLike),
Hashtype(Hashtype),
}
impl ExaDataType {
const NULL: &'static str = "NULL";
const BOOLEAN: &'static str = "BOOLEAN";
const CHAR: &'static str = "CHAR";
const DATE: &'static str = "DATE";
const DECIMAL: &'static str = "DECIMAL";
const DOUBLE: &'static str = "DOUBLE PRECISION";
const GEOMETRY: &'static str = "GEOMETRY";
const INTERVAL_DAY_TO_SECOND: &'static str = "INTERVAL DAY TO SECOND";
const INTERVAL_YEAR_TO_MONTH: &'static str = "INTERVAL YEAR TO MONTH";
const TIMESTAMP: &'static str = "TIMESTAMP";
const TIMESTAMP_WITH_LOCAL_TIME_ZONE: &'static str = "TIMESTAMP WITH LOCAL TIME ZONE";
const VARCHAR: &'static str = "VARCHAR";
const HASHTYPE: &'static str = "HASHTYPE";
pub fn compatible(&self, other: &Self) -> bool {
match self {
ExaDataType::Null => true,
ExaDataType::Boolean => matches!(other, ExaDataType::Boolean | ExaDataType::Null),
ExaDataType::Char(c) | ExaDataType::Varchar(c) => c.compatible(other),
ExaDataType::Date => matches!(
other,
ExaDataType::Date
| ExaDataType::Char(_)
| ExaDataType::Varchar(_)
| ExaDataType::Null
),
ExaDataType::Decimal(d) => d.compatible(other),
ExaDataType::Double => match other {
ExaDataType::Double | ExaDataType::Null => true,
ExaDataType::Decimal(d) if d.scale > 0 => true,
_ => false,
},
ExaDataType::Geometry(g) => g.compatible(other),
ExaDataType::IntervalDayToSecond(ids) => ids.compatible(other),
ExaDataType::IntervalYearToMonth(iym) => iym.compatible(other),
ExaDataType::Timestamp => matches!(
other,
ExaDataType::Timestamp
| ExaDataType::TimestampWithLocalTimeZone
| ExaDataType::Char(_)
| ExaDataType::Varchar(_)
| ExaDataType::Null
),
ExaDataType::TimestampWithLocalTimeZone => matches!(
other,
ExaDataType::TimestampWithLocalTimeZone
| ExaDataType::Timestamp
| ExaDataType::Char(_)
| ExaDataType::Varchar(_)
| ExaDataType::Null
),
ExaDataType::Hashtype(h) => h.compatible(other),
}
}
fn full_name(&self) -> DataTypeName {
match self {
ExaDataType::Null => Self::NULL.into(),
ExaDataType::Boolean => Self::BOOLEAN.into(),
ExaDataType::Date => Self::DATE.into(),
ExaDataType::Double => Self::DOUBLE.into(),
ExaDataType::Timestamp => Self::TIMESTAMP.into(),
ExaDataType::TimestampWithLocalTimeZone => Self::TIMESTAMP_WITH_LOCAL_TIME_ZONE.into(),
ExaDataType::Char(c) | ExaDataType::Varchar(c) => {
format_args!("{}({}) {}", self.as_ref(), c.size, c.character_set).into()
}
ExaDataType::Decimal(d) => {
format_args!("{}({}, {})", self.as_ref(), d.precision, d.scale).into()
}
ExaDataType::Geometry(g) => format_args!("{}({})", self.as_ref(), g.srid).into(),
ExaDataType::IntervalDayToSecond(ids) => format_args!(
"INTERVAL DAY({}) TO SECOND({})",
ids.precision, ids.fraction
)
.into(),
ExaDataType::IntervalYearToMonth(iym) => {
format_args!("INTERVAL YEAR({}) TO MONTH", iym.precision).into()
}
ExaDataType::Hashtype(h) => format_args!("{}({} BYTE)", self.as_ref(), h.size()).into(),
}
}
}
impl AsRef<str> for ExaDataType {
fn as_ref(&self) -> &str {
match self {
ExaDataType::Null => Self::NULL,
ExaDataType::Boolean => Self::BOOLEAN,
ExaDataType::Char(_) => Self::CHAR,
ExaDataType::Date => Self::DATE,
ExaDataType::Decimal(_) => Self::DECIMAL,
ExaDataType::Double => Self::DOUBLE,
ExaDataType::Geometry(_) => Self::GEOMETRY,
ExaDataType::IntervalDayToSecond(_) => Self::INTERVAL_DAY_TO_SECOND,
ExaDataType::IntervalYearToMonth(_) => Self::INTERVAL_YEAR_TO_MONTH,
ExaDataType::Timestamp => Self::TIMESTAMP,
ExaDataType::TimestampWithLocalTimeZone => Self::TIMESTAMP_WITH_LOCAL_TIME_ZONE,
ExaDataType::Varchar(_) => Self::VARCHAR,
ExaDataType::Hashtype(_) => Self::HASHTYPE,
}
}
}
#[derive(Debug, Clone)]
enum DataTypeName {
Static(&'static str),
Inline(ArrayString<30>),
}
impl AsRef<str> for DataTypeName {
fn as_ref(&self) -> &str {
match self {
DataTypeName::Static(s) => s,
DataTypeName::Inline(s) => s.as_str(),
}
}
}
impl Display for DataTypeName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_ref())
}
}
impl From<&'static str> for DataTypeName {
fn from(value: &'static str) -> Self {
Self::Static(value)
}
}
impl From<Arguments<'_>> for DataTypeName {
fn from(value: Arguments<'_>) -> Self {
Self::Inline(ArrayString::try_from(value).expect("inline data type name too large"))
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StringLike {
size: usize,
character_set: Charset,
}
impl StringLike {
pub const MAX_VARCHAR_LEN: usize = 2_000_000;
pub const MAX_CHAR_LEN: usize = 2000;
pub fn new(size: usize, character_set: Charset) -> Self {
Self {
size,
character_set,
}
}
pub fn size(&self) -> usize {
self.size
}
pub fn character_set(&self) -> Charset {
self.character_set
}
#[allow(clippy::unused_self)]
pub fn compatible(&self, ty: &ExaDataType) -> bool {
matches!(
ty,
ExaDataType::Char(_)
| ExaDataType::Varchar(_)
| ExaDataType::Null
| ExaDataType::Date
| ExaDataType::Geometry(_)
| ExaDataType::Hashtype(_)
| ExaDataType::IntervalDayToSecond(_)
| ExaDataType::IntervalYearToMonth(_)
| ExaDataType::Timestamp
| ExaDataType::TimestampWithLocalTimeZone
)
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum Charset {
Utf8,
Ascii,
}
impl AsRef<str> for Charset {
fn as_ref(&self) -> &str {
match self {
Charset::Utf8 => "UTF8",
Charset::Ascii => "ASCII",
}
}
}
impl Display for Charset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_ref())
}
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Decimal {
precision: u32,
scale: u32,
}
impl Decimal {
pub const MAX_8BIT_PRECISION: u32 = 3;
pub const MAX_16BIT_PRECISION: u32 = 5;
pub const MAX_32BIT_PRECISION: u32 = 10;
pub const MAX_64BIT_PRECISION: u32 = 20;
pub const MAX_128BIT_PRECISION: u32 = 39;
pub const MAX_PRECISION: u32 = 36;
pub const MAX_SCALE: u32 = 35;
pub fn new(precision: u32, scale: u32) -> Self {
Self { precision, scale }
}
pub fn precision(&self) -> u32 {
self.precision
}
pub fn scale(&self) -> u32 {
self.scale
}
pub fn compatible(&self, ty: &ExaDataType) -> bool {
match ty {
ExaDataType::Decimal(d) => self >= d,
ExaDataType::Double => self.scale > 0,
ExaDataType::Null => true,
_ => false,
}
}
}
impl PartialOrd for Decimal {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let self_diff = self.precision - self.scale;
let other_diff = other.precision - other.scale;
let scale_cmp = self.scale.partial_cmp(&other.scale);
let diff_cmp = self_diff.partial_cmp(&other_diff);
#[allow(clippy::match_same_arms)] match (scale_cmp, diff_cmp) {
(Some(Ordering::Greater), Some(Ordering::Greater)) => Some(Ordering::Greater),
(Some(Ordering::Greater), Some(Ordering::Equal)) => Some(Ordering::Greater),
(Some(Ordering::Equal), ord) => ord,
(Some(Ordering::Less), Some(Ordering::Less)) => Some(Ordering::Less),
(Some(Ordering::Less), Some(Ordering::Equal)) => Some(Ordering::Less),
_ => None,
}
}
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Geometry {
srid: u16,
}
impl Geometry {
pub fn new(srid: u16) -> Self {
Self { srid }
}
pub fn srid(&self) -> u16 {
self.srid
}
pub fn compatible(&self, ty: &ExaDataType) -> bool {
match ty {
ExaDataType::Geometry(g) => self.srid == g.srid,
ExaDataType::Varchar(_) | ExaDataType::Char(_) | ExaDataType::Null => true,
_ => false,
}
}
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct IntervalDayToSecond {
precision: u32,
fraction: u32,
}
impl PartialOrd for IntervalDayToSecond {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let precision_cmp = self.precision.partial_cmp(&other.precision);
let fraction_cmp = self.fraction.partial_cmp(&other.fraction);
match (precision_cmp, fraction_cmp) {
(Some(Ordering::Equal), Some(Ordering::Equal)) => Some(Ordering::Equal),
(Some(Ordering::Equal | Ordering::Less), Some(Ordering::Less))
| (Some(Ordering::Less), Some(Ordering::Equal)) => Some(Ordering::Less),
(Some(Ordering::Equal | Ordering::Greater), Some(Ordering::Greater))
| (Some(Ordering::Greater), Some(Ordering::Equal)) => Some(Ordering::Greater),
_ => None,
}
}
}
impl IntervalDayToSecond {
pub const MAX_SUPPORTED_FRACTION: u32 = 3;
pub const MAX_PRECISION: u32 = 9;
pub fn new(precision: u32, fraction: u32) -> Self {
Self {
precision,
fraction,
}
}
pub fn precision(&self) -> u32 {
self.precision
}
pub fn fraction(&self) -> u32 {
self.fraction
}
pub fn compatible(&self, ty: &ExaDataType) -> bool {
match ty {
ExaDataType::IntervalDayToSecond(i) => self >= i,
ExaDataType::Varchar(_) | ExaDataType::Char(_) | ExaDataType::Null => true,
_ => false,
}
}
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
#[serde(rename_all = "camelCase")]
pub struct IntervalYearToMonth {
precision: u32,
}
impl IntervalYearToMonth {
pub const MAX_PRECISION: u32 = 9;
pub fn new(precision: u32) -> Self {
Self { precision }
}
pub fn precision(&self) -> u32 {
self.precision
}
pub fn compatible(&self, ty: &ExaDataType) -> bool {
match ty {
ExaDataType::IntervalYearToMonth(i) => self >= i,
ExaDataType::Varchar(_) | ExaDataType::Char(_) | ExaDataType::Null => true,
_ => false,
}
}
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
#[serde(rename_all = "camelCase")]
pub struct Hashtype {
size: u16,
}
impl Hashtype {
pub const MAX_HASHTYPE_SIZE: u16 = 1024;
pub fn new(size: u16) -> Self {
Self { size: size * 2 }
}
pub fn size(&self) -> u16 {
self.size / 2
}
pub fn compatible(&self, ty: &ExaDataType) -> bool {
match ty {
ExaDataType::Hashtype(h) => self.size >= h.size,
ExaDataType::Varchar(_) | ExaDataType::Char(_) | ExaDataType::Null => true,
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_null_name() {
let data_type = ExaDataType::Null;
assert_eq!(data_type.full_name().as_ref(), "NULL");
}
#[test]
fn test_boolean_name() {
let data_type = ExaDataType::Boolean;
assert_eq!(data_type.full_name().as_ref(), "BOOLEAN");
}
#[test]
fn test_max_char_name() {
let string_like = StringLike::new(StringLike::MAX_CHAR_LEN, Charset::Ascii);
let data_type = ExaDataType::Char(string_like);
assert_eq!(
data_type.full_name().as_ref(),
format!("CHAR({}) ASCII", StringLike::MAX_CHAR_LEN)
);
}
#[test]
fn test_date_name() {
let data_type = ExaDataType::Date;
assert_eq!(data_type.full_name().as_ref(), "DATE");
}
#[test]
fn test_max_decimal_name() {
let decimal = Decimal::new(Decimal::MAX_PRECISION, Decimal::MAX_SCALE);
let data_type = ExaDataType::Decimal(decimal);
assert_eq!(
data_type.full_name().as_ref(),
format!(
"DECIMAL({}, {})",
Decimal::MAX_PRECISION,
Decimal::MAX_SCALE
)
);
}
#[test]
fn test_double_name() {
let data_type = ExaDataType::Double;
assert_eq!(data_type.full_name().as_ref(), "DOUBLE PRECISION");
}
#[test]
fn test_max_geometry_name() {
let geometry = Geometry::new(u16::MAX);
let data_type = ExaDataType::Geometry(geometry);
assert_eq!(
data_type.full_name().as_ref(),
format!("GEOMETRY({})", u16::MAX)
);
}
#[test]
fn test_max_interval_day_name() {
let ids = IntervalDayToSecond::new(
IntervalDayToSecond::MAX_PRECISION,
IntervalDayToSecond::MAX_SUPPORTED_FRACTION,
);
let data_type = ExaDataType::IntervalDayToSecond(ids);
assert_eq!(
data_type.full_name().as_ref(),
format!(
"INTERVAL DAY({}) TO SECOND({})",
IntervalDayToSecond::MAX_PRECISION,
IntervalDayToSecond::MAX_SUPPORTED_FRACTION
)
);
}
#[test]
fn test_max_interval_year_name() {
let iym = IntervalYearToMonth::new(IntervalYearToMonth::MAX_PRECISION);
let data_type = ExaDataType::IntervalYearToMonth(iym);
assert_eq!(
data_type.full_name().as_ref(),
format!(
"INTERVAL YEAR({}) TO MONTH",
IntervalYearToMonth::MAX_PRECISION,
)
);
}
#[test]
fn test_timestamp_name() {
let data_type = ExaDataType::Timestamp;
assert_eq!(data_type.full_name().as_ref(), "TIMESTAMP");
}
#[test]
fn test_timestamp_with_tz_name() {
let data_type = ExaDataType::TimestampWithLocalTimeZone;
assert_eq!(
data_type.full_name().as_ref(),
"TIMESTAMP WITH LOCAL TIME ZONE"
);
}
#[test]
fn test_max_varchar_name() {
let string_like = StringLike::new(StringLike::MAX_VARCHAR_LEN, Charset::Ascii);
let data_type = ExaDataType::Varchar(string_like);
assert_eq!(
data_type.full_name().as_ref(),
format!("VARCHAR({}) ASCII", StringLike::MAX_VARCHAR_LEN)
);
}
#[test]
fn test_max_hashtype_name() {
let hashtype = Hashtype::new(Hashtype::MAX_HASHTYPE_SIZE);
let data_type = ExaDataType::Hashtype(hashtype);
assert_eq!(
data_type.full_name().as_ref(),
format!("HASHTYPE({} BYTE)", Hashtype::MAX_HASHTYPE_SIZE)
);
}
}