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#L162
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,72 @@ 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]"
157+ let parsed_datetime = chrono:: NaiveDateTime :: parse_from_str ( as_str, "%Y-%m-%d %H:%M:%S" )
158+ . or_else ( |_| chrono:: NaiveDateTime :: parse_from_str ( as_str, "%Y-%m-%d %H:%M:%S%.f" ) )
159+ . or_else ( |_| chrono:: NaiveDateTime :: parse_from_str ( as_str, "%Y-%m-%dT%H:%M:%S" ) )
160+ . or_else ( |_| chrono:: NaiveDateTime :: parse_from_str ( as_str, "%Y-%m-%dT%H:%M:%S%.f" ) )
161+ . map_err ( |err| ProtocolError :: ErrorResponse {
162+ source : ErrorResponse :: error (
163+ ErrorCode :: ProtocolViolation ,
164+ format ! (
165+ "Unable to parse timestamp from text: '{}', error: {}" ,
166+ as_str, err
167+ ) ,
168+ ) ,
169+ backtrace : Backtrace :: capture ( ) ,
170+ } ) ?;
171+
172+ // Convert to Unix nanoseconds
173+ let unix_nano = parsed_datetime
174+ . and_utc ( )
175+ . timestamp_nanos_opt ( )
176+ . ok_or_else ( || ProtocolError :: ErrorResponse {
177+ source : ErrorResponse :: error (
178+ ErrorCode :: ProtocolViolation ,
179+ format ! ( "Timestamp out of range: '{}'" , as_str) ,
180+ ) ,
181+ backtrace : Backtrace :: capture ( ) ,
182+ } ) ?;
183+
184+ Ok ( TimestampValue :: new ( unix_nano, None ) )
185+ }
186+
187+ // https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/timestamp.c#234
188+ fn from_binary ( raw : & [ u8 ] ) -> Result < Self , ProtocolError > {
189+ if raw. len ( ) != 8 {
190+ return Err ( ProtocolError :: ErrorResponse {
191+ source : ErrorResponse :: error (
192+ ErrorCode :: ProtocolViolation ,
193+ format ! (
194+ "Invalid binary timestamp length: expected 8 bytes, got {}" ,
195+ raw. len( )
196+ ) ,
197+ ) ,
198+ backtrace : Backtrace :: capture ( ) ,
199+ } ) ;
200+ }
201+
202+ let pg_microseconds = BigEndian :: read_i64 ( raw) ;
203+
204+ // Convert PostgreSQL microseconds to Unix nanoseconds
205+ let unix_nano = pg_base_date_epoch ( )
206+ . and_utc ( )
207+ . timestamp_nanos_opt ( )
208+ . unwrap ( )
209+ + ( pg_microseconds * 1_000 ) ;
210+
211+ Ok ( TimestampValue :: new ( unix_nano, None ) )
212+ }
213+ }
214+
142215#[ cfg( test) ]
143216mod tests {
144217 use super :: * ;
@@ -170,4 +243,57 @@ mod tests {
170243 let ts = TimestampValue :: new ( 1650890322123456789 , None ) ;
171244 assert_eq ! ( ts. get_time_stamp( ) , 1650890322123456000 ) ;
172245 }
246+
247+ #[ test]
248+ fn test_invalid_timestamp_text ( ) {
249+ // Test that invalid text formats return errors
250+ assert ! ( TimestampValue :: from_text( b"invalid-date" ) . is_err( ) ) ;
251+ assert ! ( TimestampValue :: from_text( b"2025-13-45 25:70:99" ) . is_err( ) ) ;
252+ assert ! ( TimestampValue :: from_text( b"" ) . is_err( ) ) ;
253+ }
254+
255+ #[ test]
256+ fn test_timestamp_from_text_various_formats ( ) {
257+ // Test basic format without fractional seconds
258+ let ts1 = TimestampValue :: from_text ( b"2025-08-04 20:15:47" ) . unwrap ( ) ;
259+ assert_eq ! ( ts1. to_naive_datetime( ) . to_string( ) , "2025-08-04 20:15:47" ) ;
260+
261+ // Test PostgreSQL format with 6-digit fractional seconds
262+ let ts2 = TimestampValue :: from_text ( b"2025-08-04 20:16:54.853660" ) . unwrap ( ) ;
263+ assert_eq ! (
264+ ts2. to_naive_datetime( )
265+ . format( "%Y-%m-%d %H:%M:%S%.6f" )
266+ . to_string( ) ,
267+ "2025-08-04 20:16:54.853660"
268+ ) ;
269+
270+ // Test format with 3 fractional seconds
271+ let ts3 = TimestampValue :: from_text ( b"2025-08-04 20:15:47.953" ) . unwrap ( ) ;
272+ assert_eq ! (
273+ ts3. to_naive_datetime( )
274+ . format( "%Y-%m-%d %H:%M:%S%.3f" )
275+ . to_string( ) ,
276+ "2025-08-04 20:15:47.953"
277+ ) ;
278+
279+ // Test ISO format with T separator
280+ let ts4 = TimestampValue :: from_text ( b"2025-08-04T20:15:47" ) . unwrap ( ) ;
281+ assert_eq ! ( ts4. to_naive_datetime( ) . to_string( ) , "2025-08-04 20:15:47" ) ;
282+
283+ // Test ISO format with T separator and fractional seconds
284+ let ts5 = TimestampValue :: from_text ( b"2025-08-04T20:15:47.953116" ) . unwrap ( ) ;
285+ assert_eq ! (
286+ ts5. to_naive_datetime( )
287+ . format( "%Y-%m-%d %H:%M:%S%.6f" )
288+ . to_string( ) ,
289+ "2025-08-04 20:15:47.953116"
290+ ) ;
291+ }
292+
293+ #[ test]
294+ fn test_invalid_timestamp_binary ( ) {
295+ // Test that invalid binary data returns errors
296+ assert ! ( TimestampValue :: from_binary( & [ 1 , 2 , 3 ] ) . is_err( ) ) ; // Wrong length
297+ assert ! ( TimestampValue :: from_binary( & [ ] ) . is_err( ) ) ; // Empty
298+ }
173299}
0 commit comments