1818import static org .opensearch .sql .expression .function .FunctionDSL .define ;
1919import static org .opensearch .sql .expression .function .FunctionDSL .impl ;
2020import 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 ;
2125
2226import java .math .BigDecimal ;
2327import java .math .RoundingMode ;
28+ import java .text .DecimalFormat ;
29+ import java .time .Instant ;
2430import java .time .LocalDate ;
2531import java .time .LocalDateTime ;
2632import java .time .LocalTime ;
33+ import java .time .ZoneId ;
34+ import java .time .ZoneOffset ;
2735import java .time .format .DateTimeFormatter ;
36+ import java .time .format .DateTimeParseException ;
2837import java .time .format .TextStyle ;
2938import java .util .Locale ;
3039import java .util .concurrent .TimeUnit ;
3140import javax .annotation .Nullable ;
3241import lombok .experimental .UtilityClass ;
3342import org .opensearch .sql .data .model .ExprDateValue ;
3443import org .opensearch .sql .data .model .ExprDatetimeValue ;
44+ import org .opensearch .sql .data .model .ExprDoubleValue ;
3545import org .opensearch .sql .data .model .ExprIntegerValue ;
3646import org .opensearch .sql .data .model .ExprLongValue ;
3747import org .opensearch .sql .data .model .ExprNullValue ;
3848import org .opensearch .sql .data .model .ExprStringValue ;
3949import org .opensearch .sql .data .model .ExprTimeValue ;
4050import org .opensearch .sql .data .model .ExprTimestampValue ;
4151import org .opensearch .sql .data .model .ExprValue ;
52+ import org .opensearch .sql .data .type .ExprCoreType ;
4253import org .opensearch .sql .expression .function .BuiltinFunctionName ;
4354import org .opensearch .sql .expression .function .BuiltinFunctionRepository ;
4455import org .opensearch .sql .expression .function .DefaultFunctionResolver ;
@@ -56,6 +67,10 @@ public class DateTimeFunction {
5667 // The number of days from year zero to year 1970.
5768 private static final Long DAYS_0000_TO_1970 = (146097 * 5L ) - (30L * 365L + 7L );
5869
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+
5974 /**
6075 * Register Date and Time Functions.
6176 *
@@ -72,6 +87,7 @@ public void register(BuiltinFunctionRepository repository) {
7287 repository .register (dayOfWeek ());
7388 repository .register (dayOfYear ());
7489 repository .register (from_days ());
90+ repository .register (from_unixtime ());
7591 repository .register (hour ());
7692 repository .register (makedate ());
7793 repository .register (maketime ());
@@ -87,6 +103,7 @@ public void register(BuiltinFunctionRepository repository) {
87103 repository .register (timestamp ());
88104 repository .register (date_format ());
89105 repository .register (to_days ());
106+ repository .register (unix_timestamp ());
90107 repository .register (week ());
91108 repository .register (year ());
92109
@@ -313,6 +330,13 @@ private DefaultFunctionResolver from_days() {
313330 impl (nullMissingHandling (DateTimeFunction ::exprFromDays ), DATE , LONG ));
314331 }
315332
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+
316340 /**
317341 * HOUR(STRING/TIME/DATETIME/TIMESTAMP). return the hour value for time.
318342 */
@@ -461,6 +485,16 @@ private DefaultFunctionResolver to_days() {
461485 impl (nullMissingHandling (DateTimeFunction ::exprToDays ), LONG , DATETIME ));
462486 }
463487
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+
464498 /**
465499 * WEEK(DATE[,mode]). return the week number for date.
466500 */
@@ -601,6 +635,35 @@ private ExprValue exprFromDays(ExprValue exprValue) {
601635 return new ExprDateValue (LocalDate .ofEpochDay (exprValue .longValue () - DAYS_0000_TO_1970 ));
602636 }
603637
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+
604667 /**
605668 * Hour implementation for ExprValue.
606669 *
@@ -803,6 +866,79 @@ private ExprValue exprWeek(ExprValue date, ExprValue mode) {
803866 CalendarLookup .getWeekNumber (mode .integerValue (), date .dateValue ()));
804867 }
805868
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+
806942 /**
807943 * Week for date implementation for ExprValue.
808944 * When mode is not specified default value mode 0 is used for default_week_format.
0 commit comments