use serde::{Deserialize, Serialize};
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
zerompk::ToMessagePack,
zerompk::FromMessagePack,
)]
#[msgpack(c_enum)]
#[repr(u8)]
pub enum IntWidth {
I16 = 0,
I32 = 1,
I64 = 2,
}
impl IntWidth {
pub fn from_declared_type(declared: &str) -> Option<Self> {
let normalized = declared.trim().to_ascii_lowercase();
let is = |name: &str| super::declared_type_matches(&normalized, name);
if is("smallint") || is("int2") {
Some(Self::I16)
} else if is("integer") || is("int4") || is("int") {
Some(Self::I32)
} else if is("bigint") || is("int8") || is("int64") {
Some(Self::I64)
} else {
None
}
}
pub const fn range(self) -> (i64, i64) {
match self {
Self::I16 => (i16::MIN as i64, i16::MAX as i64),
Self::I32 => (i32::MIN as i64, i32::MAX as i64),
Self::I64 => (i64::MIN, i64::MAX),
}
}
pub const fn contains(self, value: i64) -> bool {
let (min, max) = self.range();
value >= min && value <= max
}
pub const fn pg_oid(self) -> u32 {
match self {
Self::I16 => 21,
Self::I32 => 23,
Self::I64 => 20,
}
}
pub const fn pg_type_name(self) -> &'static str {
match self {
Self::I16 => "smallint",
Self::I32 => "integer",
Self::I64 => "bigint",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_every_declared_integer_spelling() {
let cases: &[(&str, IntWidth)] = &[
("SMALLINT", IntWidth::I16),
("INT2", IntWidth::I16),
("INTEGER", IntWidth::I32),
("INT", IntWidth::I32),
("INT4", IntWidth::I32),
("BIGINT", IntWidth::I64),
("INT8", IntWidth::I64),
("INT64", IntWidth::I64),
];
for (declared, expected) in cases {
assert_eq!(
IntWidth::from_declared_type(declared),
Some(*expected),
"{declared} must resolve to {expected:?}"
);
assert_eq!(
IntWidth::from_declared_type(&declared.to_lowercase()),
Some(*expected),
"{declared} must resolve case-insensitively"
);
}
}
#[test]
fn int_does_not_prefix_match_numbered_spellings() {
for (declared, expected) in [
("int2", IntWidth::I16),
("int4", IntWidth::I32),
("int8", IntWidth::I64),
("int64", IntWidth::I64),
] {
assert_eq!(IntWidth::from_declared_type(declared), Some(expected));
}
}
#[test]
fn tolerates_trailing_modifiers_and_parameters() {
assert_eq!(
IntWidth::from_declared_type("BIGINT NOT NULL"),
Some(IntWidth::I64)
);
assert_eq!(
IntWidth::from_declared_type(" smallint "),
Some(IntWidth::I16)
);
assert_eq!(IntWidth::from_declared_type("int(11)"), Some(IntWidth::I32));
}
#[test]
fn rejects_non_integer_types() {
for declared in [
"TEXT",
"VARCHAR(20)",
"FLOAT8",
"BOOL",
"TIMESTAMP",
"GEOMETRY",
"",
"intergalactic",
] {
assert_eq!(
IntWidth::from_declared_type(declared),
None,
"{declared} must not resolve to an integer width"
);
}
}
#[test]
fn range_matches_the_rust_primitive_it_names() {
assert_eq!(IntWidth::I16.range(), (i16::MIN as i64, i16::MAX as i64));
assert_eq!(IntWidth::I32.range(), (i32::MIN as i64, i32::MAX as i64));
assert_eq!(IntWidth::I64.range(), (i64::MIN, i64::MAX));
}
#[test]
fn contains_rejects_values_one_past_the_boundary() {
assert!(IntWidth::I16.contains(i16::MAX as i64));
assert!(!IntWidth::I16.contains(i16::MAX as i64 + 1));
assert!(IntWidth::I16.contains(i16::MIN as i64));
assert!(!IntWidth::I16.contains(i16::MIN as i64 - 1));
assert!(IntWidth::I32.contains(i32::MAX as i64));
assert!(!IntWidth::I32.contains(i32::MAX as i64 + 1));
assert!(IntWidth::I32.contains(i32::MIN as i64));
assert!(!IntWidth::I32.contains(i32::MIN as i64 - 1));
assert!(IntWidth::I64.contains(i64::MAX));
assert!(IntWidth::I64.contains(i64::MIN));
}
#[test]
fn pg_oids_match_the_postgres_catalog() {
assert_eq!(IntWidth::I16.pg_oid(), 21);
assert_eq!(IntWidth::I32.pg_oid(), 23);
assert_eq!(IntWidth::I64.pg_oid(), 20);
}
}