Skip to content

Commit df511e4

Browse files
committed
feat(cubesql): Support timestamp parameter binding
1 parent 760640c commit df511e4

File tree

8 files changed

+186
-4
lines changed

8 files changed

+186
-4
lines changed

rust/cubesql/cubesql/e2e/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ impl TestsRunner {
2222

2323
pub fn register_suite(&mut self, result: AsyncTestConstructorResult) {
2424
match result {
25-
AsyncTestConstructorResult::Sucess(suite) => self.suites.push(suite),
25+
AsyncTestConstructorResult::Success(suite) => self.suites.push(suite),
2626
AsyncTestConstructorResult::Skipped(message) => {
2727
println!("Skipped: {}", message)
2828
}

rust/cubesql/cubesql/e2e/tests/basic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ pub trait AsyncTestSuite: Debug {
2323
}
2424

2525
pub enum AsyncTestConstructorResult {
26-
Sucess(Box<dyn AsyncTestSuite>),
26+
Success(Box<dyn AsyncTestSuite>),
2727
Skipped(String),
2828
}

rust/cubesql/cubesql/e2e/tests/postgres.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ impl PostgresIntegrationTestSuite {
9292
)
9393
.await;
9494

95-
AsyncTestConstructorResult::Sucess(Box::new(PostgresIntegrationTestSuite { client, port }))
95+
AsyncTestConstructorResult::Success(Box::new(PostgresIntegrationTestSuite { client, port }))
9696
}
9797

9898
async fn create_client(config: tokio_postgres::Config) -> Client {

rust/cubesql/cubesql/src/sql/statement.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,10 @@ impl<'ast> Visitor<'ast, ConnectionError> for PostgresStatementParamsBinder {
624624
BindValue::Float64(v) => {
625625
*value = ast::Value::Number(v.to_string(), *v < 0_f64);
626626
}
627+
BindValue::Timestamp(v) => {
628+
// Format timestamp as string literal for SQL processing
629+
*value = ast::Value::SingleQuotedString(v.format("%Y-%m-%d %H:%M:%S%.6f").to_string());
630+
}
627631
BindValue::Null => {
628632
*value = ast::Value::Null;
629633
}

rust/cubesql/pg-srv/src/decoding.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use crate::{
55
ProtocolError,
66
};
77
use byteorder::{BigEndian, ByteOrder};
8+
#[cfg(feature = "with-chrono")]
9+
use chrono::{NaiveDate, NaiveDateTime};
810
use std::backtrace::Backtrace;
911

1012
/// This trait explains how to decode values from the protocol
@@ -122,6 +124,81 @@ impl FromProtocolValue for f64 {
122124
}
123125
}
124126

127+
#[cfg(feature = "with-chrono")]
128+
impl FromProtocolValue for NaiveDateTime {
129+
fn from_text(raw: &[u8]) -> Result<Self, ProtocolError> {
130+
let as_str = std::str::from_utf8(raw).map_err(|err| ProtocolError::ErrorResponse {
131+
source: ErrorResponse::error(ErrorCode::ProtocolViolation, err.to_string()),
132+
backtrace: Backtrace::capture(),
133+
})?;
134+
135+
// Parse timestamp strings in various common formats
136+
// Try PostgreSQL format first: "2022-04-25 16:25:01.164774"
137+
if let Ok(dt) = NaiveDateTime::parse_from_str(as_str, "%Y-%m-%d %H:%M:%S%.f") {
138+
return Ok(dt);
139+
}
140+
141+
// Try without microseconds: "2022-04-25 16:25:01"
142+
if let Ok(dt) = NaiveDateTime::parse_from_str(as_str, "%Y-%m-%d %H:%M:%S") {
143+
return Ok(dt);
144+
}
145+
146+
// Try ISO format: "2022-04-25T16:25:01.164774"
147+
if let Ok(dt) = NaiveDateTime::parse_from_str(as_str, "%Y-%m-%dT%H:%M:%S%.f") {
148+
return Ok(dt);
149+
}
150+
151+
// Try ISO format without microseconds: "2022-04-25T16:25:01"
152+
if let Ok(dt) = NaiveDateTime::parse_from_str(as_str, "%Y-%m-%dT%H:%M:%S") {
153+
return Ok(dt);
154+
}
155+
156+
Err(ProtocolError::ErrorResponse {
157+
source: ErrorResponse::error(
158+
ErrorCode::ProtocolViolation,
159+
format!("Unable to parse timestamp from text: {}", as_str),
160+
),
161+
backtrace: Backtrace::capture(),
162+
})
163+
}
164+
165+
fn from_binary(raw: &[u8]) -> Result<Self, ProtocolError> {
166+
if raw.len() != 8 {
167+
return Err(ProtocolError::ErrorResponse {
168+
source: ErrorResponse::error(
169+
ErrorCode::ProtocolViolation,
170+
format!(
171+
"Invalid binary timestamp length: expected 8, got {}",
172+
raw.len()
173+
),
174+
),
175+
backtrace: Backtrace::capture(),
176+
});
177+
}
178+
179+
// PostgreSQL timestamp is microseconds since 2000-01-01 00:00:00 UTC
180+
let microseconds = BigEndian::read_i64(raw);
181+
let base_time = NaiveDate::from_ymd_opt(2000, 1, 1)
182+
.unwrap()
183+
.and_hms_opt(0, 0, 0)
184+
.unwrap();
185+
186+
let duration = chrono::Duration::microseconds(microseconds);
187+
base_time
188+
.checked_add_signed(duration)
189+
.ok_or_else(|| ProtocolError::ErrorResponse {
190+
source: ErrorResponse::error(
191+
ErrorCode::ProtocolViolation,
192+
format!(
193+
"Timestamp overflow: {} microseconds from epoch",
194+
microseconds
195+
),
196+
),
197+
backtrace: Backtrace::capture(),
198+
})
199+
}
200+
}
201+
125202
#[cfg(test)]
126203
mod tests {
127204
use crate::*;
@@ -156,6 +233,16 @@ mod tests {
156233
assert_test_decode(-std::f64::consts::E, Format::Text)?;
157234
assert_test_decode(0.0_f64, Format::Text)?;
158235

236+
#[cfg(feature = "with-chrono")]
237+
{
238+
use chrono::NaiveDate;
239+
let timestamp = NaiveDate::from_ymd_opt(2022, 4, 25)
240+
.unwrap()
241+
.and_hms_micro_opt(16, 25, 1, 164774)
242+
.unwrap();
243+
assert_test_decode(timestamp, Format::Text)?;
244+
}
245+
159246
Ok(())
160247
}
161248

@@ -170,6 +257,16 @@ mod tests {
170257
assert_test_decode(-std::f64::consts::E, Format::Binary)?;
171258
assert_test_decode(0.0_f64, Format::Binary)?;
172259

260+
#[cfg(feature = "with-chrono")]
261+
{
262+
use chrono::NaiveDate;
263+
let timestamp = NaiveDate::from_ymd_opt(2022, 4, 25)
264+
.unwrap()
265+
.and_hms_micro_opt(16, 25, 1, 164774)
266+
.unwrap();
267+
assert_test_decode(timestamp, Format::Binary)?;
268+
}
269+
173270
Ok(())
174271
}
175272
}

rust/cubesql/pg-srv/src/encoding.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Encoding native values to the Protocol representation
22
3-
use crate::{protocol::Format, ProtocolError};
3+
use crate::{
4+
protocol::{ErrorCode, ErrorResponse, Format},
5+
ProtocolError,
6+
};
47
use bytes::{BufMut, BytesMut};
58
#[cfg(feature = "with-chrono")]
69
use chrono::{NaiveDate, NaiveDateTime};
@@ -149,6 +152,39 @@ impl ToProtocolValue for NaiveDate {
149152
}
150153
}
151154

155+
#[cfg(feature = "with-chrono")]
156+
impl ToProtocolValue for NaiveDateTime {
157+
// timestamp_out - https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/timestamp.c#L242
158+
fn to_text(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
159+
// Format as PostgreSQL timestamp string
160+
let formatted = self.format("%Y-%m-%d %H:%M:%S%.6f").to_string();
161+
formatted.to_text(buf)
162+
}
163+
164+
// timestamp_send - https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/timestamp.c#L280
165+
fn to_binary(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
166+
// PostgreSQL timestamp is microseconds since 2000-01-01 00:00:00 UTC
167+
let base_time = pg_base_date_epoch();
168+
let duration = self.signed_duration_since(base_time);
169+
170+
let microseconds =
171+
duration
172+
.num_microseconds()
173+
.ok_or_else(|| ProtocolError::ErrorResponse {
174+
source: ErrorResponse::error(
175+
ErrorCode::ProtocolViolation,
176+
"Timestamp duration overflow when converting to microseconds".to_string(),
177+
),
178+
backtrace: std::backtrace::Backtrace::capture(),
179+
})?;
180+
181+
buf.put_i32(8); // timestamp is 8 bytes
182+
buf.put_i64(microseconds);
183+
184+
Ok(())
185+
}
186+
}
187+
152188
#[derive(Debug, Clone, Default)]
153189
pub struct IntervalValue {
154190
pub months: i32,
@@ -307,6 +343,23 @@ mod tests {
307343
assert_text_encode(false, &[0, 0, 0, 1, 102]);
308344
assert_text_encode("str".to_string(), &[0, 0, 0, 3, 115, 116, 114]);
309345

346+
#[cfg(feature = "with-chrono")]
347+
{
348+
use chrono::NaiveDate;
349+
let timestamp = NaiveDate::from_ymd_opt(2022, 4, 25)
350+
.unwrap()
351+
.and_hms_micro_opt(16, 25, 1, 164774)
352+
.unwrap();
353+
354+
let mut buf = BytesMut::new();
355+
timestamp.to_text(&mut buf).unwrap();
356+
357+
// Check that it encoded correctly - should start with length
358+
assert!(buf.len() > 4);
359+
let length = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
360+
assert_eq!(length + 4, buf.len());
361+
}
362+
310363
Ok(())
311364
}
312365

@@ -322,6 +375,22 @@ mod tests {
322375
assert_bind_encode(true, &[0, 0, 0, 1, 1]);
323376
assert_bind_encode(false, &[0, 0, 0, 1, 0]);
324377

378+
#[cfg(feature = "with-chrono")]
379+
{
380+
use chrono::NaiveDate;
381+
let timestamp = NaiveDate::from_ymd_opt(2022, 4, 25)
382+
.unwrap()
383+
.and_hms_micro_opt(16, 25, 1, 164774)
384+
.unwrap();
385+
386+
let mut buf = BytesMut::new();
387+
timestamp.to_binary(&mut buf).unwrap();
388+
389+
// Check that it encoded correctly - should be 12 bytes total (4 bytes length + 8 bytes timestamp)
390+
assert_eq!(buf.len(), 12);
391+
assert_eq!(&buf[0..4], &[0, 0, 0, 8]); // length = 8
392+
}
393+
325394
Ok(())
326395
}
327396

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
//! Implementation for Extended Query
22
3+
#[cfg(feature = "with-chrono")]
4+
use chrono::NaiveDateTime;
5+
36
#[derive(Debug, PartialEq)]
47
pub enum BindValue {
58
String(String),
69
Int64(i64),
710
Float64(f64),
811
Bool(bool),
12+
#[cfg(feature = "with-chrono")]
13+
Timestamp(NaiveDateTime),
914
Null,
1015
}

rust/cubesql/pg-srv/src/protocol.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ use tokio::io::AsyncReadExt;
1818

1919
use crate::{buffer, BindValue, FromProtocolValue, PgType, PgTypeId, ProtocolError};
2020

21+
#[cfg(feature = "with-chrono")]
22+
use chrono;
23+
2124
const DEFAULT_CAPACITY: usize = 64;
2225

2326
#[derive(Debug, PartialEq, Clone)]
@@ -786,6 +789,10 @@ impl Bind {
786789
PgTypeId::FLOAT8 => {
787790
BindValue::Float64(f64::from_protocol(raw_value, param_format)?)
788791
}
792+
#[cfg(feature = "with-chrono")]
793+
PgTypeId::TIMESTAMP => {
794+
BindValue::Timestamp(chrono::NaiveDateTime::from_protocol(raw_value, param_format)?)
795+
}
789796
_ => {
790797
return Err(ErrorResponse::error(
791798
ErrorCode::FeatureNotSupported,

0 commit comments

Comments
 (0)