Skip to content

Commit 648bbb3

Browse files
committed
feat: Allow decoding PostgreSQL interval as string
This commit allows decoding PostgreSQL `interval` values as strings. Previously, there was no way to decode an interval without knowing in advance which representation (textual or binary) it had.
1 parent 6a338bb commit 648bbb3

File tree

4 files changed

+168
-2
lines changed

4 files changed

+168
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## unpublished
99
- MySqlValueRef::format/as_bytes/as_str/as_bytes() are now public (#27)
10+
- allow decoding postgres `interval` values as strings. Previously, there was no way to decode an interval without knowing in advance which representation (textual or binary) it had.
1011

1112
## 0.6.44
1213
- Add support for mssql MONEY and SMALLMONEY types.

sqlx-core/src/postgres/types/interval.rs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ pub struct PgInterval {
1919
pub microseconds: i64,
2020
}
2121

22+
/// Decode an interval value as a string representation
23+
pub fn decode_as_string(value: PgValueRef<'_>) -> Result<String, BoxDynError> {
24+
match value.format() {
25+
PgValueFormat::Binary => {
26+
let interval = PgInterval::decode(value)?;
27+
Ok(interval.to_string())
28+
}
29+
30+
PgValueFormat::Text => {
31+
// For text format, we can just return the string as-is
32+
Ok(value.as_str()?.to_owned())
33+
}
34+
}
35+
}
36+
2237
impl Type<Postgres> for PgInterval {
2338
fn type_info() -> PgTypeInfo {
2439
PgTypeInfo::INTERVAL
@@ -69,6 +84,67 @@ impl Encode<'_, Postgres> for PgInterval {
6984
}
7085
}
7186

87+
impl std::fmt::Display for PgInterval {
88+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89+
let mut parts = Vec::new();
90+
91+
if self.months != 0 {
92+
let years = self.months / 12;
93+
let months = self.months % 12;
94+
95+
if years != 0 {
96+
parts.push(format!(
97+
"{} year{}",
98+
years,
99+
if years != 1 { "s" } else { "" }
100+
));
101+
}
102+
if months != 0 {
103+
parts.push(format!(
104+
"{} mon{}",
105+
months,
106+
if months != 1 { "s" } else { "" }
107+
));
108+
}
109+
}
110+
111+
if self.days != 0 {
112+
parts.push(format!(
113+
"{} day{}",
114+
self.days,
115+
if self.days != 1 { "s" } else { "" }
116+
));
117+
}
118+
119+
let total_seconds = self.microseconds / 1_000_000;
120+
let microseconds = self.microseconds % 1_000_000;
121+
let hours = total_seconds / 3600;
122+
let minutes = (total_seconds % 3600) / 60;
123+
let seconds = total_seconds % 60;
124+
125+
if hours != 0 || minutes != 0 || seconds != 0 || microseconds != 0 {
126+
let time_part = if microseconds != 0 {
127+
format!(
128+
"{:02}:{:02}:{:02}.{:06}",
129+
hours,
130+
minutes,
131+
seconds,
132+
microseconds.abs()
133+
)
134+
} else {
135+
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
136+
};
137+
parts.push(time_part);
138+
}
139+
140+
if parts.is_empty() {
141+
write!(f, "00:00:00")
142+
} else {
143+
write!(f, "{}", parts.join(" "))
144+
}
145+
}
146+
}
147+
72148
// We then implement Encode + Type for std Duration, chrono Duration, and time Duration
73149
// This is to enable ease-of-use for encoding when its simple
74150

@@ -392,3 +468,81 @@ fn test_pginterval_time() {
392468
assert!(PgInterval::try_from(time::Duration::seconds(10_000_000_000_000)).is_err());
393469
assert!(PgInterval::try_from(time::Duration::seconds(-10_000_000_000_000)).is_err());
394470
}
471+
472+
#[test]
473+
fn test_pginterval_display() {
474+
// Zero interval
475+
let interval = PgInterval {
476+
months: 0,
477+
days: 0,
478+
microseconds: 0,
479+
};
480+
assert_eq!(interval.to_string(), "00:00:00");
481+
482+
// Time only
483+
let interval = PgInterval {
484+
months: 0,
485+
days: 0,
486+
microseconds: 3_600_000_000, // 1 hour
487+
};
488+
assert_eq!(interval.to_string(), "01:00:00");
489+
490+
// Time with microseconds
491+
let interval = PgInterval {
492+
months: 0,
493+
days: 0,
494+
microseconds: 3_660_000_000, // 1 hour 1 minute
495+
};
496+
assert_eq!(interval.to_string(), "01:01:00");
497+
498+
// Time with microseconds
499+
let interval = PgInterval {
500+
months: 0,
501+
days: 0,
502+
microseconds: 3_660_000_000 + 30_000, // 1 hour 1 minute 30 microseconds
503+
};
504+
assert_eq!(interval.to_string(), "01:01:00.000030");
505+
506+
// Days only
507+
let interval = PgInterval {
508+
months: 0,
509+
days: 27,
510+
microseconds: 0,
511+
};
512+
assert_eq!(interval.to_string(), "27 days");
513+
514+
// Months only
515+
let interval = PgInterval {
516+
months: 11,
517+
days: 0,
518+
microseconds: 0,
519+
};
520+
assert_eq!(interval.to_string(), "11 mons");
521+
522+
// Years and months
523+
let interval = PgInterval {
524+
months: 14, // 1 year 2 months
525+
days: 0,
526+
microseconds: 0,
527+
};
528+
assert_eq!(interval.to_string(), "1 year 2 mons");
529+
530+
// Complex interval
531+
let interval = PgInterval {
532+
months: 14, // 1 year 2 months
533+
days: 27,
534+
microseconds: 43_200_000_000 + 180_000_000 + 30_000, // 12 hours 3 minutes 30 microseconds
535+
};
536+
assert_eq!(
537+
interval.to_string(),
538+
"1 year 2 mons 27 days 12:03:00.000030"
539+
);
540+
541+
// Negative microseconds
542+
let interval = PgInterval {
543+
months: 0,
544+
days: 0,
545+
microseconds: -1_000_000, // -1 second
546+
};
547+
assert_eq!(interval.to_string(), "00:00:01.000000");
548+
}

sqlx-core/src/postgres/types/str.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::decode::Decode;
22
use crate::encode::{Encode, IsNull};
33
use crate::error::BoxDynError;
4+
use crate::postgres::type_info::PgType;
45
use crate::postgres::types::array_compatible;
56
use crate::postgres::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres};
67
use crate::types::Type;
@@ -17,6 +18,8 @@ impl Type<Postgres> for str {
1718
PgTypeInfo::NAME,
1819
PgTypeInfo::BPCHAR,
1920
PgTypeInfo::VARCHAR,
21+
PgTypeInfo::INTERVAL,
22+
PgTypeInfo::MONEY,
2023
PgTypeInfo::UNKNOWN,
2124
]
2225
.contains(ty)
@@ -98,7 +101,7 @@ impl Encode<'_, Postgres> for String {
98101

99102
impl<'r> Decode<'r, Postgres> for &'r str {
100103
fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
101-
value.as_str()
104+
Ok(value.as_str()?)
102105
}
103106
}
104107

@@ -110,6 +113,9 @@ impl<'r> Decode<'r, Postgres> for Cow<'r, str> {
110113

111114
impl Decode<'_, Postgres> for String {
112115
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
113-
Ok(value.as_str()?.to_owned())
116+
match *value.type_info {
117+
PgType::Interval => super::interval::decode_as_string(value),
118+
_ => Ok(value.as_str()?.to_owned()),
119+
}
114120
}
115121
}

tests/postgres/types.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,11 @@ test_prepared_type!(interval<PgInterval>(
553553
},
554554
));
555555

556+
test_decode_type!(interval_string<String>(Postgres,
557+
"'00:00:00'::INTERVAL" == "00:00:00",
558+
"'00:01:00'::INTERVAL * 60" == "01:00:00"
559+
));
560+
556561
test_prepared_type!(money<PgMoney>(Postgres, "123.45::money" == PgMoney(12345)));
557562

558563
test_prepared_type!(money_vec<Vec<PgMoney>>(Postgres,

0 commit comments

Comments
 (0)