Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mapping PG interval type #629

Merged
merged 30 commits into from
Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bd22def
Mapping interval type
mhov Aug 15, 2022
da6a452
boundary check improvements
mhov Aug 15, 2022
c3c5913
Renaming PgInterval to Interval
mhov Aug 15, 2022
bcb341a
trait based conversions for Interval
mhov Aug 16, 2022
9daa2eb
better duration->interval range checks
mhov Aug 24, 2022
73143c6
guarded try_from_months_days_usecs
mhov Aug 24, 2022
11f3c97
restrict days integer size for method
mhov Aug 26, 2022
49811e3
documentation improvements
mhov Aug 30, 2022
da2f46b
adjustments to interval unit rollover rules
mhov Sep 1, 2022
6e17897
fix for SqlMapping update
mhov Sep 21, 2022
bc2a7fc
post rebaseline fixes
mhov Oct 2, 2022
0488f32
rebase over develop and new cargo fmt rules
mhov Oct 24, 2022
d81612f
Merge branch 'develop' into interval-type
mhov Oct 28, 2022
c3d988e
Merge branch 'develop' into interval-type
mhov Nov 3, 2022
9437a68
PgBox allocation, datum null check
mhov Nov 3, 2022
d6a7ddb
suggested Option fix
mhov Nov 3, 2022
7072880
Merge branch 'develop' into interval-type
workingjubilee Nov 17, 2022
657ea58
Merge branch 'develop' into interval-type
mhov Nov 27, 2022
965ccd1
Merge branch 'develop' into interval-type
workingjubilee Nov 29, 2022
bad4a6c
cfg out time-crate features
workingjubilee Nov 29, 2022
d8df430
also fmt
workingjubilee Nov 29, 2022
ba60b28
Merge branch 'develop' into interval-type
workingjubilee Dec 9, 2022
bc3153d
fmt again
workingjubilee Dec 9, 2022
5d84f21
Use new pgx-sql-entity-graph crate
workingjubilee Dec 9, 2022
f9bf2c3
convert Interval type to pointer wrapper
mhov Dec 22, 2022
8cc6e2f
Merge branch 'develop' into interval-type
mhov Jan 3, 2023
f1900a0
post develop merge fix
mhov Jan 3, 2023
275f612
Merge branch 'develop' into interval-type
mhov Jan 27, 2023
aea95e7
post develop merge fix
mhov Jan 28, 2023
fb5ba93
usec to micros
mhov Feb 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions pgx-tests/src/tests/datetime_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ fn timestamptz_to_i64(tstz: pg_sys::TimestampTz) -> i64 {
tstz
}

#[pg_extern]
fn accept_interval(interval: Interval) -> Interval {
interval
}

#[pg_extern]
fn accept_interval_round_trip(interval: Interval) -> Interval {
let duration: time::Duration = interval.into();
duration.try_into().expect("Error converting Duration to PgInterval")
}

#[cfg(any(test, feature = "pg_test"))]
#[pgx::pg_schema]
mod tests {
Expand Down Expand Up @@ -442,4 +453,63 @@ mod tests {
assert!(ts.is_neg_infinity());
Ok(())
}

#[pg_test]
fn test_accept_interval_random() {
let result = Spi::get_one::<bool>("SELECT accept_interval(interval'1 year 2 months 3 days 4 hours 5 minutes 6 seconds') = interval'1 year 2 months 3 days 4 hours 5 minutes 6 seconds';")
.expect("failed to get SPI result");
assert_eq!(result, Some(true));
}

#[pg_test]
fn test_accept_interval_neg_random() {
let result = Spi::get_one::<bool>("SELECT accept_interval(interval'1 year 2 months 3 days 4 hours 5 minutes 6 seconds ago') = interval'1 year 2 months 3 days 4 hours 5 minutes 6 seconds ago';")
.expect("failed to get SPI result");
assert_eq!(result, Some(true));
}

#[pg_test]
fn test_accept_interval_round_trip_random() {
let result = Spi::get_one::<bool>("SELECT accept_interval_round_trip(interval'1 year 2 months 3 days 4 hours 5 minutes 6 seconds') = interval'1 year 2 months 3 days 4 hours 5 minutes 6 seconds';")
.expect("failed to get SPI result");
assert_eq!(result, Some(true));
}

#[pg_test]
fn test_accept_interval_round_trip_neg_random() {
let result = Spi::get_one::<bool>("SELECT accept_interval_round_trip(interval'1 year 2 months 3 days 4 hours 5 minutes 6 seconds ago') = interval'1 year 2 months 3 days 4 hours 5 minutes 6 seconds ago';")
.expect("failed to get SPI result");
assert_eq!(result, Some(true));
}

#[pg_test]
fn test_interval_serialization() {
let interval = Interval::try_from_months_days_usecs(3, 4, 5_000_000).unwrap();
let json = json!({ "interval test": interval });

assert_eq!(json!({"interval test":"3 mons 4 days 00:00:05"}), json);
}

#[pg_test]
fn test_duration_to_interval_err() {
use pgx::IntervalConversionError;
// normal limit of i32::MAX months
let duration = time::Duration::days(pg_sys::DAYS_PER_MONTH as i64 * i32::MAX as i64);

let result = TryInto::<Interval>::try_into(duration);
match result {
Ok(_) => (),
_ => panic!("failed duration -> interval conversion"),
};

// one month too many, expect error
let duration =
time::Duration::days(pg_sys::DAYS_PER_MONTH as i64 * (i32::MAX as i64 + 1i64));

let result = TryInto::<Interval>::try_into(duration);
match result {
Err(IntervalConversionError::DurationMonthsOutOfBounds) => (),
_ => panic!("invalid duration -> interval conversion succeeded"),
};
}
}
189 changes: 189 additions & 0 deletions pgx/src/datum/interval.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
Portions Copyright 2019-2021 ZomboDB, LLC.
Portions Copyright 2021-2022 Technology Concepts & Design, Inc. <[email protected]>

All rights reserved.

Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

use std::ops::{Mul, Sub};
use std::ptr::NonNull;

use crate::datum::time::USECS_PER_SEC;
use crate::{direct_function_call, pg_sys, FromDatum, IntoDatum, PgBox};
use pg_sys::{DAYS_PER_MONTH, SECS_PER_DAY};
use pgx_sql_entity_graph::metadata::{
ArgumentError, Returns, ReturnsError, SqlMapping, SqlTranslatable,
};

#[cfg(feature = "time-crate")]
const MONTH_DURATION: time::Duration = time::Duration::days(DAYS_PER_MONTH as i64);

/// From the PG docs https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-INTERVAL-INPUT
/// Internally interval values are stored as months, days, and microseconds. This is done because the number of days in a month varies,
/// and a day can have 23 or 25hours if a daylight savings time adjustment is involved. The months and days fields are integers while
/// the microseconds field can store fractional seconds. Because intervals are usually created from constant strings or timestamp
/// subtraction, this storage method works well in most cases...
#[derive(Debug)]
#[repr(transparent)]
pub struct Interval(NonNull<pg_sys::Interval>);

impl Interval {
/// This function takes `months`/`days`/`usecs` as input to convert directly to the internal PG storage struct `pg_sys::Interval`
/// - the sign of all units must be all matching in the positive or all matching in the negative direction
pub fn try_from_months_days_usecs(
mhov marked this conversation as resolved.
Show resolved Hide resolved
months: i32,
days: i32,
usecs: i64,
workingjubilee marked this conversation as resolved.
Show resolved Hide resolved
) -> Result<Self, IntervalConversionError> {
// SAFETY: `pg_sys::Interval` will be uninitialized, set all fields
let mut interval = unsafe { PgBox::<pg_sys::Interval>::alloc() };
interval.day = days;
interval.month = months;
interval.time = usecs;
mhov marked this conversation as resolved.
Show resolved Hide resolved
let ptr = interval.into_pg();
let non_null = NonNull::new(ptr).expect("pointer is null");
Ok(Interval::from_ptr(non_null))
}

pub fn from_ptr(ptr: NonNull<pg_sys::Interval>) -> Self {
Interval(ptr)
}

pub fn as_ptr(&self) -> NonNull<pg_sys::Interval> {
self.0
}

/// Total number of months before/after 2000-01-01
pub fn months(&self) -> i32 {
// SAFETY: Validity asserted on construction
unsafe { (*self.0.as_ptr()).month }
}

/// Total number of days before/after the `months()` offset (sign must match `months`)
pub fn days(&self) -> i32 {
// SAFETY: Validity asserted on construction
unsafe { (*self.0.as_ptr()).day }
}

/// Total number of usecs before/after the `days()` offset (sign must match `months`/`days`)
pub fn usecs(&self) -> i64 {
mhov marked this conversation as resolved.
Show resolved Hide resolved
// SAFETY: Validity asserted on construction
unsafe { (*self.0.as_ptr()).time }
}
}

impl FromDatum for Interval {
unsafe fn from_polymorphic_datum(
datum: pg_sys::Datum,
is_null: bool,
_typoid: pg_sys::Oid,
) -> Option<Self>
where
Self: Sized,
{
if is_null {
None
} else {
let ptr = datum.cast_mut_ptr::<pg_sys::Interval>();
let non_null = NonNull::new(ptr).expect("ptr was null");
Some(Interval(non_null))
}
}
}

impl IntoDatum for Interval {
fn into_datum(self) -> Option<pg_sys::Datum> {
let interval = self.0.as_ptr();
Some(interval.into())
}
fn type_oid() -> pg_sys::Oid {
pg_sys::INTERVALOID
}
}

#[cfg(feature = "time-crate")]
impl TryFrom<time::Duration> for Interval {
type Error = IntervalConversionError;
fn try_from(duration: time::Duration) -> Result<Interval, Self::Error> {
let total_months = duration.whole_days() / (pg_sys::DAYS_PER_MONTH as i64);

if total_months >= (i32::MIN as i64) && total_months <= (i32::MAX as i64) {
let mut month = 0;
let mut d = duration;

if time::Duration::abs(d) >= MONTH_DURATION {
month = total_months as i32;
d = d.sub(MONTH_DURATION.mul(month));
}

let time = d.whole_microseconds() as i64;

Interval::try_from_months_days_usecs(month, 0, time)
} else {
Err(IntervalConversionError::DurationMonthsOutOfBounds)
}
}
}

#[cfg(feature = "time-crate")]
impl From<Interval> for time::Duration {
fn from(interval: Interval) -> time::Duration {
// SAFETY: Validity of interval's ptr asserted on construction
unsafe {
let interval = *interval.0.as_ptr(); // internal interval
let sec = interval.time / USECS_PER_SEC as i64;
let fsec = ((interval.time - (sec * USECS_PER_SEC as i64)) * 1000) as i32; // convert usec to nsec

let mut duration = time::Duration::new(sec, fsec);

if interval.month != 0 {
duration = duration.saturating_add(MONTH_DURATION.mul(interval.month));
}

if interval.day != 0 {
duration = duration.saturating_add(time::Duration::new(
(interval.day * SECS_PER_DAY as i32) as i64,
0,
));
}

duration
}
}
}

impl serde::Serialize for Interval {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
unsafe {
// SAFETY: Validity of ptr asserted on construction
let interval = self.0.as_ptr();
let cstr = direct_function_call::<&std::ffi::CStr>(
pg_sys::interval_out,
vec![Some(pg_sys::Datum::from(interval as *const _))],
)
.expect("failed to convert interval to a cstring");

serializer.serialize_str(cstr.to_str().unwrap())
}
}
}

unsafe impl SqlTranslatable for Interval {
fn argument_sql() -> Result<SqlMapping, ArgumentError> {
Ok(SqlMapping::literal("interval"))
}
fn return_sql() -> Result<Returns, ReturnsError> {
Ok(Returns::One(SqlMapping::literal("interval")))
}
}

#[derive(thiserror::Error, Debug, Clone, Copy)]
pub enum IntervalConversionError {
#[error("duration's total month count outside of valid i32::MIN..=i32::MAX range")]
DurationMonthsOutOfBounds,
}
2 changes: 2 additions & 0 deletions pgx/src/datum/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod from;
mod geo;
mod inet;
mod internal;
mod interval;
mod into;
mod item_pointer_data;
mod json;
Expand All @@ -42,6 +43,7 @@ pub use from::*;
pub use geo::*;
pub use inet::*;
pub use internal::*;
pub use interval::*;
pub use into::*;
pub use item_pointer_data::*;
pub use json::*;
Expand Down
5 changes: 3 additions & 2 deletions pgx/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ pub use crate::pgbox::{AllocatedByPostgres, AllocatedByRust, PgBox, WhoAllocated

// These could be factored into a temporal type module that could be easily imported for code which works with them.
// However, reexporting them seems fine for now.

pub use crate::datum::{
AnyNumeric, Array, Date, FromDatum, IntoDatum, Numeric, PgVarlena, PostgresType, Range,
RangeData, RangeSubType, Time, TimeWithTimeZone, Timestamp, TimestampWithTimeZone,
AnyNumeric, Array, Date, FromDatum, Interval, IntoDatum, Numeric, PgVarlena, PostgresType,
Range, RangeData, RangeSubType, Time, TimeWithTimeZone, Timestamp, TimestampWithTimeZone,
VariadicArray,
};
pub use crate::inoutfuncs::{InOutFuncs, JsonInOutFuncs, PgVarlenaInOutFuncs};
Expand Down