diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index d3f49d4349bb..6cfc280981ec 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -182,6 +182,7 @@ import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATEADD; import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATETIME; import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATETIME_TRUNC; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATE_FORMAT; import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATE_FROM_UNIX_DATE; import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATE_PART; import static org.apache.calcite.sql.fun.SqlLibraryOperators.DATE_TRUNC; @@ -833,6 +834,7 @@ Builder populate2() { // Datetime formatting methods defineReflective(TO_CHAR, BuiltInMethod.TO_CHAR.method); defineReflective(TO_CHAR_PG, BuiltInMethod.TO_CHAR_PG.method); + defineReflective(DATE_FORMAT, BuiltInMethod.DATE_FORMAT.method); defineReflective(TO_DATE, BuiltInMethod.TO_DATE.method); defineReflective(TO_DATE_PG, BuiltInMethod.TO_DATE_PG.method); defineReflective(TO_TIMESTAMP, BuiltInMethod.TO_TIMESTAMP.method); diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index 7da9705c1c09..59b4eb5e88b0 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -4313,6 +4313,14 @@ public String toCharPg(long timestamp, String pattern) { return PostgresqlDateTimeFormatter.toChar(pattern, zonedDateTime).trim(); } + public String dateFormat(long timestamp, String pattern) { + final Timestamp sqlTimestamp = internalToTimestamp(timestamp); + sb.setLength(0); + withElements(FormatModels.MYSQL, pattern, elements -> + elements.forEach(element -> element.format(sb, sqlTimestamp))); + return sb.toString().trim(); + } + public int toDate(String dateString, String fmtString) { return toInt( new java.sql.Date(internalToDateTime(dateString, fmtString))); diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java index 131ac7c3a759..fd3b009c56d2 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java @@ -1856,6 +1856,16 @@ private static RelDataType deriveTypeMapFromEntries(SqlOperatorBinding opBinding OperandHandlers.DEFAULT, OperandTypes.TIMESTAMP_STRING, 0, SqlFunctionCategory.TIMEDATE, call -> SqlMonotonicity.NOT_MONOTONIC, false) { }; + + /** The "DATE_FORMAT(timestamp, format)" function; + * converts {@code timestamp} to string according to the given {@code format}. */ + @LibraryOperator(libraries = {MYSQL}) + public static final SqlFunction DATE_FORMAT = + SqlBasicFunction.create("DATE_FORMAT", + ReturnTypes.VARCHAR_NULLABLE, + OperandTypes.TIMESTAMP_STRING, + SqlFunctionCategory.TIMEDATE); + /** The "TO_DATE(string1, string2)" function; casts string1 * to a DATE using the format specified in string2. */ @LibraryOperator(libraries = {ORACLE, REDSHIFT}) diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index 7d74ccd37a6e..d2b596f6751d 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -695,6 +695,8 @@ public enum BuiltInMethod { String.class), TO_CHAR_PG(SqlFunctions.DateFormatFunction.class, "toCharPg", long.class, String.class), + DATE_FORMAT(SqlFunctions.DateFormatFunction.class, "dateFormat", long.class, + String.class), TO_DATE(SqlFunctions.DateFormatFunction.class, "toDate", String.class, String.class), TO_DATE_PG(SqlFunctions.DateFormatFunction.class, "toDatePg", String.class, diff --git a/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java b/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java index f4f290903ff5..0c7f58dc8d6e 100644 --- a/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java +++ b/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java @@ -24,8 +24,10 @@ import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.DayOfWeek; import java.time.LocalDate; import java.time.format.TextStyle; +import java.time.temporal.ChronoField; import java.util.Calendar; import java.util.Date; import java.util.Locale; @@ -59,6 +61,13 @@ public enum FormatElementEnum implements FormatElement { sb.append(String.format(Locale.ROOT, "%d", calendar.get(Calendar.DAY_OF_WEEK))); } }, + D0("", "The weekday (Monday as the first day of the week) as a decimal number (0-6)") { + @Override public void format(StringBuilder sb, Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + sb.append(String.format(Locale.ROOT, "%d", calendar.get(Calendar.DAY_OF_WEEK) - 1)); + } + }, DAY("EEEE", "The full weekday name, in uppercase") { @Override public void format(StringBuilder sb, Date date) { final Work work = Work.get(); @@ -77,6 +86,21 @@ public enum FormatElementEnum implements FormatElement { sb.append(work.getDayFromDate(date, TextStyle.FULL).toLowerCase(Locale.ROOT)); } }, + DS("", "The month as a decimal number with suffix. (1st-31th)") { + @Override public void format(StringBuilder sb, Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + int d = calendar.get(Calendar.DAY_OF_MONTH); + sb.append(String.format(Locale.ROOT, "%d%s", d, getNumericSuffix(d))); + } + }, + D1("dd", "The day of the month as a decimal number (1-31)") { + @Override public void format(StringBuilder sb, Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + sb.append(String.format(Locale.ROOT, "%d", calendar.get(Calendar.DAY_OF_MONTH))); + } + }, DD("dd", "The day of the month as a decimal number (01-31)") { @Override public void format(StringBuilder sb, Date date) { final Calendar calendar = Work.get().calendar; @@ -221,6 +245,33 @@ public enum FormatElementEnum implements FormatElement { sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR))); } }, + V("", "The number of the week (Monday as the first day of the week)," + + "If the first day of the week belongs to the previous year, " + + "calculate the week of the previous year" + + "as a decimal number (01-53)") { + @Override public void format(StringBuilder sb, Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + calendar.setFirstDayOfWeek(Calendar.SUNDAY); + // Day of year of the first Sunday of the year + int minimalDaysInFirstWeek = + getFirstWeekdayOfYear(DayOfWeek.MONDAY, calendar.get(Calendar.YEAR)); + calendar.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek); + sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR))); + } + }, + v("", "The number of the week (Sunday as the first day of the week)," + + "If the first day of the week belongs to the previous year, " + + "calculate the week of the previous year" + + "as a decimal number (01-53)") { + @Override public void format(StringBuilder sb, Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + calendar.setFirstDayOfWeek(Calendar.MONDAY); + calendar.setMinimalDaysInFirstWeek(4); + sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR))); + } + }, MI("m", "The minute as a decimal number (00-59)") { @Override public void format(StringBuilder sb, Date date) { final Calendar calendar = Work.get().calendar; @@ -326,7 +377,16 @@ public enum FormatElementEnum implements FormatElement { sb.append(String.format(Locale.ROOT, "%03d", calendar.get(Calendar.MILLISECOND))); } }, - SS("s", "The second as a decimal number (00-60)") { + MCS("", "The microsecond as a decimal number (000000-999999)") { + @Override public void format(StringBuilder sb, Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + // It exceeds the precision of Calendar, we fill it with 0. + // So the actual range is (000000-999000) + sb.append(String.format(Locale.ROOT, "%06d", calendar.get(Calendar.MILLISECOND) * 1000)); + } + }, + SS("s", "The second as a decimal number (00-59)") { @Override public void format(StringBuilder sb, Date date) { final Calendar calendar = Work.get().calendar; calendar.setTime(date); @@ -369,10 +429,31 @@ public enum FormatElementEnum implements FormatElement { @Override public void format(StringBuilder sb, Date date) { final Calendar calendar = Work.get().calendar; calendar.setTime(date); + calendar.setMinimalDaysInFirstWeek(1); calendar.setFirstDayOfWeek(Calendar.SUNDAY); sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR))); } }, + WW1("w", "The week number of the year (Sunday as the first day of the week) as a decimal " + + "number (00-53)") { + @Override public void format(StringBuilder sb, Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + calendar.setMinimalDaysInFirstWeek(1); + calendar.setFirstDayOfWeek(Calendar.SUNDAY); + sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR) - 1)); + } + }, + WW2("", "The week number of the year (Monday as the first day of the week) as a decimal " + + "number (01-53)") { + @Override public void format(StringBuilder sb, Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + calendar.setMinimalDaysInFirstWeek(1); + calendar.setFirstDayOfWeek(Calendar.MONDAY); + sb.append(String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR))); + } + }, Y("y", "Last digit of year") { @Override public void format(StringBuilder sb, Date date) { final Work work = Work.get(); @@ -399,6 +480,27 @@ public enum FormatElementEnum implements FormatElement { sb.append(work.yyyyFormat.format(date)); } }, + WFY("", "The year for the week where Sunday is the first day of the week") { + @Override public void format(StringBuilder sb, Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + calendar.setFirstDayOfWeek(Calendar.SUNDAY); + // Day of year of the first Sunday of the year + int minimalDaysInFirstWeek = + getFirstWeekdayOfYear(DayOfWeek.MONDAY, calendar.get(Calendar.YEAR)); + calendar.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek); + sb.append(String.format(Locale.ROOT, "%04d", calendar.getWeekYear())); + } + }, + WFY0("", "The year for the week where Monday is the first day of the week") { + @Override public void format(StringBuilder sb, Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + calendar.setFirstDayOfWeek(Calendar.MONDAY); + calendar.setMinimalDaysInFirstWeek(4); + sb.append(String.format(Locale.ROOT, "%04d", calendar.getWeekYear())); + } + }, pctY("yyyy", "The year with century as a decimal number") { @Override public void format(StringBuilder sb, Date date) { final Calendar calendar = Work.get().calendar; @@ -464,4 +566,31 @@ private String getDayFromDate(Date date, TextStyle style) { return ld.getDayOfWeek().getDisplayName(style, Locale.ENGLISH); } } + + /** Calculate what day of the year the first weekdayName of the given year is. */ + private static int getFirstWeekdayOfYear(DayOfWeek weekdayName, int year) { + return LocalDate.of(year, 1, 1) + .with(ChronoField.DAY_OF_WEEK, weekdayName.getValue()) + .getDayOfYear(); + } + + /** Util to return the suffix of numeric. */ + private static String getNumericSuffix(int numeric) { + String outputSuffix; + switch (numeric) { + case 1: + outputSuffix = "st"; + break; + case 2: + outputSuffix = "nd"; + break; + case 3: + outputSuffix = "rd"; + break; + default: + outputSuffix = "th"; + break; + } + return outputSuffix; + } } diff --git a/core/src/main/java/org/apache/calcite/util/format/FormatModels.java b/core/src/main/java/org/apache/calcite/util/format/FormatModels.java index a46b091ab826..9f7e9c1c196d 100644 --- a/core/src/main/java/org/apache/calcite/util/format/FormatModels.java +++ b/core/src/main/java/org/apache/calcite/util/format/FormatModels.java @@ -33,9 +33,12 @@ import static org.apache.calcite.util.format.FormatElementEnum.AM_PM; import static org.apache.calcite.util.format.FormatElementEnum.CC; import static org.apache.calcite.util.format.FormatElementEnum.D; +import static org.apache.calcite.util.format.FormatElementEnum.D0; +import static org.apache.calcite.util.format.FormatElementEnum.D1; import static org.apache.calcite.util.format.FormatElementEnum.DAY; import static org.apache.calcite.util.format.FormatElementEnum.DD; import static org.apache.calcite.util.format.FormatElementEnum.DDD; +import static org.apache.calcite.util.format.FormatElementEnum.DS; import static org.apache.calcite.util.format.FormatElementEnum.DY; import static org.apache.calcite.util.format.FormatElementEnum.Day; import static org.apache.calcite.util.format.FormatElementEnum.Dy; @@ -52,6 +55,7 @@ import static org.apache.calcite.util.format.FormatElementEnum.HH12; import static org.apache.calcite.util.format.FormatElementEnum.HH24; import static org.apache.calcite.util.format.FormatElementEnum.IW; +import static org.apache.calcite.util.format.FormatElementEnum.MCS; import static org.apache.calcite.util.format.FormatElementEnum.MI; import static org.apache.calcite.util.format.FormatElementEnum.MM; import static org.apache.calcite.util.format.FormatElementEnum.MON; @@ -64,8 +68,13 @@ import static org.apache.calcite.util.format.FormatElementEnum.SS; import static org.apache.calcite.util.format.FormatElementEnum.SSSSS; import static org.apache.calcite.util.format.FormatElementEnum.TZR; +import static org.apache.calcite.util.format.FormatElementEnum.V; import static org.apache.calcite.util.format.FormatElementEnum.W; +import static org.apache.calcite.util.format.FormatElementEnum.WFY; +import static org.apache.calcite.util.format.FormatElementEnum.WFY0; import static org.apache.calcite.util.format.FormatElementEnum.WW; +import static org.apache.calcite.util.format.FormatElementEnum.WW1; +import static org.apache.calcite.util.format.FormatElementEnum.WW2; import static org.apache.calcite.util.format.FormatElementEnum.Y; import static org.apache.calcite.util.format.FormatElementEnum.YY; import static org.apache.calcite.util.format.FormatElementEnum.YYY; @@ -77,6 +86,7 @@ import static org.apache.calcite.util.format.FormatElementEnum.mon; import static org.apache.calcite.util.format.FormatElementEnum.month; import static org.apache.calcite.util.format.FormatElementEnum.pctY; +import static org.apache.calcite.util.format.FormatElementEnum.v; import static java.util.Objects.requireNonNull; @@ -110,6 +120,14 @@ private FormatModels() { */ public static final FormatModel POSTGRESQL; + /** Format model for MySQL. + * + *
MySQL format element reference:
+ *
+ * MySQL Standard SQL Format Elements.
+ */
+ public static final FormatModel MYSQL = createMysqlFormatModel();
+
static {
final Map