11//! Timestamp value representation for PostgreSQL protocol
22
3- use crate :: { ProtocolError , ToProtocolValue } ;
3+ use crate :: {
4+ protocol:: { ErrorCode , ErrorResponse } ,
5+ FromProtocolValue , ProtocolError , ToProtocolValue ,
6+ } ;
7+ use byteorder:: { BigEndian , ByteOrder } ;
48use bytes:: { BufMut , BytesMut } ;
59use chrono:: {
610 format:: {
@@ -11,6 +15,7 @@ use chrono::{
1115 prelude:: * ,
1216} ;
1317use chrono_tz:: Tz ;
18+ use std:: backtrace:: Backtrace ;
1419use std:: io:: Error ;
1520use std:: {
1621 fmt:: { self , Debug , Display , Formatter } ,
@@ -95,6 +100,7 @@ impl Display for TimestampValue {
95100}
96101
97102// POSTGRES_EPOCH_JDATE
103+ // https://github.com/postgres/postgres/blob/REL_14_4/src/include/datatype/timestamp.h#L163
98104fn pg_base_date_epoch ( ) -> NaiveDateTime {
99105 NaiveDate :: from_ymd_opt ( 2000 , 1 , 1 )
100106 . unwrap ( )
@@ -118,6 +124,7 @@ impl ToProtocolValue for TimestampValue {
118124 }
119125 }
120126
127+ // https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/timestamp.c#L267
121128 fn to_binary ( & self , buf : & mut BytesMut ) -> Result < ( ) , ProtocolError > {
122129 let ndt = match self . tz_ref ( ) {
123130 None => self . to_naive_datetime ( ) ,
@@ -139,6 +146,80 @@ impl ToProtocolValue for TimestampValue {
139146 }
140147}
141148
149+ impl FromProtocolValue for TimestampValue {
150+ fn from_text ( raw : & [ u8 ] ) -> Result < Self , ProtocolError > {
151+ let as_str = std:: str:: from_utf8 ( raw) . map_err ( |err| ProtocolError :: ErrorResponse {
152+ source : ErrorResponse :: error ( ErrorCode :: ProtocolViolation , err. to_string ( ) ) ,
153+ backtrace : Backtrace :: capture ( ) ,
154+ } ) ?;
155+
156+ // Parse timestamp string in format "YYYY-MM-DD HH:MM:SS[.fff]", but PostgreSQL supports
157+ // more formats, so let's align this with parse_date_str function from cubesql crate.
158+ let parsed_datetime = NaiveDateTime :: parse_from_str ( as_str, "%Y-%m-%d %H:%M:%S" )
159+ . or_else ( |_| NaiveDateTime :: parse_from_str ( as_str, "%Y-%m-%d %H:%M:%S%.f" ) )
160+ . or_else ( |_| NaiveDateTime :: parse_from_str ( as_str, "%Y-%m-%dT%H:%M:%S" ) )
161+ . or_else ( |_| NaiveDateTime :: parse_from_str ( as_str, "%Y-%m-%dT%H:%M:%S%.f" ) )
162+ . or_else ( |_| NaiveDateTime :: parse_from_str ( as_str, "%Y-%m-%dT%H:%M:%S%.fZ" ) )
163+ . or_else ( |_| {
164+ NaiveDate :: parse_from_str ( as_str, "%Y-%m-%d" ) . map ( |date| {
165+ date. and_hms_opt ( 0 , 0 , 0 )
166+ . expect ( "Unable to set time to 00:00:00" )
167+ } )
168+ } )
169+ . map_err ( |err| ProtocolError :: ErrorResponse {
170+ source : ErrorResponse :: error (
171+ ErrorCode :: ProtocolViolation ,
172+ format ! (
173+ "Unable to parse timestamp from text: '{}', error: {}" ,
174+ as_str, err
175+ ) ,
176+ ) ,
177+ backtrace : Backtrace :: capture ( ) ,
178+ } ) ?;
179+
180+ // Convert to Unix nanoseconds
181+ let unix_nano = parsed_datetime
182+ . and_utc ( )
183+ . timestamp_nanos_opt ( )
184+ . ok_or_else ( || ProtocolError :: ErrorResponse {
185+ source : ErrorResponse :: error (
186+ ErrorCode :: ProtocolViolation ,
187+ format ! ( "Timestamp out of range: '{}'" , as_str) ,
188+ ) ,
189+ backtrace : Backtrace :: capture ( ) ,
190+ } ) ?;
191+
192+ Ok ( TimestampValue :: new ( unix_nano, None ) )
193+ }
194+
195+ // https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/timestamp.c#L234
196+ fn from_binary ( raw : & [ u8 ] ) -> Result < Self , ProtocolError > {
197+ if raw. len ( ) != 8 {
198+ return Err ( ProtocolError :: ErrorResponse {
199+ source : ErrorResponse :: error (
200+ ErrorCode :: ProtocolViolation ,
201+ format ! (
202+ "Invalid binary timestamp length: expected 8 bytes, got {}" ,
203+ raw. len( )
204+ ) ,
205+ ) ,
206+ backtrace : Backtrace :: capture ( ) ,
207+ } ) ;
208+ }
209+
210+ let pg_microseconds = BigEndian :: read_i64 ( raw) ;
211+
212+ // Convert PostgreSQL microseconds to Unix nanoseconds
213+ let unix_nano = pg_base_date_epoch ( )
214+ . and_utc ( )
215+ . timestamp_nanos_opt ( )
216+ . expect ( "Unable to get timestamp nanos for pg_base_date_epoch" )
217+ + ( pg_microseconds * 1_000 ) ;
218+
219+ Ok ( TimestampValue :: new ( unix_nano, None ) )
220+ }
221+ }
222+
142223#[ cfg( test) ]
143224mod tests {
144225 use super :: * ;
@@ -170,4 +251,57 @@ mod tests {
170251 let ts = TimestampValue :: new ( 1650890322123456789 , None ) ;
171252 assert_eq ! ( ts. get_time_stamp( ) , 1650890322123456000 ) ;
172253 }
254+
255+ #[ test]
256+ fn test_invalid_timestamp_text ( ) {
257+ // Test that invalid text formats return errors
258+ assert ! ( TimestampValue :: from_text( b"invalid-date" ) . is_err( ) ) ;
259+ assert ! ( TimestampValue :: from_text( b"2025-13-45 25:70:99" ) . is_err( ) ) ;
260+ assert ! ( TimestampValue :: from_text( b"" ) . is_err( ) ) ;
261+ }
262+
263+ #[ test]
264+ fn test_timestamp_from_text_various_formats ( ) {
265+ // Test basic format without fractional seconds
266+ let ts1 = TimestampValue :: from_text ( b"2025-08-04 20:15:47" ) . unwrap ( ) ;
267+ assert_eq ! ( ts1. to_naive_datetime( ) . to_string( ) , "2025-08-04 20:15:47" ) ;
268+
269+ // Test PostgreSQL format with 6-digit fractional seconds
270+ let ts2 = TimestampValue :: from_text ( b"2025-08-04 20:16:54.853660" ) . unwrap ( ) ;
271+ assert_eq ! (
272+ ts2. to_naive_datetime( )
273+ . format( "%Y-%m-%d %H:%M:%S%.6f" )
274+ . to_string( ) ,
275+ "2025-08-04 20:16:54.853660"
276+ ) ;
277+
278+ // Test format with 3 fractional seconds
279+ let ts3 = TimestampValue :: from_text ( b"2025-08-04 20:15:47.953" ) . unwrap ( ) ;
280+ assert_eq ! (
281+ ts3. to_naive_datetime( )
282+ . format( "%Y-%m-%d %H:%M:%S%.3f" )
283+ . to_string( ) ,
284+ "2025-08-04 20:15:47.953"
285+ ) ;
286+
287+ // Test ISO format with T separator
288+ let ts4 = TimestampValue :: from_text ( b"2025-08-04T20:15:47" ) . unwrap ( ) ;
289+ assert_eq ! ( ts4. to_naive_datetime( ) . to_string( ) , "2025-08-04 20:15:47" ) ;
290+
291+ // Test ISO format with T separator and fractional seconds
292+ let ts5 = TimestampValue :: from_text ( b"2025-08-04T20:15:47.953116" ) . unwrap ( ) ;
293+ assert_eq ! (
294+ ts5. to_naive_datetime( )
295+ . format( "%Y-%m-%d %H:%M:%S%.6f" )
296+ . to_string( ) ,
297+ "2025-08-04 20:15:47.953116"
298+ ) ;
299+ }
300+
301+ #[ test]
302+ fn test_invalid_timestamp_binary ( ) {
303+ // Test that invalid binary data returns errors
304+ assert ! ( TimestampValue :: from_binary( & [ 1 , 2 , 3 ] ) . is_err( ) ) ; // Wrong length
305+ assert ! ( TimestampValue :: from_binary( & [ ] ) . is_err( ) ) ; // Empty
306+ }
173307}
0 commit comments