Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(expr): add support for make_date/time/timestamp #14827

Merged
merged 3 commits into from
Feb 1, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,31 @@ SELECT make_timestamptz(1973, 07, 15, 08, 15, 55.33);
query T
SELECT make_timestamptz(-1973, 07, 15, 08, 15, 55.33);
----
-1973-07-15 08:15:55.330+00:00
-1972-07-15 08:15:55.330+00:00

query error Invalid parameter sec: invalid sec
query error Invalid parameter year, month, day: invalid date: -3-2-29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In postgres we have date field value out of range: -4-02-29. Note the -4 here. Are we behaving correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

> SELECT make_date(-4, 02, 29);
ERROR:  date field value out of range: -3-02-29
> SELECT make_timestamp(-4, 02, 29, 08, 15, 55.33);
ERROR:  date field value out of range: -4-02-29

Pg behaves differently on make_date and make_timestamp. Seems to be a misaligned design. So I think it's OK if we don't follow it strictly.

SELECT make_timestamptz(-4, 02, 29, 08, 15, 55.33);

query T
SELECT make_timestamptz(-5, 02, 29, 08, 15, 55.33);
----
-0004-02-29 08:15:55.330+00:00

query error Invalid parameter sec: invalid sec: -55.33
SELECT make_timestamptz(1973, 07, 15, 08, 15, -55.33);

query error Invalid parameter hour, min, sec: invalid time
query error Invalid parameter hour, min, sec: invalid time: 8:-15:55.33
SELECT make_timestamptz(1973, 07, 15, 08, -15, 55.33);

query error Invalid parameter year, month, day: invalid date
query error Invalid parameter year, month, day: invalid date: 1973--7-15
SELECT make_timestamptz(1973, -07, 15, 08, 15, 55.33);

query error Invalid parameter year, month, day: invalid date
query error Invalid parameter year, month, day: invalid date: 1973-6-31
SELECT make_timestamptz(1973, 06, 31, 08, 15, 55.33);

query error Invalid parameter year, month, day: invalid date: 0-6-31
SELECT make_timestamptz(0, 06, 31, 08, 15, 55.33);

statement ok
set TimeZone to 'America/New_York';

Expand Down Expand Up @@ -88,3 +99,62 @@ SELECT make_timestamptz(2013, 7, 15, 8, 15, 23.5, 'America/New_York');

statement ok
set timezone to 'UTC';

query T
SELECT make_date(2024, 1, 26);
----
2024-01-26

query T
SELECT make_date(-2024, 1, 26);
----
2024-01-26 BC

query error Invalid parameter year, month, day: invalid date: -3-2-29
SELECT make_date(-4, 2, 29);

query T
SELECT make_date(-5, 2, 29);
----
0005-02-29 BC

query error Invalid parameter year, month, day: invalid date: 0-7-15
select make_date(0, 7, 15);

query error Invalid parameter year, month, day: invalid date: 2013-2-30
select make_date(2013, 2, 30);

query error Invalid parameter year, month, day: invalid date: 2013-13-1
select make_date(2013, 13, 1);

query error Invalid parameter year, month, day: invalid date: 2013-11--1
select make_date(2013, 11, -1);

query error Invalid parameter hour, min, sec: invalid time: 10:55:100.1
select make_time(10, 55, 100.1);

query T
SELECT make_time(14, 20, 26);
----
14:20:26

query error Invalid parameter hour, min, sec: invalid time: 24:0:2.1
select make_time(24, 0, 2.1);

query T
SELECT make_timestamp(2024, 1, 26, 14, 20, 26);
----
2024-01-26 14:20:26

query T
SELECT make_timestamp(-1973, 07, 15, 08, 15, 55.33);
----
-1972-07-15 08:15:55.330

query error Invalid parameter year, month, day: invalid date: -3-2-29
SELECT make_timestamp(-4, 02, 29, 08, 15, 55.33);

query T
SELECT make_timestamp(-5, 02, 29, 08, 15, 55.33);
----
-0004-02-29 08:15:55.330
5 changes: 4 additions & 1 deletion proto/expr.proto
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@ message ExprNode {
BITWISE_NOT = 34;
BITWISE_SHIFT_LEFT = 35;
BITWISE_SHIFT_RIGHT = 36;
// date functions
// date/time functions
EXTRACT = 101;
DATE_PART = 102;
TUMBLE_START = 103;
MAKE_DATE = 113;
MAKE_TIME = 114;
MAKE_TIMESTAMP = 115;
// From f64 to timestamp.
// e.g. `select to_timestamp(1672044740.0)`
TO_TIMESTAMP = 104;
Expand Down
160 changes: 160 additions & 0 deletions src/expr/impl/src/scalar/make_time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright 2024 RisingWave Labs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use risingwave_common::types::{Date, FloatExt, Time, Timestamp, Timestamptz, F64};
use risingwave_expr::expr_context::TIME_ZONE;
use risingwave_expr::{capture_context, function, ExprError, Result};

use crate::scalar::timestamptz::timestamp_at_time_zone;

pub fn make_naive_date(mut year: i32, month: i32, day: i32) -> Result<NaiveDate> {
if year == 0 {
return Err(ExprError::InvalidParam {
name: "year, month, day",
reason: format!("invalid date: {}-{}-{}", year, month, day).into(),
});
}
if year < 0 {
year += 1
}
NaiveDate::from_ymd_opt(year, month as u32, day as u32).ok_or_else(|| ExprError::InvalidParam {
name: "year, month, day",
reason: format!("invalid date: {}-{}-{}", year, month, day).into(),
})
}

fn make_naive_time(hour: i32, min: i32, sec: F64) -> Result<NaiveTime> {
if !sec.is_finite() || sec.0.is_sign_negative() {
return Err(ExprError::InvalidParam {
name: "sec",
reason: format!("invalid sec: {}", sec).into(),
});
}
let sec_u32 = sec.0.trunc() as u32;
let microsecond_u32 = ((sec.0 - sec.0.trunc()) * 1_000_000.0).round_ties_even() as u32;
NaiveTime::from_hms_micro_opt(hour as u32, min as u32, sec_u32, microsecond_u32).ok_or_else(
|| ExprError::InvalidParam {
name: "hour, min, sec",
reason: format!("invalid time: {}:{}:{}", hour, min, sec).into(),
},
)
}

// year int, month int, day int
#[function("make_date(int4, int4, int4) -> date")]
pub fn make_date(year: i32, month: i32, day: i32) -> Result<Date> {
Ok(Date(make_naive_date(year, month, day)?))
}

// hour int, min int, sec double precision
#[function("make_time(int4, int4, float8) -> time")]
pub fn make_time(hour: i32, min: i32, sec: F64) -> Result<Time> {
Ok(Time(make_naive_time(hour, min, sec)?))
}

// year int, month int, day int, hour int, min int, sec double precision
#[function("make_timestamp(int4, int4, int4, int4, int4, float8) -> timestamp")]
pub fn make_timestamp(
year: i32,
month: i32,
day: i32,
hour: i32,
min: i32,
sec: F64,
) -> Result<Timestamp> {
Ok(Timestamp(NaiveDateTime::new(
make_naive_date(year, month, day)?,
make_naive_time(hour, min, sec)?,
)))
}

// year int, month int, day int, hour int, min int, sec double precision
#[function("make_timestamptz(int4, int4, int4, int4, int4, float8) -> timestamptz")]
pub fn make_timestamptz(
year: i32,
month: i32,
day: i32,
hour: i32,
min: i32,
sec: F64,
) -> Result<Timestamptz> {
make_timestamptz_impl_captured(year, month, day, hour, min, sec)
}

// year int, month int, day int, hour int, min int, sec double precision, timezone text
#[function("make_timestamptz(int4, int4, int4, int4, int4, float8, varchar) -> timestamptz")]
pub fn make_timestamptz_with_time_zone(
year: i32,
month: i32,
day: i32,
hour: i32,
min: i32,
sec: F64,
time_zone: &str,
) -> Result<Timestamptz> {
make_timestamptz_impl(time_zone, year, month, day, hour, min, sec)
}

#[capture_context(TIME_ZONE)]
fn make_timestamptz_impl(
time_zone: &str,
year: i32,
month: i32,
day: i32,
hour: i32,
min: i32,
sec: F64,
) -> Result<Timestamptz> {
let naive_date_time = NaiveDateTime::new(
make_naive_date(year, month, day)?,
make_naive_time(hour, min, sec)?,
);
timestamp_at_time_zone(Timestamp(naive_date_time), time_zone)
}

#[cfg(test)]
mod tests {
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use risingwave_common::types::{Date, Timestamp};

/// This test is to testify that our `Date` expressess a year `-X` as `X+1 BC`, while `Timestamp` expresses it as `-X`.
/// Can be removed if we change the `ToText` implementation of `Date` or `Timestamp`.
#[test]
fn test_naive_date_and_time() {
let year = -1973;
let month = 2;
let day = 2;
let hour = 12;
let min = 34;
let sec: f64 = 56.789;
let naive_date = NaiveDate::from_ymd_opt(year, month as u32, day as u32).unwrap();
let naive_time = NaiveTime::from_hms_micro_opt(
hour as u32,
min as u32,
sec.trunc() as u32,
((sec - sec.trunc()) * 1_000_000.0).round() as u32,
)
.unwrap();
assert_eq!(naive_date.to_string(), String::from("-1973-02-02"));
let date = Date(naive_date);
assert_eq!(date.to_string(), String::from("1974-02-02 BC"));
assert_eq!(naive_time.to_string(), String::from("12:34:56.789"));
let date_time = Timestamp(NaiveDateTime::new(naive_date, naive_time));
assert_eq!(
date_time.to_string(),
String::from("-1973-02-02 12:34:56.789")
);
}
}
82 changes: 0 additions & 82 deletions src/expr/impl/src/scalar/make_timestamptz.rs

This file was deleted.

2 changes: 1 addition & 1 deletion src/expr/impl/src/scalar/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ mod jsonb_object;
mod jsonb_path;
mod length;
mod lower;
mod make_timestamptz;
mod make_time;
mod md5;
mod overlay;
mod position;
Expand Down
3 changes: 3 additions & 0 deletions src/frontend/src/binder/expr/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,9 @@ impl Binder {
),
("date_trunc", raw_call(ExprType::DateTrunc)),
("date_part", raw_call(ExprType::DatePart)),
("make_date", raw_call(ExprType::MakeDate)),
("make_time", raw_call(ExprType::MakeTime)),
("make_timestamp", raw_call(ExprType::MakeTimestamp)),
("to_date", raw_call(ExprType::CharToDate)),
("make_timestamptz", raw_call(ExprType::MakeTimestamptz)),
// string
Expand Down
3 changes: 3 additions & 0 deletions src/frontend/src/expr/pure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ impl ExprVisitor for ImpureAnalyzer {
| expr_node::Type::ToTimestamp
| expr_node::Type::AtTimeZone
| expr_node::Type::DateTrunc
| expr_node::Type::MakeDate
| expr_node::Type::MakeTime
| expr_node::Type::MakeTimestamp
| expr_node::Type::ToTimestamp1
| expr_node::Type::CharToDate
| expr_node::Type::CastWithTimeZone
Expand Down
Loading