use std::ops::Bound;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Range<T> {
pub lower: Bound<T>,
pub upper: Bound<T>,
}
impl<T> Range<T> {
#[must_use]
pub fn new(lower: Bound<T>, upper: Bound<T>) -> Self {
Self { lower, upper }
}
#[must_use]
pub fn closed_open(lower: T, upper: T) -> Self {
Self {
lower: Bound::Included(lower),
upper: Bound::Excluded(upper),
}
}
#[must_use]
pub fn at_least(lower: T) -> Self {
Self {
lower: Bound::Included(lower),
upper: Bound::Unbounded,
}
}
#[must_use]
pub fn less_than(upper: T) -> Self {
Self {
lower: Bound::Unbounded,
upper: Bound::Excluded(upper),
}
}
}
impl<T> From<std::ops::Range<T>> for Range<T> {
fn from(r: std::ops::Range<T>) -> Self {
Self::closed_open(r.start, r.end)
}
}
impl<T> From<(Bound<T>, Bound<T>)> for Range<T> {
fn from(v: (Bound<T>, Bound<T>)) -> Self {
Self {
lower: v.0,
upper: v.1,
}
}
}
fn pg_range_literal<T>(lower: &Bound<T>, upper: &Bound<T>, fmt: impl Fn(&T) -> String) -> String {
let (open, lo) = match lower {
Bound::Included(v) => ('[', fmt(v)),
Bound::Excluded(v) => ('(', fmt(v)),
Bound::Unbounded => ('[', String::new()),
};
let (hi, close) = match upper {
Bound::Included(v) => (fmt(v), ']'),
Bound::Excluded(v) => (fmt(v), ')'),
Bound::Unbounded => (String::new(), ')'),
};
format!("{open}{lo},{hi}{close}")
}
impl<T: serde::Serialize> serde::Serialize for Range<T> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("Range", 2)?;
s.serialize_field("lower", &BoundSer(&self.lower))?;
s.serialize_field("upper", &BoundSer(&self.upper))?;
s.end()
}
}
struct BoundSer<'a, T>(&'a Bound<T>);
impl<T: serde::Serialize> serde::Serialize for BoundSer<'_, T> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self.0 {
Bound::Included(v) | Bound::Excluded(v) => v.serialize(serializer),
Bound::Unbounded => serializer.serialize_none(),
}
}
}
macro_rules! range_into_sqlvalue {
($t:ty, $fmt:expr) => {
impl From<Range<$t>> for crate::core::SqlValue {
fn from(r: Range<$t>) -> Self {
let f: fn(&$t) -> String = $fmt;
crate::core::SqlValue::RangeLiteral(pg_range_literal(&r.lower, &r.upper, f))
}
}
};
}
range_into_sqlvalue!(i32, |v| v.to_string());
range_into_sqlvalue!(i64, |v| v.to_string());
range_into_sqlvalue!(rust_decimal::Decimal, |v| v.to_string());
range_into_sqlvalue!(chrono::NaiveDate, |v| v.to_string());
range_into_sqlvalue!(chrono::DateTime<chrono::Utc>, |v| v.to_rfc3339());
#[cfg(feature = "postgres")]
impl<'r, T> sqlx::Decode<'r, sqlx::Postgres> for Range<T>
where
sqlx::postgres::types::PgRange<T>: sqlx::Decode<'r, sqlx::Postgres>,
{
fn decode(
value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
) -> Result<Self, sqlx::error::BoxDynError> {
let pg =
<sqlx::postgres::types::PgRange<T> as sqlx::Decode<'r, sqlx::Postgres>>::decode(value)?;
Ok(Self {
lower: pg.start,
upper: pg.end,
})
}
}
#[cfg(feature = "postgres")]
impl<T> sqlx::Type<sqlx::Postgres> for Range<T>
where
sqlx::postgres::types::PgRange<T>: sqlx::Type<sqlx::Postgres>,
{
fn type_info() -> sqlx::postgres::PgTypeInfo {
<sqlx::postgres::types::PgRange<T> as sqlx::Type<sqlx::Postgres>>::type_info()
}
fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
<sqlx::postgres::types::PgRange<T> as sqlx::Type<sqlx::Postgres>>::compatible(ty)
}
}
#[cfg(feature = "mysql")]
impl<'r, T> sqlx::Decode<'r, sqlx::MySql> for Range<T> {
fn decode(
_value: <sqlx::MySql as sqlx::Database>::ValueRef<'r>,
) -> Result<Self, sqlx::error::BoxDynError> {
Err("`Range<T>` columns are PostgreSQL-only; cannot decode on MySQL (issue #343)".into())
}
}
#[cfg(feature = "mysql")]
impl<T> sqlx::Type<sqlx::MySql> for Range<T> {
fn type_info() -> sqlx::mysql::MySqlTypeInfo {
<String as sqlx::Type<sqlx::MySql>>::type_info()
}
}
#[cfg(feature = "sqlite")]
impl<'r, T> sqlx::Decode<'r, sqlx::Sqlite> for Range<T> {
fn decode(
_value: <sqlx::Sqlite as sqlx::Database>::ValueRef<'r>,
) -> Result<Self, sqlx::error::BoxDynError> {
Err("`Range<T>` columns are PostgreSQL-only; cannot decode on SQLite (issue #343)".into())
}
}
#[cfg(feature = "sqlite")]
impl<T> sqlx::Type<sqlx::Sqlite> for Range<T> {
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
<String as sqlx::Type<sqlx::Sqlite>>::type_info()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::SqlValue;
#[test]
fn closed_open_literal() {
let v: SqlValue = Range::closed_open(1_i32, 10).into();
assert!(matches!(v, SqlValue::RangeLiteral(ref s) if s == "[1,10)"));
}
#[test]
fn unbounded_sides() {
let lo: SqlValue = Range::at_least(5_i64).into();
assert!(matches!(lo, SqlValue::RangeLiteral(ref s) if s == "[5,)"));
let hi: SqlValue = Range::less_than(10_i64).into();
assert!(matches!(hi, SqlValue::RangeLiteral(ref s) if s == "[,10)"));
}
#[test]
fn from_std_range() {
let r: Range<i32> = (1..10).into();
assert_eq!(r.lower, Bound::Included(1));
assert_eq!(r.upper, Bound::Excluded(10));
}
#[test]
fn date_literal_is_iso() {
let d1 = chrono::NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
let d2 = chrono::NaiveDate::from_ymd_opt(2025, 2, 1).unwrap();
let v: SqlValue = Range::closed_open(d1, d2).into();
assert!(matches!(v, SqlValue::RangeLiteral(ref s) if s == "[2025-01-01,2025-02-01)"));
}
#[test]
fn serde_round_trips() {
let r = Range::closed_open(1_i32, 10);
let json = serde_json::to_string(&r).unwrap();
assert_eq!(json, r#"{"lower":1,"upper":10}"#);
}
}