Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CALCITE-6551] Add DATE_FORMAT function (enabled in MySQL library) #3936

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -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 year (Monday as the first day of the week),"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment seems wrong, it is about the week number

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, i will fix it

+ "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 year (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;
Expand Down Expand Up @@ -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 millisecond 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Milliseconds are usually between 0 and 999.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an incorrect function description, it means microsecond here, I'll fix it

// 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);
Expand Down Expand Up @@ -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();
Expand All @@ -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") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should say "the week of the year"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result returned by this symbol is the number of years, "the week of the year" might not be appropriate

@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") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same problem

@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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -110,6 +120,14 @@ private FormatModels() {
*/
public static final FormatModel POSTGRESQL;

/** Format model for MySQL.
*
* <p>MySQL format element reference:
* <a href="https://dev.mysql.com/doc/refman/8.4/en/date-and-time-functions.html#function_date-format">
* MySQL Standard SQL Format Elements</a>.
*/
public static final FormatModel MYSQL = createMysqlFormatModel();

static {
final Map<String, FormatElement> map = new LinkedHashMap<>();
for (FormatElementEnum fe : FormatElementEnum.values()) {
Expand Down Expand Up @@ -231,6 +249,47 @@ MI, literalElement(":"), SS, literalElement(" "),
POSTGRESQL = create(map);
}

private static FormatModel createMysqlFormatModel() {
final Map<String, FormatElement> map = new LinkedHashMap<>();
map.put("%a", Dy);
map.put("%b", Mon);
map.put("%c", MM);
map.put("%D", DS);
map.put("%d", DD);
map.put("%e", D1);
map.put("%f", MCS);
map.put("%H", HH24);
map.put("%h", HH12);
map.put("%I", HH12);
map.put("%i", MI);
map.put("%j", DDD);
map.put("%k", HH24);
map.put("%l", HH12);
map.put("%M", Month);
map.put("%m", MM);
map.put("%p", PM);
map.put("%r",
compositeElement("The date representation in hh:mm:ss am/pm format.",
HH12, literalElement(":"), MI, literalElement(":"), SS, literalElement(" "), PM));
map.put("%S", SS);
map.put("%s", SS);
map.put("%T",
compositeElement("The date representation in hh:mm:ss am/pm format.",
HH24, literalElement(":"), MI, literalElement(":"), SS));
map.put("%U", WW1);
map.put("%u", WW2);
map.put("%V", V);
map.put("%v", v);
map.put("%W", Day);
map.put("%w", D0);
map.put("%X", WFY);
map.put("%x", WFY0);
map.put("%Y", YYYY);
map.put("%y", YY);
map.put("%%", literalElement("%"));
return create(map);
}

/**
* Generates a {@link Pattern} using the keys of a {@link FormatModel} element
* map. This pattern is used in {@link FormatModel#parse(String)} to help
Expand Down
Loading
Loading