Skip to content

Commit 1868abf

Browse files
authored
feat: timestamp_tz offset support seconds (#98)
* feat: timestamp_tz offset support seconds * Switch from ryu -> zmij for float formatting
1 parent 356d2bc commit 1868abf

9 files changed

Lines changed: 126 additions & 69 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ nom = "8.0.0"
3535
num-traits = "0.2.19"
3636
ordered-float = { version = "5.1.0", default-features = false }
3737
rand = { version = "0.9.2", features = ["small_rng"] }
38-
ryu = "1.0"
3938
serde = "1.0"
4039
serde_json = { version = "1.0", default-features = false, features = ["std"] }
40+
zmij = "1.0"
4141

4242
[dev-dependencies]
4343
goldenfile = "1.8"

src/core/databend/util.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ impl ExtensionValue<'_> {
604604
writer.write_all(&[EXTENSION_TIMESTAMP_TZ])?;
605605
writer.write_all(&v.value.to_be_bytes())?;
606606
writer.write_all(&v.offset.to_be_bytes())?;
607-
Ok(10)
607+
Ok(13)
608608
}
609609
ExtensionValue::Interval(v) => {
610610
writer.write_all(&[EXTENSION_INTERVAL])?;
@@ -645,11 +645,16 @@ impl ExtensionValue<'_> {
645645
ExtensionValue::Timestamp(Timestamp { value })
646646
}
647647
EXTENSION_TIMESTAMP_TZ => {
648-
if len != 9 {
648+
if len != 9 && len != 12 {
649649
return Err(Error::InvalidJsonbNumber);
650650
}
651651
let value = i64::from_be_bytes(bytes[1..9].try_into().unwrap());
652-
let offset = i8::from_be_bytes(bytes[9..10].try_into().unwrap());
652+
let offset = if len == 9 {
653+
let hours = i8::from_be_bytes(bytes[9..10].try_into().unwrap());
654+
(hours as i32) * 3_600
655+
} else {
656+
i32::from_be_bytes(bytes[9..13].try_into().unwrap())
657+
};
653658

654659
ExtensionValue::TimestampTz(TimestampTz { offset, value })
655660
}

src/extension.rs

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const MICROS_PER_HOUR: i64 = 60 * MICROS_PER_MINUTE;
2828
const MONTHS_PER_YEAR: i32 = 12;
2929

3030
const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.6f";
31+
const TIMESTAMP_TIMEZONE_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.6f %z";
3132

3233
/// Represents extended JSON value types that are not supported in standard JSON.
3334
///
@@ -80,8 +81,8 @@ pub struct Timestamp {
8081
/// Standard JSON has no native timezone-aware timestamp type.
8182
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
8283
pub struct TimestampTz {
83-
/// Timezone offset in hours from UTC
84-
pub offset: i8,
84+
/// Timezone offset in seconds from UTC
85+
pub offset: i32,
8586
/// Microseconds since Unix epoch (January 1, 1970 00:00:00 UTC)
8687
pub value: i64,
8788
}
@@ -149,58 +150,49 @@ impl Display for TimestampTz {
149150
nanos = 0;
150151
}
151152
let ts = jiff::Timestamp::new(secs, nanos as i32).unwrap();
152-
let tz = Offset::constant(self.offset).to_time_zone();
153+
let tz_offset = Offset::from_seconds(self.offset).expect("invalid timezone offset seconds");
154+
let tz = tz_offset.to_time_zone();
153155
let zoned = ts.to_zoned(tz);
154156

155-
write!(f, "{}", strtime::format(TIMESTAMP_FORMAT, &zoned).unwrap())
157+
write!(
158+
f,
159+
"{}",
160+
strtime::format(TIMESTAMP_TIMEZONE_FORMAT, &zoned).unwrap()
161+
)
156162
}
157163
}
158164

159165
impl Display for Interval {
160166
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
161-
let mut date_parts = vec![];
167+
let mut wrote_date_part = false;
162168
let years = self.months / MONTHS_PER_YEAR;
163169
let months = self.months % MONTHS_PER_YEAR;
164-
match years.cmp(&1) {
165-
Ordering::Equal => {
166-
date_parts.push((years, "year"));
167-
}
168-
Ordering::Greater => {
169-
date_parts.push((years, "years"));
170-
}
171-
_ => {}
172-
}
173-
match months.cmp(&1) {
174-
Ordering::Equal => {
175-
date_parts.push((months, "month"));
176-
}
177-
Ordering::Greater => {
178-
date_parts.push((months, "months"));
179-
}
180-
_ => {}
181-
}
182-
match self.days.cmp(&1) {
183-
Ordering::Equal => {
184-
date_parts.push((self.days, "day"));
185-
}
186-
Ordering::Greater => {
187-
date_parts.push((self.days, "days"));
188-
}
189-
_ => {}
190-
}
191-
if !date_parts.is_empty() {
192-
for (i, (val, name)) in date_parts.into_iter().enumerate() {
193-
if i > 0 {
170+
171+
let mut write_component = |value: i32, singular: &str, plural: &str| -> std::fmt::Result {
172+
if value != 0 {
173+
if wrote_date_part {
194174
write!(f, " ")?;
195175
}
196-
write!(f, "{} {}", val, name)?;
197-
}
198-
if self.micros != 0 {
199-
write!(f, " ")?;
176+
let abs_val = value.abs();
177+
let unit = if abs_val == 1 { singular } else { plural };
178+
if value < 0 {
179+
write!(f, "-{} {}", abs_val, unit)?;
180+
} else {
181+
write!(f, "{} {}", abs_val, unit)?;
182+
}
183+
wrote_date_part = true;
200184
}
201-
}
185+
Ok(())
186+
};
187+
188+
write_component(years, "year", "years")?;
189+
write_component(months, "month", "months")?;
190+
write_component(self.days, "day", "days")?;
202191

203192
if self.micros != 0 {
193+
if wrote_date_part {
194+
write!(f, " ")?;
195+
}
204196
let mut micros = self.micros;
205197
if micros < 0 {
206198
write!(f, "-")?;
@@ -221,7 +213,7 @@ impl Display for Interval {
221213
if micros != 0 {
222214
write!(f, ".{:06}", micros)?;
223215
}
224-
} else if self.months == 0 && self.days == 0 {
216+
} else if !wrote_date_part {
225217
write!(f, "00:00:00")?;
226218
}
227219
Ok(())

src/functions/scalar.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,7 +1783,7 @@ impl RawJsonb<'_> {
17831783
/// use jsonb::Value;
17841784
///
17851785
/// // TimestampTz value
1786-
/// let timestamp_tz_value = Value::TimestampTz(TimestampTz { offset: 8, value: 1760140800000000 });
1786+
/// let timestamp_tz_value = Value::TimestampTz(TimestampTz { offset: 8 * 3600, value: 1760140800000000 });
17871787
/// let buf = timestamp_tz_value.to_vec();
17881788
/// let raw_jsonb = RawJsonb::new(&buf);
17891789
/// assert!(raw_jsonb.is_timestamp_tz().unwrap());
@@ -1830,10 +1830,10 @@ impl RawJsonb<'_> {
18301830
/// use jsonb::Value;
18311831
///
18321832
/// // TimestampTz value
1833-
/// let timestamp_tz_value = Value::TimestampTz(TimestampTz { offset: 8, value: 1760140800000000 });
1833+
/// let timestamp_tz_value = Value::TimestampTz(TimestampTz { offset: 8 * 3600, value: 1760140800000000 });
18341834
/// let buf = timestamp_tz_value.to_vec();
18351835
/// let raw_jsonb = RawJsonb::new(&buf);
1836-
/// assert_eq!(raw_jsonb.as_timestamp_tz().unwrap(), Some(TimestampTz { offset: 8, value: 1760140800000000 }));
1836+
/// assert_eq!(raw_jsonb.as_timestamp_tz().unwrap(), Some(TimestampTz { offset: 8 * 3600, value: 1760140800000000 }));
18371837
/// ```
18381838
pub fn as_timestamp_tz(&self) -> Result<Option<TimestampTz>> {
18391839
let jsonb_item = JsonbItem::from_raw_jsonb(*self)?;

src/number.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1174,7 +1174,7 @@ impl Display for Number {
11741174
write!(f, "{}", s)
11751175
}
11761176
Number::Float64(v) => {
1177-
let mut buffer = ryu::Buffer::new();
1177+
let mut buffer = zmij::Buffer::new();
11781178
let s = buffer.format(*v);
11791179
write!(f, "{}", s)
11801180
}

tests/it/decode.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,18 @@ fn test_decode_extension() {
245245
value: 1760140800000000,
246246
}),
247247
),
248+
// Backward-compatible implementation with offset as int8 hours
248249
(
249250
b"\x20\0\0\0\x60\0\0\x0a\x30\0\x06\x40\xd6\xb7\x23\x80\0\x08".to_vec(),
250251
Value::TimestampTz(TimestampTz {
251-
offset: 8,
252+
offset: 8 * 3600,
253+
value: 1760140800000000,
254+
}),
255+
),
256+
(
257+
b"\x20\0\0\0\x60\0\0\x0d\x30\0\x06\x40\xd6\xb7\x23\x80\0\0\0\x70\x80".to_vec(),
258+
Value::TimestampTz(TimestampTz {
259+
offset: 8 * 3600,
252260
value: 1760140800000000,
253261
}),
254262
),

tests/it/encode.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,11 @@ fn test_encode_array() {
177177
Value::Binary(&[100, 101, 102, 103]),
178178
Value::Date(Date {value: 20381 }),
179179
Value::Timestamp(Timestamp { value: 1540230120000000 }),
180-
Value::TimestampTz(TimestampTz { offset: 8, value: 1670389100000000 }),
180+
Value::TimestampTz(TimestampTz { offset: 8 * 3600, value: 1670389100000000 }),
181181
Value::Interval(Interval { months: 2, days: 10, micros: 500000000 }),
182182
Value::Number(Number::Decimal256(Decimal256 { scale: 2, value: I256::new(1234) })),
183183
]).to_vec(),
184-
b"\x80\0\0\x07\x30\0\0\0\x60\0\0\x05\x60\0\0\x05\x60\0\0\x09\x60\0\0\x0A\x60\0\0\x11\x20\0\0\x22\0\x64\x65\x66\x67\x10\0\0\x4F\x9D\x20\0\x05\x78\xD4\xC5\x2C\xCA\0\x30\0\x05\xEF\x35\xC4\xF1\x33\0\x08\x40\0\0\0\x02\0\0\0\x0A\0\0\0\0\x1D\xCD\x65\0\x70\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x04\xD2\x02",
184+
b"\x80\0\0\x07\x30\0\0\0\x60\0\0\x05\x60\0\0\x05\x60\0\0\x09\x60\0\0\x0D\x60\0\0\x11\x20\0\0\x22\0\x64\x65\x66\x67\x10\0\0\x4F\x9D\x20\0\x05\x78\xD4\xC5\x2C\xCA\0\x30\0\x05\xEF\x35\xC4\xF1\x33\0\0\0\x70\x80\x40\0\0\0\x02\0\0\0\x0A\0\0\0\0\x1D\xCD\x65\0\x70\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x04\xD2\x02",
185185
);
186186
}
187187

@@ -207,7 +207,7 @@ fn test_encode_object() {
207207
obj2.insert(
208208
"k5".to_string(),
209209
Value::TimestampTz(TimestampTz {
210-
offset: 8,
210+
offset: 8 * 3600,
211211
value: 1670389100000000,
212212
}),
213213
);
@@ -229,7 +229,7 @@ fn test_encode_object() {
229229

230230
assert_eq!(
231231
&Value::Object(obj2).to_vec(),
232-
b"\x40\0\0\x07\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x60\0\0\x05\x60\0\0\x05\x60\0\0\x09\x60\0\0\x0A\x60\0\0\x11\x20\0\0\x22\x6B\x31\x6B\x32\x6B\x33\x6B\x34\x6B\x35\x6B\x36\x6B\x37\x76\x31\0\xC8\xC9\xCA\xCB\x10\0\0\x4F\x9D\x20\0\x05\x78\xD4\xC5\x2C\xCA\0\x30\0\x05\xEF\x35\xC4\xF1\x33\0\x08\x40\0\0\0\x02\0\0\0\x0A\0\0\0\0\x1D\xCD\x65\0\x70\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x04\xD2\x02"
232+
b"\x40\0\0\x07\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x10\0\0\x02\x60\0\0\x05\x60\0\0\x05\x60\0\0\x09\x60\0\0\x0D\x60\0\0\x11\x20\0\0\x22\x6B\x31\x6B\x32\x6B\x33\x6B\x34\x6B\x35\x6B\x36\x6B\x37\x76\x31\0\xC8\xC9\xCA\xCB\x10\0\0\x4F\x9D\x20\0\x05\x78\xD4\xC5\x2C\xCA\0\x30\0\x05\xEF\x35\xC4\xF1\x33\0\0\0\x70\x80\x40\0\0\0\x02\0\0\0\x0A\0\0\0\0\x1D\xCD\x65\0\x70\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x04\xD2\x02"
233233
);
234234
}
235235

@@ -252,11 +252,11 @@ fn test_encode_extension() {
252252
);
253253
assert_eq!(
254254
Value::TimestampTz(TimestampTz {
255-
offset: 8,
255+
offset: 8 * 3600,
256256
value: 1760140800000000
257257
})
258258
.to_vec(),
259-
b"\x20\0\0\0\x60\0\0\x0a\x30\0\x06\x40\xd6\xb7\x23\x80\0\x08"
259+
b"\x20\0\0\0\x60\0\0\x0d\x30\0\x06\x40\xd6\xb7\x23\x80\0\0\0\x70\x80"
260260
);
261261
assert_eq!(
262262
Value::Interval(Interval {

tests/it/functions.rs

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -826,19 +826,32 @@ fn test_to_string() {
826826

827827
let extension_sources = vec![
828828
(Value::Binary(&[97, 98, 99]), r#""616263""#),
829+
(Value::Binary(&[]), r#""""#),
829830
(Value::Date(Date { value: 90570 }), r#""2217-12-22""#),
831+
(Value::Date(Date { value: -1 }), r#""1969-12-31""#),
830832
(
831833
Value::Timestamp(Timestamp {
832834
value: 190390000000,
833835
}),
834836
r#""1970-01-03 04:53:10.000000""#,
835837
),
838+
(
839+
Value::Timestamp(Timestamp { value: 0 }),
840+
r#""1970-01-01 00:00:00.000000""#,
841+
),
836842
(
837843
Value::TimestampTz(TimestampTz {
838-
offset: 8,
844+
offset: 8 * 3600,
839845
value: 190390000000,
840846
}),
841-
r#""1970-01-03 12:53:10.000000""#,
847+
r#""1970-01-03 12:53:10.000000 +0800""#,
848+
),
849+
(
850+
Value::TimestampTz(TimestampTz {
851+
offset: 90 * 60,
852+
value: 0,
853+
}),
854+
r#""1970-01-01 01:30:00.000000 +0130""#,
842855
),
843856
(
844857
Value::Interval(Interval {
@@ -848,13 +861,52 @@ fn test_to_string() {
848861
}),
849862
r#""10 months 20 days 00:05:00""#,
850863
),
864+
(
865+
Value::Interval(Interval {
866+
months: -14,
867+
days: -3,
868+
micros: -90000000,
869+
}),
870+
r#""-1 year -2 months -3 days -00:01:30""#,
871+
),
872+
(
873+
Value::Interval(Interval {
874+
months: -25,
875+
days: -1,
876+
micros: 0,
877+
}),
878+
r#""-2 years -1 month -1 day""#,
879+
),
880+
(
881+
Value::Interval(Interval {
882+
months: 0,
883+
days: -2,
884+
micros: 5_000_000,
885+
}),
886+
r#""-2 days 00:00:05""#,
887+
),
888+
(
889+
Value::Interval(Interval {
890+
months: 0,
891+
days: 0,
892+
micros: 0,
893+
}),
894+
r#""00:00:00""#,
895+
),
851896
(
852897
Value::Number(Number::Decimal128(Decimal128 {
853898
scale: 2,
854899
value: 1234,
855900
})),
856901
r#"12.34"#,
857902
),
903+
(
904+
Value::Number(Number::Decimal64(Decimal64 {
905+
scale: 2,
906+
value: -765,
907+
})),
908+
r#"-7.65"#,
909+
),
858910
(
859911
Value::Number(Number::Decimal256(Decimal256 {
860912
scale: 2,
@@ -870,7 +922,7 @@ fn test_to_string() {
870922
value: 190390000000,
871923
}),
872924
Value::TimestampTz(TimestampTz {
873-
offset: 8,
925+
offset: 8 * 3600,
874926
value: 190390000000,
875927
}),
876928
Value::Interval(Interval {
@@ -887,7 +939,7 @@ fn test_to_string() {
887939
value: I256::new(981724),
888940
})),
889941
]),
890-
r#"["616263","2217-12-22","1970-01-03 04:53:10.000000","1970-01-03 12:53:10.000000","10 months 20 days 00:05:00",12.34,9817.24]"#,
942+
r#"["616263","2217-12-22","1970-01-03 04:53:10.000000","1970-01-03 12:53:10.000000 +0800","10 months 20 days 00:05:00",12.34,9817.24]"#,
891943
),
892944
(
893945
Value::Object(BTreeMap::from([
@@ -902,7 +954,7 @@ fn test_to_string() {
902954
(
903955
"k4".to_string(),
904956
Value::TimestampTz(TimestampTz {
905-
offset: 8,
957+
offset: 8 * 3600,
906958
value: 190390000000,
907959
}),
908960
),
@@ -929,7 +981,7 @@ fn test_to_string() {
929981
})),
930982
),
931983
])),
932-
r#"{"k1":"616263","k2":"2217-12-22","k3":"1970-01-03 04:53:10.000000","k4":"1970-01-03 12:53:10.000000","k5":"10 months 20 days 00:05:00","k6":12.34,"k7":9817.24}"#,
984+
r#"{"k1":"616263","k2":"2217-12-22","k3":"1970-01-03 04:53:10.000000","k4":"1970-01-03 12:53:10.000000 +0800","k5":"10 months 20 days 00:05:00","k6":12.34,"k7":9817.24}"#,
933985
),
934986
];
935987

@@ -1117,7 +1169,7 @@ fn test_type_of() {
11171169
),
11181170
(
11191171
Value::TimestampTz(TimestampTz {
1120-
offset: 8,
1172+
offset: 8 * 3600,
11211173
value: 190390000000,
11221174
}),
11231175
"TIMESTAMP_TZ",

0 commit comments

Comments
 (0)