1
1
//! Timestamp value representation for PostgreSQL protocol
2
2
3
- use crate :: { ProtocolError , ToProtocolValue } ;
3
+ use crate :: {
4
+ protocol:: { ErrorCode , ErrorResponse } ,
5
+ FromProtocolValue , ProtocolError , ToProtocolValue ,
6
+ } ;
7
+ use byteorder:: { BigEndian , ByteOrder } ;
4
8
use bytes:: { BufMut , BytesMut } ;
5
9
use chrono:: {
6
10
format:: {
@@ -11,6 +15,7 @@ use chrono::{
11
15
prelude:: * ,
12
16
} ;
13
17
use chrono_tz:: Tz ;
18
+ use std:: backtrace:: Backtrace ;
14
19
use std:: io:: Error ;
15
20
use std:: {
16
21
fmt:: { self , Debug , Display , Formatter } ,
@@ -95,6 +100,7 @@ impl Display for TimestampValue {
95
100
}
96
101
97
102
// POSTGRES_EPOCH_JDATE
103
+ // https://github.com/postgres/postgres/blob/REL_14_4/src/include/datatype/timestamp.h#L163
98
104
fn pg_base_date_epoch ( ) -> NaiveDateTime {
99
105
NaiveDate :: from_ymd_opt ( 2000 , 1 , 1 )
100
106
. unwrap ( )
@@ -118,6 +124,7 @@ impl ToProtocolValue for TimestampValue {
118
124
}
119
125
}
120
126
127
+ // https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/timestamp.c#L267
121
128
fn to_binary ( & self , buf : & mut BytesMut ) -> Result < ( ) , ProtocolError > {
122
129
let ndt = match self . tz_ref ( ) {
123
130
None => self . to_naive_datetime ( ) ,
@@ -139,6 +146,80 @@ impl ToProtocolValue for TimestampValue {
139
146
}
140
147
}
141
148
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
+
142
223
#[ cfg( test) ]
143
224
mod tests {
144
225
use super :: * ;
@@ -170,4 +251,57 @@ mod tests {
170
251
let ts = TimestampValue :: new ( 1650890322123456789 , None ) ;
171
252
assert_eq ! ( ts. get_time_stamp( ) , 1650890322123456000 ) ;
172
253
}
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
+ }
173
307
}
0 commit comments