@@ -5,13 +5,15 @@ use std::ops::{Add, Div, Mul, Sub};
55use std:: str:: FromStr ;
66
77use chrono:: {
8- Datelike , LocalResult , Month , NaiveDate , NaiveDateTime , NaiveTime , TimeZone , Timelike , Weekday ,
8+ Datelike , Duration , LocalResult , Month , NaiveDate , NaiveDateTime , NaiveTime , Offset , TimeZone ,
9+ Timelike , Weekday ,
910} ;
1011use chrono_tz:: Tz ;
1112use itertools:: Either ;
1213use mysql_time:: MySqlTime ;
13- use readyset_data:: { DfType , DfValue } ;
14- use readyset_errors:: { invalid_query_err, unsupported, ReadySetError , ReadySetResult } ;
14+ use nom_sql:: TimestampField ;
15+ use readyset_data:: { DfType , DfValue , TimestampTz } ;
16+ use readyset_errors:: { internal, invalid_query_err, unsupported, ReadySetError , ReadySetResult } ;
1517use readyset_util:: math:: integer_rnd;
1618use rust_decimal:: prelude:: { FromPrimitive , ToPrimitive } ;
1719use rust_decimal:: Decimal ;
@@ -21,8 +23,15 @@ use vec1::Vec1;
2123
2224use crate :: { BuiltinFunction , Expr } ;
2325
26+ const MICROS_IN_SECOND : u32 = 1_000_000 ;
27+ const MILLIS_IN_SECOND : u32 = 1_000 ;
2428const NANOS_IN_MICRO : u32 = 1_000 ;
29+ const NANOS_IN_SECOND : u32 = 1_000_000_000 ;
2530const NANOS_IN_MILLI : u32 = 1_000_000 ;
31+ const SECONDS_IN_HOUR : u64 = 60 * 60 ;
32+ const SECONDS_IN_MINUTE : u64 = 60 ;
33+ const SECONDS_IN_DAY : u64 = SECONDS_IN_HOUR * 24 ;
34+ const MINUTES_IN_HOUR : u64 = 60 ;
2635
2736macro_rules! try_cast_or_none {
2837 ( $df_value: expr, $to_ty: expr, $from_ty: expr) => { {
@@ -272,6 +281,220 @@ fn date_trunc(precision: DateTruncPrecision, dt: NaiveDateTime) -> ReadySetResul
272281 }
273282}
274283
284+ // Source:
285+ // https://github.com/postgres/postgres/blob/c6cf6d353c2865d82356ac86358622a101fde8ca/src/interfaces/ecpg/pgtypeslib/dt_common.c#L581-L582
286+ fn ymd_to_julian_date ( y : i32 , m : i32 , d : i32 ) -> i32 {
287+ let month: i32 ;
288+ let year: i32 ;
289+
290+ if m > 2 {
291+ month = m + 1 ;
292+ year = y + 4800 ;
293+ } else {
294+ month = m + 13 ;
295+ year = y + 4799 ;
296+ }
297+
298+ let century = year / 100 ;
299+ let mut julian_date = year * 365 - 32167 ;
300+ julian_date += year / 4 - century + century / 4 ;
301+ julian_date += 7834 * month / 256 + d;
302+
303+ julian_date
304+ }
305+
306+ #[ inline( always) ]
307+ fn years_term ( year : i32 , term : i32 ) -> i32 {
308+ if year > 0 {
309+ ( year + ( term - 1 ) ) / term
310+ } else {
311+ -( ( ( term - 1 ) - ( year - 1 ) ) / term)
312+ }
313+ }
314+
315+ #[ inline( always) ]
316+ fn years_decade ( year : i32 ) -> i32 {
317+ if year >= 0 {
318+ year / 10
319+ } else {
320+ -( ( 8 - ( year - 1 ) ) / 10 )
321+ }
322+ }
323+
324+ #[ inline( always) ]
325+ fn seconds_term ( secs : u32 , nanos : u32 , units_per_sec : u32 ) -> Decimal {
326+ Decimal :: from ( secs * units_per_sec)
327+ + ( Decimal :: from ( nanos) / Decimal :: from ( NANOS_IN_SECOND / units_per_sec) )
328+ }
329+
330+ #[ inline( always) ]
331+ fn hms_to_seconds ( h : u32 , m : u32 , s : u32 ) -> u32 {
332+ ( ( h * MINUTES_IN_HOUR as u32 ) + m) * SECONDS_IN_MINUTE as u32 + s
333+ }
334+
335+ #[ inline( always) ]
336+ fn hms_with_nanos_to_days ( h : u32 , m : u32 , s : u32 , nanos : u32 ) -> Decimal {
337+ seconds_term ( hms_to_seconds ( h, m, s) , nanos, 1 ) / Decimal :: from ( SECONDS_IN_DAY ) . round ( )
338+ }
339+
340+ #[ inline( always) ]
341+ fn tz_to_seconds ( dt : & NaiveDateTime ) -> i32 {
342+ dt. and_utc ( ) . timezone ( ) . fix ( ) . local_minus_utc ( )
343+ }
344+
345+ /* The error message we compose here, is compatible with Postgres 15, but not with 13/14
346+ */
347+ fn invalid_extract_call_error_message ( cal_type : & str , field : TimestampField ) -> String {
348+ let mut msg = format ! (
349+ "ERROR: unit \" {}\" not supported for type {}" ,
350+ format!( "{field}" ) . to_lowercase( ) ,
351+ cal_type
352+ ) ;
353+ if cal_type. eq_ignore_ascii_case ( "time" ) || cal_type. eq_ignore_ascii_case ( "timestamp" ) {
354+ msg. push_str ( " without time zone" ) ;
355+ }
356+ msg
357+ }
358+
359+ fn invalid_extract_call_error ( calendar_type : & str , field : TimestampField ) -> ReadySetError {
360+ invalid_query_err ! (
361+ "{}" ,
362+ invalid_extract_call_error_message( calendar_type, field)
363+ )
364+ }
365+
366+ fn extract_from_time ( field : TimestampField , tm : & MySqlTime ) -> ReadySetResult < DfValue > {
367+ macro_rules! seconds_term {
368+ ( $tm: expr, $units_in_second: expr) => {
369+ seconds_term(
370+ $tm. seconds( ) as u32 ,
371+ ( $tm. microseconds( ) * NANOS_IN_MICRO ) ,
372+ $units_in_second,
373+ )
374+ } ;
375+ }
376+
377+ let result: DfValue = match field {
378+ TimestampField :: Hour => tm. hour ( ) . into ( ) ,
379+ TimestampField :: Minute => tm. minutes ( ) . into ( ) ,
380+ TimestampField :: Second => seconds_term ! ( tm, 1 ) . into ( ) ,
381+ TimestampField :: Milliseconds => seconds_term ! ( tm, MILLIS_IN_SECOND ) . into ( ) ,
382+ TimestampField :: Microseconds => seconds_term ! ( tm, MICROS_IN_SECOND ) . round ( ) . into ( ) ,
383+ TimestampField :: Epoch => Duration :: from ( * tm) . num_seconds ( ) . into ( ) ,
384+ _ => return Err ( invalid_extract_call_error ( "time" , field) ) ,
385+ } ;
386+
387+ Ok ( result)
388+ }
389+
390+ fn extract_from_timestamptz ( field : TimestampField , tz : & TimestampTz ) -> ReadySetResult < DfValue > {
391+ macro_rules! has_time_else_error {
392+ ( $tz: expr, $field: expr) => {
393+ if $tz. has_date_only( ) {
394+ return Err ( invalid_extract_call_error( "date" , $field) ) ;
395+ }
396+ } ;
397+ }
398+
399+ macro_rules! has_timezone_else_error {
400+ ( $tz: expr, $field: expr) => {
401+ if !$tz. has_timezone( ) {
402+ return Err ( invalid_extract_call_error(
403+ if $tz. has_date_only( ) {
404+ "date"
405+ } else {
406+ "timestamp"
407+ } ,
408+ $field,
409+ ) ) ;
410+ }
411+ } ;
412+ }
413+
414+ macro_rules! seconds_term {
415+ ( $dt_utc: expr, $units_in_second: expr) => {
416+ seconds_term( $dt_utc. second( ) , $dt_utc. nanosecond( ) , $units_in_second)
417+ } ;
418+ }
419+
420+ let dt_utc = tz. to_chrono ( ) . naive_utc ( ) ;
421+ let result: DfValue = match field {
422+ TimestampField :: Millennium => years_term ( dt_utc. year ( ) , 1000 ) . into ( ) ,
423+ TimestampField :: Century => years_term ( dt_utc. year ( ) , 100 ) . into ( ) ,
424+ TimestampField :: Decade => years_decade ( dt_utc. year ( ) ) . into ( ) ,
425+ TimestampField :: Year => {
426+ let year = dt_utc. year ( ) ;
427+ if year < 0 { year - 1 } else { year } . into ( )
428+ }
429+ TimestampField :: Isoyear => {
430+ let year = dt_utc. iso_week ( ) . year ( ) ;
431+ if year <= 0 { year - 1 } else { year } . into ( )
432+ }
433+ TimestampField :: Quarter => ( dt_utc. month0 ( ) / 3 + 1 ) . into ( ) ,
434+ TimestampField :: Month => dt_utc. month ( ) . into ( ) ,
435+ TimestampField :: Week => dt_utc. iso_week ( ) . week ( ) . into ( ) ,
436+ TimestampField :: Day => dt_utc. day ( ) . into ( ) ,
437+ TimestampField :: Dow => dt_utc. weekday ( ) . num_days_from_sunday ( ) . into ( ) ,
438+ TimestampField :: Isodow => dt_utc. weekday ( ) . number_from_monday ( ) . into ( ) ,
439+ TimestampField :: Doy => dt_utc. ordinal ( ) . into ( ) ,
440+ TimestampField :: Hour => {
441+ has_time_else_error ! ( tz, field) ;
442+ dt_utc. hour ( ) . into ( )
443+ }
444+ TimestampField :: Minute => {
445+ has_time_else_error ! ( tz, field) ;
446+ dt_utc. minute ( ) . into ( )
447+ }
448+ TimestampField :: Second => {
449+ has_time_else_error ! ( tz, field) ;
450+ seconds_term ! ( dt_utc, 1 ) . into ( )
451+ }
452+ TimestampField :: Milliseconds => {
453+ has_time_else_error ! ( tz, field) ;
454+ seconds_term ! ( dt_utc, MILLIS_IN_SECOND ) . into ( )
455+ }
456+ TimestampField :: Microseconds => {
457+ has_time_else_error ! ( tz, field) ;
458+ seconds_term ! ( dt_utc, MICROS_IN_SECOND ) . round ( ) . into ( )
459+ }
460+ TimestampField :: Epoch => ( Decimal :: from ( dt_utc. and_utc ( ) . timestamp_micros ( ) )
461+ / Decimal :: from ( MICROS_IN_SECOND ) )
462+ . round_dp ( 6 )
463+ . into ( ) ,
464+ TimestampField :: Julian => {
465+ let julian_date: Decimal =
466+ ymd_to_julian_date ( dt_utc. year ( ) , dt_utc. month ( ) as i32 , dt_utc. day ( ) as i32 )
467+ . into ( ) ;
468+ if tz. has_date_only ( ) {
469+ julian_date
470+ } else {
471+ julian_date
472+ + hms_with_nanos_to_days (
473+ dt_utc. hour ( ) ,
474+ dt_utc. minute ( ) ,
475+ dt_utc. second ( ) ,
476+ dt_utc. nanosecond ( ) ,
477+ )
478+ }
479+ . into ( )
480+ }
481+ TimestampField :: Timezone => {
482+ has_timezone_else_error ! ( tz, field) ;
483+ tz_to_seconds ( & dt_utc) . into ( )
484+ }
485+ TimestampField :: TimezoneHour => {
486+ has_timezone_else_error ! ( tz, field) ;
487+ ( tz_to_seconds ( & dt_utc) / SECONDS_IN_HOUR as i32 ) . into ( )
488+ }
489+ TimestampField :: TimezoneMinute => {
490+ has_timezone_else_error ! ( tz, field) ;
491+ ( ( tz_to_seconds ( & dt_utc) % SECONDS_IN_HOUR as i32 ) / SECONDS_IN_MINUTE as i32 ) . into ( )
492+ }
493+ } ;
494+
495+ Ok ( result)
496+ }
497+
275498/// Format the given time value according to the given `format_string`, using the [MySQL date
276499/// formatting rules][mysql-docs]. Since these rules don't match up well with anything available in
277500/// the Rust crate ecosystem, this is done manually.
@@ -1033,6 +1256,17 @@ impl BuiltinFunction {
10331256 date_trunc ( precision, datetime. naive_utc ( ) ) . unwrap ( ) ,
10341257 ) )
10351258 }
1259+ BuiltinFunction :: Extract ( field, expr) => {
1260+ let ts = non_null ! ( expr. eval( record) ?) ;
1261+ if let DfValue :: TimestampTz ( tz) = ts {
1262+ extract_from_timestamptz ( * field, & tz)
1263+ } else if let DfValue :: Time ( tm) = ts {
1264+ extract_from_time ( * field, & tm)
1265+ } else {
1266+ internal ! ( "EXTRACT function input expected to be DfValue::TimestampTz or DfValue::Time. Found {}" , ts) ;
1267+ }
1268+ . and_then ( |value| value. coerce_to ( ty, & DfType :: Unknown ) )
1269+ }
10361270 }
10371271 }
10381272}
0 commit comments