From 5d33f22f18559374dbf1a5bf4476ea4cb031ce3a Mon Sep 17 00:00:00 2001 From: "Eric B. Ridge" Date: Sun, 19 Feb 2023 15:19:01 -0500 Subject: [PATCH 1/8] overhaul our Postgres Range support - We now go ahead and deconstruct the range value in the `FromDatum` implementation. Immediately, this fixes issues with `pg_sys::RangeBound.val` datums potentially being able to be used after free. - `RangeData` is gone and replaced with a more idiomatic `RangeBound` enum, to which the internal Postgres `pg_sys::RangeBound` struct can be mapped. - We provide `From` impls for all of Rust's built-in Range types. - Our `pgx::Range` can be constructed from any type that can be converted into a `RangeBound` and we provide direct impls for the set of supported `RangeSubType` types along with `Option` which maps `Some` to `::Inclusive` and `None` to `::Infinite`. - The idea that a Postgres range can be "empty" is properly relayed through `Range`'s public API - add an example (albeit puny) --- .github/workflows/tests.yml | 3 + Cargo.lock | 8 + Cargo.toml | 1 + README.md | 72 +++-- pgx-examples/range/.gitignore | 7 + pgx-examples/range/Cargo.toml | 32 ++ pgx-examples/range/README.md | 4 + pgx-examples/range/range.control | 6 + pgx-examples/range/src/lib.rs | 57 ++++ pgx-pg-sys/src/submodules/mod.rs | 1 + pgx-pg-sys/src/submodules/range.rs | 21 ++ pgx-tests/src/tests/range_tests.rs | 36 +-- pgx/src/datum/range.rs | 479 +++++++++++++++++------------ pgx/src/prelude.rs | 5 +- 14 files changed, 467 insertions(+), 265 deletions(-) create mode 100644 pgx-examples/range/.gitignore create mode 100644 pgx-examples/range/Cargo.toml create mode 100644 pgx-examples/range/README.md create mode 100644 pgx-examples/range/range.control create mode 100644 pgx-examples/range/src/lib.rs create mode 100644 pgx-pg-sys/src/submodules/range.rs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a4db6fbec..d9bbccaa0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -203,6 +203,9 @@ jobs: - name: Run operators example tests run: cargo test --package operators --features "pg$PG_VER" --no-default-features + - name: Run range example tests + run: cargo test --package range --features "pg$PG_VER" --no-default-features + - name: Run schemas example tests run: cargo test --package schemas --features "pg$PG_VER" --no-default-features diff --git a/Cargo.lock b/Cargo.lock index 6fe26c0f5..23734de7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1781,6 +1781,14 @@ dependencies = [ "getrandom", ] +[[package]] +name = "range" +version = "0.1.0" +dependencies = [ + "pgx", + "pgx-tests", +] + [[package]] name = "rayon" version = "1.6.1" diff --git a/Cargo.toml b/Cargo.toml index 8f7ff7a3a..454b38f01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "pgx-examples/numeric", "pgx-examples/pgtrybuilder", "pgx-examples/operators", + "pgx-examples/range", "pgx-examples/schemas", "pgx-examples/shmem", "pgx-examples/spi", diff --git a/README.md b/README.md index 19871ef96..53d225ff1 100644 --- a/README.md +++ b/README.md @@ -163,39 +163,45 @@ cargo pgx init ### Mapping of Postgres types to Rust -Postgres Type | Rust Type (as `Option`) ---------------|----------- -`bytea` | `Vec` or `&[u8]` (zero-copy) -`text` | `String` or `&str` (zero-copy) -`varchar` | `String` or `&str` (zero-copy) or `char` -`"char"` | `i8` -`smallint` | `i16` -`integer` | `i32` -`bigint` | `i64` -`oid` | `u32` -`real` | `f32` -`double precision` | `f64` -`bool` | `bool` -`json` | `pgx::Json(serde_json::Value)` -`jsonb` | `pgx::JsonB(serde_json::Value)` -`date` | `pgx::Date` -`time` | `pgx::Time` -`timestamp` | `pgx::Timestamp` -`time with time zone` | `pgx::TimeWithTimeZone` -`timestamp with time zone` | `pgx::TimestampWithTimeZone` -`anyarray` | `pgx::AnyArray` -`anyelement` | `pgx::AnyElement` -`box` | `pgx::pg_sys::BOX` -`point` | `pgx::pgx_sys::Point` -`tid` | `pgx::pg_sys::ItemPointerData` -`cstring` | `&core::ffi::CStr` -`inet` | `pgx::Inet(String)` -- TODO: needs better support -`numeric` | `pgx::Numeric or pgx::AnyNumeric` -`void` | `()` -`ARRAY[]::` | `Vec>` or `pgx::Array` (zero-copy) -`NULL` | `Option::None` -`internal` | `pgx::PgBox` where `T` is any Rust/Postgres struct -`uuid` | `pgx::Uuid([u8; 16])` +| Postgres Type | Rust Type (as `Option`) | +|----------------------------|-------------------------------------------------------| +| `bytea` | `Vec` or `&[u8]` (zero-copy) | +| `text` | `String` or `&str` (zero-copy) | +| `varchar` | `String` or `&str` (zero-copy) or `char` | +| `"char"` | `i8` | +| `smallint` | `i16` | +| `integer` | `i32` | +| `bigint` | `i64` | +| `oid` | `u32` | +| `real` | `f32` | +| `double precision` | `f64` | +| `bool` | `bool` | +| `json` | `pgx::Json(serde_json::Value)` | +| `jsonb` | `pgx::JsonB(serde_json::Value)` | +| `date` | `pgx::Date` | +| `time` | `pgx::Time` | +| `timestamp` | `pgx::Timestamp` | +| `time with time zone` | `pgx::TimeWithTimeZone` | +| `timestamp with time zone` | `pgx::TimestampWithTimeZone` | +| `anyarray` | `pgx::AnyArray` | +| `anyelement` | `pgx::AnyElement` | +| `box` | `pgx::pg_sys::BOX` | +| `point` | `pgx::pgx_sys::Point` | +| `tid` | `pgx::pg_sys::ItemPointerData` | +| `cstring` | `&core::ffi::CStr` | +| `inet` | `pgx::Inet(String)` -- TODO: needs better support | +| `numeric` | `pgx::Numeric or pgx::AnyNumeric` | +| `void` | `()` | +| `ARRAY[]::` | `Vec>` or `pgx::Array` (zero-copy) | +| `int4range` | `pgx::Range` | +| `int8range` | `pgx::Range` | +| `numrange` | `pgx::Range>` or `pgx::Range` | +| `daterange` | `pgx::Range` | +| `tsrange` | `pgx::Range` | +| `tstzrange` | `pgx::Range` | +| `NULL` | `Option::None` | +| `internal` | `pgx::PgBox` where `T` is any Rust/Postgres struct | +| `uuid` | `pgx::Uuid([u8; 16])` | There are also `IntoDatum` and `FromDatum` traits for implementing additional type conversions, along with `#[derive(PostgresType)]` and `#[derive(PostgresEnum)]` for automatic conversion of diff --git a/pgx-examples/range/.gitignore b/pgx-examples/range/.gitignore new file mode 100644 index 000000000..bdbe7939b --- /dev/null +++ b/pgx-examples/range/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.idea/ +/target +*.iml +**/*.rs.bk +Cargo.lock +sql/arrays-1.0.sql diff --git a/pgx-examples/range/Cargo.toml b/pgx-examples/range/Cargo.toml new file mode 100644 index 000000000..ac2b44b52 --- /dev/null +++ b/pgx-examples/range/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "range" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[features] +default = ["pg13"] +pg11 = ["pgx/pg11", "pgx-tests/pg11" ] +pg12 = ["pgx/pg12", "pgx-tests/pg12" ] +pg13 = ["pgx/pg13", "pgx-tests/pg13" ] +pg14 = ["pgx/pg14", "pgx-tests/pg14" ] +pg15 = ["pgx/pg15", "pgx-tests/pg15" ] +pg_test = [] + +[dependencies] +pgx = { path = "../../pgx", default-features = false } + +[dev-dependencies] +pgx-tests = { path = "../../pgx-tests" } + +# uncomment these if compiling outside of 'pgx' +# [profile.dev] +# panic = "unwind" + +# [profile.release] +# panic = "unwind" +# opt-level = 3 +# lto = "fat" +# codegen-units = 1 diff --git a/pgx-examples/range/README.md b/pgx-examples/range/README.md new file mode 100644 index 000000000..5a8122440 --- /dev/null +++ b/pgx-examples/range/README.md @@ -0,0 +1,4 @@ +Examples for using pgx' `Range` support. + +pgx supports the Postgres `int4range`, `int8range`, `numrange`, `daterange`, `tsrange`, and `tstzrange` types, safely +mapped to `pgx::Range` where `T` is any of `i32`, `i64`, `Numeric`, `AnyNumeric`, `Date`, `Timestamp`, and `TimestampWithTimeZone`. \ No newline at end of file diff --git a/pgx-examples/range/range.control b/pgx-examples/range/range.control new file mode 100644 index 000000000..bac83eb7e --- /dev/null +++ b/pgx-examples/range/range.control @@ -0,0 +1,6 @@ +comment = 'range: Created by pgx' +default_version = '0.1.0' +module_pathname = '$libdir/range' +relocatable = false +superuser = false +schema = range \ No newline at end of file diff --git a/pgx-examples/range/src/lib.rs b/pgx-examples/range/src/lib.rs new file mode 100644 index 000000000..5fa045e95 --- /dev/null +++ b/pgx-examples/range/src/lib.rs @@ -0,0 +1,57 @@ +/* +Portions Copyright 2019-2021 ZomboDB, LLC. +Portions Copyright 2021-2022 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the MIT license that can be found in the LICENSE file. +*/ + +use pgx::prelude::*; + +pgx::pg_module_magic!(); + +#[pg_extern] +fn range(s: i32, e: i32) -> pgx::Range { + (s..e).into() +} + +#[pg_extern] +fn range_from(s: i32) -> pgx::Range { + (s..).into() +} + +#[pg_extern] +fn range_full() -> pgx::Range { + (..).into() +} + +#[pg_extern] +fn range_inclusive(s: i32, e: i32) -> pgx::Range { + (s..=e).into() +} + +#[pg_extern] +fn range_to(e: i32) -> pgx::Range { + (..e).into() +} + +#[pg_extern] +fn range_to_inclusive(e: i32) -> pgx::Range { + (..=e).into() +} + +#[pg_extern] +fn empty() -> pgx::Range { + pgx::Range::empty() +} + +#[pg_extern] +fn infinite() -> pgx::Range { + pgx::Range::infinite() +} + +#[pg_extern] +fn assert_range(r: pgx::Range, s: i32, e: i32) -> bool { + r == (s..e).into() +} diff --git a/pgx-pg-sys/src/submodules/mod.rs b/pgx-pg-sys/src/submodules/mod.rs index a6702eaf9..d9474be42 100644 --- a/pgx-pg-sys/src/submodules/mod.rs +++ b/pgx-pg-sys/src/submodules/mod.rs @@ -17,6 +17,7 @@ pub mod oids; pub mod panic; pub mod pg_try; pub mod polyfill; +pub mod range; pub(crate) mod thread_check; pub mod tupdesc; diff --git a/pgx-pg-sys/src/submodules/range.rs b/pgx-pg-sys/src/submodules/range.rs new file mode 100644 index 000000000..511e03487 --- /dev/null +++ b/pgx-pg-sys/src/submodules/range.rs @@ -0,0 +1,21 @@ +use std::hash::{Hash, Hasher}; + +impl PartialEq for crate::RangeBound { + fn eq(&self, other: &Self) -> bool { + self.val == other.val + && self.infinite == other.infinite + && self.inclusive == other.inclusive + && self.lower == other.lower + } +} + +impl Eq for crate::RangeBound {} + +impl Hash for crate::RangeBound { + fn hash(&self, state: &mut H) { + state.write_usize(self.val.value()); + state.write_u8(self.infinite as u8); + state.write_u8(self.inclusive as u8); + state.write_u8(self.lower as u8); + } +} diff --git a/pgx-tests/src/tests/range_tests.rs b/pgx-tests/src/tests/range_tests.rs index 99875b098..0f8881335 100644 --- a/pgx-tests/src/tests/range_tests.rs +++ b/pgx-tests/src/tests/range_tests.rs @@ -8,6 +8,7 @@ Use of this source code is governed by the MIT license that can be found in the */ use pgx::prelude::*; +use pgx::RangeBound; #[pg_extern] fn accept_range_i32(range: Range) -> Range { @@ -43,43 +44,16 @@ fn range_round_trip_values(range: Range) -> Range where T: FromDatum + IntoDatum + RangeSubType, { - let range_data: RangeData = range.into(); - if range_data.is_empty { - RangeData::::empty_range_data().into() - } else { - let range_data = RangeData::::from_range_values( - range_data.lower_val(), - range_data.upper_val(), - range_data.lower.inclusive, - range_data.upper.inclusive, - ); - range_data.into() - } + range } fn range_round_trip_bounds(range: Range) -> Range where T: FromDatum + IntoDatum + RangeSubType, { - let range_data: RangeData = range.into(); - if range_data.is_empty { - RangeData::::empty_range_data().into() - } else { - let mut lower_bound = pg_sys::RangeBound::default(); - lower_bound.lower = true; - lower_bound.inclusive = range_data.lower.inclusive; - lower_bound.infinite = range_data.lower.infinite; - lower_bound.val = range_data.lower.val.clone(); - - let mut upper_bound = pg_sys::RangeBound::default(); - upper_bound.lower = false; - upper_bound.inclusive = range_data.upper.inclusive; - upper_bound.infinite = range_data.upper.infinite; - upper_bound.val = range_data.upper.val.clone(); - - let range_data = RangeData::::from_range_bounds(lower_bound, upper_bound); - - range_data.into() + match range.as_ref() { + None => Range::empty(), + Some((l, u)) => Range::new(l, u), } } diff --git a/pgx/src/datum/range.rs b/pgx/src/datum/range.rs index b34971d9f..333c034fb 100644 --- a/pgx/src/datum/range.rs +++ b/pgx/src/datum/range.rs @@ -9,56 +9,207 @@ Use of this source code is governed by the MIT license that can be found in the //! Utility functions for working with `pg_sys::RangeType` structs use crate::{ - pg_sys, void_mut_ptr, AnyNumeric, Date, FromDatum, IntoDatum, Numeric, Timestamp, - TimestampWithTimeZone, + pg_sys, AnyNumeric, Date, FromDatum, IntoDatum, Numeric, Timestamp, TimestampWithTimeZone, }; -use pgx_pg_sys::{Oid, RangeBound}; use pgx_sql_entity_graph::metadata::{ ArgumentError, Returns, ReturnsError, SqlMapping, SqlTranslatable, }; -use std::marker::PhantomData; +use std::ops::{Deref, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive}; + +/// A Postgres range bound can be one of these types +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum RangeBound { + Infinite, + Inclusive(T), + Exclusive(T), +} -/// Represents Datum to serialized RangeType PG struct -pub struct Range { - ptr: *mut pg_sys::varlena, - range_type: *mut pg_sys::RangeType, - _marker: PhantomData, +impl RangeBound +where + T: RangeSubType, +{ + /// Convert this pgx [`RangeBound`] into the equivalent Postgres [`pg_sys::RangeBound`]. + /// + /// Note that the `lower` property is always set to false as a [`RangeBound`] doesn't know the + /// end on which it's placed. + pub fn into_pg(self) -> pg_sys::RangeBound { + match self { + RangeBound::Infinite => pg_sys::RangeBound { + val: pg_sys::Datum::from(0), + infinite: true, + inclusive: false, + lower: false, + }, + RangeBound::Inclusive(v) => pg_sys::RangeBound { + val: v.into_datum().unwrap(), + infinite: false, + inclusive: true, + lower: false, + }, + RangeBound::Exclusive(v) => pg_sys::RangeBound { + val: v.into_datum().unwrap(), + infinite: false, + inclusive: false, + lower: false, + }, + } + } + + /// Create a typed pgx [`RangeBound`] from an arbitrary Postgres [`pg_sys::RangeBound`]. + /// + /// # Safety + /// + /// This function is unsafe as it cannot guarantee that the `val` property, which is a + /// [`pg_sys::Datum`], points to (or is) something correct for the generic type `T`. + pub unsafe fn from_pg(range_bound: pg_sys::RangeBound) -> RangeBound { + if range_bound.infinite { + RangeBound::Infinite + } else if range_bound.inclusive { + // SAFETY: caller has asserted that `val` is a proper Datum for `T` + unsafe { RangeBound::Inclusive(T::from_datum(range_bound.val, false).unwrap()) } + } else { + // SAFETY: caller has asserted that `val` is a proper Datum for `T` + unsafe { RangeBound::Exclusive(T::from_datum(range_bound.val, false).unwrap()) } + } + } } -impl Range +impl From<&RangeBound> for RangeBound where - T: FromDatum + IntoDatum + RangeSubType, + T: RangeSubType, +{ + #[inline] + fn from(value: &RangeBound) -> Self { + Clone::clone(value) + } +} + +impl From for RangeBound +where + T: RangeSubType, { - /// ## Safety - /// This function is safe, but requires that - /// - datum is not null - /// - datum represents a PG RangeType datum #[inline] - unsafe fn from_pg(datum: pg_sys::Datum) -> Option { - unsafe { Self::from_polymorphic_datum(datum, false, T::range_type_oid()) } + fn from(value: T) -> Self { + RangeBound::Inclusive(value) } } -impl TryFrom for Range + +impl From> for RangeBound where - T: FromDatum + IntoDatum + RangeSubType, + T: RangeSubType, { - type Error = RangeConversionError; + /// Conversion of an [`Option`] to a [`RangeBound`]. + /// + /// `Some` maps to the [`RangeBound::Inclusive`] variant and `None` maps to the + /// [`RangeBound::infinite`] value. + #[inline] + fn from(value: Option) -> Self { + match value { + Some(value) => RangeBound::Inclusive(value), + None => RangeBound::Infinite, + } + } +} - fn try_from(datum: pg_sys::Datum) -> Result { - if datum.is_null() { - Err(RangeConversionError::NullDatum) - } else { - match unsafe { Self::from_pg(datum) } { - Some(range) => Ok(range), - None => Err(RangeConversionError::InvalidDatum), - } +/// A safe deconstruction of a Postgres `pg_sys::RangeType` struct. +/// +/// Unlike Rust ranges, a Postgres range is capable of being "empty", and as such, expect the +/// various getter methods on [`Range`] to return `Option>`. +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct Range { + inner: Option<(RangeBound, RangeBound)>, +} + +impl Range +where + T: RangeSubType, +{ + /// Create a new [`Range`] with bounds. + /// + /// # Examples + /// + /// ```rust,no_run + /// use pgx::{Range, RangeBound}; + /// let _ = Range::::new(1, 10); // `(1..=10)` + /// let _ = Range::::new(None, 10); // `(..=10)` + /// let _ = Range::::new(1, None); // `(1..)` + /// let _ = Range::::new(None, RangeBound::Exclusive(10)); // `(..10)` + /// let _ = Range::::new(1, RangeBound::Exclusive(10)); // (`1..10)` + /// let _ = Range::::new(None, None); // `(..)` + /// let _ = Range::::new(RangeBound::Infinite, RangeBound::Infinite); // `(..)` + #[inline] + pub fn new(lower: L, upper: U) -> Self + where + L: Into>, + U: Into>, + { + Self { inner: Some((lower.into(), upper.into())) } + } + + /// Builds an "empty" range + /// + /// Unlike Rust ranges (from `std::ops::`), Postgres ranges can be empty, meaning they don't + /// represent any range of values. + #[inline] + pub fn empty() -> Self { + Self { inner: None } + } + + /// Builds an "infinite" range. This is equivalent to Rust's [`std::ops::RangeFull`] (`(..)`). + #[inline] + pub fn infinite() -> Self { + Self::new(RangeBound::Infinite, RangeBound::Infinite) + } + + /// Returns the lower [`RangeBound`] + #[inline] + pub fn lower(&self) -> Option<&RangeBound> { + match &self.inner { + Some((l, _)) => Some(l), + None => None, + } + } + + /// Returns the upper [`RangeBound`] + #[inline] + pub fn upper(&self) -> Option<&RangeBound> { + match &self.inner { + Some((_, u)) => Some(u), + None => None, + } + } + + /// Returns 'true' if the range is "empty". + #[inline] + pub fn is_empty(&self) -> bool { + self.inner.is_none() + } + + /// Returns `true` if the range is "infinite". This is equivalent to Rust's [`std::ops::RangeFull`] (`(..)`) + #[inline] + pub fn is_infinite(&self) -> bool { + match (self.lower(), self.upper()) { + (Some(RangeBound::Infinite), Some(RangeBound::Infinite)) => true, + _ => false, } } } +impl Deref for Range +where + T: RangeSubType, +{ + type Target = Option<(RangeBound, RangeBound)>; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } +} + impl FromDatum for Range where - T: FromDatum + IntoDatum + RangeSubType, + T: RangeSubType, { /// ## Safety /// function requires that @@ -79,18 +230,71 @@ where // Datum should be non-null and point to PG RangeType let range_type = unsafe { pg_sys::pg_detoast_datum(datum.cast_mut_ptr()) as *mut pg_sys::RangeType }; - Some(Range { ptr, range_type, _marker: PhantomData }) + + let mut lower_bound: pg_sys::RangeBound = Default::default(); + let mut upper_bound: pg_sys::RangeBound = Default::default(); + let mut is_empty = false; + + unsafe { + // SAFETY: range.range_type came from PG, so assume its rangetypid is valid + let typecache = pg_sys::lookup_type_cache( + (*(range_type)).rangetypid, + pg_sys::TYPECACHE_RANGE_INFO as i32, + ); + + // SAFETY: PG will deserialize into lower/upper RangeBounds and is_empty + pg_sys::range_deserialize( + typecache, + range_type, + &mut lower_bound, + &mut upper_bound, + &mut is_empty, + ); + + // SAFETY: The lower_bound/upper_bound RangeBound value's .val will be a valid Datum of the T type + // If the range is_empty or either bound is infinite then .val = (Datum) 0 + let lower = RangeBound::from_pg(lower_bound); + let upper = RangeBound::from_pg(upper_bound); + + if std::ptr::eq(ptr, range_type.cast()) == false { + // SAFETY: range_type was allocated by Postgres in the call to + // pg_detoast_datum above, so we know it's a valid pointer and needs to be freed + pg_sys::pfree(range_type.cast()); + } + + Some(Range { inner: if is_empty { None } else { Some((lower, upper)) } }) + } } } } impl IntoDatum for Range where - T: FromDatum + IntoDatum + RangeSubType, + T: RangeSubType, { #[inline] fn into_datum(self) -> Option { - Some(self.range_type.into()) + unsafe { + // T must have a valid registered "Range" Type ex. int4 -> int4range, + let typecache = + pg_sys::lookup_type_cache(T::range_type_oid(), pg_sys::TYPECACHE_RANGE_INFO as i32); + + let is_empty = self.is_empty(); + let (mut lower_bound, mut upper_bound) = self.inner.map_or_else( + || (pg_sys::RangeBound::default(), pg_sys::RangeBound::default()), + |(l, u)| (l.into_pg(), u.into_pg()), + ); + + // the lower_bound is the lower + lower_bound.lower = true; + + // PG will serialize these lower/upper RangeBounds to a *RangeType ptr/datum + let range_type = + pg_sys::make_range(typecache, &mut lower_bound, &mut upper_bound, is_empty); + + // *RangeType into Datum + Some(pg_sys::Datum::from(range_type)) + } } #[inline] @@ -99,244 +303,123 @@ where } } -impl Drop for Range +impl From> for Range where - T: FromDatum + IntoDatum + RangeSubType, + T: RangeSubType, { - fn drop(&mut self) { - // Detoasting the varlena may have allocated: the toasted varlena cloned as a detoasted RangeType - // Checking for pointer equivalence is the only way we can truly tell - if !self.range_type.is_null() && self.range_type as *mut pg_sys::varlena != self.ptr { - unsafe { - // SAFETY: if pgx detoasted a clone of this varlena, pfree the clone - pg_sys::pfree(self.range_type as void_mut_ptr); - } - } + #[inline] + fn from(value: std::ops::Range) -> Self { + Range::new(RangeBound::Inclusive(value.start), RangeBound::Exclusive(value.end)) } } -/// Represents a deserialized state of the RangeType's data -/// indicates the subtype of the lower/upper bounds' datum -pub struct RangeData { - pub lower: RangeBound, - pub upper: RangeBound, - pub is_empty: bool, - __marker: PhantomData, -} - -impl RangeData +impl From> for Range where - T: FromDatum + IntoDatum + RangeSubType, + T: RangeSubType, { - /// The lower bound's datum as Option - /// Empty ranges or lower infinite bounds will be None #[inline] - pub fn lower_val(&self) -> Option { - if self.is_empty || self.lower.infinite { - None - } else { - unsafe { T::from_polymorphic_datum(self.lower.val, false, T::type_oid()) } - } + fn from(value: RangeFrom) -> Self { + Range::new(Some(value.start), None) } +} - /// The upper bound's datum as Option - /// Empty ranges or upper infinite bounds will be None +impl From for Range +where + T: RangeSubType, +{ #[inline] - pub fn upper_val(&self) -> Option { - if self.is_empty || self.upper.infinite { - None - } else { - unsafe { T::from_polymorphic_datum(self.upper.val, false, T::type_oid()) } - } - } - - /// Builds an "empty" range - pub fn empty_range_data() -> Self { - let lower_bound = RangeBound { lower: true, ..RangeBound::default() }; - let upper_bound = RangeBound { lower: false, ..RangeBound::default() }; - Self::from_range_bounds_internal(lower_bound, upper_bound, true) + fn from(_: std::ops::RangeFull) -> Self { + Range::new(RangeBound::Infinite, RangeBound::Infinite) } +} - /// Generate a RangeData from the lower/upper RangeBounds, implies non-empty +impl From> for Range +where + T: RangeSubType, +{ #[inline] - pub fn from_range_bounds(lower_bound: RangeBound, upper_bound: RangeBound) -> Self { - Self::from_range_bounds_internal(lower_bound, upper_bound, false) - } - - pub(crate) fn from_range_bounds_internal( - lower_bound: RangeBound, - upper_bound: RangeBound, - is_empty: bool, - ) -> Self { - RangeData { lower: lower_bound, upper: upper_bound, is_empty, __marker: PhantomData } - } - - /// Generate a RangeData from the T values for lower/upper bounds, lower/upper inclusive - /// None for lower_val or upper_val will represent lower_inf/upper_inf bounds - pub fn from_range_values( - lower_val: Option, - upper_val: Option, - lower_inc: bool, - upper_inc: bool, - ) -> Self { - let mut lower_bound = - RangeBound { lower: true, inclusive: lower_inc, ..Default::default() }; - - let mut upper_bound = - RangeBound { lower: false, inclusive: upper_inc, ..Default::default() }; - - match lower_val { - Some(lower_val) => { - lower_bound.val = - lower_val.into_datum().expect("Couldn't convert lower_val to Datum"); - } - None => { - lower_bound.infinite = true; - } - } - - match upper_val { - Some(upper_val) => { - upper_bound.val = - upper_val.into_datum().expect("Couldn't convert upper_val to Datum"); - } - None => { - upper_bound.infinite = true; - } - } - - RangeData::from_range_bounds(lower_bound, upper_bound) + fn from(value: RangeInclusive) -> Self { + Range::new( + RangeBound::Inclusive(Clone::clone(value.start())), + RangeBound::Inclusive(Clone::clone(value.end())), + ) } } -impl From> for RangeData +impl From> for Range where - T: FromDatum + IntoDatum + RangeSubType, + T: RangeSubType, { - /// ## Safety - /// Requires that: - /// - range.range_type is valid pointer to RangeType PG struct - /// - /// Only PG will create the range_type, so this should always be valid - fn from(range: Range) -> Self { - let mut lower_bound: RangeBound = Default::default(); - let mut upper_bound: RangeBound = Default::default(); - let mut is_empty = false; - - unsafe { - // range.range_type came from PG, so assume its rangetypid is valid - let typecache = pg_sys::lookup_type_cache( - (*(range.range_type)).rangetypid, - pg_sys::TYPECACHE_RANGE_INFO as i32, - ); - - // PG will deserialize into lower/upper RangeBounds and is_empty - pg_sys::range_deserialize( - typecache, - range.range_type, - &mut lower_bound, - &mut upper_bound, - &mut is_empty, - ); - } - // The lower_bound/upper_bound RangeBound value's .val will be a valid Datum of the T type - // If the range is_empty or either bound is infinite then .val = (Datum) 0 - RangeData::from_range_bounds_internal(lower_bound, upper_bound, is_empty) + #[inline] + fn from(value: RangeTo) -> Self { + Range::new(RangeBound::Infinite, RangeBound::Exclusive(value.end)) } } -impl From> for Range +impl From> for Range where - T: FromDatum + IntoDatum + RangeSubType, + T: RangeSubType, { - fn from(range_data: RangeData) -> Self { - let datum: pg_sys::Datum = unsafe { - // T must have a valid registered "Range" Type ex. int4 -> int4range, - let typecache = - pg_sys::lookup_type_cache(T::range_type_oid(), pg_sys::TYPECACHE_RANGE_INFO as i32); - - let mut lower_bound = range_data.lower; - let mut upper_bound = range_data.upper; - - // PG will serialize these lower/upper RangeBounds to a *RangeType ptr/datum - let range_type = pg_sys::make_range( - typecache, - &mut lower_bound, - &mut upper_bound, - range_data.is_empty, - ); - - // *RangeType into Datum - range_type.into() - }; - - // SAFETY: We expect PG returned us a valid datum, pointing to *mut pg_sys::RangeType - unsafe { Range::::from_pg(datum) }.expect("Invalid RangeType Datum") + #[inline] + fn from(value: RangeToInclusive) -> Self { + Range::new(RangeBound::Infinite, RangeBound::Inclusive(value.end)) } } /// This trait allows a struct to be a valid subtype for a RangeType -pub unsafe trait RangeSubType { - fn range_type_oid() -> Oid; +pub unsafe trait RangeSubType: Clone + FromDatum + IntoDatum { + fn range_type_oid() -> pg_sys::Oid; } /// for int/int4range unsafe impl RangeSubType for i32 { - fn range_type_oid() -> Oid { + fn range_type_oid() -> pg_sys::Oid { pg_sys::INT4RANGEOID } } /// for bigint/int8range unsafe impl RangeSubType for i64 { - fn range_type_oid() -> Oid { + fn range_type_oid() -> pg_sys::Oid { pg_sys::INT8RANGEOID } } /// for numeric/numrange unsafe impl RangeSubType for AnyNumeric { - fn range_type_oid() -> Oid { + fn range_type_oid() -> pg_sys::Oid { pg_sys::NUMRANGEOID } } /// for numeric/numrange unsafe impl RangeSubType for Numeric { - fn range_type_oid() -> Oid { + fn range_type_oid() -> pg_sys::Oid { pg_sys::NUMRANGEOID } } /// for date/daterange unsafe impl RangeSubType for Date { - fn range_type_oid() -> Oid { + fn range_type_oid() -> pg_sys::Oid { pg_sys::DATERANGEOID } } /// for Timestamp/tsrange unsafe impl RangeSubType for Timestamp { - fn range_type_oid() -> Oid { + fn range_type_oid() -> pg_sys::Oid { pg_sys::TSRANGEOID } } /// for Timestamp With Time Zone/tstzrange unsafe impl RangeSubType for TimestampWithTimeZone { - fn range_type_oid() -> Oid { + fn range_type_oid() -> pg_sys::Oid { pg_sys::TSTZRANGEOID } } -#[derive(Debug, thiserror::Error)] -pub enum RangeConversionError { - #[error("Datum was null, unable to convert to RangeType")] - NullDatum, - #[error("Datum was not a valid pg_sys::RangeType, unable to convert to RangeType")] - InvalidDatum, -} - unsafe impl SqlTranslatable for Range { fn argument_sql() -> Result { Ok(SqlMapping::literal("int4range")) diff --git a/pgx/src/prelude.rs b/pgx/src/prelude.rs index 97b92bd3b..78cc0b51e 100644 --- a/pgx/src/prelude.rs +++ b/pgx/src/prelude.rs @@ -19,9 +19,8 @@ pub use crate::pgbox::{AllocatedByPostgres, AllocatedByRust, PgBox, WhoAllocated // However, reexporting them seems fine for now. pub use crate::datum::{ - AnyNumeric, Array, Date, FromDatum, Interval, IntoDatum, Numeric, PgVarlena, PostgresType, - Range, RangeData, RangeSubType, Time, TimeWithTimeZone, Timestamp, TimestampWithTimeZone, - VariadicArray, + AnyNumeric, Array, Date, FromDatum, Interval IntoDatum, Numeric, PgVarlena, PostgresType, Range, + RangeSubType, Time, TimeWithTimeZone, Timestamp, TimestampWithTimeZone, VariadicArray, }; pub use crate::inoutfuncs::{InOutFuncs, JsonInOutFuncs, PgVarlenaInOutFuncs}; From a23389c4d3345071314263389f761150302d47f7 Mon Sep 17 00:00:00 2001 From: "Eric B. Ridge" Date: Sun, 19 Feb 2023 15:24:42 -0500 Subject: [PATCH 2/8] this turned out to be unnecessary --- pgx-pg-sys/src/submodules/mod.rs | 1 - pgx-pg-sys/src/submodules/range.rs | 21 --------------------- 2 files changed, 22 deletions(-) delete mode 100644 pgx-pg-sys/src/submodules/range.rs diff --git a/pgx-pg-sys/src/submodules/mod.rs b/pgx-pg-sys/src/submodules/mod.rs index d9474be42..a6702eaf9 100644 --- a/pgx-pg-sys/src/submodules/mod.rs +++ b/pgx-pg-sys/src/submodules/mod.rs @@ -17,7 +17,6 @@ pub mod oids; pub mod panic; pub mod pg_try; pub mod polyfill; -pub mod range; pub(crate) mod thread_check; pub mod tupdesc; diff --git a/pgx-pg-sys/src/submodules/range.rs b/pgx-pg-sys/src/submodules/range.rs deleted file mode 100644 index 511e03487..000000000 --- a/pgx-pg-sys/src/submodules/range.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::hash::{Hash, Hasher}; - -impl PartialEq for crate::RangeBound { - fn eq(&self, other: &Self) -> bool { - self.val == other.val - && self.infinite == other.infinite - && self.inclusive == other.inclusive - && self.lower == other.lower - } -} - -impl Eq for crate::RangeBound {} - -impl Hash for crate::RangeBound { - fn hash(&self, state: &mut H) { - state.write_usize(self.val.value()); - state.write_u8(self.infinite as u8); - state.write_u8(self.inclusive as u8); - state.write_u8(self.lower as u8); - } -} From 1689bcfa6c1760068864db88f9b6f6604e0b9de2 Mon Sep 17 00:00:00 2001 From: "Eric B. Ridge" Date: Mon, 20 Feb 2023 14:34:40 -0500 Subject: [PATCH 3/8] fix merge conflict --- pgx/src/prelude.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgx/src/prelude.rs b/pgx/src/prelude.rs index 78cc0b51e..9fe45c6d6 100644 --- a/pgx/src/prelude.rs +++ b/pgx/src/prelude.rs @@ -19,8 +19,8 @@ pub use crate::pgbox::{AllocatedByPostgres, AllocatedByRust, PgBox, WhoAllocated // However, reexporting them seems fine for now. pub use crate::datum::{ - AnyNumeric, Array, Date, FromDatum, Interval IntoDatum, Numeric, PgVarlena, PostgresType, Range, - RangeSubType, Time, TimeWithTimeZone, Timestamp, TimestampWithTimeZone, VariadicArray, + AnyNumeric, Array, Date, FromDatum, Interval, IntoDatum, Numeric, PgVarlena, PostgresType, + Range, RangeSubType, Time, TimeWithTimeZone, Timestamp, TimestampWithTimeZone, VariadicArray, }; pub use crate::inoutfuncs::{InOutFuncs, JsonInOutFuncs, PgVarlenaInOutFuncs}; From 84f42f802614c3c19240c502bb0ff2a55248d3f7 Mon Sep 17 00:00:00 2001 From: "Eric B. Ridge" Date: Sun, 19 Feb 2023 15:26:52 -0500 Subject: [PATCH 4/8] fix docs --- pgx/src/datum/range.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgx/src/datum/range.rs b/pgx/src/datum/range.rs index 333c034fb..ad421e33c 100644 --- a/pgx/src/datum/range.rs +++ b/pgx/src/datum/range.rs @@ -16,7 +16,7 @@ use pgx_sql_entity_graph::metadata::{ }; use std::ops::{Deref, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive}; -/// A Postgres range bound can be one of these types +/// A Postgres Range's "lower" or "upper" value #[derive(Debug, Clone, Hash, Eq, PartialEq)] pub enum RangeBound { Infinite, From d55c26eaf1713d6cd880894fecf29f6a9abc1894 Mon Sep 17 00:00:00 2001 From: "Eric B. Ridge" Date: Sun, 19 Feb 2023 16:03:58 -0500 Subject: [PATCH 5/8] a few more public-facing functions, `impl DerefMut for Range`, and doc cleanup --- pgx/src/datum/range.rs | 70 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/pgx/src/datum/range.rs b/pgx/src/datum/range.rs index ad421e33c..50e97ec75 100644 --- a/pgx/src/datum/range.rs +++ b/pgx/src/datum/range.rs @@ -14,7 +14,7 @@ use crate::{ use pgx_sql_entity_graph::metadata::{ ArgumentError, Returns, ReturnsError, SqlMapping, SqlTranslatable, }; -use std::ops::{Deref, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive}; +use std::ops::{Deref, DerefMut, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive}; /// A Postgres Range's "lower" or "upper" value #[derive(Debug, Clone, Hash, Eq, PartialEq)] @@ -113,8 +113,21 @@ where /// A safe deconstruction of a Postgres `pg_sys::RangeType` struct. /// -/// Unlike Rust ranges, a Postgres range is capable of being "empty", and as such, expect the -/// various getter methods on [`Range`] to return `Option>`. +/// In spirit, Postgres ranges are not dissimilar from Rust ranges, however they are represented +/// quite differently. You'll use a [`RangeBound`] for the lower and upper bounds of a Postgres +/// [`Range`]. +/// +/// Unlike Rust, Postgres also has the concept of an "empty" range. Such ranges are constructed via +/// the [`Range::empty()`] function. As such, expect the various direct accessor methods on [`Range`] +/// to return `Option<&RangeBound>` or `Option<(RangeBound, RangeBound)>`. +/// +/// pgx provides [`From`] implementations for Rust's built-in range types for easy conversion into +/// a Postgres range. For example: +/// +/// ```rust,no_run +/// use pgx::Range; +/// let r: Range = (1..10).into(); +/// ``` #[derive(Debug, Clone, Hash, Eq, PartialEq)] pub struct Range { inner: Option<(RangeBound, RangeBound)>, @@ -124,7 +137,7 @@ impl Range where T: RangeSubType, { - /// Create a new [`Range`] with bounds. + /// Builds a new [`Range`] with bounds. /// /// # Examples /// @@ -193,6 +206,45 @@ where _ => false, } } + + /// Consumes `self` and returns the internal representation, which can be easily mapped or + /// unwrapped. + /// + /// A return value of [`Option::None`] indicates that this range represents the "empty" range. + #[inline] + pub fn into_inner(self) -> Option<(RangeBound, RangeBound)> { + self.inner + } + + /// Takes the bounds out of this [`Range`] and converts self to represent the "empty" range. + /// + /// A return value of [`Option::None`] indicates that this range already represents the "empty" range. + #[inline] + pub fn take(&mut self) -> Option<(RangeBound, RangeBound)> { + self.inner.take() + } + + /// Consumes `self` and returns the contained [`RangeBound`]s. + /// + /// # Panics + /// + /// This function panics if this [`Range`] represents an "empty" range. + #[inline] + #[track_caller] + pub fn unwrap(self) -> (RangeBound, RangeBound) { + self.inner.unwrap() + } + + /// Replace the bounds of this [`Range`], returning the old bounds. + /// + /// An [`Option::None`] will replace this with the "empty" range. + #[inline] + pub fn replace( + &mut self, + new: Option<(RangeBound, RangeBound)>, + ) -> Option<(RangeBound, RangeBound)> { + std::mem::replace(&mut self.inner, new) + } } impl Deref for Range @@ -207,6 +259,16 @@ where } } +impl DerefMut for Range +where + T: RangeSubType, +{ + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + impl FromDatum for Range where T: RangeSubType, From 61c27fccfaa12ac6eefed0223b4fe6dc873e5791 Mon Sep 17 00:00:00 2001 From: "Eric B. Ridge" Date: Mon, 20 Feb 2023 14:21:54 -0500 Subject: [PATCH 6/8] add a getter method to RangeBound --- pgx/src/datum/range.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pgx/src/datum/range.rs b/pgx/src/datum/range.rs index 50e97ec75..13571312b 100644 --- a/pgx/src/datum/range.rs +++ b/pgx/src/datum/range.rs @@ -28,6 +28,18 @@ impl RangeBound where T: RangeSubType, { + /// Returns a reference to this [`RangeBound`]'s underlying value. + /// + /// If this [`RangeBound`] is [`RangeBound::Infinite`], then `None` is returned, otherwise + /// `Some(&T)` + #[inline] + pub fn get(&self) -> Option<&T> { + match self { + RangeBound::Infinite => None, + RangeBound::Inclusive(v) | RangeBound::Exclusive(v) => Some(v), + } + } + /// Convert this pgx [`RangeBound`] into the equivalent Postgres [`pg_sys::RangeBound`]. /// /// Note that the `lower` property is always set to false as a [`RangeBound`] doesn't know the From 992245492427836fb6e23cf32a33d0afa15de7fc Mon Sep 17 00:00:00 2001 From: "Eric B. Ridge" Date: Mon, 20 Feb 2023 14:36:30 -0500 Subject: [PATCH 7/8] resolving conflicts --- pgx/src/prelude.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgx/src/prelude.rs b/pgx/src/prelude.rs index 9fe45c6d6..401137d70 100644 --- a/pgx/src/prelude.rs +++ b/pgx/src/prelude.rs @@ -20,7 +20,8 @@ pub use crate::pgbox::{AllocatedByPostgres, AllocatedByRust, PgBox, WhoAllocated pub use crate::datum::{ AnyNumeric, Array, Date, FromDatum, Interval, IntoDatum, Numeric, PgVarlena, PostgresType, - Range, RangeSubType, Time, TimeWithTimeZone, Timestamp, TimestampWithTimeZone, VariadicArray, + Range, RangeBound, RangeSubType, Time, TimeWithTimeZone, Timestamp, TimestampWithTimeZone, + VariadicArray, }; pub use crate::inoutfuncs::{InOutFuncs, JsonInOutFuncs, PgVarlenaInOutFuncs}; From c04bd8c09d6d11d64f34aa3e6381b0ab4b3d3344 Mon Sep 17 00:00:00 2001 From: "Eric B. Ridge" Date: Tue, 21 Feb 2023 11:32:56 -0500 Subject: [PATCH 8/8] - code review cleanup - add a few help methods to `RangeBound` - `impl Display for Range where T: Display` --- pgx-tests/src/tests/range_tests.rs | 3 +- pgx/src/datum/range.rs | 66 ++++++++++++++++++++---------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/pgx-tests/src/tests/range_tests.rs b/pgx-tests/src/tests/range_tests.rs index 0f8881335..595c49e51 100644 --- a/pgx-tests/src/tests/range_tests.rs +++ b/pgx-tests/src/tests/range_tests.rs @@ -8,7 +8,6 @@ Use of this source code is governed by the MIT license that can be found in the */ use pgx::prelude::*; -use pgx::RangeBound; #[pg_extern] fn accept_range_i32(range: Range) -> Range { @@ -53,7 +52,7 @@ where { match range.as_ref() { None => Range::empty(), - Some((l, u)) => Range::new(l, u), + Some((l, u)) => Range::new(l.clone(), u.clone()), } } diff --git a/pgx/src/datum/range.rs b/pgx/src/datum/range.rs index 13571312b..722595629 100644 --- a/pgx/src/datum/range.rs +++ b/pgx/src/datum/range.rs @@ -11,6 +11,7 @@ Use of this source code is governed by the MIT license that can be found in the use crate::{ pg_sys, AnyNumeric, Date, FromDatum, IntoDatum, Numeric, Timestamp, TimestampWithTimeZone, }; +use core::fmt::{Display, Formatter}; use pgx_sql_entity_graph::metadata::{ ArgumentError, Returns, ReturnsError, SqlMapping, SqlTranslatable, }; @@ -40,11 +41,29 @@ where } } + /// Returns true if this [`RangeBound`] represents the `Infinite` variant + #[inline] + pub fn is_infinite(&self) -> bool { + matches!(self, RangeBound::Infinite) + } + + /// Returns true if this [`RangeBound`] represents the `Inclusive` variant + #[inline] + pub fn is_inclusive(&self) -> bool { + matches!(self, RangeBound::Inclusive(_)) + } + + /// Returns true if this [`RangeBound`] represents the `Exclusive` variant + #[inline] + pub fn is_exclusive(&self) -> bool { + matches!(self, RangeBound::Exclusive(_)) + } + /// Convert this pgx [`RangeBound`] into the equivalent Postgres [`pg_sys::RangeBound`]. /// /// Note that the `lower` property is always set to false as a [`RangeBound`] doesn't know the /// end on which it's placed. - pub fn into_pg(self) -> pg_sys::RangeBound { + fn into_pg(self) -> pg_sys::RangeBound { match self { RangeBound::Infinite => pg_sys::RangeBound { val: pg_sys::Datum::from(0), @@ -86,16 +105,6 @@ where } } -impl From<&RangeBound> for RangeBound -where - T: RangeSubType, -{ - #[inline] - fn from(value: &RangeBound) -> Self { - Clone::clone(value) - } -} - impl From for RangeBound where T: RangeSubType, @@ -145,6 +154,30 @@ pub struct Range { inner: Option<(RangeBound, RangeBound)>, } +impl Display for Range +where + T: RangeSubType + Display, +{ + /// Follows Postgres' format for displaying ranges + #[rustfmt::skip] + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self.as_ref() { + None => write!(f, "empty"), + Some((RangeBound::Infinite, RangeBound::Infinite)) => write!(f, "(,)"), + Some((RangeBound::Infinite, RangeBound::Inclusive(v))) => write!(f, "(,{}]", v), + Some((RangeBound::Infinite, RangeBound::Exclusive(v))) => write!(f, "(,{})", v), + + Some((RangeBound::Inclusive(v), RangeBound::Infinite)) => write!(f, "[{},)", v), + Some((RangeBound::Inclusive(l), RangeBound::Inclusive(u))) => write!(f, "[{},{}]", l, u), + Some((RangeBound::Inclusive(l), RangeBound::Exclusive(u))) => write!(f, "[{},{})", l, u), + + Some((RangeBound::Exclusive(v), RangeBound::Infinite)) => write!(f, "({},)", v), + Some((RangeBound::Exclusive(l), RangeBound::Inclusive(u))) => write!(f, "({},{}]", l, u), + Some((RangeBound::Exclusive(l), RangeBound::Exclusive(u))) => write!(f, "({},{})", l, u), + } + } +} + impl Range where T: RangeSubType, @@ -236,17 +269,6 @@ where self.inner.take() } - /// Consumes `self` and returns the contained [`RangeBound`]s. - /// - /// # Panics - /// - /// This function panics if this [`Range`] represents an "empty" range. - #[inline] - #[track_caller] - pub fn unwrap(self) -> (RangeBound, RangeBound) { - self.inner.unwrap() - } - /// Replace the bounds of this [`Range`], returning the old bounds. /// /// An [`Option::None`] will replace this with the "empty" range.