Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions rust/cubesql/cubesql/src/sql/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,9 @@ impl<'ast> Visitor<'ast, ConnectionError> for PostgresStatementParamsBinder {
BindValue::Timestamp(v) => {
*value = ast::Value::SingleQuotedString(v.to_string());
}
BindValue::Date(v) => {
*value = ast::Value::SingleQuotedString(v.to_string());
}
BindValue::Null => {
*value = ast::Value::Null;
}
Expand Down Expand Up @@ -1073,6 +1076,7 @@ impl<'ast> Visitor<'ast, ConnectionError> for SensitiveDataSanitizer {
mod tests {
use super::*;
use crate::CubeError;
use pg_srv::{DateValue, TimestampValue};
use sqlparser::{dialect::PostgreSqlDialect, parser::Parser};

fn run_cast_replacer(input: &str, output: &str) -> Result<(), CubeError> {
Expand Down Expand Up @@ -1254,6 +1258,26 @@ mod tests {
vec![BindValue::String("test1".to_string())],
)?;

// test TimestampValue binding in the WHERE clause
run_pg_binder(
"SELECT * FROM events WHERE created_at BETWEEN $1 AND $2",
"SELECT * FROM events WHERE created_at BETWEEN '2022-04-25T12:38:42.000' AND '2025-08-08T09:30:45.123'",
vec![
BindValue::Timestamp(TimestampValue::new(1650890322000000000, None)),
BindValue::Timestamp(TimestampValue::new(1754645445123456000, None)),
],
)?;

// test DateValue binding in the WHERE clause
run_pg_binder(
"SELECT * FROM orders WHERE order_date >= $1 AND order_date <= $2",
"SELECT * FROM orders WHERE order_date >= '1999-12-31' AND order_date <= '2000-01-01'",
vec![
BindValue::Date(DateValue::from_ymd_opt(1999, 12, 31).unwrap()),
BindValue::Date(DateValue::from_ymd_opt(2000, 1, 1).unwrap()),
],
)?;

Ok(())
}

Expand Down
19 changes: 19 additions & 0 deletions rust/cubesql/pg-srv/src/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ mod tests {
use crate::protocol::Format;
use crate::values::timestamp::TimestampValue;
use bytes::BytesMut;
#[cfg(feature = "with-chrono")]
use chrono::NaiveDate;

fn assert_test_decode<T: ToProtocolValue + FromProtocolValue + std::cmp::PartialEq>(
value: T,
Expand Down Expand Up @@ -160,6 +162,13 @@ mod tests {
assert_test_decode(TimestampValue::new(0, None), Format::Text)?;
assert_test_decode(TimestampValue::new(1234567890123456000, None), Format::Text)?;

#[cfg(feature = "with-chrono")]
{
assert_test_decode(NaiveDate::from_ymd_opt(2025, 8, 8).unwrap(), Format::Text)?;
assert_test_decode(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), Format::Text)?;
assert_test_decode(NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(), Format::Text)?;
}

Ok(())
}

Expand All @@ -183,6 +192,16 @@ mod tests {
Format::Binary,
)?;

#[cfg(feature = "with-chrono")]
{
assert_test_decode(NaiveDate::from_ymd_opt(2025, 8, 8).unwrap(), Format::Binary)?;
assert_test_decode(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), Format::Binary)?;
assert_test_decode(
NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
Format::Binary,
)?;
}

Ok(())
}
}
147 changes: 83 additions & 64 deletions rust/cubesql/pg-srv/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

use crate::{protocol::Format, ProtocolError};
use bytes::{BufMut, BytesMut};
#[cfg(feature = "with-chrono")]
use chrono::{NaiveDate, NaiveDateTime};
use std::io::{Error, ErrorKind};

/// This trait explains how to encode values to the protocol format
pub trait ToProtocolValue: std::fmt::Debug {
Expand Down Expand Up @@ -107,49 +104,12 @@ impl_primitive!(i64);
impl_primitive!(f32);
impl_primitive!(f64);

// POSTGRES_EPOCH_JDATE
#[cfg(feature = "with-chrono")]
fn pg_base_date_epoch() -> NaiveDateTime {
NaiveDate::from_ymd_opt(2000, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
}

#[cfg(feature = "with-chrono")]
impl ToProtocolValue for NaiveDate {
// date_out - https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/date.c#L176
fn to_text(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
self.to_string().to_text(buf)
}

// date_send - https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/date.c#L223
fn to_binary(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
let n = self
.signed_duration_since(pg_base_date_epoch().date())
.num_days();
if n > (i32::MAX as i64) {
return Err(Error::new(
ErrorKind::Other,
format!(
"value too large to store in the binary format (i32), actual: {}",
n
),
)
.into());
}

buf.put_i32(4);
buf.put_i32(n as i32);

Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::*;
use bytes::BytesMut;
#[cfg(feature = "with-chrono")]
use chrono::NaiveDate;

fn assert_text_encode<T: ToProtocolValue>(value: T, expected: &[u8]) {
let mut buf = BytesMut::new();
Expand Down Expand Up @@ -179,20 +139,48 @@ mod tests {
105, 110, 115, 32, 53, 46, 48, 48, 48, 48, 48, 54, 32, 115, 101, 99, 115,
],
);
assert_text_encode(
TimestampValue::new(0, None),
&[
0, 0, 0, 26, 49, 57, 55, 48, 45, 48, 49, 45, 48, 49, 32, 48, 48, 58, 48, 48, 58,
48, 48, 46, 48, 48, 48, 48, 48, 48,
],
);
assert_text_encode(
TimestampValue::new(1650890322000000000, None),
&[
0, 0, 0, 26, 50, 48, 50, 50, 45, 48, 52, 45, 50, 53, 32, 49, 50, 58, 51, 56, 58,
52, 50, 46, 48, 48, 48, 48, 48, 48,
],
);

#[cfg(feature = "with-chrono")]
{
// Test TimestampValue encoding
assert_text_encode(
TimestampValue::new(0, None),
&[
0, 0, 0, 26, 49, 57, 55, 48, 45, 48, 49, 45, 48, 49, 32, 48, 48, 58, 48, 48,
58, 48, 48, 46, 48, 48, 48, 48, 48, 48,
],
);
assert_text_encode(
TimestampValue::new(1650890322000000000, None),
&[
0, 0, 0, 26, 50, 48, 50, 50, 45, 48, 52, 45, 50, 53, 32, 49, 50, 58, 51, 56,
58, 52, 50, 46, 48, 48, 48, 48, 48, 48,
],
);

// Test NaiveDate encoding
assert_text_encode(
NaiveDate::from_ymd_opt(2025, 8, 8).unwrap(),
&[
0, 0, 0, 10, // length: 10 bytes
50, 48, 50, 53, 45, 48, 56, 45, 48, 56, // "2025-08-08"
],
);
assert_text_encode(
NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
&[
0, 0, 0, 10, // length: 10 bytes
50, 48, 48, 48, 45, 48, 49, 45, 48, 49, // "2000-01-01"
],
);
assert_text_encode(
NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
&[
0, 0, 0, 10, // length: 10 bytes
49, 57, 57, 57, 45, 49, 50, 45, 51, 49, // "1999-12-31"
],
);
}

Ok(())
}
Expand All @@ -218,14 +206,45 @@ mod tests {
0, 0, 0, 16, 0, 0, 0, 2, 146, 85, 83, 70, 0, 0, 0, 2, 0, 0, 0, 1,
],
);
assert_bind_encode(
TimestampValue::new(0, None),
&[0, 0, 0, 8, 255, 252, 162, 254, 196, 200, 32, 0],
);
assert_bind_encode(
TimestampValue::new(1650890322000000000, None),
&[0, 0, 0, 8, 0, 2, 128, 120, 159, 252, 216, 128],
);

#[cfg(feature = "with-chrono")]
{
// Test TimestampValue binary encoding
assert_bind_encode(
TimestampValue::new(0, None),
&[0, 0, 0, 8, 255, 252, 162, 254, 196, 200, 32, 0],
);
assert_bind_encode(
TimestampValue::new(1650890322000000000, None),
&[0, 0, 0, 8, 0, 2, 128, 120, 159, 252, 216, 128],
);

// Test NaiveDate binary encoding
// PostgreSQL epoch is 2000-01-01, so this date should be 0 days
assert_bind_encode(
NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
&[
0, 0, 0, 4, // length: 4 bytes
0, 0, 0, 0, // 0 days from epoch
],
);
// Date after epoch: 2025-08-08 is 9351 days after 2000-01-01
assert_bind_encode(
NaiveDate::from_ymd_opt(2025, 8, 8).unwrap(),
&[
0, 0, 0, 4, // length: 4 bytes
0, 0, 36, 135, // 9351 days from epoch (0x2487 in hex)
],
);
// Date before epoch: 1999-12-31 is -1 day from 2000-01-01
assert_bind_encode(
NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
&[
0, 0, 0, 4, // length: 4 bytes
255, 255, 255, 255, // -1 in two's complement
],
);
}

Ok(())
}
Expand Down
4 changes: 3 additions & 1 deletion rust/cubesql/pg-srv/src/extended.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Implementation for Extended Query

#[cfg(feature = "with-chrono")]
use crate::TimestampValue;
use crate::{DateValue, TimestampValue};

#[derive(Debug, PartialEq)]
pub enum BindValue {
Expand All @@ -11,5 +11,7 @@ pub enum BindValue {
Bool(bool),
#[cfg(feature = "with-chrono")]
Timestamp(TimestampValue),
#[cfg(feature = "with-chrono")]
Date(DateValue),
Null,
}
58 changes: 58 additions & 0 deletions rust/cubesql/pg-srv/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,10 @@ impl Bind {
raw_value,
param_format,
)?),
#[cfg(feature = "with-chrono")]
PgTypeId::DATE => {
BindValue::Date(chrono::NaiveDate::from_protocol(raw_value, param_format)?)
}
_ => {
return Err(ErrorResponse::error(
ErrorCode::FeatureNotSupported,
Expand Down Expand Up @@ -1348,6 +1352,60 @@ mod tests {
Ok(())
}

#[cfg(feature = "with-chrono")]
#[tokio::test]
async fn test_frontend_message_parse_bind_date() -> Result<(), ProtocolError> {
use chrono::NaiveDate;

// Test text format date "2025-08-08"
let buffer = parse_hex_dump(
r#"
42 00 00 00 1e 00 73 30 00 00 01 00 00 00 01 00 B.....s0........
00 00 0a 32 30 32 35 2d 30 38 2d 30 38 00 00 00 ...2025-08-08...
00 .
"#
.to_string(),
);
let mut cursor = Cursor::new(buffer);
let message = read_message(&mut cursor, MessageTagParserDefaultImpl::with_arc()).await?;
match message {
FrontendMessage::Bind(body) => {
assert_eq!(
body.to_bind_values(&ParameterDescription::new(vec![PgTypeId::DATE]))?,
vec![BindValue::Date(
NaiveDate::from_ymd_opt(2025, 8, 8).unwrap()
)]
);
}
_ => panic!("Wrong message, must be Bind"),
}

// Test binary format date (9351 days from 2000-01-01 for 2025-08-08)
let buffer = parse_hex_dump(
r#"
42 00 00 00 1a 00 73 30 00 00 01 00 01 00 01 00 B.....s0........
00 00 04 00 00 24 87 00 00 00 00 .....$......
"#
.to_string(),
);
let mut cursor = Cursor::new(buffer);
let message = read_message(&mut cursor, MessageTagParserDefaultImpl::with_arc()).await?;
match message {
FrontendMessage::Bind(body) => {
assert_eq!(body.parameter_formats, vec![Format::Binary]);
assert_eq!(
body.to_bind_values(&ParameterDescription::new(vec![PgTypeId::DATE]))?,
vec![BindValue::Date(
NaiveDate::from_ymd_opt(2025, 8, 8).unwrap()
)]
);
}
_ => panic!("Wrong message, must be Bind"),
}

Ok(())
}

#[tokio::test]
async fn test_frontend_message_parse_describe() -> Result<(), ProtocolError> {
let buffer = parse_hex_dump(
Expand Down
Loading
Loading