Skip to content

Commit 5ef4ab2

Browse files
committed
feat(cubesql): Support date type for parameter binding
1 parent 9d5794e commit 5ef4ab2

File tree

8 files changed

+303
-65
lines changed

8 files changed

+303
-65
lines changed

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

Lines changed: 3 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
}

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
33
#[cfg(feature = "with-chrono")]
44
use crate::TimestampValue;
5+
#[cfg(feature = "with-chrono")]
6+
use chrono::NaiveDate;
57

68
#[derive(Debug, PartialEq)]
79
pub enum BindValue {
@@ -11,5 +13,7 @@ pub enum BindValue {
1113
Bool(bool),
1214
#[cfg(feature = "with-chrono")]
1315
Timestamp(TimestampValue),
16+
#[cfg(feature = "with-chrono")]
17+
Date(NaiveDate),
1418
Null,
1519
}

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,11 @@ impl Bind {
793793
raw_value,
794794
param_format,
795795
)?),
796+
#[cfg(feature = "with-chrono")]
797+
PgTypeId::DATE => BindValue::Date(chrono::NaiveDate::from_protocol(
798+
raw_value,
799+
param_format,
800+
)?),
796801
_ => {
797802
return Err(ErrorResponse::error(
798803
ErrorCode::FeatureNotSupported,
@@ -1348,6 +1353,56 @@ mod tests {
13481353
Ok(())
13491354
}
13501355

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

0 commit comments

Comments
 (0)