use bytes::{BufMut, BytesMut};
use postgres_types::{FromSql, IsNull, ToSql, Type, to_sql_checked};
use std::error::Error;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Interval {
pub months: i32,
pub days: i32,
pub microseconds: i64,
}
impl Interval {
pub const fn new(months: i32, days: i32, microseconds: i64) -> Self {
Self {
months,
days,
microseconds,
}
}
pub const fn months_only(months: i32) -> Self {
Self {
months,
days: 0,
microseconds: 0,
}
}
pub const fn days_only(days: i32) -> Self {
Self {
months: 0,
days,
microseconds: 0,
}
}
pub const fn microseconds_only(microseconds: i64) -> Self {
Self {
months: 0,
days: 0,
microseconds,
}
}
}
impl ToSql for Interval {
fn to_sql(
&self,
_ty: &Type,
out: &mut BytesMut,
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
out.put_i64(self.microseconds);
out.put_i32(self.days);
out.put_i32(self.months);
Ok(IsNull::No)
}
fn accepts(ty: &Type) -> bool {
*ty == Type::INTERVAL
}
to_sql_checked!();
}
impl<'a> FromSql<'a> for Interval {
fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
if raw.len() != 16 {
return Err(format!(
"Postgres INTERVAL wire format must be exactly 16 bytes, got {}",
raw.len()
)
.into());
}
let microseconds = i64::from_be_bytes(raw[0..8].try_into().unwrap());
let days = i32::from_be_bytes(raw[8..12].try_into().unwrap());
let months = i32::from_be_bytes(raw[12..16].try_into().unwrap());
Ok(Interval {
months,
days,
microseconds,
})
}
fn accepts(ty: &Type) -> bool {
*ty == Type::INTERVAL
}
}
impl crate::descriptor::DjogiSqlType for Interval {
const SQL_TYPE: &'static str = "INTERVAL";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RangeBound<T> {
Inclusive(T),
Exclusive(T),
Unbounded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Range<T> {
lower: RangeBound<T>,
upper: RangeBound<T>,
empty: bool,
}
impl<T> Range<T> {
pub const fn empty() -> Self {
Self {
lower: RangeBound::Unbounded,
upper: RangeBound::Unbounded,
empty: true,
}
}
pub const fn new(lower: RangeBound<T>, upper: RangeBound<T>) -> Self {
Self {
lower,
upper,
empty: false,
}
}
pub const fn is_empty(&self) -> bool {
self.empty
}
pub const fn lower(&self) -> &RangeBound<T> {
&self.lower
}
pub const fn upper(&self) -> &RangeBound<T> {
&self.upper
}
}
impl<T> Range<T> {
pub fn inclusive(lo: T, hi: T) -> Self {
Self::new(RangeBound::Inclusive(lo), RangeBound::Inclusive(hi))
}
pub fn exclusive(lo: T, hi: T) -> Self {
Self::new(RangeBound::Exclusive(lo), RangeBound::Exclusive(hi))
}
pub fn inclusive_exclusive(lo: T, hi: T) -> Self {
Self::new(RangeBound::Inclusive(lo), RangeBound::Exclusive(hi))
}
pub fn exclusive_inclusive(lo: T, hi: T) -> Self {
Self::new(RangeBound::Exclusive(lo), RangeBound::Inclusive(hi))
}
}
impl<T> Default for Range<T> {
fn default() -> Self {
Self::empty()
}
}
pub trait RangeSubtype: Sized {
fn pg_range_type() -> Type;
fn pg_element_type() -> Type;
const PG_RANGE_NAME: &'static str;
}
impl RangeSubtype for i32 {
fn pg_range_type() -> Type {
Type::INT4_RANGE
}
fn pg_element_type() -> Type {
Type::INT4
}
const PG_RANGE_NAME: &'static str = "INT4RANGE";
}
impl RangeSubtype for i64 {
fn pg_range_type() -> Type {
Type::INT8_RANGE
}
fn pg_element_type() -> Type {
Type::INT8
}
const PG_RANGE_NAME: &'static str = "INT8RANGE";
}
impl RangeSubtype for rust_decimal::Decimal {
fn pg_range_type() -> Type {
Type::NUM_RANGE
}
fn pg_element_type() -> Type {
Type::NUMERIC
}
const PG_RANGE_NAME: &'static str = "NUMRANGE";
}
impl RangeSubtype for time::PrimitiveDateTime {
fn pg_range_type() -> Type {
Type::TS_RANGE
}
fn pg_element_type() -> Type {
Type::TIMESTAMP
}
const PG_RANGE_NAME: &'static str = "TSRANGE";
}
impl RangeSubtype for time::OffsetDateTime {
fn pg_range_type() -> Type {
Type::TSTZ_RANGE
}
fn pg_element_type() -> Type {
Type::TIMESTAMPTZ
}
const PG_RANGE_NAME: &'static str = "TSTZRANGE";
}
impl RangeSubtype for time::Date {
fn pg_range_type() -> Type {
Type::DATE_RANGE
}
fn pg_element_type() -> Type {
Type::DATE
}
const PG_RANGE_NAME: &'static str = "DATERANGE";
}
impl<T: RangeSubtype> crate::descriptor::DjogiSqlType for Range<T> {
const SQL_TYPE: &'static str = T::PG_RANGE_NAME;
}
const RANGE_EMPTY: u8 = 0x01;
const RANGE_LB_INC: u8 = 0x02;
const RANGE_UB_INC: u8 = 0x04;
const RANGE_LB_INF: u8 = 0x08;
const RANGE_UB_INF: u8 = 0x10;
const RANGE_LB_NULL: u8 = 0x20;
const RANGE_UB_NULL: u8 = 0x40;
fn encode_range<T>(
range: &Range<T>,
element_ty: &Type,
out: &mut BytesMut,
) -> Result<IsNull, Box<dyn Error + Sync + Send>>
where
T: ToSql,
{
if range.empty {
out.put_u8(RANGE_EMPTY);
return Ok(IsNull::No);
}
let mut flags = 0u8;
match &range.lower {
RangeBound::Inclusive(_) => flags |= RANGE_LB_INC,
RangeBound::Exclusive(_) => {}
RangeBound::Unbounded => flags |= RANGE_LB_INF,
}
match &range.upper {
RangeBound::Inclusive(_) => flags |= RANGE_UB_INC,
RangeBound::Exclusive(_) => {}
RangeBound::Unbounded => flags |= RANGE_UB_INF,
}
out.put_u8(flags);
if let RangeBound::Inclusive(v) | RangeBound::Exclusive(v) = &range.lower {
encode_bound(v, element_ty, out)?;
}
if let RangeBound::Inclusive(v) | RangeBound::Exclusive(v) = &range.upper {
encode_bound(v, element_ty, out)?;
}
Ok(IsNull::No)
}
fn encode_bound<T: ToSql>(
value: &T,
element_ty: &Type,
out: &mut BytesMut,
) -> Result<(), Box<dyn Error + Sync + Send>> {
let len_pos = out.len();
out.put_i32(0);
let value_start = out.len();
match value.to_sql(element_ty, out)? {
IsNull::No => {}
IsNull::Yes => {
return Err(
"Postgres range bounds cannot be NULL; use RangeBound::Unbounded for an open end"
.into(),
);
}
}
let value_len = i32::try_from(out.len() - value_start)
.map_err(|_| "range bound length exceeds i32::MAX bytes")?;
out[len_pos..len_pos + 4].copy_from_slice(&value_len.to_be_bytes());
Ok(())
}
fn decode_range<'a, T>(
mut raw: &'a [u8],
element_ty: &Type,
) -> Result<Range<T>, Box<dyn Error + Sync + Send>>
where
T: FromSql<'a>,
{
if raw.is_empty() {
return Err("Postgres range wire format must contain at least one flags byte".into());
}
let flags = raw[0];
raw = &raw[1..];
if flags & RANGE_EMPTY != 0 {
if !raw.is_empty() {
return Err(format!(
"Postgres range wire format: empty-range flag set but {} trailing bytes present",
raw.len()
)
.into());
}
return Ok(Range::empty());
}
if flags & RANGE_LB_NULL != 0 || flags & RANGE_UB_NULL != 0 {
return Err(format!(
"Postgres range wire format: NULL-bound flag set (flags = 0x{flags:02x}); not supported"
)
.into());
}
let lower = if flags & RANGE_LB_INF != 0 {
RangeBound::Unbounded
} else {
let (value, rest) = decode_bound::<T>(raw, element_ty)?;
raw = rest;
if flags & RANGE_LB_INC != 0 {
RangeBound::Inclusive(value)
} else {
RangeBound::Exclusive(value)
}
};
let upper = if flags & RANGE_UB_INF != 0 {
RangeBound::Unbounded
} else {
let (value, rest) = decode_bound::<T>(raw, element_ty)?;
raw = rest;
if flags & RANGE_UB_INC != 0 {
RangeBound::Inclusive(value)
} else {
RangeBound::Exclusive(value)
}
};
if !raw.is_empty() {
return Err(format!(
"Postgres range wire format: {} trailing bytes after upper bound",
raw.len()
)
.into());
}
Ok(Range::new(lower, upper))
}
fn decode_bound<'a, T: FromSql<'a>>(
buf: &'a [u8],
element_ty: &Type,
) -> Result<(T, &'a [u8]), Box<dyn Error + Sync + Send>> {
if buf.len() < 4 {
return Err("Postgres range wire format: bound length prefix truncated".into());
}
let len = i32::from_be_bytes(buf[..4].try_into().unwrap());
if len < 0 {
return Err(format!("Postgres range wire format: negative bound length {len}").into());
}
let len = len as usize;
let buf = &buf[4..];
if buf.len() < len {
return Err(format!(
"Postgres range wire format: bound length says {len} bytes but only {} available",
buf.len()
)
.into());
}
let body = &buf[..len];
let rest = &buf[len..];
let value = T::from_sql(element_ty, body)?;
Ok((value, rest))
}
impl<T> ToSql for Range<T>
where
T: ToSql + RangeSubtype + std::fmt::Debug,
{
fn to_sql(
&self,
_ty: &Type,
out: &mut BytesMut,
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
encode_range(self, &T::pg_element_type(), out)
}
fn accepts(ty: &Type) -> bool {
*ty == T::pg_range_type()
}
to_sql_checked!();
}
impl<'a, T> FromSql<'a> for Range<T>
where
T: FromSql<'a> + RangeSubtype + std::fmt::Debug,
{
fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
decode_range::<T>(raw, &T::pg_element_type())
}
fn accepts(ty: &Type) -> bool {
*ty == T::pg_range_type()
}
}
#[cfg(feature = "network")]
const PGSQL_AF_INET: u8 = 2;
#[cfg(feature = "network")]
const PGSQL_AF_INET6: u8 = 3;
#[cfg(feature = "network")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MacAddr([u8; 6]);
#[cfg(feature = "network")]
impl MacAddr {
pub const fn new(octets: [u8; 6]) -> Self {
Self(octets)
}
pub const fn octets(&self) -> [u8; 6] {
self.0
}
}
#[cfg(feature = "network")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MacAddrParseError {
WrongLength { found: usize },
InvalidOctet { index: usize, octet: String },
MixedSeparators,
}
#[cfg(feature = "network")]
impl std::fmt::Display for MacAddrParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WrongLength { found } => {
write!(f, "MAC address must have 6 octets, got {found}")
}
Self::InvalidOctet { index, octet } => {
write!(
f,
"MAC address octet at index {index} is not 2 hex digits: {octet:?}"
)
}
Self::MixedSeparators => {
f.write_str("MAC address mixes ':' and '-' separators; use one or the other")
}
}
}
}
#[cfg(feature = "network")]
impl std::error::Error for MacAddrParseError {}
#[cfg(feature = "network")]
impl std::fmt::Display for MacAddr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let [a, b, c, d, e, g] = self.0;
write!(f, "{a:02x}:{b:02x}:{c:02x}:{d:02x}:{e:02x}:{g:02x}")
}
}
#[cfg(feature = "network")]
impl std::str::FromStr for MacAddr {
type Err = MacAddrParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let has_colon = s.contains(':');
let has_hyphen = s.contains('-');
if has_colon && has_hyphen {
return Err(MacAddrParseError::MixedSeparators);
}
let sep = if has_hyphen { '-' } else { ':' };
let parts: Vec<&str> = s.split(sep).collect();
if parts.len() != 6 {
return Err(MacAddrParseError::WrongLength { found: parts.len() });
}
let mut out = [0u8; 6];
for (i, part) in parts.iter().enumerate() {
if part.len() != 2 {
return Err(MacAddrParseError::InvalidOctet {
index: i,
octet: (*part).to_string(),
});
}
out[i] = u8::from_str_radix(part, 16).map_err(|_| MacAddrParseError::InvalidOctet {
index: i,
octet: (*part).to_string(),
})?;
}
Ok(MacAddr(out))
}
}
#[cfg(feature = "network")]
impl ToSql for MacAddr {
fn to_sql(
&self,
_ty: &Type,
out: &mut BytesMut,
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
out.put_slice(&self.0);
Ok(IsNull::No)
}
fn accepts(ty: &Type) -> bool {
*ty == Type::MACADDR
}
to_sql_checked!();
}
#[cfg(feature = "network")]
impl<'a> FromSql<'a> for MacAddr {
fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
if raw.len() != 6 {
return Err(format!(
"Postgres MACADDR wire format must be exactly 6 bytes, got {}",
raw.len()
)
.into());
}
let mut out = [0u8; 6];
out.copy_from_slice(raw);
Ok(MacAddr(out))
}
fn accepts(ty: &Type) -> bool {
*ty == Type::MACADDR
}
}
#[cfg(feature = "network")]
impl crate::descriptor::DjogiSqlType for MacAddr {
const SQL_TYPE: &'static str = "MACADDR";
}
#[cfg(feature = "network")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CidrAddr {
addr: std::net::IpAddr,
prefix: u8,
}
#[cfg(feature = "network")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CidrAddrError {
PrefixTooLarge { prefix: u8, max: u8 },
HostBitsSet,
}
#[cfg(feature = "network")]
impl std::fmt::Display for CidrAddrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PrefixTooLarge { prefix, max } => {
write!(
f,
"CIDR prefix {prefix} exceeds maximum {max} for this address family"
)
}
Self::HostBitsSet => f.write_str(
"CIDR host bits past the prefix must be zero; \
mask the host bits before constructing CidrAddr",
),
}
}
}
#[cfg(feature = "network")]
impl std::error::Error for CidrAddrError {}
#[cfg(feature = "network")]
impl CidrAddr {
pub fn new(addr: std::net::IpAddr, prefix: u8) -> Result<Self, CidrAddrError> {
let max_prefix: u8 = match addr {
std::net::IpAddr::V4(_) => 32,
std::net::IpAddr::V6(_) => 128,
};
if prefix > max_prefix {
return Err(CidrAddrError::PrefixTooLarge {
prefix,
max: max_prefix,
});
}
if !host_bits_zero(addr, prefix) {
return Err(CidrAddrError::HostBitsSet);
}
Ok(Self { addr, prefix })
}
pub const fn addr(&self) -> std::net::IpAddr {
self.addr
}
pub const fn prefix(&self) -> u8 {
self.prefix
}
}
#[cfg(feature = "network")]
fn host_bits_zero(addr: std::net::IpAddr, prefix: u8) -> bool {
fn check(octets: &[u8], prefix: u8) -> bool {
let prefix = prefix as usize;
for (i, byte) in octets.iter().enumerate() {
let start_bit = i * 8;
if start_bit >= prefix {
if *byte != 0 {
return false;
}
} else if start_bit + 8 > prefix {
let net_bits = prefix - start_bit;
let mask: u8 = if net_bits == 0 {
0xff
} else {
0xffu8.checked_shr(net_bits as u32).unwrap_or(0)
};
if byte & mask != 0 {
return false;
}
}
}
true
}
match addr {
std::net::IpAddr::V4(a) => check(&a.octets(), prefix),
std::net::IpAddr::V6(a) => check(&a.octets(), prefix),
}
}
#[cfg(feature = "network")]
impl ToSql for CidrAddr {
fn to_sql(
&self,
_ty: &Type,
out: &mut BytesMut,
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
write_inet_payload(self.addr, self.prefix, 1, out);
Ok(IsNull::No)
}
fn accepts(ty: &Type) -> bool {
*ty == Type::CIDR
}
to_sql_checked!();
}
#[cfg(feature = "network")]
impl<'a> FromSql<'a> for CidrAddr {
fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
let (addr, prefix) = read_inet_payload(raw)?;
if !host_bits_zero(addr, prefix) {
return Err(format!(
"decoded CIDR has non-zero host bits: addr={addr}, prefix={prefix}"
)
.into());
}
Ok(CidrAddr { addr, prefix })
}
fn accepts(ty: &Type) -> bool {
*ty == Type::CIDR
}
}
#[cfg(feature = "network")]
impl crate::descriptor::DjogiSqlType for CidrAddr {
const SQL_TYPE: &'static str = "CIDR";
}
#[cfg(feature = "network")]
impl std::fmt::Display for CidrAddr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.addr, self.prefix)
}
}
#[cfg(feature = "network")]
fn write_inet_payload(addr: std::net::IpAddr, prefix: u8, is_cidr: u8, out: &mut BytesMut) {
let family = match addr {
std::net::IpAddr::V4(_) => PGSQL_AF_INET,
std::net::IpAddr::V6(_) => PGSQL_AF_INET6,
};
out.put_u8(family);
out.put_u8(prefix);
out.put_u8(is_cidr);
match addr {
std::net::IpAddr::V4(a) => {
out.put_u8(4);
out.put_slice(&a.octets());
}
std::net::IpAddr::V6(a) => {
out.put_u8(16);
out.put_slice(&a.octets());
}
}
}
#[cfg(feature = "network")]
fn read_inet_payload(
mut raw: &[u8],
) -> Result<(std::net::IpAddr, u8), Box<dyn Error + Sync + Send>> {
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
if raw.len() < 4 {
return Err(
"Postgres INET/CIDR wire format: header truncated (need 4 header bytes)".into(),
);
}
let family = raw[0];
let prefix = raw[1];
let _is_cidr = raw[2];
let len = raw[3];
raw = &raw[4..];
let addr = match family {
PGSQL_AF_INET => {
if prefix > 32 {
return Err(format!(
"Postgres INET/CIDR wire format: IPv4 prefix {prefix} exceeds maximum 32"
)
.into());
}
if len != 4 {
return Err(format!(
"Postgres INET/CIDR wire format: IPv4 length byte must be 4, got {len}"
)
.into());
}
if raw.len() != 4 {
return Err(format!(
"Postgres INET/CIDR wire format: IPv4 needs 4 address bytes, got {}",
raw.len()
)
.into());
}
let mut octets = [0u8; 4];
octets.copy_from_slice(raw);
IpAddr::V4(Ipv4Addr::from(octets))
}
PGSQL_AF_INET6 => {
if prefix > 128 {
return Err(format!(
"Postgres INET/CIDR wire format: IPv6 prefix {prefix} exceeds maximum 128"
)
.into());
}
if len != 16 {
return Err(format!(
"Postgres INET/CIDR wire format: IPv6 length byte must be 16, got {len}"
)
.into());
}
if raw.len() != 16 {
return Err(format!(
"Postgres INET/CIDR wire format: IPv6 needs 16 address bytes, got {}",
raw.len()
)
.into());
}
let mut octets = [0u8; 16];
octets.copy_from_slice(raw);
IpAddr::V6(Ipv6Addr::from(octets))
}
other => {
return Err(format!(
"Postgres INET/CIDR wire format: unknown address family {other} \
(expected {PGSQL_AF_INET} for IPv4 or {PGSQL_AF_INET6} for IPv6)"
)
.into());
}
};
Ok((addr, prefix))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interval_constructors_set_only_the_named_field() {
assert_eq!(
Interval::months_only(5),
Interval {
months: 5,
days: 0,
microseconds: 0
}
);
assert_eq!(
Interval::days_only(10),
Interval {
months: 0,
days: 10,
microseconds: 0
}
);
assert_eq!(
Interval::microseconds_only(1_500_000),
Interval {
months: 0,
days: 0,
microseconds: 1_500_000
}
);
}
#[test]
fn interval_to_sql_writes_16_bytes_big_endian_us_days_months() {
let iv = Interval::new(13, 7, 1_500_000);
let mut buf = BytesMut::new();
iv.to_sql(&Type::INTERVAL, &mut buf).unwrap();
assert_eq!(buf.len(), 16);
assert_eq!(&buf[..8], &1_500_000i64.to_be_bytes());
assert_eq!(&buf[8..12], &7i32.to_be_bytes());
assert_eq!(&buf[12..16], &13i32.to_be_bytes());
}
#[test]
fn interval_round_trip_through_wire_codec() {
let iv = Interval::new(-3, 42, -999_999);
let mut buf = BytesMut::new();
iv.to_sql(&Type::INTERVAL, &mut buf).unwrap();
let decoded = Interval::from_sql(&Type::INTERVAL, &buf).unwrap();
assert_eq!(decoded, iv);
}
#[test]
fn interval_from_sql_rejects_wrong_length() {
let err = Interval::from_sql(&Type::INTERVAL, &[0u8; 15]).unwrap_err();
assert!(
err.to_string().contains("16 bytes"),
"error must name the expected length, got: {err}"
);
}
#[test]
fn interval_accepts_only_interval_type() {
assert!(<Interval as ToSql>::accepts(&Type::INTERVAL));
assert!(!<Interval as ToSql>::accepts(&Type::INT8));
assert!(!<Interval as ToSql>::accepts(&Type::TIMESTAMPTZ));
assert!(<Interval as FromSql>::accepts(&Type::INTERVAL));
assert!(!<Interval as FromSql>::accepts(&Type::INT8));
}
#[test]
fn range_empty_constructor_carries_unbounded_sides_and_empty_flag() {
let r: Range<i32> = Range::empty();
assert!(r.is_empty());
assert_eq!(*r.lower(), RangeBound::Unbounded);
assert_eq!(*r.upper(), RangeBound::Unbounded);
}
#[test]
fn range_default_is_empty_for_any_element_type() {
assert!(<Range<i32> as Default>::default().is_empty());
assert!(<Range<i64> as Default>::default().is_empty());
assert!(<Range<rust_decimal::Decimal> as Default>::default().is_empty());
assert!(<Range<time::PrimitiveDateTime> as Default>::default().is_empty());
assert!(<Range<time::Date> as Default>::default().is_empty());
assert!(<Range<time::OffsetDateTime> as Default>::default().is_empty());
}
#[test]
fn range_inclusive_exclusive_constructor_is_lower_inclusive_upper_exclusive() {
let r: Range<i32> = Range::inclusive_exclusive(1, 10);
assert!(!r.is_empty());
assert_eq!(*r.lower(), RangeBound::Inclusive(1));
assert_eq!(*r.upper(), RangeBound::Exclusive(10));
}
#[test]
fn range_inclusive_inclusive_constructor_carries_both_inclusive_bounds() {
let r: Range<i32> = Range::inclusive(0, 100);
assert_eq!(*r.lower(), RangeBound::Inclusive(0));
assert_eq!(*r.upper(), RangeBound::Inclusive(100));
}
#[test]
fn range_exclusive_exclusive_constructor_carries_both_exclusive_bounds() {
let r: Range<i32> = Range::exclusive(0, 100);
assert_eq!(*r.lower(), RangeBound::Exclusive(0));
assert_eq!(*r.upper(), RangeBound::Exclusive(100));
}
#[test]
fn range_exclusive_inclusive_constructor_is_lower_exclusive_upper_inclusive() {
let r: Range<i32> = Range::exclusive_inclusive(0, 100);
assert_eq!(*r.lower(), RangeBound::Exclusive(0));
assert_eq!(*r.upper(), RangeBound::Inclusive(100));
}
#[test]
fn range_new_accepts_unbounded_on_both_sides_and_is_not_empty() {
let universal: Range<i32> = Range::new(RangeBound::Unbounded, RangeBound::Unbounded);
assert!(!universal.is_empty());
assert_eq!(*universal.lower(), RangeBound::Unbounded);
assert_eq!(*universal.upper(), RangeBound::Unbounded);
}
#[test]
fn range_subtype_mapping_matches_postgres_type_constants() {
assert_eq!(<i32 as RangeSubtype>::pg_range_type(), Type::INT4_RANGE);
assert_eq!(<i32 as RangeSubtype>::pg_element_type(), Type::INT4);
assert_eq!(<i64 as RangeSubtype>::pg_range_type(), Type::INT8_RANGE);
assert_eq!(<i64 as RangeSubtype>::pg_element_type(), Type::INT8);
assert_eq!(
<rust_decimal::Decimal as RangeSubtype>::pg_range_type(),
Type::NUM_RANGE
);
assert_eq!(
<rust_decimal::Decimal as RangeSubtype>::pg_element_type(),
Type::NUMERIC
);
assert_eq!(
<time::PrimitiveDateTime as RangeSubtype>::pg_range_type(),
Type::TS_RANGE
);
assert_eq!(
<time::PrimitiveDateTime as RangeSubtype>::pg_element_type(),
Type::TIMESTAMP
);
assert_eq!(
<time::OffsetDateTime as RangeSubtype>::pg_range_type(),
Type::TSTZ_RANGE
);
assert_eq!(
<time::OffsetDateTime as RangeSubtype>::pg_element_type(),
Type::TIMESTAMPTZ
);
assert_eq!(
<time::Date as RangeSubtype>::pg_range_type(),
Type::DATE_RANGE
);
assert_eq!(<time::Date as RangeSubtype>::pg_element_type(), Type::DATE);
}
#[test]
fn range_djogi_sql_type_renders_canonical_uppercase_name() {
use crate::descriptor::DjogiSqlType;
assert_eq!(<Range<i32> as DjogiSqlType>::SQL_TYPE, "INT4RANGE");
assert_eq!(<Range<i64> as DjogiSqlType>::SQL_TYPE, "INT8RANGE");
assert_eq!(
<Range<rust_decimal::Decimal> as DjogiSqlType>::SQL_TYPE,
"NUMRANGE"
);
assert_eq!(
<Range<time::OffsetDateTime> as DjogiSqlType>::SQL_TYPE,
"TSTZRANGE"
);
assert_eq!(
<Range<time::PrimitiveDateTime> as DjogiSqlType>::SQL_TYPE,
"TSRANGE"
);
assert_eq!(<Range<time::Date> as DjogiSqlType>::SQL_TYPE, "DATERANGE");
}
#[test]
fn range_to_sql_empty_writes_single_flag_byte() {
let r: Range<i32> = Range::empty();
let mut buf = BytesMut::new();
r.to_sql(&Type::INT4_RANGE, &mut buf).unwrap();
assert_eq!(buf.as_ref(), &[RANGE_EMPTY]);
}
#[test]
fn range_to_sql_inclusive_exclusive_writes_flags_then_two_int4_bounds() {
let r: Range<i32> = Range::inclusive_exclusive(1, 10);
let mut buf = BytesMut::new();
r.to_sql(&Type::INT4_RANGE, &mut buf).unwrap();
assert_eq!(buf[0], RANGE_LB_INC);
assert_eq!(&buf[1..5], &4i32.to_be_bytes());
assert_eq!(&buf[5..9], &1i32.to_be_bytes());
assert_eq!(&buf[9..13], &4i32.to_be_bytes());
assert_eq!(&buf[13..17], &10i32.to_be_bytes());
assert_eq!(buf.len(), 17);
}
#[test]
fn range_to_sql_unbounded_lower_omits_lower_bytes_and_sets_lb_inf() {
let r: Range<i32> = Range::new(RangeBound::Unbounded, RangeBound::Inclusive(5));
let mut buf = BytesMut::new();
r.to_sql(&Type::INT4_RANGE, &mut buf).unwrap();
assert_eq!(buf[0], RANGE_LB_INF | RANGE_UB_INC);
assert_eq!(&buf[1..5], &4i32.to_be_bytes());
assert_eq!(&buf[5..9], &5i32.to_be_bytes());
assert_eq!(buf.len(), 9);
}
#[test]
fn range_to_sql_unbounded_upper_omits_upper_bytes_and_sets_ub_inf() {
let r: Range<i32> = Range::new(RangeBound::Inclusive(5), RangeBound::Unbounded);
let mut buf = BytesMut::new();
r.to_sql(&Type::INT4_RANGE, &mut buf).unwrap();
assert_eq!(buf[0], RANGE_LB_INC | RANGE_UB_INF);
assert_eq!(&buf[1..5], &4i32.to_be_bytes());
assert_eq!(&buf[5..9], &5i32.to_be_bytes());
assert_eq!(buf.len(), 9);
}
#[test]
fn range_to_sql_fully_unbounded_writes_only_flag_byte() {
let r: Range<i32> = Range::new(RangeBound::Unbounded, RangeBound::Unbounded);
let mut buf = BytesMut::new();
r.to_sql(&Type::INT4_RANGE, &mut buf).unwrap();
assert_eq!(buf.as_ref(), &[RANGE_LB_INF | RANGE_UB_INF]);
}
#[test]
fn range_round_trip_through_wire_codec_preserves_bounds() {
let cases: &[Range<i32>] = &[
Range::empty(),
Range::inclusive_exclusive(1, 10),
Range::inclusive(-5, 5),
Range::exclusive(0, 100),
Range::exclusive_inclusive(0, 100),
Range::new(RangeBound::Unbounded, RangeBound::Inclusive(5)),
Range::new(RangeBound::Inclusive(5), RangeBound::Unbounded),
Range::new(RangeBound::Unbounded, RangeBound::Unbounded),
];
for original in cases {
let mut buf = BytesMut::new();
original
.to_sql(&Type::INT4_RANGE, &mut buf)
.expect("encode");
let decoded =
<Range<i32> as FromSql>::from_sql(&Type::INT4_RANGE, &buf).expect("decode");
assert_eq!(decoded, *original, "round-trip mismatch for {original:?}");
}
}
#[test]
fn range_round_trip_through_wire_codec_preserves_int8_bounds() {
let big = i64::MAX - 7;
let original: Range<i64> = Range::inclusive_exclusive(-big, big);
let mut buf = BytesMut::new();
original.to_sql(&Type::INT8_RANGE, &mut buf).unwrap();
let decoded = <Range<i64> as FromSql>::from_sql(&Type::INT8_RANGE, &buf).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn range_round_trip_through_wire_codec_preserves_numrange_bounds() {
let lo = rust_decimal::Decimal::new(-99_999_999, 4);
let hi = rust_decimal::Decimal::new(12_345, 2);
let original: Range<rust_decimal::Decimal> = Range::inclusive_exclusive(lo, hi);
let mut buf = BytesMut::new();
original.to_sql(&Type::NUM_RANGE, &mut buf).unwrap();
let decoded =
<Range<rust_decimal::Decimal> as FromSql>::from_sql(&Type::NUM_RANGE, &buf).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn range_round_trip_through_wire_codec_preserves_date_bounds() {
let lo = time::Date::from_calendar_date(2020, time::Month::January, 1).unwrap();
let hi = time::Date::from_calendar_date(2030, time::Month::December, 31).unwrap();
let original: Range<time::Date> = Range::inclusive_exclusive(lo, hi);
let mut buf = BytesMut::new();
original.to_sql(&Type::DATE_RANGE, &mut buf).unwrap();
let decoded = <Range<time::Date> as FromSql>::from_sql(&Type::DATE_RANGE, &buf).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn range_round_trip_through_wire_codec_preserves_tstz_bounds() {
let lo = time::OffsetDateTime::UNIX_EPOCH;
let hi = lo + time::Duration::days(365);
let original: Range<time::OffsetDateTime> = Range::inclusive_exclusive(lo, hi);
let mut buf = BytesMut::new();
original.to_sql(&Type::TSTZ_RANGE, &mut buf).unwrap();
let decoded =
<Range<time::OffsetDateTime> as FromSql>::from_sql(&Type::TSTZ_RANGE, &buf).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn range_round_trip_through_wire_codec_preserves_ts_bounds() {
let lo = time::PrimitiveDateTime::new(
time::Date::from_calendar_date(2026, time::Month::January, 1).unwrap(),
time::Time::MIDNIGHT,
);
let hi = time::PrimitiveDateTime::new(
time::Date::from_calendar_date(2026, time::Month::January, 2).unwrap(),
time::Time::MIDNIGHT,
);
let original: Range<time::PrimitiveDateTime> = Range::inclusive_exclusive(lo, hi);
let mut buf = BytesMut::new();
original.to_sql(&Type::TS_RANGE, &mut buf).unwrap();
let decoded =
<Range<time::PrimitiveDateTime> as FromSql>::from_sql(&Type::TS_RANGE, &buf).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn range_accepts_only_the_matching_range_type() {
assert!(<Range<i32> as ToSql>::accepts(&Type::INT4_RANGE));
assert!(!<Range<i32> as ToSql>::accepts(&Type::INT8_RANGE));
assert!(!<Range<i32> as ToSql>::accepts(&Type::TSTZ_RANGE));
assert!(!<Range<i32> as ToSql>::accepts(&Type::INT4));
assert!(<Range<i32> as FromSql>::accepts(&Type::INT4_RANGE));
assert!(!<Range<i32> as FromSql>::accepts(&Type::INT8_RANGE));
assert!(<Range<i64> as ToSql>::accepts(&Type::INT8_RANGE));
assert!(!<Range<i64> as ToSql>::accepts(&Type::INT4_RANGE));
assert!(<Range<time::OffsetDateTime> as ToSql>::accepts(
&Type::TSTZ_RANGE
));
assert!(!<Range<time::OffsetDateTime> as ToSql>::accepts(
&Type::DATE_RANGE
));
assert!(<Range<time::PrimitiveDateTime> as ToSql>::accepts(
&Type::TS_RANGE
));
assert!(!<Range<time::PrimitiveDateTime> as ToSql>::accepts(
&Type::TSTZ_RANGE
));
assert!(<Range<time::Date> as ToSql>::accepts(&Type::DATE_RANGE));
assert!(!<Range<time::Date> as ToSql>::accepts(&Type::TSTZ_RANGE));
}
#[test]
fn range_from_sql_rejects_empty_payload() {
let err = <Range<i32> as FromSql>::from_sql(&Type::INT4_RANGE, &[]).unwrap_err();
assert!(
err.to_string().contains("flags byte"),
"error should mention the missing flags byte; got: {err}"
);
}
#[test]
fn range_from_sql_rejects_empty_flag_with_trailing_bytes() {
let err = <Range<i32> as FromSql>::from_sql(&Type::INT4_RANGE, &[RANGE_EMPTY, 0, 0, 0, 0])
.unwrap_err();
assert!(
err.to_string().contains("trailing"),
"error should mention trailing bytes; got: {err}"
);
}
#[test]
fn range_from_sql_rejects_null_bound_flags() {
let err =
<Range<i32> as FromSql>::from_sql(&Type::INT4_RANGE, &[RANGE_LB_NULL]).unwrap_err();
assert!(
err.to_string().contains("NULL-bound flag"),
"error should reject NULL-bound flag; got: {err}"
);
}
#[test]
fn range_from_sql_rejects_truncated_bound_length_prefix() {
let err = <Range<i32> as FromSql>::from_sql(&Type::INT4_RANGE, &[RANGE_LB_INC, 0, 0])
.unwrap_err();
assert!(
err.to_string().contains("length prefix"),
"error should mention length prefix; got: {err}"
);
}
#[test]
fn range_from_sql_rejects_negative_bound_length() {
let payload = [RANGE_LB_INC, 0xff, 0xff, 0xff, 0xff];
let err = <Range<i32> as FromSql>::from_sql(&Type::INT4_RANGE, &payload).unwrap_err();
assert!(
err.to_string().contains("negative bound length"),
"error should mention negative length; got: {err}"
);
}
#[test]
fn range_from_sql_rejects_bound_body_shorter_than_length_prefix() {
let payload = [RANGE_LB_INC, 0, 0, 0, 4, 0, 0];
let err = <Range<i32> as FromSql>::from_sql(&Type::INT4_RANGE, &payload).unwrap_err();
assert!(
err.to_string().contains("only"),
"error should mention truncated body; got: {err}"
);
}
#[test]
fn range_from_sql_rejects_trailing_bytes_after_upper_bound() {
let mut payload = vec![RANGE_LB_INC];
payload.extend_from_slice(&4i32.to_be_bytes());
payload.extend_from_slice(&1i32.to_be_bytes());
payload.extend_from_slice(&4i32.to_be_bytes());
payload.extend_from_slice(&10i32.to_be_bytes());
payload.push(0xff); let err = <Range<i32> as FromSql>::from_sql(&Type::INT4_RANGE, &payload).unwrap_err();
assert!(
err.to_string().contains("trailing"),
"error should mention trailing bytes; got: {err}"
);
}
#[cfg(feature = "network")]
#[test]
fn macaddr_constructor_round_trips_octets() {
let m = MacAddr::new([0x01, 0x23, 0x45, 0x67, 0x89, 0xab]);
assert_eq!(m.octets(), [0x01, 0x23, 0x45, 0x67, 0x89, 0xab]);
}
#[cfg(feature = "network")]
#[test]
fn macaddr_display_renders_canonical_colon_form_lowercase_hex() {
let m = MacAddr::new([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
assert_eq!(format!("{m}"), "aa:bb:cc:dd:ee:ff");
}
#[cfg(feature = "network")]
#[test]
fn macaddr_from_str_parses_canonical_colon_form() {
let m: MacAddr = "aa:bb:cc:dd:ee:ff".parse().unwrap();
assert_eq!(m, MacAddr::new([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]));
}
#[cfg(feature = "network")]
#[test]
fn macaddr_from_str_parses_hyphen_form() {
let m: MacAddr = "AA-BB-CC-DD-EE-FF".parse().unwrap();
assert_eq!(m, MacAddr::new([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]));
}
#[cfg(feature = "network")]
#[test]
fn macaddr_from_str_rejects_mixed_separators() {
let err = "aa:bb-cc:dd-ee:ff".parse::<MacAddr>().unwrap_err();
assert_eq!(err, MacAddrParseError::MixedSeparators);
}
#[cfg(feature = "network")]
#[test]
fn macaddr_from_str_rejects_wrong_length() {
let err = "aa:bb:cc:dd:ee".parse::<MacAddr>().unwrap_err();
assert_eq!(err, MacAddrParseError::WrongLength { found: 5 });
}
#[cfg(feature = "network")]
#[test]
fn macaddr_from_str_rejects_non_hex_octet() {
let err = "aa:zz:cc:dd:ee:ff".parse::<MacAddr>().unwrap_err();
assert_eq!(
err,
MacAddrParseError::InvalidOctet {
index: 1,
octet: "zz".into(),
}
);
}
#[cfg(feature = "network")]
#[test]
fn macaddr_to_sql_writes_six_bytes_in_order() {
let m = MacAddr::new([0x01, 0x23, 0x45, 0x67, 0x89, 0xab]);
let mut buf = BytesMut::new();
m.to_sql(&Type::MACADDR, &mut buf).unwrap();
assert_eq!(buf.as_ref(), &[0x01, 0x23, 0x45, 0x67, 0x89, 0xab]);
}
#[cfg(feature = "network")]
#[test]
fn macaddr_round_trip_through_wire_codec() {
let m = MacAddr::new([0xde, 0xad, 0xbe, 0xef, 0x00, 0x42]);
let mut buf = BytesMut::new();
m.to_sql(&Type::MACADDR, &mut buf).unwrap();
let decoded = MacAddr::from_sql(&Type::MACADDR, &buf).unwrap();
assert_eq!(decoded, m);
}
#[cfg(feature = "network")]
#[test]
fn macaddr_from_sql_rejects_wrong_length() {
let err = MacAddr::from_sql(&Type::MACADDR, &[0u8; 5]).unwrap_err();
assert!(
err.to_string().contains("6 bytes"),
"error should name the expected length; got: {err}"
);
}
#[cfg(feature = "network")]
#[test]
fn macaddr_accepts_only_macaddr_type() {
assert!(<MacAddr as ToSql>::accepts(&Type::MACADDR));
assert!(!<MacAddr as ToSql>::accepts(&Type::BYTEA));
assert!(<MacAddr as FromSql>::accepts(&Type::MACADDR));
assert!(!<MacAddr as FromSql>::accepts(&Type::BYTEA));
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_accepts_valid_ipv4_network() {
let addr = std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 0));
let cidr = CidrAddr::new(addr, 24).unwrap();
assert_eq!(cidr.addr(), addr);
assert_eq!(cidr.prefix(), 24);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_rejects_ipv4_with_host_bits_set() {
let addr = std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 5));
let err = CidrAddr::new(addr, 24).unwrap_err();
assert_eq!(err, CidrAddrError::HostBitsSet);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_rejects_prefix_too_large_for_ipv4() {
let addr = std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0));
let err = CidrAddr::new(addr, 33).unwrap_err();
assert_eq!(
err,
CidrAddrError::PrefixTooLarge {
prefix: 33,
max: 32
}
);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_rejects_prefix_too_large_for_ipv6() {
let addr = std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED);
let err = CidrAddr::new(addr, 129).unwrap_err();
assert_eq!(
err,
CidrAddrError::PrefixTooLarge {
prefix: 129,
max: 128
}
);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_accepts_all_bits_zero_with_zero_prefix() {
let v4 = std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED);
let v6 = std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED);
assert!(CidrAddr::new(v4, 0).is_ok());
assert!(CidrAddr::new(v6, 0).is_ok());
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_accepts_full_prefix_with_any_address() {
let v4 = std::net::IpAddr::V4(std::net::Ipv4Addr::new(10, 20, 30, 40));
assert!(CidrAddr::new(v4, 32).is_ok());
let v6: std::net::IpAddr = "2001:db8::1".parse().unwrap();
assert!(CidrAddr::new(v6, 128).is_ok());
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_ipv6_network_validation_walks_boundary_octet() {
let net: std::net::IpAddr = "2001:db8::".parse().unwrap();
assert!(CidrAddr::new(net, 32).is_ok());
let bad: std::net::IpAddr = "2001:db8:1::".parse().unwrap();
assert_eq!(
CidrAddr::new(bad, 32).unwrap_err(),
CidrAddrError::HostBitsSet
);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_to_sql_writes_ipv4_header_then_address() {
let addr = std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 0));
let cidr = CidrAddr::new(addr, 24).unwrap();
let mut buf = BytesMut::new();
cidr.to_sql(&Type::CIDR, &mut buf).unwrap();
assert_eq!(buf.len(), 8);
assert_eq!(buf[0], PGSQL_AF_INET); assert_eq!(buf[1], 24); assert_eq!(buf[2], 1); assert_eq!(buf[3], 4); assert_eq!(&buf[4..8], &[192, 168, 1, 0]);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_to_sql_writes_ipv6_header_then_address() {
let addr: std::net::IpAddr = "2001:db8::".parse().unwrap();
let cidr = CidrAddr::new(addr, 32).unwrap();
let mut buf = BytesMut::new();
cidr.to_sql(&Type::CIDR, &mut buf).unwrap();
assert_eq!(buf.len(), 20);
assert_eq!(buf[0], PGSQL_AF_INET6);
assert_eq!(buf[1], 32);
assert_eq!(buf[2], 1);
assert_eq!(buf[3], 16);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_round_trip_through_wire_codec_ipv4() {
let addr = std::net::IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 0));
let cidr = CidrAddr::new(addr, 8).unwrap();
let mut buf = BytesMut::new();
cidr.to_sql(&Type::CIDR, &mut buf).unwrap();
let decoded = CidrAddr::from_sql(&Type::CIDR, &buf).unwrap();
assert_eq!(decoded, cidr);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_round_trip_through_wire_codec_ipv6() {
let addr: std::net::IpAddr = "2001:db8:cafe::".parse().unwrap();
let cidr = CidrAddr::new(addr, 48).unwrap();
let mut buf = BytesMut::new();
cidr.to_sql(&Type::CIDR, &mut buf).unwrap();
let decoded = CidrAddr::from_sql(&Type::CIDR, &buf).unwrap();
assert_eq!(decoded, cidr);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_from_sql_rejects_truncated_header() {
let err = CidrAddr::from_sql(&Type::CIDR, &[2, 24, 1]).unwrap_err();
assert!(
err.to_string().contains("truncated"),
"error should mention truncated header; got: {err}"
);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_from_sql_rejects_unknown_address_family() {
let err = CidrAddr::from_sql(&Type::CIDR, &[99, 24, 1, 4, 0, 0, 0, 0]).unwrap_err();
assert!(
err.to_string().contains("address family"),
"error should mention address family; got: {err}"
);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_from_sql_rejects_oversized_ipv4_prefix() {
let err = CidrAddr::from_sql(&Type::CIDR, &[PGSQL_AF_INET, 33, 1, 4, 192, 168, 1, 0])
.unwrap_err();
assert!(
err.to_string().contains("prefix"),
"error should mention prefix; got: {err}"
);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_from_sql_rejects_address_with_host_bits_set() {
let err = CidrAddr::from_sql(&Type::CIDR, &[PGSQL_AF_INET, 24, 1, 4, 192, 168, 1, 5])
.unwrap_err();
assert!(
err.to_string().contains("host bits"),
"error should mention host bits; got: {err}"
);
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_accepts_only_cidr_type() {
assert!(<CidrAddr as ToSql>::accepts(&Type::CIDR));
assert!(!<CidrAddr as ToSql>::accepts(&Type::INET));
assert!(<CidrAddr as FromSql>::accepts(&Type::CIDR));
assert!(!<CidrAddr as FromSql>::accepts(&Type::INET));
}
#[cfg(feature = "network")]
#[test]
fn cidraddr_display_renders_addr_slash_prefix() {
let addr = std::net::IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 0));
let cidr = CidrAddr::new(addr, 8).unwrap();
assert_eq!(format!("{cidr}"), "10.0.0.0/8");
}
}