Skip to content

Commit 5246fa0

Browse files
authored
feat(cubesql): Support date type for parameter binding (#9864)
1 parent 0fb183a commit 5246fa0

File tree

8 files changed

+329
-66
lines changed

8 files changed

+329
-66
lines changed

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,9 @@ impl<'ast> Visitor<'ast, ConnectionError> for PostgresStatementParamsBinder {
627627
BindValue::Timestamp(v) => {
628628
*value = ast::Value::SingleQuotedString(v.to_string());
629629
}
630+
BindValue::Date(v) => {
631+
*value = ast::Value::SingleQuotedString(v.to_string());
632+
}
630633
BindValue::Null => {
631634
*value = ast::Value::Null;
632635
}
@@ -1073,6 +1076,7 @@ impl<'ast> Visitor<'ast, ConnectionError> for SensitiveDataSanitizer {
10731076
mod tests {
10741077
use super::*;
10751078
use crate::CubeError;
1079+
use pg_srv::{DateValue, TimestampValue};
10761080
use sqlparser::{dialect::PostgreSqlDialect, parser::Parser};
10771081

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

1261+
// test TimestampValue binding in the WHERE clause
1262+
run_pg_binder(
1263+
"SELECT * FROM events WHERE created_at BETWEEN $1 AND $2",
1264+
"SELECT * FROM events WHERE created_at BETWEEN '2022-04-25T12:38:42.000' AND '2025-08-08T09:30:45.123'",
1265+
vec![
1266+
BindValue::Timestamp(TimestampValue::new(1650890322000000000, None)),
1267+
BindValue::Timestamp(TimestampValue::new(1754645445123456000, None)),
1268+
],
1269+
)?;
1270+
1271+
// test DateValue binding in the WHERE clause
1272+
run_pg_binder(
1273+
"SELECT * FROM orders WHERE order_date >= $1 AND order_date <= $2",
1274+
"SELECT * FROM orders WHERE order_date >= '1999-12-31' AND order_date <= '2000-01-01'",
1275+
vec![
1276+
BindValue::Date(DateValue::from_ymd_opt(1999, 12, 31).unwrap()),
1277+
BindValue::Date(DateValue::from_ymd_opt(2000, 1, 1).unwrap()),
1278+
],
1279+
)?;
1280+
12571281
Ok(())
12581282
}
12591283

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ mod tests {
129129
use crate::protocol::Format;
130130
use crate::values::timestamp::TimestampValue;
131131
use bytes::BytesMut;
132+
#[cfg(feature = "with-chrono")]
133+
use chrono::NaiveDate;
132134

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

165+
#[cfg(feature = "with-chrono")]
166+
{
167+
assert_test_decode(NaiveDate::from_ymd_opt(2025, 8, 8).unwrap(), Format::Text)?;
168+
assert_test_decode(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), Format::Text)?;
169+
assert_test_decode(NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(), Format::Text)?;
170+
}
171+
163172
Ok(())
164173
}
165174

@@ -183,6 +192,16 @@ mod tests {
183192
Format::Binary,
184193
)?;
185194

195+
#[cfg(feature = "with-chrono")]
196+
{
197+
assert_test_decode(NaiveDate::from_ymd_opt(2025, 8, 8).unwrap(), Format::Binary)?;
198+
assert_test_decode(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), Format::Binary)?;
199+
assert_test_decode(
200+
NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
201+
Format::Binary,
202+
)?;
203+
}
204+
186205
Ok(())
187206
}
188207
}

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

Lines changed: 83 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
33
use crate::{protocol::Format, ProtocolError};
44
use bytes::{BufMut, BytesMut};
5-
#[cfg(feature = "with-chrono")]
6-
use chrono::{NaiveDate, NaiveDateTime};
7-
use std::io::{Error, ErrorKind};
85

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

110-
// POSTGRES_EPOCH_JDATE
111-
#[cfg(feature = "with-chrono")]
112-
fn pg_base_date_epoch() -> NaiveDateTime {
113-
NaiveDate::from_ymd_opt(2000, 1, 1)
114-
.unwrap()
115-
.and_hms_opt(0, 0, 0)
116-
.unwrap()
117-
}
118-
119-
#[cfg(feature = "with-chrono")]
120-
impl ToProtocolValue for NaiveDate {
121-
// date_out - https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/date.c#L176
122-
fn to_text(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
123-
self.to_string().to_text(buf)
124-
}
125-
126-
// date_send - https://github.com/postgres/postgres/blob/REL_14_4/src/backend/utils/adt/date.c#L223
127-
fn to_binary(&self, buf: &mut BytesMut) -> Result<(), ProtocolError> {
128-
let n = self
129-
.signed_duration_since(pg_base_date_epoch().date())
130-
.num_days();
131-
if n > (i32::MAX as i64) {
132-
return Err(Error::new(
133-
ErrorKind::Other,
134-
format!(
135-
"value too large to store in the binary format (i32), actual: {}",
136-
n
137-
),
138-
)
139-
.into());
140-
}
141-
142-
buf.put_i32(4);
143-
buf.put_i32(n as i32);
144-
145-
Ok(())
146-
}
147-
}
148-
149107
#[cfg(test)]
150108
mod tests {
151109
use crate::*;
152110
use bytes::BytesMut;
111+
#[cfg(feature = "with-chrono")]
112+
use chrono::NaiveDate;
153113

154114
fn assert_text_encode<T: ToProtocolValue>(value: T, expected: &[u8]) {
155115
let mut buf = BytesMut::new();
@@ -179,20 +139,48 @@ mod tests {
179139
105, 110, 115, 32, 53, 46, 48, 48, 48, 48, 48, 54, 32, 115, 101, 99, 115,
180140
],
181141
);
182-
assert_text_encode(
183-
TimestampValue::new(0, None),
184-
&[
185-
0, 0, 0, 26, 49, 57, 55, 48, 45, 48, 49, 45, 48, 49, 32, 48, 48, 58, 48, 48, 58,
186-
48, 48, 46, 48, 48, 48, 48, 48, 48,
187-
],
188-
);
189-
assert_text_encode(
190-
TimestampValue::new(1650890322000000000, None),
191-
&[
192-
0, 0, 0, 26, 50, 48, 50, 50, 45, 48, 52, 45, 50, 53, 32, 49, 50, 58, 51, 56, 58,
193-
52, 50, 46, 48, 48, 48, 48, 48, 48,
194-
],
195-
);
142+
143+
#[cfg(feature = "with-chrono")]
144+
{
145+
// Test TimestampValue encoding
146+
assert_text_encode(
147+
TimestampValue::new(0, None),
148+
&[
149+
0, 0, 0, 26, 49, 57, 55, 48, 45, 48, 49, 45, 48, 49, 32, 48, 48, 58, 48, 48,
150+
58, 48, 48, 46, 48, 48, 48, 48, 48, 48,
151+
],
152+
);
153+
assert_text_encode(
154+
TimestampValue::new(1650890322000000000, None),
155+
&[
156+
0, 0, 0, 26, 50, 48, 50, 50, 45, 48, 52, 45, 50, 53, 32, 49, 50, 58, 51, 56,
157+
58, 52, 50, 46, 48, 48, 48, 48, 48, 48,
158+
],
159+
);
160+
161+
// Test NaiveDate encoding
162+
assert_text_encode(
163+
NaiveDate::from_ymd_opt(2025, 8, 8).unwrap(),
164+
&[
165+
0, 0, 0, 10, // length: 10 bytes
166+
50, 48, 50, 53, 45, 48, 56, 45, 48, 56, // "2025-08-08"
167+
],
168+
);
169+
assert_text_encode(
170+
NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
171+
&[
172+
0, 0, 0, 10, // length: 10 bytes
173+
50, 48, 48, 48, 45, 48, 49, 45, 48, 49, // "2000-01-01"
174+
],
175+
);
176+
assert_text_encode(
177+
NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
178+
&[
179+
0, 0, 0, 10, // length: 10 bytes
180+
49, 57, 57, 57, 45, 49, 50, 45, 51, 49, // "1999-12-31"
181+
],
182+
);
183+
}
196184

197185
Ok(())
198186
}
@@ -218,14 +206,45 @@ mod tests {
218206
0, 0, 0, 16, 0, 0, 0, 2, 146, 85, 83, 70, 0, 0, 0, 2, 0, 0, 0, 1,
219207
],
220208
);
221-
assert_bind_encode(
222-
TimestampValue::new(0, None),
223-
&[0, 0, 0, 8, 255, 252, 162, 254, 196, 200, 32, 0],
224-
);
225-
assert_bind_encode(
226-
TimestampValue::new(1650890322000000000, None),
227-
&[0, 0, 0, 8, 0, 2, 128, 120, 159, 252, 216, 128],
228-
);
209+
210+
#[cfg(feature = "with-chrono")]
211+
{
212+
// Test TimestampValue binary encoding
213+
assert_bind_encode(
214+
TimestampValue::new(0, None),
215+
&[0, 0, 0, 8, 255, 252, 162, 254, 196, 200, 32, 0],
216+
);
217+
assert_bind_encode(
218+
TimestampValue::new(1650890322000000000, None),
219+
&[0, 0, 0, 8, 0, 2, 128, 120, 159, 252, 216, 128],
220+
);
221+
222+
// Test NaiveDate binary encoding
223+
// PostgreSQL epoch is 2000-01-01, so this date should be 0 days
224+
assert_bind_encode(
225+
NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
226+
&[
227+
0, 0, 0, 4, // length: 4 bytes
228+
0, 0, 0, 0, // 0 days from epoch
229+
],
230+
);
231+
// Date after epoch: 2025-08-08 is 9351 days after 2000-01-01
232+
assert_bind_encode(
233+
NaiveDate::from_ymd_opt(2025, 8, 8).unwrap(),
234+
&[
235+
0, 0, 0, 4, // length: 4 bytes
236+
0, 0, 36, 135, // 9351 days from epoch (0x2487 in hex)
237+
],
238+
);
239+
// Date before epoch: 1999-12-31 is -1 day from 2000-01-01
240+
assert_bind_encode(
241+
NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
242+
&[
243+
0, 0, 0, 4, // length: 4 bytes
244+
255, 255, 255, 255, // -1 in two's complement
245+
],
246+
);
247+
}
229248

230249
Ok(())
231250
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Implementation for Extended Query
22
33
#[cfg(feature = "with-chrono")]
4-
use crate::TimestampValue;
4+
use crate::{DateValue, TimestampValue};
55

66
#[derive(Debug, PartialEq)]
77
pub enum BindValue {
@@ -11,5 +11,7 @@ pub enum BindValue {
1111
Bool(bool),
1212
#[cfg(feature = "with-chrono")]
1313
Timestamp(TimestampValue),
14+
#[cfg(feature = "with-chrono")]
15+
Date(DateValue),
1416
Null,
1517
}

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,10 @@ impl Bind {
793793
raw_value,
794794
param_format,
795795
)?),
796+
#[cfg(feature = "with-chrono")]
797+
PgTypeId::DATE => {
798+
BindValue::Date(chrono::NaiveDate::from_protocol(raw_value, param_format)?)
799+
}
796800
_ => {
797801
return Err(ErrorResponse::error(
798802
ErrorCode::FeatureNotSupported,
@@ -1348,6 +1352,60 @@ mod tests {
13481352
Ok(())
13491353
}
13501354

1355+
#[cfg(feature = "with-chrono")]
1356+
#[tokio::test]
1357+
async fn test_frontend_message_parse_bind_date() -> Result<(), ProtocolError> {
1358+
use chrono::NaiveDate;
1359+
1360+
// Test text format date "2025-08-08"
1361+
let buffer = parse_hex_dump(
1362+
r#"
1363+
42 00 00 00 1e 00 73 30 00 00 01 00 00 00 01 00 B.....s0........
1364+
00 00 0a 32 30 32 35 2d 30 38 2d 30 38 00 00 00 ...2025-08-08...
1365+
00 .
1366+
"#
1367+
.to_string(),
1368+
);
1369+
let mut cursor = Cursor::new(buffer);
1370+
let message = read_message(&mut cursor, MessageTagParserDefaultImpl::with_arc()).await?;
1371+
match message {
1372+
FrontendMessage::Bind(body) => {
1373+
assert_eq!(
1374+
body.to_bind_values(&ParameterDescription::new(vec![PgTypeId::DATE]))?,
1375+
vec![BindValue::Date(
1376+
NaiveDate::from_ymd_opt(2025, 8, 8).unwrap()
1377+
)]
1378+
);
1379+
}
1380+
_ => panic!("Wrong message, must be Bind"),
1381+
}
1382+
1383+
// Test binary format date (9351 days from 2000-01-01 for 2025-08-08)
1384+
let buffer = parse_hex_dump(
1385+
r#"
1386+
42 00 00 00 1a 00 73 30 00 00 01 00 01 00 01 00 B.....s0........
1387+
00 00 04 00 00 24 87 00 00 00 00 .....$......
1388+
"#
1389+
.to_string(),
1390+
);
1391+
let mut cursor = Cursor::new(buffer);
1392+
let message = read_message(&mut cursor, MessageTagParserDefaultImpl::with_arc()).await?;
1393+
match message {
1394+
FrontendMessage::Bind(body) => {
1395+
assert_eq!(body.parameter_formats, vec![Format::Binary]);
1396+
assert_eq!(
1397+
body.to_bind_values(&ParameterDescription::new(vec![PgTypeId::DATE]))?,
1398+
vec![BindValue::Date(
1399+
NaiveDate::from_ymd_opt(2025, 8, 8).unwrap()
1400+
)]
1401+
);
1402+
}
1403+
_ => panic!("Wrong message, must be Bind"),
1404+
}
1405+
1406+
Ok(())
1407+
}
1408+
13511409
#[tokio::test]
13521410
async fn test_frontend_message_parse_describe() -> Result<(), ProtocolError> {
13531411
let buffer = parse_hex_dump(

0 commit comments

Comments
 (0)