use serde::{Deserialize, Serialize};
use crate::value::Value;
#[non_exhaustive]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
zerompk::ToMessagePack,
zerompk::FromMessagePack,
)]
#[serde(tag = "type", content = "params")]
pub enum ColumnType {
Int64,
Float64,
String,
Bool,
Bytes,
Timestamp,
Timestamptz,
SystemTimestamp,
Decimal {
precision: u8,
scale: u8,
},
Geometry,
Vector(u32),
Uuid,
Json,
Ulid,
Duration,
Array,
Set,
Regex,
Range,
Record,
}
impl ColumnType {
pub fn fixed_size(&self) -> Option<usize> {
match self {
Self::Int64
| Self::Float64
| Self::Timestamp
| Self::Timestamptz
| Self::SystemTimestamp
| Self::Duration => Some(8),
Self::Bool => Some(1),
Self::Decimal { .. } | Self::Uuid | Self::Ulid => Some(16),
Self::Vector(dim) => Some(*dim as usize * 4),
Self::String
| Self::Bytes
| Self::Geometry
| Self::Json
| Self::Array
| Self::Set
| Self::Regex
| Self::Range
| Self::Record => None,
}
}
pub fn is_variable_length(&self) -> bool {
self.fixed_size().is_none()
}
pub fn to_pg_oid(&self) -> u32 {
match self {
Self::Bool => 16,
Self::Bytes => 17,
Self::Int64 => 20,
Self::Float64 => 701,
Self::String => 25,
Self::Timestamp | Self::SystemTimestamp => 1114,
Self::Timestamptz => 1184,
Self::Decimal { .. } => 1700,
Self::Uuid | Self::Ulid => 2950,
Self::Json => 3802,
Self::Duration => 1186,
Self::Geometry => 25,
Self::Vector(_) => 1021,
Self::Array | Self::Set | Self::Range | Self::Record | Self::Regex => 3802,
}
}
pub fn accepts(&self, value: &Value) -> bool {
matches!(
(self, value),
(Self::Int64, Value::Integer(_))
| (Self::Float64, Value::Float(_) | Value::Integer(_))
| (Self::String, Value::String(_))
| (Self::Bool, Value::Bool(_))
| (Self::Bytes, Value::Bytes(_))
| (
Self::Timestamp,
Value::NaiveDateTime(_) | Value::Integer(_) | Value::String(_)
)
| (
Self::Timestamptz,
Value::DateTime(_) | Value::Integer(_) | Value::String(_)
)
| (
Self::SystemTimestamp,
Value::DateTime(_) | Value::Integer(_)
)
| (
Self::Decimal { .. },
Value::Decimal(_) | Value::String(_) | Value::Float(_) | Value::Integer(_)
)
| (Self::Geometry, Value::Geometry(_) | Value::String(_))
| (Self::Vector(_), Value::Array(_) | Value::Bytes(_))
| (Self::Uuid, Value::Uuid(_) | Value::String(_))
| (Self::Ulid, Value::Ulid(_) | Value::String(_))
| (
Self::Duration,
Value::Duration(_) | Value::Integer(_) | Value::String(_)
)
| (Self::Array, Value::Array(_))
| (Self::Set, Value::Set(_) | Value::Array(_))
| (Self::Regex, Value::Regex(_) | Value::String(_))
| (Self::Range, Value::Range { .. })
| (Self::Record, Value::Record { .. } | Value::String(_))
| (Self::Json, _)
| (_, Value::Null)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn nodedb_types_datetime_epoch() -> crate::datetime::NdbDateTime {
crate::datetime::NdbDateTime::from_micros(0)
}
#[test]
fn to_pg_oid_stable() {
assert_eq!(ColumnType::Bool.to_pg_oid(), 16);
assert_eq!(ColumnType::Bytes.to_pg_oid(), 17);
assert_eq!(ColumnType::Int64.to_pg_oid(), 20);
assert_eq!(ColumnType::String.to_pg_oid(), 25);
assert_eq!(ColumnType::Float64.to_pg_oid(), 701);
assert_eq!(ColumnType::Timestamp.to_pg_oid(), 1114);
assert_eq!(ColumnType::Timestamptz.to_pg_oid(), 1184);
assert_eq!(ColumnType::SystemTimestamp.to_pg_oid(), 1114);
assert_eq!(ColumnType::Duration.to_pg_oid(), 1186);
assert_eq!(
ColumnType::Decimal {
precision: 38,
scale: 10
}
.to_pg_oid(),
1700
);
assert_eq!(ColumnType::Uuid.to_pg_oid(), 2950);
assert_eq!(ColumnType::Ulid.to_pg_oid(), 2950);
assert_eq!(ColumnType::Json.to_pg_oid(), 3802);
assert_eq!(ColumnType::Geometry.to_pg_oid(), 25);
assert_eq!(ColumnType::Vector(768).to_pg_oid(), 1021);
assert_eq!(ColumnType::Array.to_pg_oid(), 3802);
assert_eq!(ColumnType::Set.to_pg_oid(), 3802);
assert_eq!(ColumnType::Range.to_pg_oid(), 3802);
assert_eq!(ColumnType::Record.to_pg_oid(), 3802);
assert_eq!(ColumnType::Regex.to_pg_oid(), 3802);
}
#[test]
fn parse_system_timestamp() {
assert_eq!(
"SYSTEM_TIMESTAMP".parse::<ColumnType>().unwrap(),
ColumnType::SystemTimestamp
);
assert_eq!(
"SystemTimestamp".parse::<ColumnType>().unwrap(),
ColumnType::SystemTimestamp
);
assert_eq!(ColumnType::SystemTimestamp.fixed_size(), Some(8));
assert!(!ColumnType::SystemTimestamp.is_variable_length());
assert_eq!(ColumnType::SystemTimestamp.to_string(), "SYSTEM_TIMESTAMP");
assert!(!ColumnType::SystemTimestamp.accepts(&Value::String("2024-01-01".into())));
assert!(ColumnType::SystemTimestamp.accepts(&Value::Integer(1_700_000_000)));
}
#[test]
fn parse_canonical() {
assert_eq!("BIGINT".parse::<ColumnType>().unwrap(), ColumnType::Int64);
assert_eq!(
"FLOAT64".parse::<ColumnType>().unwrap(),
ColumnType::Float64
);
assert_eq!("TEXT".parse::<ColumnType>().unwrap(), ColumnType::String);
assert_eq!("BOOL".parse::<ColumnType>().unwrap(), ColumnType::Bool);
assert_eq!(
"TIMESTAMP".parse::<ColumnType>().unwrap(),
ColumnType::Timestamp
);
assert_eq!(
"TIMESTAMPTZ".parse::<ColumnType>().unwrap(),
ColumnType::Timestamptz
);
assert_eq!(
"TIMESTAMP WITH TIME ZONE".parse::<ColumnType>().unwrap(),
ColumnType::Timestamptz
);
assert_eq!(
"GEOMETRY".parse::<ColumnType>().unwrap(),
ColumnType::Geometry
);
assert_eq!("UUID".parse::<ColumnType>().unwrap(), ColumnType::Uuid);
}
#[test]
fn parse_vector() {
assert_eq!(
"VECTOR(768)".parse::<ColumnType>().unwrap(),
ColumnType::Vector(768)
);
assert!("VECTOR(0)".parse::<ColumnType>().is_err());
}
#[test]
fn display_roundtrip() {
for ct in [
ColumnType::Int64,
ColumnType::Float64,
ColumnType::String,
ColumnType::Timestamp,
ColumnType::Timestamptz,
ColumnType::Vector(768),
ColumnType::Decimal {
precision: 10,
scale: 2,
},
ColumnType::Decimal {
precision: 38,
scale: 10,
},
] {
let s = ct.to_string();
let parsed: ColumnType = s.parse().unwrap();
assert_eq!(parsed, ct);
}
}
#[test]
fn decimal_parse_with_params() {
assert_eq!(
"NUMERIC(10,2)".parse::<ColumnType>().unwrap(),
ColumnType::Decimal {
precision: 10,
scale: 2
}
);
assert_eq!(
"DECIMAL(38,10)".parse::<ColumnType>().unwrap(),
ColumnType::Decimal {
precision: 38,
scale: 10
}
);
assert_eq!(
"NUMERIC".parse::<ColumnType>().unwrap(),
ColumnType::Decimal {
precision: 38,
scale: 10
}
);
assert_eq!(
"DECIMAL".parse::<ColumnType>().unwrap(),
ColumnType::Decimal {
precision: 38,
scale: 10
}
);
}
#[test]
fn decimal_parse_invalid() {
assert!("DECIMAL(5,6)".parse::<ColumnType>().is_err());
assert!("DECIMAL(0,0)".parse::<ColumnType>().is_err());
assert!("DECIMAL(39,0)".parse::<ColumnType>().is_err());
}
#[test]
fn decimal_fixed_size() {
assert_eq!(
ColumnType::Decimal {
precision: 10,
scale: 2
}
.fixed_size(),
Some(16)
);
}
#[test]
fn decimal_to_pg_oid_is_1700() {
assert_eq!(
ColumnType::Decimal {
precision: 10,
scale: 2
}
.to_pg_oid(),
1700
);
}
#[test]
fn accepts_native_values() {
assert!(ColumnType::Int64.accepts(&Value::Integer(42)));
assert!(ColumnType::Float64.accepts(&Value::Float(42.0)));
assert!(ColumnType::Float64.accepts(&Value::Integer(42)));
assert!(ColumnType::String.accepts(&Value::String("x".into())));
assert!(ColumnType::Bool.accepts(&Value::Bool(true)));
assert!(ColumnType::Bytes.accepts(&Value::Bytes(vec![1])));
assert!(
ColumnType::Uuid.accepts(&Value::Uuid("550e8400-e29b-41d4-a716-446655440000".into()))
);
assert!(
ColumnType::Decimal {
precision: 38,
scale: 10
}
.accepts(&Value::Decimal(rust_decimal::Decimal::ZERO))
);
let naive = Value::NaiveDateTime(nodedb_types_datetime_epoch());
let tz = Value::DateTime(nodedb_types_datetime_epoch());
assert!(ColumnType::Timestamp.accepts(&naive));
assert!(!ColumnType::Timestamp.accepts(&tz));
assert!(ColumnType::Timestamptz.accepts(&tz));
assert!(!ColumnType::Timestamptz.accepts(&naive));
assert!(ColumnType::Int64.accepts(&Value::Null));
assert!(!ColumnType::Int64.accepts(&Value::String("x".into())));
assert!(!ColumnType::Bool.accepts(&Value::Integer(1)));
}
#[test]
fn accepts_coercion_sources() {
assert!(ColumnType::Timestamp.accepts(&Value::String("2024-01-01".into())));
assert!(ColumnType::Timestamp.accepts(&Value::Integer(1_700_000_000)));
assert!(ColumnType::Timestamptz.accepts(&Value::String("2024-01-01T00:00:00Z".into())));
assert!(ColumnType::Timestamptz.accepts(&Value::Integer(1_700_000_000)));
assert!(ColumnType::Uuid.accepts(&Value::String(
"550e8400-e29b-41d4-a716-446655440000".into()
)));
assert!(
ColumnType::Decimal {
precision: 10,
scale: 2
}
.accepts(&Value::String("99.95".into()))
);
assert!(
ColumnType::Decimal {
precision: 10,
scale: 2
}
.accepts(&Value::Float(99.95))
);
assert!(ColumnType::Geometry.accepts(&Value::String("POINT(0 0)".into())));
}
#[test]
fn column_def_display() {
use super::super::column_def::ColumnDef;
let col = ColumnDef::required("id", ColumnType::Int64).with_primary_key();
assert_eq!(col.to_string(), "id BIGINT NOT NULL PRIMARY KEY");
}
}