diff --git a/doc/user/content/sql/functions/justify-days.md b/doc/user/content/sql/functions/justify-days.md index 37b0f5cddd833..a761f12a02e15 100644 --- a/doc/user/content/sql/functions/justify-days.md +++ b/doc/user/content/sql/functions/justify-days.md @@ -30,5 +30,5 @@ SELECT justify_days(interval '35 days'); ```nofmt justify_days ---------------- - 1 month 5 days + 1 mon 5 days ``` diff --git a/doc/user/content/sql/types/interval.md b/doc/user/content/sql/types/interval.md index 52a59f6646010..0eeba16e98513 100644 --- a/doc/user/content/sql/types/interval.md +++ b/doc/user/content/sql/types/interval.md @@ -19,8 +19,8 @@ Detail | Info **Size** | 16 bytes **Catalog name** | `pg_catalog.interval` **OID** | 1186 -**Min value** | -178956970 years -8 months -2147483648 days -2562047788:00:54.775808 -**Max value** | 178956970 years 7 months 2147483647 days 2562047788:00:54.775807 +**Min value** | -178956970 years -8 mons -2147483648 days -2562047788:00:54.775808 +**Max value** | 178956970 years 7 mons 2147483647 days 2562047788:00:54.775807 ## Syntax @@ -125,7 +125,7 @@ SELECT INTERVAL '1-2 3 4:5:6.7' AS interval_p; ```nofmt interval_f ----------------------------------- - 1 year 2 months 3 days 04:05:06.7 + 1 year 2 mons 3 days 04:05:06.7 ``` ### PostgreSQL syntax @@ -182,7 +182,7 @@ SELECT INTERVAL '1 day 2-3 4' MINUTE AS interval_w; ```nofmt interval_w --------------------------------- - 2 years 3 months 1 day 00:04:00 + 2 years 3 mons 1 day 00:04:00 ``` ### Interaction with timestamps diff --git a/misc/python/materialize/checks/all_checks/datetime_functions.py b/misc/python/materialize/checks/all_checks/datetime_functions.py index c954302300618..2150d0cfe974e 100644 --- a/misc/python/materialize/checks/all_checks/datetime_functions.py +++ b/misc/python/materialize/checks/all_checks/datetime_functions.py @@ -75,7 +75,13 @@ def validate(self) -> Testdrive: 3 334 "2024-12-31 23:59:59" "2024-12-31 18:59:59" "2024-12-31 18:59:59" 4 335 "2025-01-01 00:00:00" "2024-12-31 19:00:00" "2024-12-31 19:00:00" - > SELECT id, jd::text, jh::text, ji::text FROM datetime_fns_convert1 + >[version>=2603500] SELECT id, jd::text, jh::text, ji::text FROM datetime_fns_convert1 + 1 "1 mon 5 days" "35 days" "1 mon 5 days" + 2 27:00:00 "1 day 03:00:00" "1 day 03:00:00" + 3 "1 mon -01:00:00" "1 mon -01:00:00" "29 days 23:00:00" + 4 00:00:00 00:00:00 00:00:00 + + >[version<2603500] SELECT id, jd::text, jh::text, ji::text FROM datetime_fns_convert1 1 "1 month 5 days" "35 days" "1 month 5 days" 2 27:00:00 "1 day 03:00:00" "1 day 03:00:00" 3 "1 month -01:00:00" "1 month -01:00:00" "29 days 23:00:00" diff --git a/misc/python/materialize/version_consistency/ignore_filter/version_consistency_ignore_filter.py b/misc/python/materialize/version_consistency/ignore_filter/version_consistency_ignore_filter.py index 0563ba504df93..f83d0dcdaf99d 100644 --- a/misc/python/materialize/version_consistency/ignore_filter/version_consistency_ignore_filter.py +++ b/misc/python/materialize/version_consistency/ignore_filter/version_consistency_ignore_filter.py @@ -43,6 +43,9 @@ from materialize.output_consistency.input_data.operations.string_operations_provider import ( TAG_REGEX, ) +from materialize.output_consistency.input_data.types.date_time_types_provider import ( + INTERVAL_TYPE_IDENTIFIER, +) from materialize.output_consistency.input_data.types.number_types_provider import ( NON_INTEGER_TYPE_IDENTIFIERS, ) @@ -66,6 +69,7 @@ MZ_VERSION_0_117_0 = MzVersion.parse_mz("v0.117.0") MZ_VERSION_0_118_0 = MzVersion.parse_mz("v0.118.0") MZ_VERSION_0_128_0 = MzVersion.parse_mz("v0.128.0") +MZ_VERSION_26_35_0 = MzVersion.parse_mz("v26.35.0") class VersionConsistencyIgnoreFilter(GenericInconsistencyIgnoreFilter): @@ -315,6 +319,20 @@ def _shall_ignore_content_mismatch( "Involvement of unnest changes order with PR#29422 for both DFR and CTF" ) + if ( + self.lower_version <= MZ_VERSION_26_35_0 <= self.higher_version + and query_template.matches_any_expression( + partial( + is_known_to_involve_exact_data_types, + internal_data_type_identifiers={INTERVAL_TYPE_IDENTIFIER}, + ), + True, + ) + ): + return YesIgnore( + "Interval text output changed from 'month(s)' to 'mon(s)' with SQL-463" + ) + return super()._shall_ignore_content_mismatch( error, query_template, diff --git a/src/expr/src/scalar/func/impls/date.rs b/src/expr/src/scalar/func/impls/date.rs index f36c887df72e1..8da16e5721e6c 100644 --- a/src/expr/src/scalar/func/impls/date.rs +++ b/src/expr/src/scalar/func/impls/date.rs @@ -139,7 +139,7 @@ pub fn extract_date_inner(units: DateTimeUnits, date: NaiveDate) -> Result Ok(Numeric::from(date.millennium())), DateTimeUnits::Century => Ok(Numeric::from(date.century())), DateTimeUnits::Decade => Ok(Numeric::from(date.decade())), - DateTimeUnits::Year => Ok(Numeric::from(date.year())), + DateTimeUnits::Year => Ok(Numeric::from(date.extract_year())), DateTimeUnits::Quarter => Ok(Numeric::from(date.quarter())), DateTimeUnits::Week => Ok(Numeric::from(date.iso_week_number())), DateTimeUnits::Month => Ok(Numeric::from(date.month())), diff --git a/src/expr/src/scalar/func/impls/timestamp.rs b/src/expr/src/scalar/func/impls/timestamp.rs index 5648020899542..38084ba147568 100644 --- a/src/expr/src/scalar/func/impls/timestamp.rs +++ b/src/expr/src/scalar/func/impls/timestamp.rs @@ -417,7 +417,7 @@ where DateTimeUnits::Millennium => Ok(D::from(ts.millennium())), DateTimeUnits::Century => Ok(D::from(ts.century())), DateTimeUnits::Decade => Ok(D::from(ts.decade())), - DateTimeUnits::Year => Ok(D::from(ts.year())), + DateTimeUnits::Year => Ok(D::from(ts.extract_year())), DateTimeUnits::Quarter => Ok(D::from(ts.quarter())), DateTimeUnits::Week => Ok(D::from(ts.iso_week_number())), DateTimeUnits::Month => Ok(D::from(ts.month())), diff --git a/src/expr/src/scalar/func/variadic.rs b/src/expr/src/scalar/func/variadic.rs index 125784661cc36..1f1164272ad9d 100644 --- a/src/expr/src/scalar/func/variadic.rs +++ b/src/expr/src/scalar/func/variadic.rs @@ -1056,7 +1056,6 @@ fn make_mz_acl_item( } #[sqlfunc(sqlname = "makets")] -// TODO(benesch): remove potentially dangerous usage of `as`. #[allow(clippy::as_conversions)] fn make_timestamp( year: i64, @@ -1066,36 +1065,37 @@ fn make_timestamp( minute: i64, second_float: f64, ) -> Result>, EvalError> { - let year: i32 = match year.try_into() { - Ok(year) => year, - Err(_) => return Ok(None), - }; - let month: u32 = match month.try_into() { - Ok(month) => month, - Err(_) => return Ok(None), - }; - let day: u32 = match day.try_into() { - Ok(day) => day, - Err(_) => return Ok(None), - }; - let hour: u32 = match hour.try_into() { - Ok(hour) => hour, - Err(_) => return Ok(None), - }; - let minute: u32 = match minute.try_into() { - Ok(minute) => minute, - Err(_) => return Ok(None), - }; - let second = second_float as u32; - let micros = ((second_float - second as f64) * 1_000_000.0) as u32; - let date = match NaiveDate::from_ymd_opt(year, month, day) { - Some(date) => date, - None => return Ok(None), - }; - let timestamp = match date.and_hms_micro_opt(hour, minute, second, micros) { - Some(timestamp) => timestamp, - None => return Ok(None), - }; + // NOTE: This function never returns `Ok(None)`. The `Option` stays so + // that the derived `introduces_nulls` (and with it the nullability of + // existing objects planned with this function) does not change. + + // Negative years are BC and there is no year 0. Chrono's astronomical + // numbering has 1 BC as year 0, so BC years shift by one. + if year == 0 { + return Err(EvalError::DateOutOfRange); + } + let year = if year < 0 { year + 1 } else { year }; + let year: i32 = year.try_into().map_err(|_| EvalError::DateOutOfRange)?; + let month: u32 = month.try_into().map_err(|_| EvalError::DateOutOfRange)?; + let day: u32 = day.try_into().map_err(|_| EvalError::DateOutOfRange)?; + let date = NaiveDate::from_ymd_opt(year, month, day).ok_or(EvalError::DateOutOfRange)?; + // Allow exactly 24:00:00 (rolls over to midnight of the next day) and + // 60 seconds (rolls over into the next minute). + if !(0..=24).contains(&hour) + || !(0..=59).contains(&minute) + || !(0.0..=60.0).contains(&second_float) + || (hour == 24 && (minute > 0 || second_float > 0.0)) + { + return Err(EvalError::TimestampOutOfRange); + } + // The cast is exact: second_float is in [0, 60]. + let micros = (second_float * 1_000_000.0).round() as i64; + let time = chrono::Duration::microseconds((hour * 60 + minute) * 60 * 1_000_000 + micros); + let timestamp = date + .and_hms_opt(0, 0, 0) + .unwrap() + .checked_add_signed(time) + .ok_or(EvalError::TimestampOutOfRange)?; Ok(Some(timestamp.try_into()?)) } diff --git a/src/repr/src/adt/datetime.rs b/src/repr/src/adt/datetime.rs index 12232874f325e..72aea07e75dcd 100644 --- a/src/repr/src/adt/datetime.rs +++ b/src/repr/src/adt/datetime.rs @@ -735,7 +735,10 @@ impl ParsedDateTime { if fmt.is_none() { fmt = match value_parts.pop_front() { Some(next_part) => { - match determine_format_w_datetimefield(next_part.clone(), None)? { + match determine_format_w_datetimefield( + next_part.clone(), + leading_time_precision, + )? { Some(TimePartFormat::SqlStandard(f)) => { match f { // Do not capture this token because expression @@ -813,12 +816,13 @@ impl ParsedDateTime { pub fn build_parsed_datetime_timestamp( value: &str, era: CalendarEra, + order: DateOrder, ) -> Result { let mut pdt = ParsedDateTime::default(); let mut ts_actual = tokenize_time_str(value)?; - fill_pdt_date(&mut pdt, &mut ts_actual)?; + fill_pdt_date(&mut pdt, &mut ts_actual, order, era)?; if let CalendarEra::BC = era { pdt.year = pdt.year.map(|mut y| { @@ -1080,6 +1084,8 @@ impl ParsedDateTime { fn fill_pdt_date( pdt: &mut ParsedDateTime, actual: &mut VecDeque, + order: DateOrder, + era: CalendarEra, ) -> Result<(), String> { use TimeStrToken::*; @@ -1124,6 +1130,26 @@ fn fill_pdt_date( _ => (), } + // PostgreSQL under the pinned `DateStyle = ISO, MDY` reads a separated + // date whose leading field has one or two digits as month-day-year, and + // only a leading field of three or more digits as year-month-day. The + // formats below fill fields positionally as year-month-day, so record the + // digit counts up front and reorder the fields after a successful fill. + let mdy = match (order, actual.front()) { + (DateOrder::Mdy, Some(&Num(_, digits))) => digits <= 2, + _ => false, + }; + // Digit count of the third numeric field, the year in a month-day-year + // date, which decides two-digit-year windowing. + let year_digits = actual + .iter() + .take_while(|t| matches!(t, Num(..) | Dash | Delim)) + .filter_map(|t| match t { + Num(_, digits) => Some(*digits), + _ => None, + }) + .nth(2); + let valid_formats = [ [ Num(0, 1), // year @@ -1153,6 +1179,22 @@ fn fill_pdt_date( for expected in valid_formats { match fill_pdt_from_tokens(pdt, actual, &expected, DateTimeField::Year, 1) { Ok(()) => { + // In a month-day-year date the positional fill above put the + // month in `year`, the day in `month`, and the year in `day`. + if mdy { + if let (Some(month), Some(day), Some(mut year)) = (pdt.year, pdt.month, pdt.day) + { + // Window two-digit years into 1970..=2069, as + // PostgreSQL does. Explicit BC years are never + // windowed. + if era == CalendarEra::AD && year_digits.is_some_and(|d| d <= 2) { + year.unit += if year.unit < 70 { 2000 } else { 1900 }; + } + pdt.year = Some(year); + pdt.month = Some(month); + pdt.day = Some(day); + } + } return Ok(()); } Err(_) => { @@ -1810,12 +1852,25 @@ pub(crate) fn tokenize_time_str(value: &str) -> Result, S Ok(toks) } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CalendarEra { BC, AD, } +/// Determines how ambiguous all-numeric dates like `01/02/03` are interpreted. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DateOrder { + /// PostgreSQL semantics under the pinned `DateStyle = ISO, MDY`: a date + /// whose leading field has one or two digits is month-day-year, and a + /// two-digit year is windowed into 1970..=2069. + Mdy, + /// Always year-month-day, with no year windowing. Frozen behavior for + /// storage source casts, which must not change across releases (see the + /// stability contract in `mz_storage_types::sources::casts`). + LegacyYmd, +} + /// Takes a 'date timezone' 'date time timezone' string and splits it into 'date /// {time}' and 'timezone' components then checks for a 'CalenderEra' defaulting /// to 'AD'. @@ -2839,10 +2894,79 @@ mod tests { ..Default::default() }, ); + // A one- or two-digit leading field starts a month-day-year date, and + // a two-digit year is windowed into 1970..=2069. + run_test_build_parsed_datetime_timestamp( + "01/02/03", + ParsedDateTime { + year: Some(DateTimeFieldValue::new(2003, 0)), + month: Some(DateTimeFieldValue::new(1, 0)), + day: Some(DateTimeFieldValue::new(2, 0)), + ..Default::default() + }, + ); + run_test_build_parsed_datetime_timestamp( + "01-02-70 3:4:5.6", + ParsedDateTime { + year: Some(DateTimeFieldValue::new(1970, 0)), + month: Some(DateTimeFieldValue::new(1, 0)), + day: Some(DateTimeFieldValue::new(2, 0)), + hour: Some(DateTimeFieldValue::new(3, 0)), + minute: Some(DateTimeFieldValue::new(4, 0)), + second: Some(DateTimeFieldValue::new(5, 600_000_000)), + ..Default::default() + }, + ); + // A four-digit year in month-day-year position is not windowed. + run_test_build_parsed_datetime_timestamp( + "1/2/1999", + ParsedDateTime { + year: Some(DateTimeFieldValue::new(1999, 0)), + month: Some(DateTimeFieldValue::new(1, 0)), + day: Some(DateTimeFieldValue::new(2, 0)), + ..Default::default() + }, + ); + // BC years are never windowed. + assert_eq!( + ParsedDateTime::build_parsed_datetime_timestamp( + "01/02/03", + CalendarEra::BC, + DateOrder::Mdy, + ) + .unwrap(), + ParsedDateTime { + year: Some(DateTimeFieldValue::new(-3, 0)), + month: Some(DateTimeFieldValue::new(1, 0)), + day: Some(DateTimeFieldValue::new(2, 0)), + ..Default::default() + }, + ); + // The legacy order keeps the frozen year-month-day interpretation + // with no windowing. + assert_eq!( + ParsedDateTime::build_parsed_datetime_timestamp( + "01/02/03", + CalendarEra::AD, + DateOrder::LegacyYmd, + ) + .unwrap(), + ParsedDateTime { + year: Some(DateTimeFieldValue::new(1, 0)), + month: Some(DateTimeFieldValue::new(2, 0)), + day: Some(DateTimeFieldValue::new(3, 0)), + ..Default::default() + }, + ); fn run_test_build_parsed_datetime_timestamp(test: &str, res: ParsedDateTime) { assert_eq!( - ParsedDateTime::build_parsed_datetime_timestamp(test, CalendarEra::AD).unwrap(), + ParsedDateTime::build_parsed_datetime_timestamp( + test, + CalendarEra::AD, + DateOrder::Mdy + ) + .unwrap(), res ); } diff --git a/src/repr/src/adt/interval.rs b/src/repr/src/adt/interval.rs index 65e775b5914d4..a61d64b1253e5 100644 --- a/src/repr/src/adt/interval.rs +++ b/src/repr/src/adt/interval.rs @@ -448,27 +448,6 @@ impl Interval { Ok(Duration::from_micros(micros)) } - /// Truncate the "head" of the interval, removing all time units greater than `f`. - pub fn truncate_high_fields(&mut self, f: DateTimeField) { - match f { - DateTimeField::Year => {} - DateTimeField::Month => self.months %= 12, - DateTimeField::Day => self.months = 0, - DateTimeField::Hour | DateTimeField::Minute | DateTimeField::Second => { - self.months = 0; - self.days = 0; - self.micros %= f.next_largest().micros_multiplier() - } - DateTimeField::Millennium - | DateTimeField::Century - | DateTimeField::Decade - | DateTimeField::Milliseconds - | DateTimeField::Microseconds => { - unreachable!("Cannot truncate interval by {f}"); - } - } - } - /// Truncate the "tail" of the interval, removing all time units less than `f`. /// # Arguments /// - `f`: Round the interval down to the specified time unit. @@ -753,7 +732,7 @@ impl Interval { /// /// Example outputs: /// -/// * 1 year 2 months 5 days 03:04:00 +/// * 1 year 2 mons 5 days 03:04:00 /// * -1 year +5 days +18:59:29.3 /// * 00:00:00 impl fmt::Display for Interval { @@ -793,7 +772,7 @@ impl fmt::Display for Interval { if neg_months { f.write_char('-')?; } - write!(f, "{} month", months)?; + write!(f, "{} mon", months)?; if months > 1 || neg_months { f.write_char('s')?; } @@ -943,12 +922,12 @@ mod test { .to_string() } - assert_eq!(mon(1), "1 month"); + assert_eq!(mon(1), "1 mon"); assert_eq!(mon(12), "1 year"); - assert_eq!(mon(13), "1 year 1 month"); + assert_eq!(mon(13), "1 year 1 mon"); assert_eq!(mon(24), "2 years"); - assert_eq!(mon(25), "2 years 1 month"); - assert_eq!(mon(26), "2 years 2 months"); + assert_eq!(mon(25), "2 years 1 mon"); + assert_eq!(mon(26), "2 years 2 mons"); fn dur(days: i32, micros: i64) -> String { Interval::new(0, days, micros).to_string() @@ -1022,14 +1001,14 @@ mod test { fn mon_dur(mon: i32, days: i32, micros: i64) -> String { Interval::new(mon, days, micros).to_string() } - assert_eq!(&mon_dur(1, 2, 6 * 1_000_000), "1 month 2 days 00:00:06"); + assert_eq!(&mon_dur(1, 2, 6 * 1_000_000), "1 mon 2 days 00:00:06"); assert_eq!( &mon_dur(1, 2, (45 * 60 * 1_000_000) + (6 * 1_000_000)), - "1 month 2 days 00:45:06" + "1 mon 2 days 00:45:06" ); assert_eq!( &mon_dur(1, 2, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)), - "1 month 2 days 03:00:06" + "1 mon 2 days 03:00:06" ); assert_eq!( &mon_dur( @@ -1037,37 +1016,37 @@ mod test { 0, (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (6 * 1_000_000) ), - "2 years 2 months 03:45:06" + "2 years 2 mons 03:45:06" ); assert_eq!( &mon_dur(26, 0, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)), - "2 years 2 months 03:00:06" + "2 years 2 mons 03:00:06" ); assert_eq!( &mon_dur(26, 0, 3 * 60 * 60 * 1_000_000), - "2 years 2 months 03:00:00" + "2 years 2 mons 03:00:00" ); assert_eq!( &mon_dur(26, 0, (45 * 60 * 1_000_000) + (6 * 1_000_000)), - "2 years 2 months 00:45:06" + "2 years 2 mons 00:45:06" ); assert_eq!( &mon_dur(26, 0, 45 * 60 * 1_000_000), - "2 years 2 months 00:45:00" + "2 years 2 mons 00:45:00" ); - assert_eq!(&mon_dur(26, 0, 6 * 1_000_000), "2 years 2 months 00:00:06"); + assert_eq!(&mon_dur(26, 0, 6 * 1_000_000), "2 years 2 mons 00:00:06"); assert_eq!( &mon_dur(26, -2, -6 * 1_000_000), - "2 years 2 months -2 days -00:00:06" + "2 years 2 mons -2 days -00:00:06" ); assert_eq!( &mon_dur(26, -2, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)), - "2 years 2 months -2 days -00:45:06" + "2 years 2 mons -2 days -00:45:06" ); assert_eq!( &mon_dur(26, -2, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)), - "2 years 2 months -2 days -03:00:06" + "2 years 2 mons -2 days -03:00:06" ); assert_eq!( &mon_dur( @@ -1075,37 +1054,34 @@ mod test { 0, (-3 * 60 * 60 * 1_000_000) + (-45 * 60 * 1_000_000) + (-6 * 1_000_000) ), - "2 years 2 months -03:45:06" + "2 years 2 mons -03:45:06" ); assert_eq!( &mon_dur(26, 0, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)), - "2 years 2 months -03:00:06" + "2 years 2 mons -03:00:06" ); assert_eq!( &mon_dur(26, 0, -3 * 60 * 60 * 1_000_000), - "2 years 2 months -03:00:00" + "2 years 2 mons -03:00:00" ); assert_eq!( &mon_dur(26, 0, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)), - "2 years 2 months -00:45:06" + "2 years 2 mons -00:45:06" ); assert_eq!( &mon_dur(26, 0, -45 * 60 * 1_000_000), - "2 years 2 months -00:45:00" - ); - assert_eq!( - &mon_dur(26, 0, -6 * 1_000_000), - "2 years 2 months -00:00:06" + "2 years 2 mons -00:45:00" ); + assert_eq!(&mon_dur(26, 0, -6 * 1_000_000), "2 years 2 mons -00:00:06"); - assert_eq!(&mon_dur(-1, 2, 6 * 1_000_000), "-1 months +2 days 00:00:06"); + assert_eq!(&mon_dur(-1, 2, 6 * 1_000_000), "-1 mons +2 days 00:00:06"); assert_eq!( &mon_dur(-1, 2, (45 * 60 * 1_000_000) + (6 * 1_000_000)), - "-1 months +2 days 00:45:06" + "-1 mons +2 days 00:45:06" ); assert_eq!( &mon_dur(-1, 2, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)), - "-1 months +2 days 03:00:06" + "-1 mons +2 days 03:00:06" ); assert_eq!( &mon_dur( @@ -1113,40 +1089,40 @@ mod test { 0, (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (6 * 1_000_000) ), - "-2 years -2 months +03:45:06" + "-2 years -2 mons +03:45:06" ); assert_eq!( &mon_dur(-26, 0, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)), - "-2 years -2 months +03:00:06" + "-2 years -2 mons +03:00:06" ); assert_eq!( &mon_dur(-26, 0, 3 * 60 * 60 * 1_000_000), - "-2 years -2 months +03:00:00" + "-2 years -2 mons +03:00:00" ); assert_eq!( &mon_dur(-26, 0, (45 * 60 * 1_000_000) + (6 * 1_000_000)), - "-2 years -2 months +00:45:06" + "-2 years -2 mons +00:45:06" ); assert_eq!( &mon_dur(-26, 0, 45 * 60 * 1_000_000), - "-2 years -2 months +00:45:00" + "-2 years -2 mons +00:45:00" ); assert_eq!( &mon_dur(-26, 0, 6 * 1_000_000), - "-2 years -2 months +00:00:06" + "-2 years -2 mons +00:00:06" ); assert_eq!( &mon_dur(-26, -2, -6 * 1_000_000), - "-2 years -2 months -2 days -00:00:06" + "-2 years -2 mons -2 days -00:00:06" ); assert_eq!( &mon_dur(-26, -2, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)), - "-2 years -2 months -2 days -00:45:06" + "-2 years -2 mons -2 days -00:45:06" ); assert_eq!( &mon_dur(-26, -2, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)), - "-2 years -2 months -2 days -03:00:06" + "-2 years -2 mons -2 days -03:00:06" ); assert_eq!( &mon_dur( @@ -1154,27 +1130,27 @@ mod test { 0, (-3 * 60 * 60 * 1_000_000) + (-45 * 60 * 1_000_000) + (-6 * 1_000_000) ), - "-2 years -2 months -03:45:06" + "-2 years -2 mons -03:45:06" ); assert_eq!( &mon_dur(-26, 0, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)), - "-2 years -2 months -03:00:06" + "-2 years -2 mons -03:00:06" ); assert_eq!( &mon_dur(-26, 0, -3 * 60 * 60 * 1_000_000), - "-2 years -2 months -03:00:00" + "-2 years -2 mons -03:00:00" ); assert_eq!( &mon_dur(-26, 0, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)), - "-2 years -2 months -00:45:06" + "-2 years -2 mons -00:45:06" ); assert_eq!( &mon_dur(-26, 0, -45 * 60 * 1_000_000), - "-2 years -2 months -00:45:00" + "-2 years -2 mons -00:45:00" ); assert_eq!( &mon_dur(-26, 0, -6 * 1_000_000), - "-2 years -2 months -00:00:06" + "-2 years -2 mons -00:00:06" ); } @@ -1293,99 +1269,6 @@ mod test { } } - #[mz_ore::test] - fn test_interval_value_truncate_high_fields() { - use DateTimeField::*; - - // (month, day, microsecond) tuples - let mut test_cases = [ - ( - Year, - ( - 321, - 7, - (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000), - ), - ( - 321, - 7, - (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000), - ), - ), - ( - Month, - ( - 321, - 7, - (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000), - ), - ( - 9, - 7, - (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000), - ), - ), - ( - Day, - ( - 321, - 7, - (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000), - ), - ( - 0, - 7, - (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000), - ), - ), - ( - Hour, - ( - 321, - 7, - (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000), - ), - ( - 0, - 0, - (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000), - ), - ), - ( - Minute, - ( - 321, - 7, - (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000), - ), - (0, 0, (45 * 60 * 1_000_000) + (21 * 1_000_000)), - ), - ( - Second, - ( - 321, - 7, - (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000), - ), - (0, 0, 21 * 1_000_000), - ), - ]; - - for test in test_cases.iter_mut() { - let mut i = Interval::new((test.1).0, (test.1).1, (test.1).2); - let j = Interval::new((test.2).0, (test.2).1, (test.2).2); - - i.truncate_high_fields(test.0); - - if i != j { - panic!( - "test_interval_value_truncate_high_fields failed on {} \n actual: {:?} \n expected: {:?}", - test.0, i, j - ); - } - } - } - #[mz_ore::test] fn test_convert_date_time_unit() { assert_eq!( diff --git a/src/repr/src/adt/timestamp.rs b/src/repr/src/adt/timestamp.rs index a3b0853db29c3..8ea0b3c8d6ff2 100644 --- a/src/repr/src/adt/timestamp.rs +++ b/src/repr/src/adt/timestamp.rs @@ -184,6 +184,14 @@ pub trait DateLike: chrono::Datelike { naive_date.and_utc().timestamp() } + /// The year in SQL's numbering, where 1 BC is year -1 and there is no + /// year 0. Chrono's `year` uses astronomical numbering, where 1 BC is + /// year 0. + fn extract_year(&self) -> i32 { + let year = self.year(); + if year <= 0 { year - 1 } else { year } + } + fn millennium(&self) -> i32 { (self.year() + if self.year() > 0 { 999 } else { -1_000 }) / 1_000 } diff --git a/src/repr/src/strconv.rs b/src/repr/src/strconv.rs index 84023a9a9fa36..b09784f58f0bb 100644 --- a/src/repr/src/strconv.rs +++ b/src/repr/src/strconv.rs @@ -52,7 +52,7 @@ use uuid::Uuid; use crate::adt::array::ArrayDimension; use crate::adt::date::Date; -use crate::adt::datetime::{self, DateTimeField, ParsedDateTime}; +use crate::adt::datetime::{self, DateOrder, DateTimeField, ParsedDateTime}; use crate::adt::interval::Interval; use crate::adt::jsonb::{Jsonb, JsonbRef}; use crate::adt::mz_acl_item::{AclItem, MzAclItem}; @@ -225,6 +225,18 @@ pub fn parse_mz_timestamp(s: &str) -> Result { .map_err(|e| ParseError::invalid_input_syntax("mz_timestamp", s).with_details(e)) } +/// Parses an `mz_timestamp` from `s` with the frozen legacy date parsing in +/// its timestamp fallback. +/// +/// NOTE: This exists solely to keep the storage source cast +/// `CastStringToMzTimestamp` evaluation-stable across releases (see the +/// stability contract in `mz_storage_types::sources::casts`). Use +/// [`parse_mz_timestamp`] everywhere else. +pub fn parse_mz_timestamp_legacy(s: &str) -> Result { + crate::Timestamp::from_str_legacy(s.trim()) + .map_err(|e| ParseError::invalid_input_syntax("mz_timestamp", s).with_details(e)) +} + /// Writes an `mz_timestamp` to `buf`. pub fn format_mz_timestamp(buf: &mut F, u: crate::Timestamp) -> Nestable where @@ -399,7 +411,10 @@ where ///