18
18
import static org .opensearch .sql .expression .function .FunctionDSL .define ;
19
19
import static org .opensearch .sql .expression .function .FunctionDSL .impl ;
20
20
import static org .opensearch .sql .expression .function .FunctionDSL .nullMissingHandling ;
21
+ import static org .opensearch .sql .utils .DateTimeFormatters .DATE_FORMATTER_LONG_YEAR ;
22
+ import static org .opensearch .sql .utils .DateTimeFormatters .DATE_FORMATTER_SHORT_YEAR ;
23
+ import static org .opensearch .sql .utils .DateTimeFormatters .DATE_TIME_FORMATTER_LONG_YEAR ;
24
+ import static org .opensearch .sql .utils .DateTimeFormatters .DATE_TIME_FORMATTER_SHORT_YEAR ;
21
25
22
26
import java .math .BigDecimal ;
23
27
import java .math .RoundingMode ;
28
+ import java .text .DecimalFormat ;
29
+ import java .time .Instant ;
24
30
import java .time .LocalDate ;
25
31
import java .time .LocalDateTime ;
26
32
import java .time .LocalTime ;
33
+ import java .time .ZoneId ;
34
+ import java .time .ZoneOffset ;
27
35
import java .time .format .DateTimeFormatter ;
36
+ import java .time .format .DateTimeParseException ;
28
37
import java .time .format .TextStyle ;
29
38
import java .util .Locale ;
30
39
import java .util .concurrent .TimeUnit ;
31
40
import javax .annotation .Nullable ;
32
41
import lombok .experimental .UtilityClass ;
33
42
import org .opensearch .sql .data .model .ExprDateValue ;
34
43
import org .opensearch .sql .data .model .ExprDatetimeValue ;
44
+ import org .opensearch .sql .data .model .ExprDoubleValue ;
35
45
import org .opensearch .sql .data .model .ExprIntegerValue ;
36
46
import org .opensearch .sql .data .model .ExprLongValue ;
37
47
import org .opensearch .sql .data .model .ExprNullValue ;
38
48
import org .opensearch .sql .data .model .ExprStringValue ;
39
49
import org .opensearch .sql .data .model .ExprTimeValue ;
40
50
import org .opensearch .sql .data .model .ExprTimestampValue ;
41
51
import org .opensearch .sql .data .model .ExprValue ;
52
+ import org .opensearch .sql .data .type .ExprCoreType ;
42
53
import org .opensearch .sql .expression .function .BuiltinFunctionName ;
43
54
import org .opensearch .sql .expression .function .BuiltinFunctionRepository ;
44
55
import org .opensearch .sql .expression .function .DefaultFunctionResolver ;
@@ -56,6 +67,10 @@ public class DateTimeFunction {
56
67
// The number of days from year zero to year 1970.
57
68
private static final Long DAYS_0000_TO_1970 = (146097 * 5L ) - (30L * 365L + 7L );
58
69
70
+ // MySQL doesn't process any datetime/timestamp values which are greater than
71
+ // 32536771199.999999, or equivalent '3001-01-18 23:59:59.999999' UTC
72
+ private static final Double MYSQL_MAX_TIMESTAMP = 32536771200d ;
73
+
59
74
/**
60
75
* Register Date and Time Functions.
61
76
*
@@ -72,6 +87,7 @@ public void register(BuiltinFunctionRepository repository) {
72
87
repository .register (dayOfWeek ());
73
88
repository .register (dayOfYear ());
74
89
repository .register (from_days ());
90
+ repository .register (from_unixtime ());
75
91
repository .register (hour ());
76
92
repository .register (makedate ());
77
93
repository .register (maketime ());
@@ -87,6 +103,7 @@ public void register(BuiltinFunctionRepository repository) {
87
103
repository .register (timestamp ());
88
104
repository .register (date_format ());
89
105
repository .register (to_days ());
106
+ repository .register (unix_timestamp ());
90
107
repository .register (week ());
91
108
repository .register (year ());
92
109
@@ -313,6 +330,13 @@ private DefaultFunctionResolver from_days() {
313
330
impl (nullMissingHandling (DateTimeFunction ::exprFromDays ), DATE , LONG ));
314
331
}
315
332
333
+ private FunctionResolver from_unixtime () {
334
+ return define (BuiltinFunctionName .FROM_UNIXTIME .getName (),
335
+ impl (nullMissingHandling (DateTimeFunction ::exprFromUnixTime ), DATETIME , DOUBLE ),
336
+ impl (nullMissingHandling (DateTimeFunction ::exprFromUnixTimeFormat ),
337
+ STRING , DOUBLE , STRING ));
338
+ }
339
+
316
340
/**
317
341
* HOUR(STRING/TIME/DATETIME/TIMESTAMP). return the hour value for time.
318
342
*/
@@ -461,6 +485,16 @@ private DefaultFunctionResolver to_days() {
461
485
impl (nullMissingHandling (DateTimeFunction ::exprToDays ), LONG , DATETIME ));
462
486
}
463
487
488
+ private FunctionResolver unix_timestamp () {
489
+ return define (BuiltinFunctionName .UNIX_TIMESTAMP .getName (),
490
+ impl (DateTimeFunction ::unixTimeStamp , LONG ),
491
+ impl (nullMissingHandling (DateTimeFunction ::unixTimeStampOf ), DOUBLE , DATE ),
492
+ impl (nullMissingHandling (DateTimeFunction ::unixTimeStampOf ), DOUBLE , DATETIME ),
493
+ impl (nullMissingHandling (DateTimeFunction ::unixTimeStampOf ), DOUBLE , TIMESTAMP ),
494
+ impl (nullMissingHandling (DateTimeFunction ::unixTimeStampOf ), DOUBLE , DOUBLE )
495
+ );
496
+ }
497
+
464
498
/**
465
499
* WEEK(DATE[,mode]). return the week number for date.
466
500
*/
@@ -601,6 +635,35 @@ private ExprValue exprFromDays(ExprValue exprValue) {
601
635
return new ExprDateValue (LocalDate .ofEpochDay (exprValue .longValue () - DAYS_0000_TO_1970 ));
602
636
}
603
637
638
+ private ExprValue exprFromUnixTime (ExprValue time ) {
639
+ if (0 > time .doubleValue ()) {
640
+ return ExprNullValue .of ();
641
+ }
642
+ // According to MySQL documentation:
643
+ // effective maximum is 32536771199.999999, which returns '3001-01-18 23:59:59.999999' UTC.
644
+ // Regardless of platform or version, a greater value for first argument than the effective
645
+ // maximum returns 0.
646
+ if (MYSQL_MAX_TIMESTAMP <= time .doubleValue ()) {
647
+ return ExprNullValue .of ();
648
+ }
649
+ return new ExprDatetimeValue (exprFromUnixTimeImpl (time ));
650
+ }
651
+
652
+ private LocalDateTime exprFromUnixTimeImpl (ExprValue time ) {
653
+ return LocalDateTime .ofInstant (
654
+ Instant .ofEpochSecond ((long )Math .floor (time .doubleValue ())),
655
+ ZoneId .of ("UTC" ))
656
+ .withNano ((int )((time .doubleValue () % 1 ) * 1E9 ));
657
+ }
658
+
659
+ private ExprValue exprFromUnixTimeFormat (ExprValue time , ExprValue format ) {
660
+ var value = exprFromUnixTime (time );
661
+ if (value .equals (ExprNullValue .of ())) {
662
+ return ExprNullValue .of ();
663
+ }
664
+ return DateTimeFormatterUtil .getFormattedDate (value , format );
665
+ }
666
+
604
667
/**
605
668
* Hour implementation for ExprValue.
606
669
*
@@ -803,6 +866,79 @@ private ExprValue exprWeek(ExprValue date, ExprValue mode) {
803
866
CalendarLookup .getWeekNumber (mode .integerValue (), date .dateValue ()));
804
867
}
805
868
869
+ private ExprValue unixTimeStamp () {
870
+ return new ExprLongValue (Instant .now ().getEpochSecond ());
871
+ }
872
+
873
+ private ExprValue unixTimeStampOf (ExprValue value ) {
874
+ var res = unixTimeStampOfImpl (value );
875
+ if (res == null ) {
876
+ return ExprNullValue .of ();
877
+ }
878
+ if (res < 0 ) {
879
+ // According to MySQL returns 0 if year < 1970, don't return negative values as java does.
880
+ return new ExprDoubleValue (0 );
881
+ }
882
+ if (res >= MYSQL_MAX_TIMESTAMP ) {
883
+ // Return 0 also for dates > '3001-01-19 03:14:07.999999' UTC (32536771199.999999 sec)
884
+ return new ExprDoubleValue (0 );
885
+ }
886
+ return new ExprDoubleValue (res );
887
+ }
888
+
889
+ private Double unixTimeStampOfImpl (ExprValue value ) {
890
+ // Also, according to MySQL documentation:
891
+ // The date argument may be a DATE, DATETIME, or TIMESTAMP ...
892
+ switch ((ExprCoreType )value .type ()) {
893
+ case DATE : return value .dateValue ().toEpochSecond (LocalTime .MIN , ZoneOffset .UTC ) + 0d ;
894
+ case DATETIME : return value .datetimeValue ().toEpochSecond (ZoneOffset .UTC )
895
+ + value .datetimeValue ().getNano () / 1E9 ;
896
+ case TIMESTAMP : return value .timestampValue ().getEpochSecond ()
897
+ + value .timestampValue ().getNano () / 1E9 ;
898
+ default :
899
+ // ... or a number in YYMMDD, YYMMDDhhmmss, YYYYMMDD, or YYYYMMDDhhmmss format.
900
+ // If the argument includes a time part, it may optionally include a fractional
901
+ // seconds part.
902
+
903
+ var format = new DecimalFormat ("0.#" );
904
+ format .setMinimumFractionDigits (0 );
905
+ format .setMaximumFractionDigits (6 );
906
+ String input = format .format (value .doubleValue ());
907
+ double fraction = 0 ;
908
+ if (input .contains ("." )) {
909
+ // Keeping fraction second part and adding it to the result, don't parse it
910
+ // Because `toEpochSecond` returns only `long`
911
+ // input = 12345.6789 becomes input = 12345 and fraction = 0.6789
912
+ fraction = value .doubleValue () - Math .round (Math .ceil (value .doubleValue ()));
913
+ input = input .substring (0 , input .indexOf ('.' ));
914
+ }
915
+ try {
916
+ var res = LocalDateTime .parse (input , DATE_TIME_FORMATTER_SHORT_YEAR );
917
+ return res .toEpochSecond (ZoneOffset .UTC ) + fraction ;
918
+ } catch (DateTimeParseException ignored ) {
919
+ // nothing to do, try another format
920
+ }
921
+ try {
922
+ var res = LocalDateTime .parse (input , DATE_TIME_FORMATTER_LONG_YEAR );
923
+ return res .toEpochSecond (ZoneOffset .UTC ) + fraction ;
924
+ } catch (DateTimeParseException ignored ) {
925
+ // nothing to do, try another format
926
+ }
927
+ try {
928
+ var res = LocalDate .parse (input , DATE_FORMATTER_SHORT_YEAR );
929
+ return res .toEpochSecond (LocalTime .MIN , ZoneOffset .UTC ) + 0d ;
930
+ } catch (DateTimeParseException ignored ) {
931
+ // nothing to do, try another format
932
+ }
933
+ try {
934
+ var res = LocalDate .parse (input , DATE_FORMATTER_LONG_YEAR );
935
+ return res .toEpochSecond (LocalTime .MIN , ZoneOffset .UTC ) + 0d ;
936
+ } catch (DateTimeParseException ignored ) {
937
+ return null ;
938
+ }
939
+ }
940
+ }
941
+
806
942
/**
807
943
* Week for date implementation for ExprValue.
808
944
* When mode is not specified default value mode 0 is used for default_week_format.
0 commit comments