diff --git a/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java b/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java index bb9f233a72..a71f3e52e5 100644 --- a/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java +++ b/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java @@ -63,6 +63,10 @@ private DateTimeUtils() {} private static final Pattern ISO_DATE_PATTERN = Pattern.compile("^(\\d{4})-([0]\\d|1[0-2])-([0-2]\\d|3[01])$"); + /** Regex for lenient date patterns. */ + private static final Pattern LENIENT_DATE_PATTERN = + Pattern.compile("^\\s*(\\d{1,4})-(\\d{1,2})-(\\d{1,2})\\s*$"); + /** Regex for time, HH:MM:SS. */ private static final Pattern ISO_TIME_PATTERN = Pattern.compile("^([0-2]\\d):[0-5]\\d:[0-5]\\d(\\.\\d*)*$"); @@ -653,6 +657,7 @@ private static void fraction(StringBuilder buf, int scale, long ms) { } public static int dateStringToUnixDate(String s) { + validateLenientDate(s); int hyphen1 = s.indexOf('-'); int y; int m; @@ -739,15 +744,44 @@ private static int parseFraction(String v, int multiplier) { return r; } + /** Check that the combination year, month, date forms a legal date. */ + static void checkLegalDate(int year, int month, int day, String full) { + if (day > daysInMonth(year, month)) { + throw fieldOutOfRange("DAY", full); + } + if (month < 1 || month > 12) { + throw fieldOutOfRange("MONTH", full); + } + if (year <= 0) { + // Year 0 is not really a legal value. + throw fieldOutOfRange("YEAR", full); + } + } + + /** Lenient date validation. This accepts more date strings + * than validateDate: it does not insist on having two-digit + * values for days and months, and accepts spaces around the value. + * @param s A string representing a date. + */ + private static void validateLenientDate(String s) { + Matcher matcher = LENIENT_DATE_PATTERN.matcher(s); + if (matcher.find()) { + int year = Integer.parseInt(matcher.group(1)); + int month = Integer.parseInt(matcher.group(2)); + int day = Integer.parseInt(matcher.group(3)); + checkLegalDate(year, month, day, s); + } else { + throw invalidType("DATE", s); + } + } + private static void validateDate(String s, String full) { Matcher matcher = ISO_DATE_PATTERN.matcher(s); if (matcher.find()) { int year = Integer.parseInt(matcher.group(1)); int month = Integer.parseInt(matcher.group(2)); int day = Integer.parseInt(matcher.group(3)); - if (day > daysInMonth(year, month)) { - throw fieldOutOfRange("DAY", full); - } + checkLegalDate(year, month, day, full); } else { throw invalidType("DATE", full); } diff --git a/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java b/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java index 2cae4a7435..b4e0203fb4 100644 --- a/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java +++ b/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java @@ -790,12 +790,10 @@ private void checkTimestampString(String s, int p, long d) { checkDateString("0200-01-01", d200); final int d100 = d200 - century + 1; checkDateString("0100-01-01", d100); - final int d000 = d100 - century; - checkDateString("0000-01-01", d000); } @Test public void testDateConversion() { - for (int i = 0; i < 4000; ++i) { + for (int i = 1; i < 4000; ++i) { for (int j = 1; j <= 12; ++j) { String date = String.format(Locale.ENGLISH, "%04d-%02d-28", i, j); assertThat(unixDateToString(ymdToUnixDate(i, j, 28)), is(date)); @@ -1681,6 +1679,24 @@ private static void assertThrows(Runnable runnable, * Test exception is raised if date in inappropriate meaning. */ @Test public void testBrokenDate() { + // Test case for https://issues.apache.org/jira/browse/CALCITE-6248 + // [CALCITE-6248] Illegal dates are accepted by casts + assertThrows(() -> DateTimeUtils.dateStringToUnixDate("2023-02-29"), + IllegalArgumentException.class, + is("Value of DAY field is out of range in '2023-02-29'")); + + // Test case for https://issues.apache.org/jira/browse/CALCITE-6248 + // [CALCITE-6248] Illegal dates are accepted by casts + assertThrows(() -> DateTimeUtils.dateStringToUnixDate("2023-13-1"), + IllegalArgumentException.class, + is("Value of MONTH field is out of range in '2023-13-1'")); + + // Test case for https://issues.apache.org/jira/browse/CALCITE-6248 + // [CALCITE-6248] Illegal dates are accepted by casts + assertThrows(() -> DateTimeUtils.dateStringToUnixDate("0-1-1"), + IllegalArgumentException.class, + is("Value of YEAR field is out of range in '0-1-1'")); + // 2023 is not a leap year assertThrows(() -> DateTimeUtils.timestampStringToUnixDate("2023-02-29 12:00:00.123"),