diff --git a/pkg/sql/plan/function/func_unary.go b/pkg/sql/plan/function/func_unary.go index f9c59f806cdbf..36804d1b3576c 100644 --- a/pkg/sql/plan/function/func_unary.go +++ b/pkg/sql/plan/function/func_unary.go @@ -4753,6 +4753,735 @@ func TimeToSecond(ivecs []*vector.Vector, result vector.FunctionResultWrapper, p }, selectList) } +func timeStringToFixedWithNullOnError[T types.FixedSizeTExceptStrType]( + ivecs []*vector.Vector, + result vector.FunctionResultWrapper, + length int, + selectList *FunctionSelectList, + fn func(hour uint32, minute, second uint8) T, +) error { + strParam := vector.GenerateFunctionStrParameter(ivecs[0]) + rs := vector.MustFunctionResult[T](result) + var zero T + + for i := uint64(0); i < uint64(length); i++ { + if selectList != nil && (selectList.IgnoreAllRow() || + (!selectList.ShouldEvalAllRow() && selectList.Contains(i))) { + if err := rs.Append(zero, true); err != nil { + return err + } + continue + } + + strVal, null := strParam.GetStrValue(i) + str := functionUtil.QuickBytesToStr(strVal) + if null || mysqlAllWhitespaceForExtract(str) { + if err := rs.Append(zero, true); err != nil { + return err + } + continue + } + + hour, minute, second, ok := timeStringToClockForExtract(str) + if !ok { + if err := rs.Append(zero, true); err != nil { + return err + } + continue + } + + if err := rs.Append(fn(uint32(hour), minute, second), false); err != nil { + return err + } + } + return nil +} + +// timeStringToClockForExtract follows MySQL's string-to-TIME coercion for +// HOUR, MINUTE, and SECOND. Its grammar ownership is ordered deliberately: +// +// - A single outer '-' belongs to the TIME sign. +// - An unambiguous separated DATETIME owns its date and clock fields. +// - Compact DATETIME owns only its exact numeric/fractional form. +// - Remaining input is scanned once as TIME/day-TIME/compact-TIME. +// - A complete DATE prefix is the final fallback (00:MM:YY). +// +// The general temporal parsers accept different grammars and must not receive +// arbitrary TIME-shaped user input here. +func timeStringToClockForExtract(str string) (uint64, uint8, uint8, bool) { + // Leading whitespace belongs to the TIME scanner. Keep trailing whitespace: + // it can decide whether an otherwise ambiguous prefix is DATETIME or TIME. + str = mysqlTrimLeftWhitespaceForExtract(str) + if len(str) == 0 { + return 0, 0, 0, false + } + if str[0] == '-' { + str = str[1:] + if len(str) == 0 { + return 0, 0, 0, false + } + } + if result := mysqlSeparatedDatetimeClockForExtract(str); result.matched { + return result.hour, result.minute, result.second, result.valid + } + if result := parseCompactDatetimeClockForExtract(str); result.matched { + return result.hour, result.minute, result.second, result.valid + } + return mysqlTimeStringToClockForExtract(str) +} + +func mysqlTimeStringToClockForExtract(str string) (uint64, uint8, uint8, bool) { + if mysqlLeadingDigitsExceedUint32(str) { + return 0, 0, 0, false + } + + // Retain a valid TIME prefix before trailing text, for example + // "15:30:45abc". Invalid clocks such as "12:60:00" still fail because + // their complete prefix cannot be parsed as TIME. + if hour, minute, second, ok := mysqlTimePrefixClockForExtract(str); ok { + return hour, minute, second, true + } + + // MySQL consumes a complete date prefix through its leading year field: + // "2024-12-20" and "2024-12-20foo" both become the compact TIME + // 00:20:24. An ISO-T suffix does not make it a DATETIME for this TIME + // coercion, so it follows the same date-prefix rule. + if mysqlDatePrefixTimeStringForExtract(str) { + if hour, minute, second, ok := mysqlTimePrefixClockForExtract(str[:4]); ok { + return hour, minute, second, true + } + } + return 0, 0, 0, false +} + +func mysqlDateOnlyStringForExtract(str string) bool { + return len(str) == 10 && asciiDigits(str[:4]) && asciiDigits(str[5:7]) && + asciiDigits(str[8:10]) && (str[4] == '-' || str[4] == '/') && str[7] == str[4] +} + +func mysqlDatePrefixTimeStringForExtract(str string) bool { + return len(str) >= 10 && mysqlDateOnlyStringForExtract(str[:10]) +} + +func mysqlLeadingDigitsExceedUint32(str string) bool { + value := uint64(0) + for i := 0; i < len(str) && str[i] >= '0' && str[i] <= '9'; i++ { + digit := uint64(str[i] - '0') + if value > (math.MaxUint32-digit)/10 { + return true + } + value = value*10 + digit + } + return false +} + +func asciiDigits(str string) bool { + for i := 0; i < len(str); i++ { + if str[i] < '0' || str[i] > '9' { + return false + } + } + return true +} + +type timeExtractParseResult struct { + hour uint64 + minute, second uint8 + matched, valid bool +} + +func mysqlSeparatedDatetimeClockForExtract(str string) timeExtractParseResult { + pos := 0 + year, yearDigits, ok := mysqlVariableDigitsForExtract(str, &pos) + if !ok || yearDigits > 4 || pos >= len(str) || !mysqlDatetimePunctuationForExtract(str[pos]) { + return timeExtractParseResult{} + } + // A one-digit year shares a prefix with compact TIME. It belongs to + // separated DATETIME only when the complete following clock is an + // unambiguous HH:MM:SS spelling; otherwise the TIME scanner owns it. + if yearDigits == 1 && !mysqlOneDigitYearDatetimeClockShapeForExtract(str, pos) { + return timeExtractParseResult{} + } + dateSeparator := str[pos] + // A two-digit colon prefix is a TIME first (for example, "12:34:56"), + // not a date with an invalid month. Four-digit colon dates remain valid. + if yearDigits == 2 && dateSeparator == ':' { + return timeExtractParseResult{} + } + if yearDigits == 2 { + year = uint64(adjustYear(int(year))) + } + for pos < len(str) && mysqlDatetimePunctuationForExtract(str[pos]) { + pos++ + } + month, _, ok := mysqlVariableDigitsForExtract(str, &pos) + if !ok || pos >= len(str) || !mysqlDatetimePunctuationForExtract(str[pos]) { + return timeExtractParseResult{} + } + for pos < len(str) && mysqlDatetimePunctuationForExtract(str[pos]) { + pos++ + } + day, _, ok := mysqlVariableDigitsForExtract(str, &pos) + if !ok || pos >= len(str) || !mysqlWhitespaceForExtract(str[pos]) { + // A complete date without a clock is still handled by the TIME path. + return timeExtractParseResult{} + } + result := timeExtractParseResult{matched: true} + if !mysqlDatetimeDateForExtract(year, month, day) { + // A padded three-digit date-shaped prefix is a TIME prefix in MySQL. + // Do not let the DATETIME classifier discard its consumed clock fields. + if yearDigits == 3 && mysqlEndsWithWhitespaceForExtract(str) { + return timeExtractParseResult{} + } + return result + } + for pos < len(str) && mysqlWhitespaceForExtract(str[pos]) { + pos++ + } + mysqlConsumeDatetimeClockSignsForExtract(str, &pos) + hour, _, ok := mysqlVariableDigitsForExtract(str, &pos) + if !ok { + return result + } + minute := uint64(0) + second := uint64(0) + if pos < len(str) && mysqlDatetimePunctuationForExtract(str[pos]) { + pos++ + for pos < len(str) && mysqlDatetimePunctuationForExtract(str[pos]) { + pos++ + } + if pos < len(str) && str[pos] >= '0' && str[pos] <= '9' { + minute, _, ok = mysqlVariableDigitsForExtract(str, &pos) + if !ok { + return result + } + // MySQL accepts a DATETIME whose clock ends after the minute and + // supplies the omitted seconds as zero. Once a second-field + // separator is consumed, repeated separators still retain the + // previously parsed hour and minute (for example, + // "... 12:34::56"). + if pos < len(str) && mysqlDatetimePunctuationForExtract(str[pos]) { + pos++ + for pos < len(str) && mysqlDatetimePunctuationForExtract(str[pos]) { + pos++ + } + if pos < len(str) && str[pos] >= '0' && str[pos] <= '9' { + second, _, ok = mysqlVariableDigitsForExtract(str, &pos) + if !ok { + return result + } + } + } + } + } + // Whitespace or an unconsumed sign after a clock prefix terminates DATETIME + // ownership. The DATE-prefix fallback then applies its own compact TIME + // coercion. + if !mysqlDatetimeClockSuffixForExtract(str, pos) { + return timeExtractParseResult{} + } + // A complete date followed by an hour or a trailing field separator is a + // valid DATETIME prefix. Do not fall back to compact TIME coercion after + // this branch has consumed the date and hour. + if !mysqlDatetimeDateForExtract(year, month, day) || hour > 23 || minute > 59 || second > 59 { + return result + } + result.hour = hour + result.minute = uint8(minute) + result.second = uint8(second) + result.valid = true + return result +} + +func mysqlOneDigitYearDatetimeClockShapeForExtract(str string, pos int) bool { + // pos points at the separator immediately after the one-digit year. + for pos < len(str) && mysqlDatetimePunctuationForExtract(str[pos]) { + pos++ + } + if _, _, ok := mysqlVariableDigitsForExtract(str, &pos); !ok || + pos >= len(str) || !mysqlDatetimePunctuationForExtract(str[pos]) { + return false + } + for pos < len(str) && mysqlDatetimePunctuationForExtract(str[pos]) { + pos++ + } + if _, _, ok := mysqlVariableDigitsForExtract(str, &pos); !ok || + pos >= len(str) || !mysqlWhitespaceForExtract(str[pos]) { + return false + } + for pos < len(str) && mysqlWhitespaceForExtract(str[pos]) { + pos++ + } + mysqlConsumeDatetimeClockSignsForExtract(str, &pos) + if _, digits, ok := mysqlVariableDigitsForExtract(str, &pos); !ok || digits != 2 || + pos >= len(str) || !mysqlDatetimePunctuationForExtract(str[pos]) { + return false + } + for pos < len(str) && mysqlDatetimePunctuationForExtract(str[pos]) { + pos++ + } + if _, digits, ok := mysqlVariableDigitsForExtract(str, &pos); !ok || digits != 2 || + pos >= len(str) || !mysqlDatetimePunctuationForExtract(str[pos]) { + return false + } + for pos < len(str) && mysqlDatetimePunctuationForExtract(str[pos]) { + pos++ + } + _, digits, ok := mysqlVariableDigitsForExtract(str, &pos) + return ok && digits == 2 +} + +func mysqlConsumeDatetimeClockSignsForExtract(str string, pos *int) { + for *pos < len(str) && (str[*pos] == '+' || str[*pos] == '-') { + *pos = *pos + 1 + } +} + +func mysqlDatetimeClockSuffixForExtract(str string, pos int) bool { + if pos == len(str) { + return true + } + if mysqlWhitespaceForExtract(str[pos]) { + // A trailing whitespace-only suffix terminates a complete DATETIME + // clock. Whitespace followed by another token transfers ownership back + // to the TIME/date-prefix grammar. + for pos < len(str) && mysqlWhitespaceForExtract(str[pos]) { + pos++ + } + return pos == len(str) + } + if str[pos] == '+' || str[pos] == '-' { + return false + } + return true +} + +func mysqlDatetimeDateForExtract(year, month, day uint64) bool { + if year > 9999 || month > 12 || day > 31 { + return false + } + // A zero month or day is an incomplete calendar value; string-to-TIME + // coercion can still extract its clock. Once both are nonzero, retain + // calendar validation even when the year is zero. + if month == 0 || day == 0 { + return true + } + if year == 0 { + return day <= mysqlDaysInMonthForExtract(year, month) + } + return types.ValidDate(int32(year), uint8(month), uint8(day)) +} + +func mysqlDaysInMonthForExtract(year, month uint64) uint64 { + switch month { + case 4, 6, 9, 11: + return 30 + case 2: + // MySQL's zero year accepts February 28 but not February 29. Do not + // apply the proleptic Gregorian leap-year rule to year zero. + if year == 0 { + return 28 + } + if year%4 == 0 && (year%100 != 0 || year%400 == 0) { + return 29 + } + return 28 + default: + return 31 + } +} + +func mysqlDateSeparatorForExtract(c byte) bool { + return c == '-' || c == '/' || c == ':' +} + +func mysqlDatetimePunctuationForExtract(c byte) bool { + return (c >= '!' && c <= '/') || (c >= ':' && c <= '@') || + (c >= '[' && c <= '`') || (c >= '{' && c <= '~') +} + +func mysqlWhitespaceForExtract(c byte) bool { + switch c { + case ' ', '\t', '\n', '\r', '\f', '\v': + return true + default: + return false + } +} + +func mysqlAllWhitespaceForExtract(str string) bool { + if len(str) == 0 { + return true + } + for i := 0; i < len(str); i++ { + if !mysqlWhitespaceForExtract(str[i]) { + return false + } + } + return true +} + +func mysqlEndsWithWhitespaceForExtract(str string) bool { + return len(str) > 0 && mysqlWhitespaceForExtract(str[len(str)-1]) +} + +func mysqlTrimLeftWhitespaceForExtract(str string) string { + pos := 0 + for pos < len(str) && mysqlWhitespaceForExtract(str[pos]) { + pos++ + } + return str[pos:] +} + +func mysqlFirstWhitespaceForExtract(str string) int { + for i := 0; i < len(str); i++ { + if mysqlWhitespaceForExtract(str[i]) { + return i + } + } + return -1 +} + +func mysqlTimePrefixClockForExtract(str string) (uint64, uint8, uint8, bool) { + prefix := mysqlTimePrefixForExtract(str) + if len(prefix) == 0 { + return 0, 0, 0, false + } + + prefix = mysqlTrimLeftWhitespaceForExtract(prefix) + if len(prefix) == 0 { + return 0, 0, 0, false + } + + if dot := strings.IndexByte(prefix, '.'); dot >= 0 { + // MySQL consumes the complete TIME prefix before a fractional separator. + // The remainder is irrelevant to HOUR/MINUTE/SECOND extraction, including + // an empty fraction or trailing non-numeric text. + prefix = prefix[:dot] + } + + day := uint64(0) + hasDay := false + if space := mysqlFirstWhitespaceForExtract(prefix); space >= 0 { + postDay := mysqlTrimLeftWhitespaceForExtract(prefix[space:]) + if space > 0 && asciiDigits(prefix[:space]) && mysqlDayTimeClockCandidateForExtract(postDay) { + day = mysqlClampedDigitsForExtract(prefix[:space], 35) + hasDay = true + prefix = postDay + if len(prefix) == 0 { + return 0, 0, 0, false + } + if clockEnd := mysqlFirstWhitespaceForExtract(prefix); clockEnd >= 0 { + prefix = prefix[:clockEnd] + } + } else { + // A clock prefix followed by whitespace keeps the fields consumed + // before that whitespace. It is not a day separator unless every + // byte before it is a digit. + prefix = prefix[:space] + } + } + + parseClock := mysqlClockFieldsForExtract + if hasDay { + parseClock = mysqlDayClockFieldsForExtract + } + hour, minute, second, ok := parseClock(prefix) + if !ok { + return 0, 0, 0, false + } + if hasDay { + if day >= 35 || hour > 838-day*24 { + hour = 839 + } else { + hour += day * 24 + } + } + if hour > 838 { + return 838, 59, 59, true + } + return hour, minute, second, true +} + +func mysqlDayTimeClockCandidateForExtract(str string) bool { + digits := 0 + for digits < len(str) && str[digits] >= '0' && str[digits] <= '9' { + digits++ + } + // A zero-padded/multi-digit field owns day-TIME even without a separator. + // A one-digit field owns it only when ':' proves that the field is an hour; + // otherwise inputs such as "1 2" retain the already consumed compact prefix. + return digits >= 2 || (digits == 1 && digits < len(str) && str[digits] == ':') +} + +func mysqlClockFieldsForExtract(str string) (uint64, uint8, uint8, bool) { + if len(str) == 0 { + return 0, 0, 0, false + } + + pos := 0 + for pos < len(str) && str[pos] >= '0' && str[pos] <= '9' { + pos++ + } + hourText := str[:pos] + if pos == len(str) || str[pos] != ':' { + // A punctuation-only suffix terminates a valid compact TIME prefix. This + // keeps inputs such as "12-.abc" as 00:00:12 without turning a + // date-looking value such as "2024-12-20" into a compact TIME here. + if len(hourText) == 0 || !mysqlCompactTimePrefixBoundary(hourText, str[pos:]) { + return 0, 0, 0, false + } + switch len(hourText) { + case 1, 2: + second := mysqlClampedDigitsForExtract(hourText, 60) + if second >= 60 { + return 0, 0, 0, false + } + return 0, 0, uint8(second), true + case 3, 4: + minute := mysqlClampedDigitsForExtract(hourText[:len(hourText)-2], 60) + second := mysqlClampedDigitsForExtract(hourText[len(hourText)-2:], 60) + if minute >= 60 || second >= 60 { + return 0, 0, 0, false + } + return 0, uint8(minute), uint8(second), true + default: + hour := mysqlClampedDigitsForExtract(hourText[:len(hourText)-4], 839) + minute := mysqlClampedDigitsForExtract(hourText[len(hourText)-4:len(hourText)-2], 60) + second := mysqlClampedDigitsForExtract(hourText[len(hourText)-2:], 60) + if minute >= 60 || second >= 60 { + return 0, 0, 0, false + } + return hour, uint8(minute), uint8(second), true + } + } + + pos++ + minuteStart := pos + for pos < len(str) && str[pos] >= '0' && str[pos] <= '9' { + pos++ + } + minuteText := str[minuteStart:pos] + if len(minuteText) == 0 { + // MySQL ignores a trailing colon and applies its compact TIME coercion + // to the preceding digits. This also stops at the first colon in + // "12::56", preserving 00:00:12 rather than rejecting the input. + if len(hourText) == 0 { + return 0, 0, 0, false + } + return mysqlClockFieldsForExtract(hourText) + } + + minute := mysqlClampedDigitsForExtract(minuteText, 60) + if minute >= 60 { + return 0, 0, 0, false + } + hour := uint64(0) + if len(hourText) > 0 { + hour = mysqlClampedDigitsForExtract(hourText, 839) + } + if pos == len(str) || str[pos] != ':' { + // Stop at the valid HOUR:MINUTE prefix. mysqlTimePrefixForExtract has + // already retained intentionally tolerated trailing text. + return hour, uint8(minute), 0, true + } + + pos++ + secondStart := pos + for pos < len(str) && str[pos] >= '0' && str[pos] <= '9' { + pos++ + } + secondText := str[secondStart:pos] + if len(secondText) == 0 { + // A trailing second separator is a valid prefix with omitted seconds. + return hour, uint8(minute), 0, true + } + second := mysqlClampedDigitsForExtract(secondText, 60) + if second >= 60 { + return 0, 0, 0, false + } + // Stop after a complete HOUR:MINUTE:SECOND prefix. Following punctuation, + // including a third colon, does not discard the parsed clock. + return hour, uint8(minute), uint8(second), true +} + +func mysqlCompactTimePrefixBoundary(prefix, suffix string) bool { + // Once compact TIME has consumed its leading digits, later punctuation and + // digits cannot reinterpret that field. Preserve the sole exception for a + // separated date shape, which is handled by the date branch (and + // therefore still validates its calendar fields). + if len(prefix) < 4 || len(suffix) < 3 || !mysqlDateSeparatorForExtract(suffix[0]) { + return true + } + pos := 1 + _, _, ok := mysqlVariableDigitsForExtract(suffix, &pos) + return !ok || pos == len(suffix) || suffix[pos] != suffix[0] +} + +func mysqlDayClockFieldsForExtract(str string) (uint64, uint8, uint8, bool) { + pos := 0 + for pos < len(str) && str[pos] >= '0' && str[pos] <= '9' { + pos++ + } + if pos == 0 { + return 0, 0, 0, false + } + hour := mysqlClampedDigitsForExtract(str[:pos], 839) + if pos == len(str) || str[pos] != ':' { + return hour, 0, 0, true + } + + pos++ + minuteStart := pos + for pos < len(str) && str[pos] >= '0' && str[pos] <= '9' { + pos++ + } + if minuteStart == pos { + return hour, 0, 0, true + } + minute := mysqlClampedDigitsForExtract(str[minuteStart:pos], 60) + if minute >= 60 { + return 0, 0, 0, false + } + if pos == len(str) || str[pos] != ':' { + return hour, uint8(minute), 0, true + } + + pos++ + secondStart := pos + for pos < len(str) && str[pos] >= '0' && str[pos] <= '9' { + pos++ + } + if secondStart == pos { + return hour, uint8(minute), 0, true + } + second := mysqlClampedDigitsForExtract(str[secondStart:pos], 60) + if second >= 60 { + return 0, 0, 0, false + } + return hour, uint8(minute), uint8(second), true +} + +func mysqlClampedDigitsForExtract(str string, limit uint64) uint64 { + if len(str) == 0 { + return limit + } + value := uint64(0) + for i := 0; i < len(str); i++ { + if str[i] < '0' || str[i] > '9' || value > (limit-uint64(str[i]-'0'))/10 { + return limit + } + value = value*10 + uint64(str[i]-'0') + } + return value +} + +func mysqlVariableDigitsForExtract(str string, pos *int) (uint64, int, bool) { + start := *pos + for *pos < len(str) && str[*pos] >= '0' && str[*pos] <= '9' { + *pos = *pos + 1 + } + if *pos == start { + return 0, 0, false + } + return mysqlClampedDigitsForExtract(str[start:*pos], math.MaxUint64), *pos - start, true +} + +func parseCompactDatetimeClockForExtract(str string) timeExtractParseResult { + digitCount := 0 + for digitCount < len(str) && str[digitCount] >= '0' && str[digitCount] <= '9' { + digitCount++ + } + + if digitCount >= 14 { + if !mysqlCompactDatetimeSuffixForExtract(str[14:]) { + return timeExtractParseResult{matched: true} + } + hour, minute, second, ok := compactDatetimeClockForExtract(str[:14], false) + return timeExtractParseResult{hour: hour, minute: minute, second: second, matched: true, valid: ok} + } + if digitCount >= 12 { + if !mysqlCompactDatetimeSuffixForExtract(str[12:]) { + return timeExtractParseResult{matched: true} + } + hour, minute, second, ok := compactDatetimeClockForExtract(str[:12], true) + return timeExtractParseResult{hour: hour, minute: minute, second: second, matched: true, valid: ok} + } + return timeExtractParseResult{} +} + +func mysqlCompactDatetimeSuffixForExtract(suffix string) bool { + if len(suffix) == 0 { + return true + } + // MySQL consumes a compact DATETIME through the fractional separator. The + // fraction may be empty or may stop before trailing non-numeric text, but a + // suffix without a decimal separator is not part of this coercion. + if suffix[0] != '.' { + return false + } + pos := 1 + for pos < len(suffix) && suffix[pos] >= '0' && suffix[pos] <= '9' { + pos++ + } + return pos == len(suffix) || (suffix[pos] != '+' && suffix[pos] != '-') +} + +func compactDatetimeClockForExtract(str string, twoDigitYear bool) (uint64, uint8, uint8, bool) { + yearWidth := 4 + if twoDigitYear { + yearWidth = 2 + } + + year := 0 + for i := 0; i < yearWidth; i++ { + year = year*10 + int(str[i]-'0') + } + if twoDigitYear { + year = adjustYear(year) + } + + month := (str[yearWidth]-'0')*10 + str[yearWidth+1] - '0' + day := (str[yearWidth+2]-'0')*10 + str[yearWidth+3] - '0' + hour := (str[yearWidth+4]-'0')*10 + str[yearWidth+5] - '0' + minute := (str[yearWidth+6]-'0')*10 + str[yearWidth+7] - '0' + second := (str[yearWidth+8]-'0')*10 + str[yearWidth+9] - '0' + if !mysqlDatetimeDateForExtract(uint64(year), uint64(month), uint64(day)) || !types.ValidTimeInDay(hour, minute, second) { + return 0, 0, 0, false + } + return uint64(hour), minute, second, true +} + +func mysqlTimePrefixForExtract(str string) string { + end := 0 + for end < len(str) { + c := str[end] + if (c < '0' || c > '9') && c != ':' && c != '.' && c != '-' && !mysqlWhitespaceForExtract(c) { + break + } + end++ + } + return str[:end] +} + +func StringToHour(ivecs []*vector.Vector, result vector.FunctionResultWrapper, _ *process.Process, length int, selectList *FunctionSelectList) error { + return timeStringToFixedWithNullOnError(ivecs, result, length, selectList, func(hour uint32, _ uint8, _ uint8) uint32 { + return hour + }) +} + +func StringToMinute(ivecs []*vector.Vector, result vector.FunctionResultWrapper, _ *process.Process, length int, selectList *FunctionSelectList) error { + return timeStringToFixedWithNullOnError(ivecs, result, length, selectList, func(_ uint32, minute uint8, _ uint8) uint8 { + return minute + }) +} + +func StringToSecond(ivecs []*vector.Vector, result vector.FunctionResultWrapper, _ *process.Process, length int, selectList *FunctionSelectList) error { + return timeStringToFixedWithNullOnError(ivecs, result, length, selectList, func(_ uint32, _ uint8, second uint8) uint8 { + return second + }) +} + // TimeToSec returns the time argument, converted to seconds (total seconds) func TimeToSec(ivecs []*vector.Vector, result vector.FunctionResultWrapper, proc *process.Process, length int, selectList *FunctionSelectList) error { return opUnaryFixedToFixed[types.Time, int64](ivecs, result, proc, length, func(v types.Time) int64 { diff --git a/pkg/sql/plan/function/func_unary_test.go b/pkg/sql/plan/function/func_unary_test.go index ae988601f5d45..55fcb3dd1f115 100644 --- a/pkg/sql/plan/function/func_unary_test.go +++ b/pkg/sql/plan/function/func_unary_test.go @@ -4159,6 +4159,647 @@ func TestSecond(t *testing.T) { } } +func TestStringTimeExtract(t *testing.T) { + proc := testutil.NewProcess(t) + inputs := []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), + []string{ + "12:30:45", "272:59:59", "-272:59:59", "2 03:04:05", "123045", + "2024-12-20 15:30:45", "20241220153045", "241220153045", "2024-12-20", + "15:30:45abc", "2024-12-20 15:30:45abc", "2024-12-20foo", "invalid", "", " ", "\t", + "2 03:04:05.123", "12:30:45.123456", "272:59:59.123456", "-272:59:59.123456", + "-2 03:04:05.123", "839:00:00", "-839:00:00", "20241220", "12:60:00", "12:30:60", + "2024-12-20T15:30:45.123456", " 12:34:56 ", + }, + []bool{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false}), + } + + testCases := []struct { + name string + expect FunctionTestResult + fn fEvalFn + }{ + { + name: "hour", + expect: NewFunctionTestResult(types.T_uint32.ToType(), false, + []uint32{12, 272, 272, 51, 12, 15, 15, 15, 0, 15, 15, 0, 0, 0, 0, 0, + 51, 12, 272, 272, 51, 838, 838, 838, 0, 0, 0, 12}, + []bool{false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, + false, false, false, false, false, false, false, false, true, true, false, false}), + fn: StringToHour, + }, + { + name: "minute", + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{30, 59, 59, 4, 30, 30, 30, 30, 20, 30, 30, 20, 0, 0, 0, 0, + 4, 30, 59, 59, 4, 59, 59, 59, 0, 0, 20, 34}, + []bool{false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, + false, false, false, false, false, false, false, false, true, true, false, false}), + fn: StringToMinute, + }, + { + name: "second", + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{45, 59, 59, 5, 45, 45, 45, 45, 24, 45, 45, 24, 0, 0, 0, 0, + 5, 45, 59, 59, 5, 59, 59, 59, 0, 0, 24, 56}, + []bool{false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, + false, false, false, false, false, false, false, false, true, true, false, false}), + fn: StringToSecond, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tcc := NewFunctionTestCase(proc, inputs, tc.expect, tc.fn) + succeed, info := tcc.Run() + require.True(t, succeed, info) + }) + } + +} + +func TestStringTimeExtractZeroDateAndISOSeparator(t *testing.T) { + proc := testutil.NewProcess(t) + inputs := []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{ + "0000-01-01 12:34:56", + "2024-00-01 11:22:33", + "2024-01-00 10:20:30", + "0000-00-00 09:08:07", + "2024-12-20T15:30:45.123456", + }, nil), + } + + for _, tc := range []struct { + name string + expect FunctionTestResult + fn fEvalFn + }{ + {"hour", NewFunctionTestResult(types.T_uint32.ToType(), false, []uint32{12, 11, 10, 9, 0}, nil), StringToHour}, + {"minute", NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{34, 22, 20, 8, 20}, nil), StringToMinute}, + {"second", NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{56, 33, 30, 7, 24}, nil), StringToSecond}, + } { + t.Run(tc.name, func(t *testing.T) { + tcc := NewFunctionTestCase(proc, inputs, tc.expect, tc.fn) + succeed, info := tcc.Run() + require.True(t, succeed, info) + }) + } +} + +func TestStringTimeExtractIncompleteDatetimeAndZeroYearCalendar(t *testing.T) { + proc := testutil.NewProcess(t) + inputs := []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{ + "2024-12-20 12:34", + "0000-02-28 12:34:56", + "0000-02-29 12:34:56", + "0000-02-31 12:34:56", + "0000-04-31 12:34:56", + "1900-02-29 12:34:56", + "2000-02-29 01:02:03", + }, nil), + } + + for _, tc := range []struct { + name string + expect FunctionTestResult + fn fEvalFn + }{ + {"hour", NewFunctionTestResult(types.T_uint32.ToType(), false, []uint32{12, 12, 0, 0, 0, 0, 1}, []bool{false, false, true, true, true, true, false}), StringToHour}, + {"minute", NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{34, 34, 0, 0, 0, 0, 2}, []bool{false, false, true, true, true, true, false}), StringToMinute}, + {"second", NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{0, 56, 0, 0, 0, 0, 3}, []bool{false, false, true, true, true, true, false}), StringToSecond}, + } { + t.Run(tc.name, func(t *testing.T) { + tcc := NewFunctionTestCase(proc, inputs, tc.expect, tc.fn) + succeed, info := tcc.Run() + require.True(t, succeed, info) + }) + } +} + +func TestStringTimeExtractCompactDatetimeSuffix(t *testing.T) { + for _, typ := range []types.T{types.T_varchar, types.T_char, types.T_text} { + t.Run(typ.String(), func(t *testing.T) { + proc := testutil.NewProcess(t) + inputs := []FunctionTestInput{ + NewFunctionTestInput(typ.ToType(), []string{ + "20241220153045", "241220153045", "20241220153045.999999", "241220153045.9", + "20241220153045.123abc", "20241220153045.", "241220153045.123abc", "241220153045.", + "202412201530451", "2412201530451", + "20241220153045abc", "241220153045abc", + "20241220153045.-", "20241220153045.+", "241220153045.-", "241220153045.+", + "20241220153045.123-", "20241220153045.123+", "241220153045.123-", "241220153045.123+", + "20241220153045.123abc-", "241220153045.123abc+", + }, nil), + } + + for _, tc := range []struct { + name string + expect FunctionTestResult + fn fEvalFn + }{ + {"hour", NewFunctionTestResult(types.T_uint32.ToType(), false, []uint32{15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15}, []bool{false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, false, false}), StringToHour}, + {"minute", NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{30, 30, 30, 30, 30, 30, 30, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 30}, []bool{false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, false, false}), StringToMinute}, + {"second", NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{45, 45, 45, 45, 45, 45, 45, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 45}, []bool{false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, false, false}), StringToSecond}, + } { + t.Run(tc.name, func(t *testing.T) { + tcc := NewFunctionTestCase(proc, inputs, tc.expect, tc.fn) + succeed, info := tcc.Run() + require.True(t, succeed, info) + }) + } + }) + } +} + +func TestStringTimeExtractPartialClockPrefix(t *testing.T) { + proc := testutil.NewProcess(t) + inputs := []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{ + "12:", ":34", "12:34:56-", "12:34:56..", "123:", "1234:", "12345:", "123456:", + }, nil), + } + + for _, tc := range []struct { + name string + expect FunctionTestResult + fn fEvalFn + }{ + {"hour", NewFunctionTestResult(types.T_uint32.ToType(), false, []uint32{0, 0, 12, 12, 0, 0, 1, 12}, nil), StringToHour}, + {"minute", NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{0, 34, 34, 34, 1, 12, 23, 34}, nil), StringToMinute}, + {"second", NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{12, 0, 56, 56, 23, 34, 45, 56}, nil), StringToSecond}, + } { + t.Run(tc.name, func(t *testing.T) { + tcc := NewFunctionTestCase(proc, inputs, tc.expect, tc.fn) + succeed, info := tcc.Run() + require.True(t, succeed, info) + }) + } +} + +func TestStringTimeExtractMySQLPartialPrefixBoundaries(t *testing.T) { + for _, typ := range []types.T{types.T_varchar, types.T_char, types.T_text} { + t.Run(typ.String(), func(t *testing.T) { + proc := testutil.NewProcess(t) + input := NewFunctionTestInput(typ.ToType(), []string{ + ":34:56", "12:34:", "12::56", "12:34:56:", "12-.abc", + }, nil) + + for _, tc := range []struct { + name string + fn fEvalFn + expect FunctionTestResult + }{ + { + name: "hour", + fn: StringToHour, + expect: NewFunctionTestResult(types.T_uint32.ToType(), false, + []uint32{0, 12, 0, 12, 0}, nil), + }, + { + name: "minute", + fn: StringToMinute, + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{34, 34, 0, 34, 0}, nil), + }, + { + name: "second", + fn: StringToSecond, + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{56, 0, 12, 56, 12}, nil), + }, + } { + t.Run(tc.name, func(t *testing.T) { + ftc := NewFunctionTestCase(proc, []FunctionTestInput{input}, tc.expect, tc.fn) + success, info := ftc.Run() + require.True(t, success, info) + }) + } + }) + } +} + +func TestStringTimeExtractContextAwarePrefixBoundaries(t *testing.T) { + for _, typ := range []types.T{types.T_varchar, types.T_char, types.T_text} { + t.Run(typ.String(), func(t *testing.T) { + proc := testutil.NewProcess(t) + input := NewFunctionTestInput(typ.ToType(), []string{ + "12-34", "1234-56", + "1 02", "1 02:", "1 02-34", + "2024-12-20 12", "2024-12-20 12:", "2024-12-20 12::56", + "2024-12-20foo", "2024-12-20 12:34::56", "2024-12-20 12-34", + "2024-12-20 12/34/56", "2024@12@20 12@34@56", "2024-12-20\t12:34:56", "1\t02:34:56", + "12:34 56", "1 02:34 56", "1 02 34", + "2024--12--20 12:34:56", "2024/-12/-20 12:34:56", + "2024-12-20 -12:34:56", "2024-12-20 +12:34:56", + "2024-12-20 12:34 56", "2024-12-20 12 34", + }, nil) + + for _, tc := range []struct { + name string + fn fEvalFn + expect FunctionTestResult + }{ + { + name: "hour", + fn: StringToHour, + expect: NewFunctionTestResult(types.T_uint32.ToType(), false, + []uint32{0, 0, 26, 26, 26, 12, 12, 12, 0, 12, 12, 12, 12, 12, 26, 12, 26, 26, 12, 12, 12, 12, 0, 0}, nil), + }, + { + name: "minute", + fn: StringToMinute, + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{0, 12, 0, 0, 0, 0, 0, 56, 20, 34, 34, 34, 34, 34, 34, 34, 34, 0, 34, 34, 34, 34, 20, 20}, nil), + }, + { + name: "second", + fn: StringToSecond, + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{12, 34, 0, 0, 0, 0, 0, 0, 24, 56, 0, 56, 56, 56, 56, 0, 0, 0, 56, 56, 56, 56, 24, 24}, nil), + }, + } { + t.Run(tc.name, func(t *testing.T) { + ftc := NewFunctionTestCase(proc, []FunctionTestInput{input}, tc.expect, tc.fn) + success, info := ftc.Run() + require.True(t, success, info) + }) + } + }) + } +} + +func TestStringTimeExtractAmbiguousPrefixOwnership(t *testing.T) { + for _, typ := range []types.T{types.T_varchar, types.T_char, types.T_text} { + t.Run(typ.String(), func(t *testing.T) { + proc := testutil.NewProcess(t) + input := NewFunctionTestInput(typ.ToType(), []string{ + // A one-digit field before whitespace remains a compact TIME prefix; + // a zero-padded post-day field owns the day-TIME grammar instead. + "1 2", "1 02", + // A colon-delimited TIME prefix must not be claimed as a two-digit + // year DATE when its apparent date components are out of range. + "12:34:56", "12:34:56 78", + // One-digit date-like fields are compact TIME, while a long compact + // prefix followed by a repeated date separator is invalid. + "1-2-3 4:5:6", "12345:", "12345-1-1 1:2:3", + // The outer sign is consumed exactly once. + "-12:34", "--12:34", + // A DATETIME clock owns a run of leading signs, but a sign followed + // by text terminates that clock and leaves the DATE-prefix coercion. + "2024-12-20 +12:34", "2024-12-20 ++12:34", + "2024-12-20 12:34:56abc", + "2024-12-20 12:34:56+abc", "2024-12-20 12:34:56-abc", + }, nil) + + for _, tc := range []struct { + name string + fn fEvalFn + expect FunctionTestResult + }{ + { + name: "hour", + fn: StringToHour, + expect: NewFunctionTestResult(types.T_uint32.ToType(), false, + []uint32{0, 26, 12, 12, 0, 1, 0, 12, 0, 12, 12, 12, 0, 0}, + []bool{false, false, false, false, false, false, true, false, true, false, false, false, false, false}), + }, + { + name: "minute", + fn: StringToMinute, + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{0, 0, 34, 34, 0, 23, 0, 34, 0, 34, 34, 34, 20, 20}, + []bool{false, false, false, false, false, false, true, false, true, false, false, false, false, false}), + }, + { + name: "second", + fn: StringToSecond, + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{1, 0, 56, 56, 1, 45, 0, 0, 0, 0, 0, 56, 24, 24}, + []bool{false, false, false, false, false, false, true, false, true, false, false, false, false, false}), + }, + } { + t.Run(tc.name, func(t *testing.T) { + ftc := NewFunctionTestCase(proc, []FunctionTestInput{input}, tc.expect, tc.fn) + success, info := ftc.Run() + require.True(t, success, info) + }) + } + }) + } +} + +func TestStringTimeExtractReviewerGrammarBoundaries(t *testing.T) { + for _, typ := range []types.T{types.T_varchar, types.T_char, types.T_text} { + t.Run(typ.String(), func(t *testing.T) { + proc := testutil.NewProcess(t) + input := NewFunctionTestInput(typ.ToType(), []string{ + // A bare one-digit post-space field remains a compact prefix. A + // following clock separator transfers ownership to day-TIME. + "1 2", "1 2:3", "1 2:3:4", + "12 3", "12 3:4", "12 3:4:5", + // Separated DATETIME accepts two through four year digits. One + // digit remains compact TIME, and five digits remain invalid. + "1-2-3 4:5:6", "12-2-3 4:5:6", "123-2-3 4:5:6", + "1234-2-3 4:5:6", "12345-2-3 4:5:6", + // An unconsumed trailing sign after a complete clock terminates + // DATETIME ownership and leaves the DATE-prefix fallback. + "2024-12-20 12:34:56+", "2024-12-20 12:34:56-", + }, nil) + + for _, tc := range []struct { + name string + fn fEvalFn + expect FunctionTestResult + }{ + { + name: "hour", + fn: StringToHour, + expect: NewFunctionTestResult(types.T_uint32.ToType(), false, + []uint32{0, 26, 26, 0, 291, 291, 0, 4, 4, 4, 0, 0, 0}, + []bool{false, false, false, false, false, false, false, false, false, false, true, false, false}), + }, + { + name: "minute", + fn: StringToMinute, + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{0, 3, 3, 0, 4, 4, 0, 5, 5, 5, 0, 20, 20}, + []bool{false, false, false, false, false, false, false, false, false, false, true, false, false}), + }, + { + name: "second", + fn: StringToSecond, + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{1, 0, 4, 12, 0, 5, 1, 6, 6, 6, 0, 24, 24}, + []bool{false, false, false, false, false, false, false, false, false, false, true, false, false}), + }, + } { + t.Run(tc.name, func(t *testing.T) { + ftc := NewFunctionTestCase(proc, []FunctionTestInput{input}, tc.expect, tc.fn) + success, info := ftc.Run() + require.True(t, success, info) + }) + } + }) + } +} + +func TestStringTimeExtractOneDigitYearAndWhitespaceOwnership(t *testing.T) { + for _, typ := range []types.T{types.T_varchar, types.T_char, types.T_text} { + t.Run(typ.String(), func(t *testing.T) { + proc := testutil.NewProcess(t) + input := NewFunctionTestInput(typ.ToType(), []string{ + // One-digit years are DATETIME only with a complete two-digit + // clock. Every one-/two-digit clock-width boundary otherwise + // retains compact-TIME ownership. + "1-1-1 1:2:3", "1-1-1 01:2:3", "1-1-1 1:02:3", "1-1-1 1:2:03", + "1-1-1 01:02:3", "1-1-1 01:2:03", "1-1-1 1:02:03", "1-1-1 01:02:03", + // The same width contract applies to the zero-year spelling. + "0-1-1 1:2:3", "0-1-1 12:34:56", + // Trailing whitespace is part of the ordered grammar. It must not + // be removed before the date-shaped TIME prefix is classified. + "123:34:56 78", "123:34:56 78 ", "\t123:34:56 78\t", + "838:59:59 78", "838:59:59 78 ", "\t838:59:59 78\t", + // A valid DATETIME remains owned with trailing whitespace; this is + // the nearest control for the padded invalid date-shaped prefixes. + "2024-12-20 12:34:56 ", + }, nil) + + for _, tc := range []struct { + name string + fn fEvalFn + expect FunctionTestResult + }{ + { + name: "hour", + fn: StringToHour, + expect: NewFunctionTestResult(types.T_uint32.ToType(), false, + []uint32{0, 0, 0, 0, 0, 0, 0, 1, 0, 12, 0, 123, 123, 0, 838, 838, 12}, + []bool{false, false, false, false, false, false, false, false, false, false, true, false, false, true, false, false, false}), + }, + { + name: "minute", + fn: StringToMinute, + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{0, 0, 0, 0, 0, 0, 0, 2, 0, 34, 0, 34, 34, 0, 59, 59, 34}, + []bool{false, false, false, false, false, false, false, false, false, false, true, false, false, true, false, false, false}), + }, + { + name: "second", + fn: StringToSecond, + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{1, 1, 1, 1, 1, 1, 1, 3, 0, 56, 0, 56, 56, 0, 59, 59, 56}, + []bool{false, false, false, false, false, false, false, false, false, false, true, false, false, true, false, false, false}), + }, + } { + t.Run(tc.name, func(t *testing.T) { + ftc := NewFunctionTestCase(proc, []FunctionTestInput{input}, tc.expect, tc.fn) + success, info := ftc.Run() + require.True(t, success, info) + }) + } + }) + } +} + +func TestStringTimeExtractWhitespace(t *testing.T) { + for _, typ := range []types.T{types.T_char, types.T_varchar, types.T_text} { + t.Run(typ.String(), func(t *testing.T) { + proc := testutil.NewProcess(t) + inputs := []FunctionTestInput{ + NewFunctionTestInput(typ.ToType(), []string{" ", "\t"}, []bool{false, false}), + } + + for _, tc := range []struct { + name string + expect FunctionTestResult + fn fEvalFn + }{ + {"hour", NewFunctionTestResult(types.T_uint32.ToType(), false, []uint32{0, 0}, []bool{true, true}), StringToHour}, + {"minute", NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{0, 0}, []bool{true, true}), StringToMinute}, + {"second", NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{0, 0}, []bool{true, true}), StringToSecond}, + } { + t.Run(tc.name, func(t *testing.T) { + tcc := NewFunctionTestCase(proc, inputs, tc.expect, tc.fn) + succeed, info := tcc.Run() + require.True(t, succeed, info) + }) + } + }) + } +} + +func TestStringTimeExtractTomorrowDatetime(t *testing.T) { + proc := testutil.NewProcess(t) + tomorrow := types.Today(time.UTC) + 1 + input := tomorrow.String() + " 01:02:03" + + testCases := []struct { + name string + expect FunctionTestResult + fn fEvalFn + }{ + { + name: "hour", + expect: NewFunctionTestResult(types.T_uint32.ToType(), false, []uint32{1}, []bool{false}), + fn: StringToHour, + }, + { + name: "minute", + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{2}, []bool{false}), + fn: StringToMinute, + }, + { + name: "second", + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, []uint8{3}, []bool{false}), + fn: StringToSecond, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + inputs := []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{input}, []bool{false}), + } + tcc := NewFunctionTestCase(proc, inputs, tc.expect, tc.fn) + succeed, info := tcc.Run() + require.True(t, succeed, info) + }) + } +} + +func TestStringTimeExtractRegisteredOverloads(t *testing.T) { + proc := testutil.NewProcess(t) + inputValues := []string{ + "12:30:45", "272:59:59", "2024-12-20 15:30:45", "2024-12-20", "invalid", "", + "2 03:04:05.123", "12:30:45.123456", "839:00:00", "20241220", "12:60:00", "12:30:60", + } + wantNulls := []bool{false, false, false, false, true, true, false, false, false, false, true, true} + + typeCases := []types.T{types.T_varchar, types.T_char, types.T_text} + functionCases := []struct { + name string + returnType types.T + hours []uint32 + parts []uint8 + }{ + {name: "hour", returnType: types.T_uint32, hours: []uint32{12, 272, 15, 0, 0, 0, 51, 12, 838, 838, 0, 0}}, + {name: "minute", returnType: types.T_uint8, parts: []uint8{30, 59, 30, 20, 0, 0, 4, 30, 59, 59, 0, 0}}, + {name: "second", returnType: types.T_uint8, parts: []uint8{45, 59, 45, 24, 0, 0, 5, 45, 59, 59, 0, 0}}, + } + + for _, inputType := range typeCases { + for _, functionCase := range functionCases { + t.Run(functionCase.name+"/"+inputType.String(), func(t *testing.T) { + input := newVectorByType(proc.Mp(), inputType.ToType(), inputValues, nil) + defer input.Free(proc.Mp()) + + fn, err := GetFunctionByName(proc.Ctx, functionCase.name, []types.Type{inputType.ToType()}) + require.NoError(t, err) + require.Equal(t, functionCase.returnType, fn.GetReturnType().Oid) + + out, err := RunFunctionDirectly(proc, fn.GetEncodedOverloadID(), []*vector.Vector{input}, len(inputValues)) + require.NoError(t, err) + defer out.Free(proc.Mp()) + + for i, wantNull := range wantNulls { + require.Equal(t, wantNull, out.IsNull(uint64(i))) + } + switch functionCase.returnType { + case types.T_uint32: + require.Equal(t, functionCase.hours, vector.MustFixedColWithTypeCheck[uint32](out)) + case types.T_uint8: + require.Equal(t, functionCase.parts, vector.MustFixedColWithTypeCheck[uint8](out)) + } + }) + } + } +} + +func TestStringTimeExtractMySQLBoundaryRegressions(t *testing.T) { + proc := testutil.NewProcess(t) + inputs := []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{ + "-2024-12-20 15:30:45", + "-20241220153045", + "20240230010203", + "2 30:00:00", + "60", + "24-12-20 15:30:45", + "2000:01:01 12:34:56", + "1998-01-01 00:00:009", + "2024-12-20 15:30:4560", + "2001-11-00 01:02:03", + "99999990000", + "- 12:34:56", + "12:34:56.", + }, []bool{false, false, false, false, false, false, false, false, false, false, false, false, false}), + } + + testCases := []struct { + name string + expect FunctionTestResult + fn fEvalFn + }{ + { + name: "hour", + expect: NewFunctionTestResult(types.T_uint32.ToType(), false, + []uint32{15, 15, 0, 78, 0, 15, 12, 0, 0, 1, 0, 12, 12}, + []bool{false, false, true, false, true, false, false, false, true, false, true, false, false}), + fn: StringToHour, + }, + { + name: "minute", + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{30, 30, 0, 0, 0, 30, 34, 0, 0, 2, 0, 34, 34}, + []bool{false, false, true, false, true, false, false, false, true, false, true, false, false}), + fn: StringToMinute, + }, + { + name: "second", + expect: NewFunctionTestResult(types.T_uint8.ToType(), false, + []uint8{45, 45, 0, 0, 0, 45, 56, 9, 0, 3, 0, 56, 56}, + []bool{false, false, true, false, true, false, false, false, true, false, true, false, false}), + fn: StringToSecond, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tcc := NewFunctionTestCase(proc, inputs, tc.expect, tc.fn) + succeed, info := tcc.Run() + require.True(t, succeed, info) + }) + } +} + +func TestStringTimeExtractSelectList(t *testing.T) { + proc := testutil.NewProcess(t) + fcTC := NewFunctionTestCase(proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{"12:30:45", "invalid"}, []bool{false, false}), + }, + NewFunctionTestResult(types.T_uint32.ToType(), false, nil, nil), + StringToHour) + + require.NoError(t, fcTC.result.PreExtendAndReset(fcTC.fnLength)) + require.NoError(t, fcTC.fn(fcTC.parameters, fcTC.result, fcTC.proc, fcTC.fnLength, + &FunctionSelectList{AnyNull: true, SelectList: []bool{true, false}})) + resultVec := fcTC.result.GetResultVector() + require.Equal(t, uint32(12), vector.GetFixedAtNoTypeCheck[uint32](resultVec, 0)) + require.True(t, resultVec.IsNull(1)) + + require.NoError(t, fcTC.result.PreExtendAndReset(fcTC.fnLength)) + require.NoError(t, fcTC.fn(fcTC.parameters, fcTC.result, fcTC.proc, fcTC.fnLength, + &FunctionSelectList{AllNull: true})) + resultVec = fcTC.result.GetResultVector() + for i := 0; i < fcTC.fnLength; i++ { + require.True(t, resultVec.IsNull(uint64(i))) + } +} + func initBinaryTestCase() []tcTemp { return []tcTemp{ { diff --git a/pkg/sql/plan/function/list_builtIn.go b/pkg/sql/plan/function/list_builtIn.go index ba7e8be51ede0..3a68f7306986a 100644 --- a/pkg/sql/plan/function/list_builtIn.go +++ b/pkg/sql/plan/function/list_builtIn.go @@ -9561,6 +9561,36 @@ var supportedDateAndTimeBuiltIns = []FuncNew{ return TimeToHour }, }, + { + overloadId: 3, + args: []types.T{types.T_varchar}, + retType: func(parameters []types.Type) types.Type { + return types.T_uint32.ToType() + }, + newOp: func() executeLogicOfOverload { + return StringToHour + }, + }, + { + overloadId: 4, + args: []types.T{types.T_char}, + retType: func(parameters []types.Type) types.Type { + return types.T_uint32.ToType() + }, + newOp: func() executeLogicOfOverload { + return StringToHour + }, + }, + { + overloadId: 5, + args: []types.T{types.T_text}, + retType: func(parameters []types.Type) types.Type { + return types.T_uint32.ToType() + }, + newOp: func() executeLogicOfOverload { + return StringToHour + }, + }, }, }, @@ -9602,6 +9632,36 @@ var supportedDateAndTimeBuiltIns = []FuncNew{ return TimeToMinute }, }, + { + overloadId: 3, + args: []types.T{types.T_varchar}, + retType: func(parameters []types.Type) types.Type { + return types.T_uint8.ToType() + }, + newOp: func() executeLogicOfOverload { + return StringToMinute + }, + }, + { + overloadId: 4, + args: []types.T{types.T_char}, + retType: func(parameters []types.Type) types.Type { + return types.T_uint8.ToType() + }, + newOp: func() executeLogicOfOverload { + return StringToMinute + }, + }, + { + overloadId: 5, + args: []types.T{types.T_text}, + retType: func(parameters []types.Type) types.Type { + return types.T_uint8.ToType() + }, + newOp: func() executeLogicOfOverload { + return StringToMinute + }, + }, }, }, @@ -9962,6 +10022,36 @@ var supportedDateAndTimeBuiltIns = []FuncNew{ return TimeToSecond }, }, + { + overloadId: 3, + args: []types.T{types.T_varchar}, + retType: func(parameters []types.Type) types.Type { + return types.T_uint8.ToType() + }, + newOp: func() executeLogicOfOverload { + return StringToSecond + }, + }, + { + overloadId: 4, + args: []types.T{types.T_char}, + retType: func(parameters []types.Type) types.Type { + return types.T_uint8.ToType() + }, + newOp: func() executeLogicOfOverload { + return StringToSecond + }, + }, + { + overloadId: 5, + args: []types.T{types.T_text}, + retType: func(parameters []types.Type) types.Type { + return types.T_uint8.ToType() + }, + newOp: func() executeLogicOfOverload { + return StringToSecond + }, + }, }, }, diff --git a/test/distributed/cases/function/func_datetime_hour.result b/test/distributed/cases/function/func_datetime_hour.result index d59d208c3ec4c..a259f1d0c1ba8 100644 --- a/test/distributed/cases/function/func_datetime_hour.result +++ b/test/distributed/cases/function/func_datetime_hour.result @@ -1,49 +1,50 @@ SELECT HOUR('15:30:45') AS result1; -invalid argument parse timestamp, bad value 15:30:45 +➤ result1[4,32,0] 𝄀 +15 SELECT HOUR('2024-12-20 15:30:45') AS result2; -result2 +➤ result2[4,32,0] 𝄀 15 SELECT HOUR(NOW()) AS result3; -result3 -22 +➤ result3[-6,8,0] 𝄀 +12 SELECT HOUR(CAST('15:30:45' AS TIME)) AS time_cast; -time_cast +➤ time_cast[4,32,0] 𝄀 15 SELECT HOUR(CAST('00:00:00' AS TIME)) AS midnight; -midnight +➤ midnight[4,32,0] 𝄀 0 SELECT HOUR(CAST('23:59:59' AS TIME)) AS end_of_day; -end_of_day +➤ end_of_day[4,32,0] 𝄀 23 SELECT HOUR(CAST('272:59:59' AS TIME)) AS extended_positive; -extended_positive +➤ extended_positive[4,32,0] 𝄀 272 SELECT HOUR(CAST('-272:59:59' AS TIME)) AS extended_negative; -extended_negative +➤ extended_negative[4,32,0] 𝄀 272 SELECT HOUR(CAST('2024-12-20 10:20:30' AS DATETIME)) AS datetime_hour; -datetime_hour +➤ datetime_hour[-6,8,0] 𝄀 10 SELECT HOUR(CAST('2024-12-20 00:00:00' AS DATETIME)) AS datetime_midnight; -datetime_midnight +➤ datetime_midnight[-6,8,0] 𝄀 0 SELECT HOUR(CAST('2024-12-20 23:59:59' AS DATETIME)) AS datetime_end; -datetime_end +➤ datetime_end[-6,8,0] 𝄀 23 SELECT HOUR(CAST('2024-12-20 12:30:45' AS TIMESTAMP)) AS timestamp_hour; -timestamp_hour +➤ timestamp_hour[-6,8,0] 𝄀 12 SELECT HOUR(CAST('00:00:00' AS TIME)) AS zero_hour; -zero_hour +➤ zero_hour[4,32,0] 𝄀 0 SELECT HOUR(CAST('12:00:00' AS TIME)) AS noon; -noon +➤ noon[4,32,0] 𝄀 12 SELECT HOUR(CAST('13:00:00' AS TIME)) AS one_pm; -one_pm +➤ one_pm[4,32,0] 𝄀 13 SELECT HOUR(NULL) AS null_result; -null_result +➤ null_result[-6,8,0] 𝄀 null CREATE TABLE t1(t TIME, dt DATETIME, ts TIMESTAMP); INSERT INTO t1 VALUES @@ -51,18 +52,252 @@ INSERT INTO t1 VALUES ('00:00:00', '2024-12-20 00:00:00', '2024-12-20 00:00:00'), ('23:59:59', '2024-12-20 23:59:59', '2024-12-20 23:59:59'); SELECT t, HOUR(t) AS hour_from_time, dt, HOUR(dt) AS hour_from_datetime, ts, HOUR(ts) AS hour_from_timestamp FROM t1; -t hour_from_time dt hour_from_datetime ts hour_from_timestamp -15:30:45 15 2024-12-20 15:30:45 15 2024-12-20 15:30:45 15 -00:00:00 0 2024-12-20 00:00:00 0 2024-12-20 00:00:00 0 -23:59:59 23 2024-12-20 23:59:59 23 2024-12-20 23:59:59 23 +➤ t[92,64,0] ¦ hour_from_time[4,32,0] ¦ dt[93,64,0] ¦ hour_from_datetime[-6,8,0] ¦ ts[93,64,0] ¦ hour_from_timestamp[-6,8,0] 𝄀 +15:30:45 ¦ 15 ¦ 2024-12-20 15:30:45 ¦ 15 ¦ 2024-12-20 15:30:45 ¦ 15 𝄀 +00:00:00 ¦ 0 ¦ 2024-12-20 00:00:00 ¦ 0 ¦ 2024-12-20 00:00:00 ¦ 0 𝄀 +23:59:59 ¦ 23 ¦ 2024-12-20 23:59:59 ¦ 23 ¦ 2024-12-20 23:59:59 ¦ 23 DROP TABLE t1; CREATE TABLE t1(t TIME); INSERT INTO t1 VALUES ('15:30:45'), ('10:20:30'), ('23:59:59'); SELECT * FROM t1 WHERE HOUR(t) = 15; -t +➤ t[92,64,0] 𝄀 15:30:45 SELECT * FROM t1 WHERE HOUR(t) > 12; -t -15:30:45 +➤ t[92,64,0] 𝄀 +15:30:45 𝄀 23:59:59 DROP TABLE t1; +SELECT HOUR(CAST('12:30:00' AS VARCHAR)) AS varchar_hour, +HOUR(CAST('272:59:59' AS VARCHAR)) AS varchar_extended_hour, +HOUR(CAST('2024-12-20 15:30:45' AS VARCHAR)) AS varchar_datetime_hour, +HOUR(CAST(NULL AS VARCHAR)) AS varchar_null_hour; +➤ varchar_hour[4,32,0] ¦ varchar_extended_hour[4,32,0] ¦ varchar_datetime_hour[4,32,0] ¦ varchar_null_hour[4,32,0] 𝄀 +12 ¦ 272 ¦ 15 ¦ null +SELECT HOUR('2024-12-20') AS date_string_hour, +HOUR(CONCAT(DATE_ADD(CURRENT_DATE, INTERVAL 1 DAY), ' 01:02:03')) AS tomorrow_datetime_hour; +➤ date_string_hour[4,32,0] ¦ tomorrow_datetime_hour[4,32,0] 𝄀 +0 ¦ 1 +SELECT HOUR(CAST(' ' AS CHAR)) AS char_spaces_hour, +HOUR(CAST('\t' AS VARCHAR)) AS varchar_tab_hour, +HOUR(CAST(' ' AS TEXT)) AS text_spaces_hour; +➤ char_spaces_hour[4,32,0] ¦ varchar_tab_hour[4,32,0] ¦ text_spaces_hour[4,32,0] 𝄀 +null ¦ null ¦ null +SELECT HOUR('241220153045') AS compact_datetime_hour, +HOUR('15:30:45abc') AS trailing_time_hour, +HOUR('2024-12-20 15:30:45abc') AS trailing_datetime_hour, +HOUR('2024-12-20foo') AS malformed_datetime_hour; +➤ compact_datetime_hour[4,32,0] ¦ trailing_time_hour[4,32,0] ¦ trailing_datetime_hour[4,32,0] ¦ malformed_datetime_hour[4,32,0] 𝄀 +15 ¦ 15 ¦ 15 ¦ 0 +SELECT HOUR('12:34:56.789012') AS fractional_time_hour, +HOUR('2 03:04:05.9') AS day_fractional_time_hour, +HOUR('-12:34:56.789012') AS negative_fractional_time_hour, +HOUR('-2 03:04:05.9') AS negative_day_fractional_time_hour, +HOUR('839:00:00') AS overflow_time_hour, +HOUR('-839:00:00') AS negative_overflow_time_hour, +HOUR('12:60:00') AS invalid_minute_hour, +HOUR('12:34:60') AS invalid_second_hour, +HOUR('123045.987654') AS compact_time_hour, +HOUR('20241220153045.999999') AS compact_datetime_fractional_hour, +HOUR('20241220156045') AS invalid_compact_datetime_hour, +HOUR('2024-12-20') AS date_only_time_hour, +HOUR('2024-12-20T15:30:45.123456') AS t_datetime_hour, +HOUR(' 12:34:56 ') AS trimmed_time_hour, +HOUR('foo12:34:56') AS malformed_time_hour; +➤ fractional_time_hour[4,32,0] ¦ day_fractional_time_hour[4,32,0] ¦ negative_fractional_time_hour[4,32,0] ¦ negative_day_fractional_time_hour[4,32,0] ¦ overflow_time_hour[4,32,0] ¦ negative_overflow_time_hour[4,32,0] ¦ invalid_minute_hour[4,32,0] ¦ invalid_second_hour[4,32,0] ¦ compact_time_hour[4,32,0] ¦ compact_datetime_fractional_hour[4,32,0] ¦ invalid_compact_datetime_hour[4,32,0] ¦ date_only_time_hour[4,32,0] ¦ t_datetime_hour[4,32,0] ¦ trimmed_time_hour[4,32,0] ¦ malformed_time_hour[4,32,0] 𝄀 +12 ¦ 51 ¦ 12 ¦ 51 ¦ 838 ¦ 838 ¦ null ¦ null ¦ 12 ¦ 15 ¦ null ¦ 0 ¦ 0 ¦ 12 ¦ null +SELECT HOUR('-2024-12-20 15:30:45') AS negative_datetime_hour, +HOUR('-20241220153045') AS negative_compact_datetime_hour, +HOUR('20240230010203') AS invalid_calendar_compact_datetime_hour, +HOUR('2 30:00:00') AS extended_day_time_hour, +HOUR('60') AS invalid_compact_second_hour; +➤ negative_datetime_hour[4,32,0] ¦ negative_compact_datetime_hour[4,32,0] ¦ invalid_calendar_compact_datetime_hour[4,32,0] ¦ extended_day_time_hour[4,32,0] ¦ invalid_compact_second_hour[4,32,0] 𝄀 +15 ¦ 15 ¦ null ¦ 78 ¦ null +SELECT HOUR('24-12-20 15:30:45') AS two_digit_year_hour, +HOUR('2000:01:01 12:34:56') AS colon_datetime_hour, +HOUR('1998-01-01 00:00:009') AS wide_second_datetime_hour, +HOUR('2024-12-20 15:30:4560') AS invalid_wide_second_datetime_hour, +HOUR('2001-11-00 01:02:03') AS incomplete_datetime_hour, +HOUR('99999990000') AS uint32_overflow_hour, +HOUR('- 12:34:56') AS signed_space_time_hour, +HOUR('12:34:56.') AS trailing_dot_time_hour; +➤ two_digit_year_hour[4,32,0] ¦ colon_datetime_hour[4,32,0] ¦ wide_second_datetime_hour[4,32,0] ¦ invalid_wide_second_datetime_hour[4,32,0] ¦ incomplete_datetime_hour[4,32,0] ¦ uint32_overflow_hour[4,32,0] ¦ signed_space_time_hour[4,32,0] ¦ trailing_dot_time_hour[4,32,0] 𝄀 +15 ¦ 12 ¦ 0 ¦ null ¦ 1 ¦ null ¦ 12 ¦ 12 +SELECT HOUR('0000-01-01 12:34:56') AS zero_year_datetime_hour, +HOUR('2024-00-01 11:22:33') AS zero_month_datetime_hour, +HOUR('2024-01-00 10:20:30') AS zero_day_datetime_hour, +HOUR('0000-00-00 09:08:07') AS zero_date_datetime_hour, +HOUR('2024-12-20T15:30:45.123456') AS iso_t_datetime_hour; +➤ zero_year_datetime_hour[4,32,0] ¦ zero_month_datetime_hour[4,32,0] ¦ zero_day_datetime_hour[4,32,0] ¦ zero_date_datetime_hour[4,32,0] ¦ iso_t_datetime_hour[4,32,0] 𝄀 +12 ¦ 11 ¦ 10 ¦ 9 ¦ 0 +SELECT HOUR('2024-12-20 12:34') AS minute_datetime_hour, +HOUR('0000-02-28 12:34:56') AS valid_zero_year_february_hour, +HOUR('0000-02-29 12:34:56') AS invalid_zero_year_leap_day_hour, +HOUR('0000-02-31 12:34:56') AS invalid_zero_year_february_hour, +HOUR('0000-04-31 12:34:56') AS invalid_zero_year_april_hour, +HOUR('1900-02-29 12:34:56') AS invalid_nonleap_datetime_hour, +HOUR('2000-02-29 01:02:03') AS leap_datetime_hour; +➤ minute_datetime_hour[4,32,0] ¦ valid_zero_year_february_hour[4,32,0] ¦ invalid_zero_year_leap_day_hour[4,32,0] ¦ invalid_zero_year_february_hour[4,32,0] ¦ invalid_zero_year_april_hour[4,32,0] ¦ invalid_nonleap_datetime_hour[4,32,0] ¦ leap_datetime_hour[4,32,0] 𝄀 +12 ¦ 12 ¦ null ¦ null ¦ null ¦ null ¦ 1 +SELECT HOUR('20241220153045') AS compact_14_hour, +HOUR('241220153045') AS compact_12_hour, +HOUR('202412201530451') AS compact_14_extra_digit_hour, +HOUR('2412201530451') AS compact_12_extra_digit_hour, +HOUR('20241220153045abc') AS compact_14_alpha_suffix_hour, +HOUR('241220153045abc') AS compact_12_alpha_suffix_hour; +➤ compact_14_hour[4,32,0] ¦ compact_12_hour[4,32,0] ¦ compact_14_extra_digit_hour[4,32,0] ¦ compact_12_extra_digit_hour[4,32,0] ¦ compact_14_alpha_suffix_hour[4,32,0] ¦ compact_12_alpha_suffix_hour[4,32,0] 𝄀 +15 ¦ 15 ¦ null ¦ null ¦ null ¦ null +SELECT HOUR('20241220153045.123abc') AS compact_14_fraction_text_hour, +HOUR('20241220153045.') AS compact_14_empty_fraction_hour, +HOUR('241220153045.123abc') AS compact_12_fraction_text_hour, +HOUR('241220153045.') AS compact_12_empty_fraction_hour, +HOUR('20241220153045.-') AS compact_14_negative_fraction_hour, +HOUR('20241220153045.+') AS compact_14_positive_fraction_hour, +HOUR('241220153045.-') AS compact_12_negative_fraction_hour, +HOUR('241220153045.+') AS compact_12_positive_fraction_hour, +HOUR('20241220153045.123-') AS compact_14_fraction_negative_suffix_hour, +HOUR('20241220153045.123+') AS compact_14_fraction_positive_suffix_hour, +HOUR('241220153045.123-') AS compact_12_fraction_negative_suffix_hour, +HOUR('241220153045.123+') AS compact_12_fraction_positive_suffix_hour, +HOUR('20241220153045.123abc-') AS compact_14_fraction_text_negative_suffix_hour, +HOUR('241220153045.123abc+') AS compact_12_fraction_text_positive_suffix_hour; +➤ compact_14_fraction_text_hour[4,32,0] ¦ compact_14_empty_fraction_hour[4,32,0] ¦ compact_12_fraction_text_hour[4,32,0] ¦ compact_12_empty_fraction_hour[4,32,0] ¦ compact_14_negative_fraction_hour[4,32,0] ¦ compact_14_positive_fraction_hour[4,32,0] ¦ compact_12_negative_fraction_hour[4,32,0] ¦ compact_12_positive_fraction_hour[4,32,0] ¦ compact_14_fraction_negative_suffix_hour[4,32,0] ¦ compact_14_fraction_positive_suffix_hour[4,32,0] ¦ compact_12_fraction_negative_suffix_hour[4,32,0] ¦ compact_12_fraction_positive_suffix_hour[4,32,0] ¦ compact_14_fraction_text_negative_suffix_hour[4,32,0] ¦ compact_12_fraction_text_positive_suffix_hour[4,32,0] 𝄀 +15 ¦ 15 ¦ 15 ¦ 15 ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 15 ¦ 15 +SELECT HOUR('12:') AS trailing_colon_hour, +HOUR(':34') AS leading_colon_hour, +HOUR('12:34:56-') AS trailing_dash_hour, +HOUR('12:34:56..') AS trailing_dots_hour, +HOUR('123:') AS compact_3_trailing_colon_hour, +HOUR('1234:') AS compact_4_trailing_colon_hour, +HOUR('12345:') AS compact_5_trailing_colon_hour, +HOUR('123456:') AS compact_6_trailing_colon_hour; +➤ trailing_colon_hour[4,32,0] ¦ leading_colon_hour[4,32,0] ¦ trailing_dash_hour[4,32,0] ¦ trailing_dots_hour[4,32,0] ¦ compact_3_trailing_colon_hour[4,32,0] ¦ compact_4_trailing_colon_hour[4,32,0] ¦ compact_5_trailing_colon_hour[4,32,0] ¦ compact_6_trailing_colon_hour[4,32,0] 𝄀 +0 ¦ 0 ¦ 12 ¦ 12 ¦ 0 ¦ 0 ¦ 1 ¦ 12 +SELECT HOUR(':34:56') AS leading_colon_with_seconds_hour, +HOUR('12:34:') AS trailing_second_colon_hour, +HOUR('12::56') AS empty_minute_hour, +HOUR('12:34:56:') AS fourth_field_colon_hour, +HOUR('12-.abc') AS compact_prefix_before_fraction_hour; +➤ leading_colon_with_seconds_hour[4,32,0] ¦ trailing_second_colon_hour[4,32,0] ¦ empty_minute_hour[4,32,0] ¦ fourth_field_colon_hour[4,32,0] ¦ compact_prefix_before_fraction_hour[4,32,0] 𝄀 +0 ¦ 12 ¦ 0 ¦ 12 ¦ 0 +CREATE TABLE time_extract_prefix_boundaries(v VARCHAR(32), c CHAR(32), t TEXT); +INSERT INTO time_extract_prefix_boundaries VALUES +('12-34', '12-34', '12-34'), +('1234-56', '1234-56', '1234-56'), +('1 02', '1 02', '1 02'), +('1 02:', '1 02:', '1 02:'), +('1 02-34', '1 02-34', '1 02-34'), +('2024-12-20 12', '2024-12-20 12', '2024-12-20 12'), +('2024-12-20 12:', '2024-12-20 12:', '2024-12-20 12:'), +('2024-12-20 12::56', '2024-12-20 12::56', '2024-12-20 12::56'), +('2024-12-20foo', '2024-12-20foo', '2024-12-20foo'), +('2024-12-20 12:34::56', '2024-12-20 12:34::56', '2024-12-20 12:34::56'), +('2024-12-20 12-34', '2024-12-20 12-34', '2024-12-20 12-34'), +('2024-12-20 12/34/56', '2024-12-20 12/34/56', '2024-12-20 12/34/56'), +('2024@12@20 12@34@56', '2024@12@20 12@34@56', '2024@12@20 12@34@56'), +('2024-12-20\t12:34:56', '2024-12-20\t12:34:56', '2024-12-20\t12:34:56'), +('1\t02:34:56', '1\t02:34:56', '1\t02:34:56'), +('12:34 56', '12:34 56', '12:34 56'), +('1 02:34 56', '1 02:34 56', '1 02:34 56'), +('1 02 34', '1 02 34', '1 02 34'), +('2024--12--20 12:34:56', '2024--12--20 12:34:56', '2024--12--20 12:34:56'), +('2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56'), +('2024-12-20 -12:34:56', '2024-12-20 -12:34:56', '2024-12-20 -12:34:56'), +('2024-12-20 +12:34:56', '2024-12-20 +12:34:56', '2024-12-20 +12:34:56'), +('2024-12-20 12:34 56', '2024-12-20 12:34 56', '2024-12-20 12:34 56'), +('2024-12-20 12 34', '2024-12-20 12 34', '2024-12-20 12 34'); +SELECT HOUR(v), HOUR(c), HOUR(t) FROM time_extract_prefix_boundaries; +➤ HOUR(v)[4,32,0] ¦ HOUR(c)[4,32,0] ¦ HOUR(t)[4,32,0] 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +26 ¦ 26 ¦ 26 𝄀 +26 ¦ 26 ¦ 26 𝄀 +26 ¦ 26 ¦ 26 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +0 ¦ 0 ¦ 0 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +26 ¦ 26 ¦ 26 𝄀 +12 ¦ 12 ¦ 12 𝄀 +26 ¦ 26 ¦ 26 𝄀 +26 ¦ 26 ¦ 26 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 +DROP TABLE time_extract_prefix_boundaries; +CREATE TABLE time_extract_ambiguous_prefix_ownership(v VARCHAR(40), c CHAR(40), t TEXT); +INSERT INTO time_extract_ambiguous_prefix_ownership VALUES +('1 2', '1 2', '1 2'), +('1 02', '1 02', '1 02'), +('12:34:56', '12:34:56', '12:34:56'), +('12:34:56 78', '12:34:56 78', '12:34:56 78'), +('1-2-3 4:5:6', '1-2-3 4:5:6', '1-2-3 4:5:6'), +('12345:', '12345:', '12345:'), +('12345-1-1 1:2:3', '12345-1-1 1:2:3', '12345-1-1 1:2:3'), +('-12:34', '-12:34', '-12:34'), +('--12:34', '--12:34', '--12:34'), +('2024-12-20 +12:34', '2024-12-20 +12:34', '2024-12-20 +12:34'), +('2024-12-20 ++12:34', '2024-12-20 ++12:34', '2024-12-20 ++12:34'), +('2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc'), +('2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc'), +('2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc'), +('1 2:3', '1 2:3', '1 2:3'), +('1 2:3:4', '1 2:3:4', '1 2:3:4'), +('12 3', '12 3', '12 3'), +('12 3:4', '12 3:4', '12 3:4'), +('12 3:4:5', '12 3:4:5', '12 3:4:5'), +('12-2-3 4:5:6', '12-2-3 4:5:6', '12-2-3 4:5:6'), +('123-2-3 4:5:6', '123-2-3 4:5:6', '123-2-3 4:5:6'), +('1234-2-3 4:5:6', '1234-2-3 4:5:6', '1234-2-3 4:5:6'), +('1-1-1 1:2:3', '1-1-1 1:2:3', '1-1-1 1:2:3'), +('1-1-1 01:02:03', '1-1-1 01:02:03', '1-1-1 01:02:03'), +('1-2-3 12:34:56', '1-2-3 12:34:56', '1-2-3 12:34:56'), +('0-1-1 12:34:56', '0-1-1 12:34:56', '0-1-1 12:34:56'), +('2024-12-20 12:34:56+', '2024-12-20 12:34:56+', '2024-12-20 12:34:56+'), +('2024-12-20 12:34:56-', '2024-12-20 12:34:56-', '2024-12-20 12:34:56-'); +SELECT HOUR(v), HOUR(c), HOUR(t) FROM time_extract_ambiguous_prefix_ownership; +➤ HOUR(v)[4,32,0] ¦ HOUR(c)[4,32,0] ¦ HOUR(t)[4,32,0] 𝄀 +0 ¦ 0 ¦ 0 𝄀 +26 ¦ 26 ¦ 26 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +0 ¦ 0 ¦ 0 𝄀 +1 ¦ 1 ¦ 1 𝄀 +null ¦ null ¦ null 𝄀 +12 ¦ 12 ¦ 12 𝄀 +null ¦ null ¦ null 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +26 ¦ 26 ¦ 26 𝄀 +26 ¦ 26 ¦ 26 𝄀 +0 ¦ 0 ¦ 0 𝄀 +291 ¦ 291 ¦ 291 𝄀 +291 ¦ 291 ¦ 291 𝄀 +4 ¦ 4 ¦ 4 𝄀 +4 ¦ 4 ¦ 4 𝄀 +4 ¦ 4 ¦ 4 𝄀 +0 ¦ 0 ¦ 0 𝄀 +1 ¦ 1 ¦ 1 𝄀 +12 ¦ 12 ¦ 12 𝄀 +12 ¦ 12 ¦ 12 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 +DROP TABLE time_extract_ambiguous_prefix_ownership; +SELECT HOUR('123:34:56 78') AS unpadded_123_hour, +HOUR('123:34:56 78 ') AS padded_123_hour, +HOUR('\t123:34:56 78\t') AS tab_padded_123_hour, +HOUR('838:59:59 78') AS unpadded_838_hour, +HOUR('838:59:59 78 ') AS padded_838_hour, +HOUR('\t838:59:59 78\t') AS tab_padded_838_hour; +➤ unpadded_123_hour[4,32,0] ¦ padded_123_hour[4,32,0] ¦ tab_padded_123_hour[4,32,0] ¦ unpadded_838_hour[4,32,0] ¦ padded_838_hour[4,32,0] ¦ tab_padded_838_hour[4,32,0] 𝄀 +null ¦ 123 ¦ 123 ¦ null ¦ 838 ¦ 838 diff --git a/test/distributed/cases/function/func_datetime_hour.test b/test/distributed/cases/function/func_datetime_hour.test index 43b575079e80b..1f5a75a8c4224 100644 --- a/test/distributed/cases/function/func_datetime_hour.test +++ b/test/distributed/cases/function/func_datetime_hour.test @@ -44,3 +44,187 @@ SELECT * FROM t1 WHERE HOUR(t) = 15; SELECT * FROM t1 WHERE HOUR(t) > 12; DROP TABLE t1; +-- VARCHAR time-string input +SELECT HOUR(CAST('12:30:00' AS VARCHAR)) AS varchar_hour, + HOUR(CAST('272:59:59' AS VARCHAR)) AS varchar_extended_hour, + HOUR(CAST('2024-12-20 15:30:45' AS VARCHAR)) AS varchar_datetime_hour, + HOUR(CAST(NULL AS VARCHAR)) AS varchar_null_hour; + +-- Date-only strings use MySQL TIME coercion; complete DATETIME strings use clock fields. +SELECT HOUR('2024-12-20') AS date_string_hour, + HOUR(CONCAT(DATE_ADD(CURRENT_DATE, INTERVAL 1 DAY), ' 01:02:03')) AS tomorrow_datetime_hour; + +-- Whitespace-only string values are invalid TIME inputs. +SELECT HOUR(CAST(' ' AS CHAR)) AS char_spaces_hour, + HOUR(CAST('\t' AS VARCHAR)) AS varchar_tab_hour, + HOUR(CAST(' ' AS TEXT)) AS text_spaces_hour; + +-- MySQL str_to_time recognizes YYMMDDHHMMSS and keeps a valid TIME prefix. +SELECT HOUR('241220153045') AS compact_datetime_hour, + HOUR('15:30:45abc') AS trailing_time_hour, + HOUR('2024-12-20 15:30:45abc') AS trailing_datetime_hour, + HOUR('2024-12-20foo') AS malformed_datetime_hour; + +-- String TIME coercion: fractional/day/compact forms, range clamp, and invalid fields. +SELECT HOUR('12:34:56.789012') AS fractional_time_hour, + HOUR('2 03:04:05.9') AS day_fractional_time_hour, + HOUR('-12:34:56.789012') AS negative_fractional_time_hour, + HOUR('-2 03:04:05.9') AS negative_day_fractional_time_hour, + HOUR('839:00:00') AS overflow_time_hour, + HOUR('-839:00:00') AS negative_overflow_time_hour, + HOUR('12:60:00') AS invalid_minute_hour, + HOUR('12:34:60') AS invalid_second_hour, + HOUR('123045.987654') AS compact_time_hour, + HOUR('20241220153045.999999') AS compact_datetime_fractional_hour, + HOUR('20241220156045') AS invalid_compact_datetime_hour, + HOUR('2024-12-20') AS date_only_time_hour, + HOUR('2024-12-20T15:30:45.123456') AS t_datetime_hour, + HOUR(' 12:34:56 ') AS trimmed_time_hour, + HOUR('foo12:34:56') AS malformed_time_hour; + +-- Signed DATETIME, invalid compact DATETIME, extended day-TIME, and invalid compact seconds. +SELECT HOUR('-2024-12-20 15:30:45') AS negative_datetime_hour, + HOUR('-20241220153045') AS negative_compact_datetime_hour, + HOUR('20240230010203') AS invalid_calendar_compact_datetime_hour, + HOUR('2 30:00:00') AS extended_day_time_hour, + HOUR('60') AS invalid_compact_second_hour; + +-- MySQL DATETIME grammar and numeric overflow for string TIME coercion. +SELECT HOUR('24-12-20 15:30:45') AS two_digit_year_hour, + HOUR('2000:01:01 12:34:56') AS colon_datetime_hour, + HOUR('1998-01-01 00:00:009') AS wide_second_datetime_hour, + HOUR('2024-12-20 15:30:4560') AS invalid_wide_second_datetime_hour, + HOUR('2001-11-00 01:02:03') AS incomplete_datetime_hour, + HOUR('99999990000') AS uint32_overflow_hour, + HOUR('- 12:34:56') AS signed_space_time_hour, + HOUR('12:34:56.') AS trailing_dot_time_hour; + +-- Zero date components retain the clock; ISO-T uses DATE-prefix TIME coercion. +SELECT HOUR('0000-01-01 12:34:56') AS zero_year_datetime_hour, + HOUR('2024-00-01 11:22:33') AS zero_month_datetime_hour, + HOUR('2024-01-00 10:20:30') AS zero_day_datetime_hour, + HOUR('0000-00-00 09:08:07') AS zero_date_datetime_hour, + HOUR('2024-12-20T15:30:45.123456') AS iso_t_datetime_hour; + +-- A DATETIME may omit seconds; zero year still requires a valid nonzero month/day. +SELECT HOUR('2024-12-20 12:34') AS minute_datetime_hour, + HOUR('0000-02-28 12:34:56') AS valid_zero_year_february_hour, + HOUR('0000-02-29 12:34:56') AS invalid_zero_year_leap_day_hour, + HOUR('0000-02-31 12:34:56') AS invalid_zero_year_february_hour, + HOUR('0000-04-31 12:34:56') AS invalid_zero_year_april_hour, + HOUR('1900-02-29 12:34:56') AS invalid_nonleap_datetime_hour, + HOUR('2000-02-29 01:02:03') AS leap_datetime_hour; + +-- Compact DATETIME only accepts exact 14/12-digit forms or a fractional suffix. +SELECT HOUR('20241220153045') AS compact_14_hour, + HOUR('241220153045') AS compact_12_hour, + HOUR('202412201530451') AS compact_14_extra_digit_hour, + HOUR('2412201530451') AS compact_12_extra_digit_hour, + HOUR('20241220153045abc') AS compact_14_alpha_suffix_hour, + HOUR('241220153045abc') AS compact_12_alpha_suffix_hour; + +-- Compact DATETIME consumes the fractional prefix, including an empty fraction +-- or non-numeric text after the fractional digits. +SELECT HOUR('20241220153045.123abc') AS compact_14_fraction_text_hour, + HOUR('20241220153045.') AS compact_14_empty_fraction_hour, + HOUR('241220153045.123abc') AS compact_12_fraction_text_hour, + HOUR('241220153045.') AS compact_12_empty_fraction_hour, + HOUR('20241220153045.-') AS compact_14_negative_fraction_hour, + HOUR('20241220153045.+') AS compact_14_positive_fraction_hour, + HOUR('241220153045.-') AS compact_12_negative_fraction_hour, + HOUR('241220153045.+') AS compact_12_positive_fraction_hour, + HOUR('20241220153045.123-') AS compact_14_fraction_negative_suffix_hour, + HOUR('20241220153045.123+') AS compact_14_fraction_positive_suffix_hour, + HOUR('241220153045.123-') AS compact_12_fraction_negative_suffix_hour, + HOUR('241220153045.123+') AS compact_12_fraction_positive_suffix_hour, + HOUR('20241220153045.123abc-') AS compact_14_fraction_text_negative_suffix_hour, + HOUR('241220153045.123abc+') AS compact_12_fraction_text_positive_suffix_hour; + +-- TIME coercion stops after the valid clock prefix. +SELECT HOUR('12:') AS trailing_colon_hour, + HOUR(':34') AS leading_colon_hour, + HOUR('12:34:56-') AS trailing_dash_hour, + HOUR('12:34:56..') AS trailing_dots_hour, + HOUR('123:') AS compact_3_trailing_colon_hour, + HOUR('1234:') AS compact_4_trailing_colon_hour, + HOUR('12345:') AS compact_5_trailing_colon_hour, + HOUR('123456:') AS compact_6_trailing_colon_hour; + +-- MySQL retains every valid field consumed before a partial TIME suffix. +SELECT HOUR(':34:56') AS leading_colon_with_seconds_hour, + HOUR('12:34:') AS trailing_second_colon_hour, + HOUR('12::56') AS empty_minute_hour, + HOUR('12:34:56:') AS fourth_field_colon_hour, + HOUR('12-.abc') AS compact_prefix_before_fraction_hour; + +-- Context-aware prefix consumption must agree across all string types. +CREATE TABLE time_extract_prefix_boundaries(v VARCHAR(32), c CHAR(32), t TEXT); +INSERT INTO time_extract_prefix_boundaries VALUES + ('12-34', '12-34', '12-34'), + ('1234-56', '1234-56', '1234-56'), + ('1 02', '1 02', '1 02'), + ('1 02:', '1 02:', '1 02:'), + ('1 02-34', '1 02-34', '1 02-34'), + ('2024-12-20 12', '2024-12-20 12', '2024-12-20 12'), + ('2024-12-20 12:', '2024-12-20 12:', '2024-12-20 12:'), + ('2024-12-20 12::56', '2024-12-20 12::56', '2024-12-20 12::56'), + ('2024-12-20foo', '2024-12-20foo', '2024-12-20foo'), + ('2024-12-20 12:34::56', '2024-12-20 12:34::56', '2024-12-20 12:34::56'), + ('2024-12-20 12-34', '2024-12-20 12-34', '2024-12-20 12-34'), + ('2024-12-20 12/34/56', '2024-12-20 12/34/56', '2024-12-20 12/34/56'), + ('2024@12@20 12@34@56', '2024@12@20 12@34@56', '2024@12@20 12@34@56'), + ('2024-12-20\t12:34:56', '2024-12-20\t12:34:56', '2024-12-20\t12:34:56'), + ('1\t02:34:56', '1\t02:34:56', '1\t02:34:56'), + ('12:34 56', '12:34 56', '12:34 56'), + ('1 02:34 56', '1 02:34 56', '1 02:34 56'), + ('1 02 34', '1 02 34', '1 02 34'), + ('2024--12--20 12:34:56', '2024--12--20 12:34:56', '2024--12--20 12:34:56'), + ('2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56'), + ('2024-12-20 -12:34:56', '2024-12-20 -12:34:56', '2024-12-20 -12:34:56'), + ('2024-12-20 +12:34:56', '2024-12-20 +12:34:56', '2024-12-20 +12:34:56'), + ('2024-12-20 12:34 56', '2024-12-20 12:34 56', '2024-12-20 12:34 56'), + ('2024-12-20 12 34', '2024-12-20 12 34', '2024-12-20 12 34'); +SELECT HOUR(v), HOUR(c), HOUR(t) FROM time_extract_prefix_boundaries; +DROP TABLE time_extract_prefix_boundaries; + +-- DATE/DATETIME/day-TIME/compact-TIME ownership uses one ordered prefix grammar. +CREATE TABLE time_extract_ambiguous_prefix_ownership(v VARCHAR(40), c CHAR(40), t TEXT); +INSERT INTO time_extract_ambiguous_prefix_ownership VALUES + ('1 2', '1 2', '1 2'), + ('1 02', '1 02', '1 02'), + ('12:34:56', '12:34:56', '12:34:56'), + ('12:34:56 78', '12:34:56 78', '12:34:56 78'), + ('1-2-3 4:5:6', '1-2-3 4:5:6', '1-2-3 4:5:6'), + ('12345:', '12345:', '12345:'), + ('12345-1-1 1:2:3', '12345-1-1 1:2:3', '12345-1-1 1:2:3'), + ('-12:34', '-12:34', '-12:34'), + ('--12:34', '--12:34', '--12:34'), + ('2024-12-20 +12:34', '2024-12-20 +12:34', '2024-12-20 +12:34'), + ('2024-12-20 ++12:34', '2024-12-20 ++12:34', '2024-12-20 ++12:34'), + ('2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc'), + ('2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc'), + ('2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc'), + ('1 2:3', '1 2:3', '1 2:3'), + ('1 2:3:4', '1 2:3:4', '1 2:3:4'), + ('12 3', '12 3', '12 3'), + ('12 3:4', '12 3:4', '12 3:4'), + ('12 3:4:5', '12 3:4:5', '12 3:4:5'), + ('12-2-3 4:5:6', '12-2-3 4:5:6', '12-2-3 4:5:6'), + ('123-2-3 4:5:6', '123-2-3 4:5:6', '123-2-3 4:5:6'), + ('1234-2-3 4:5:6', '1234-2-3 4:5:6', '1234-2-3 4:5:6'), + ('1-1-1 1:2:3', '1-1-1 1:2:3', '1-1-1 1:2:3'), + ('1-1-1 01:02:03', '1-1-1 01:02:03', '1-1-1 01:02:03'), + ('1-2-3 12:34:56', '1-2-3 12:34:56', '1-2-3 12:34:56'), + ('0-1-1 12:34:56', '0-1-1 12:34:56', '0-1-1 12:34:56'), + ('2024-12-20 12:34:56+', '2024-12-20 12:34:56+', '2024-12-20 12:34:56+'), + ('2024-12-20 12:34:56-', '2024-12-20 12:34:56-', '2024-12-20 12:34:56-'); +SELECT HOUR(v), HOUR(c), HOUR(t) FROM time_extract_ambiguous_prefix_ownership; +DROP TABLE time_extract_ambiguous_prefix_ownership; + +-- Preserve nonempty trailing whitespace until the ordered parser assigns its owner. +SELECT HOUR('123:34:56 78') AS unpadded_123_hour, + HOUR('123:34:56 78 ') AS padded_123_hour, + HOUR('\t123:34:56 78\t') AS tab_padded_123_hour, + HOUR('838:59:59 78') AS unpadded_838_hour, + HOUR('838:59:59 78 ') AS padded_838_hour, + HOUR('\t838:59:59 78\t') AS tab_padded_838_hour; diff --git a/test/distributed/cases/function/func_datetime_minute.result b/test/distributed/cases/function/func_datetime_minute.result index 13314f499e4e1..731bea23fe8de 100644 --- a/test/distributed/cases/function/func_datetime_minute.result +++ b/test/distributed/cases/function/func_datetime_minute.result @@ -1,51 +1,291 @@ SELECT MINUTE('15:30:45') AS result1; -invalid argument parse timestamp, bad value 15:30:45 +➤ result1[-6,8,0] 𝄀 +30 SELECT MINUTE('15:30:45') AS time_with_minutes; -invalid argument parse timestamp, bad value 15:30:45 +➤ time_with_minutes[-6,8,0] 𝄀 +30 SELECT MINUTE('00:00:00') AS zero_minutes; -invalid argument parse timestamp, bad value 00:00:00 +➤ zero_minutes[-6,8,0] 𝄀 +0 SELECT MINUTE('23:59:59') AS max_minutes; -invalid argument parse timestamp, bad value 23:59:59 +➤ max_minutes[-6,8,0] 𝄀 +59 SELECT MINUTE('12:34:56') AS various_minutes; -invalid argument parse timestamp, bad value 12:34:56 +➤ various_minutes[-6,8,0] 𝄀 +34 SELECT MINUTE('2024-12-20 15:30:45') AS datetime_with_minutes; -datetime_with_minutes +➤ datetime_with_minutes[-6,8,0] 𝄀 30 SELECT MINUTE('2024-12-20 00:00:00') AS datetime_zero; -datetime_zero +➤ datetime_zero[-6,8,0] 𝄀 0 SELECT MINUTE('2024-12-20 23:59:59') AS datetime_max; -datetime_max +➤ datetime_max[-6,8,0] 𝄀 59 SELECT MINUTE(TIMESTAMP('2024-12-20 15:30:45')) AS timestamp_with_minutes; -timestamp_with_minutes +➤ timestamp_with_minutes[-6,8,0] 𝄀 30 SELECT MINUTE(NULL) AS null_input; -null_input +➤ null_input[-6,8,0] 𝄀 null CREATE TABLE t1(t TIME); INSERT INTO t1 VALUES ('15:30:45'), ('00:00:00'), ('23:59:59'), ('12:34:56'); SELECT t, MINUTE(t) AS minute FROM t1; -t minute -15:30:45 30 -00:00:00 0 -23:59:59 59 -12:34:56 34 +➤ t[92,64,0] ¦ minute[-6,8,0] 𝄀 +15:30:45 ¦ 30 𝄀 +00:00:00 ¦ 0 𝄀 +23:59:59 ¦ 59 𝄀 +12:34:56 ¦ 34 DROP TABLE t1; CREATE TABLE t1(dt DATETIME); INSERT INTO t1 VALUES ('2024-12-20 15:30:45'), ('2024-12-20 00:00:00'), ('2024-12-20 23:59:59'); SELECT dt, MINUTE(dt) AS minute FROM t1; -dt minute -2024-12-20 15:30:45 30 -2024-12-20 00:00:00 0 -2024-12-20 23:59:59 59 +➤ dt[93,64,0] ¦ minute[-6,8,0] 𝄀 +2024-12-20 15:30:45 ¦ 30 𝄀 +2024-12-20 00:00:00 ¦ 0 𝄀 +2024-12-20 23:59:59 ¦ 59 DROP TABLE t1; CREATE TABLE t1(t TIME); INSERT INTO t1 VALUES ('15:30:45'), ('00:00:00'), ('23:59:59'), ('12:34:56'); SELECT * FROM t1 WHERE MINUTE(t) > 30; -t -23:59:59 +➤ t[92,64,0] 𝄀 +23:59:59 𝄀 12:34:56 DROP TABLE t1; SELECT MINUTE('15:30:45') AS result1, MINUTE('15:00:45') AS result2, MINUTE('15:59:45') AS result3; -invalid argument parse timestamp, bad value 15:30:45 +➤ result1[-6,8,0] ¦ result2[-6,8,0] ¦ result3[-6,8,0] 𝄀 +30 ¦ 0 ¦ 59 +SELECT MINUTE(CAST('12:30:00' AS VARCHAR)) AS varchar_minute, +MINUTE(CAST('272:59:59' AS VARCHAR)) AS varchar_extended_minute, +MINUTE(CAST('2024-12-20 15:30:45' AS VARCHAR)) AS varchar_datetime_minute, +MINUTE(CAST(NULL AS VARCHAR)) AS varchar_null_minute; +➤ varchar_minute[-6,8,0] ¦ varchar_extended_minute[-6,8,0] ¦ varchar_datetime_minute[-6,8,0] ¦ varchar_null_minute[-6,8,0] 𝄀 +30 ¦ 59 ¦ 30 ¦ null +SELECT MINUTE('2024-12-20') AS date_string_minute, +MINUTE(CONCAT(DATE_ADD(CURRENT_DATE, INTERVAL 1 DAY), ' 01:02:03')) AS tomorrow_datetime_minute; +➤ date_string_minute[-6,8,0] ¦ tomorrow_datetime_minute[-6,8,0] 𝄀 +20 ¦ 2 +SELECT MINUTE(CAST(' ' AS CHAR)) AS char_spaces_minute, +MINUTE(CAST('\t' AS VARCHAR)) AS varchar_tab_minute, +MINUTE(CAST(' ' AS TEXT)) AS text_spaces_minute; +➤ char_spaces_minute[-6,8,0] ¦ varchar_tab_minute[-6,8,0] ¦ text_spaces_minute[-6,8,0] 𝄀 +null ¦ null ¦ null +SELECT MINUTE('241220153045') AS compact_datetime_minute, +MINUTE('15:30:45abc') AS trailing_time_minute, +MINUTE('2024-12-20 15:30:45abc') AS trailing_datetime_minute, +MINUTE('2024-12-20foo') AS malformed_datetime_minute; +➤ compact_datetime_minute[-6,8,0] ¦ trailing_time_minute[-6,8,0] ¦ trailing_datetime_minute[-6,8,0] ¦ malformed_datetime_minute[-6,8,0] 𝄀 +30 ¦ 30 ¦ 30 ¦ 20 +SELECT MINUTE('12:34:56.789012') AS fractional_time_minute, +MINUTE('2 03:04:05.9') AS day_fractional_time_minute, +MINUTE('-12:34:56.789012') AS negative_fractional_time_minute, +MINUTE('-2 03:04:05.9') AS negative_day_fractional_time_minute, +MINUTE('839:00:00') AS overflow_time_minute, +MINUTE('-839:00:00') AS negative_overflow_time_minute, +MINUTE('12:60:00') AS invalid_minute_minute, +MINUTE('12:34:60') AS invalid_second_minute, +MINUTE('123045.987654') AS compact_time_minute, +MINUTE('20241220153045.999999') AS compact_datetime_fractional_minute, +MINUTE('20241220156045') AS invalid_compact_datetime_minute, +MINUTE('2024-12-20') AS date_only_time_minute, +MINUTE('2024-12-20T15:30:45.123456') AS t_datetime_minute, +MINUTE(' 12:34:56 ') AS trimmed_time_minute, +MINUTE('foo12:34:56') AS malformed_time_minute; +➤ fractional_time_minute[-6,8,0] ¦ day_fractional_time_minute[-6,8,0] ¦ negative_fractional_time_minute[-6,8,0] ¦ negative_day_fractional_time_minute[-6,8,0] ¦ overflow_time_minute[-6,8,0] ¦ negative_overflow_time_minute[-6,8,0] ¦ invalid_minute_minute[-6,8,0] ¦ invalid_second_minute[-6,8,0] ¦ compact_time_minute[-6,8,0] ¦ compact_datetime_fractional_minute[-6,8,0] ¦ invalid_compact_datetime_minute[-6,8,0] ¦ date_only_time_minute[-6,8,0] ¦ t_datetime_minute[-6,8,0] ¦ trimmed_time_minute[-6,8,0] ¦ malformed_time_minute[-6,8,0] 𝄀 +34 ¦ 4 ¦ 34 ¦ 4 ¦ 59 ¦ 59 ¦ null ¦ null ¦ 30 ¦ 30 ¦ null ¦ 20 ¦ 20 ¦ 34 ¦ null +SELECT MINUTE('-2024-12-20 15:30:45') AS negative_datetime_minute, +MINUTE('-20241220153045') AS negative_compact_datetime_minute, +MINUTE('20240230010203') AS invalid_calendar_compact_datetime_minute, +MINUTE('2 30:00:00') AS extended_day_time_minute, +MINUTE('60') AS invalid_compact_second_minute; +➤ negative_datetime_minute[-6,8,0] ¦ negative_compact_datetime_minute[-6,8,0] ¦ invalid_calendar_compact_datetime_minute[-6,8,0] ¦ extended_day_time_minute[-6,8,0] ¦ invalid_compact_second_minute[-6,8,0] 𝄀 +30 ¦ 30 ¦ null ¦ 0 ¦ null +SELECT MINUTE('24-12-20 15:30:45') AS two_digit_year_minute, +MINUTE('2000:01:01 12:34:56') AS colon_datetime_minute, +MINUTE('1998-01-01 00:00:009') AS wide_second_datetime_minute, +MINUTE('2024-12-20 15:30:4560') AS invalid_wide_second_datetime_minute, +MINUTE('2001-11-00 01:02:03') AS incomplete_datetime_minute, +MINUTE('99999990000') AS uint32_overflow_minute, +MINUTE('- 12:34:56') AS signed_space_time_minute, +MINUTE('12:34:56.') AS trailing_dot_time_minute; +➤ two_digit_year_minute[-6,8,0] ¦ colon_datetime_minute[-6,8,0] ¦ wide_second_datetime_minute[-6,8,0] ¦ invalid_wide_second_datetime_minute[-6,8,0] ¦ incomplete_datetime_minute[-6,8,0] ¦ uint32_overflow_minute[-6,8,0] ¦ signed_space_time_minute[-6,8,0] ¦ trailing_dot_time_minute[-6,8,0] 𝄀 +30 ¦ 34 ¦ 0 ¦ null ¦ 2 ¦ null ¦ 34 ¦ 34 +SELECT MINUTE('0000-01-01 12:34:56') AS zero_year_datetime_minute, +MINUTE('2024-00-01 11:22:33') AS zero_month_datetime_minute, +MINUTE('2024-01-00 10:20:30') AS zero_day_datetime_minute, +MINUTE('0000-00-00 09:08:07') AS zero_date_datetime_minute, +MINUTE('2024-12-20T15:30:45.123456') AS iso_t_datetime_minute; +➤ zero_year_datetime_minute[-6,8,0] ¦ zero_month_datetime_minute[-6,8,0] ¦ zero_day_datetime_minute[-6,8,0] ¦ zero_date_datetime_minute[-6,8,0] ¦ iso_t_datetime_minute[-6,8,0] 𝄀 +34 ¦ 22 ¦ 20 ¦ 8 ¦ 20 +SELECT MINUTE('2024-12-20 12:34') AS minute_datetime_minute, +MINUTE('0000-02-28 12:34:56') AS valid_zero_year_february_minute, +MINUTE('0000-02-29 12:34:56') AS invalid_zero_year_leap_day_minute, +MINUTE('0000-02-31 12:34:56') AS invalid_zero_year_february_minute, +MINUTE('0000-04-31 12:34:56') AS invalid_zero_year_april_minute, +MINUTE('1900-02-29 12:34:56') AS invalid_nonleap_datetime_minute, +MINUTE('2000-02-29 01:02:03') AS leap_datetime_minute; +➤ minute_datetime_minute[-6,8,0] ¦ valid_zero_year_february_minute[-6,8,0] ¦ invalid_zero_year_leap_day_minute[-6,8,0] ¦ invalid_zero_year_february_minute[-6,8,0] ¦ invalid_zero_year_april_minute[-6,8,0] ¦ invalid_nonleap_datetime_minute[-6,8,0] ¦ leap_datetime_minute[-6,8,0] 𝄀 +34 ¦ 34 ¦ null ¦ null ¦ null ¦ null ¦ 2 +SELECT MINUTE('20241220153045') AS compact_14_minute, +MINUTE('241220153045') AS compact_12_minute, +MINUTE('202412201530451') AS compact_14_extra_digit_minute, +MINUTE('2412201530451') AS compact_12_extra_digit_minute, +MINUTE('20241220153045abc') AS compact_14_alpha_suffix_minute, +MINUTE('241220153045abc') AS compact_12_alpha_suffix_minute; +➤ compact_14_minute[-6,8,0] ¦ compact_12_minute[-6,8,0] ¦ compact_14_extra_digit_minute[-6,8,0] ¦ compact_12_extra_digit_minute[-6,8,0] ¦ compact_14_alpha_suffix_minute[-6,8,0] ¦ compact_12_alpha_suffix_minute[-6,8,0] 𝄀 +30 ¦ 30 ¦ null ¦ null ¦ null ¦ null +SELECT MINUTE('20241220153045.123abc') AS compact_14_fraction_text_minute, +MINUTE('20241220153045.') AS compact_14_empty_fraction_minute, +MINUTE('241220153045.123abc') AS compact_12_fraction_text_minute, +MINUTE('241220153045.') AS compact_12_empty_fraction_minute, +MINUTE('20241220153045.-') AS compact_14_negative_fraction_minute, +MINUTE('20241220153045.+') AS compact_14_positive_fraction_minute, +MINUTE('241220153045.-') AS compact_12_negative_fraction_minute, +MINUTE('241220153045.+') AS compact_12_positive_fraction_minute, +MINUTE('20241220153045.123-') AS compact_14_fraction_negative_suffix_minute, +MINUTE('20241220153045.123+') AS compact_14_fraction_positive_suffix_minute, +MINUTE('241220153045.123-') AS compact_12_fraction_negative_suffix_minute, +MINUTE('241220153045.123+') AS compact_12_fraction_positive_suffix_minute, +MINUTE('20241220153045.123abc-') AS compact_14_fraction_text_negative_suffix_minute, +MINUTE('241220153045.123abc+') AS compact_12_fraction_text_positive_suffix_minute; +➤ compact_14_fraction_text_minute[-6,8,0] ¦ compact_14_empty_fraction_minute[-6,8,0] ¦ compact_12_fraction_text_minute[-6,8,0] ¦ compact_12_empty_fraction_minute[-6,8,0] ¦ compact_14_negative_fraction_minute[-6,8,0] ¦ compact_14_positive_fraction_minute[-6,8,0] ¦ compact_12_negative_fraction_minute[-6,8,0] ¦ compact_12_positive_fraction_minute[-6,8,0] ¦ compact_14_fraction_negative_suffix_minute[-6,8,0] ¦ compact_14_fraction_positive_suffix_minute[-6,8,0] ¦ compact_12_fraction_negative_suffix_minute[-6,8,0] ¦ compact_12_fraction_positive_suffix_minute[-6,8,0] ¦ compact_14_fraction_text_negative_suffix_minute[-6,8,0] ¦ compact_12_fraction_text_positive_suffix_minute[-6,8,0] 𝄀 +30 ¦ 30 ¦ 30 ¦ 30 ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 30 ¦ 30 +SELECT MINUTE('12:') AS trailing_colon_minute, +MINUTE(':34') AS leading_colon_minute, +MINUTE('12:34:56-') AS trailing_dash_minute, +MINUTE('12:34:56..') AS trailing_dots_minute, +MINUTE('123:') AS compact_3_trailing_colon_minute, +MINUTE('1234:') AS compact_4_trailing_colon_minute, +MINUTE('12345:') AS compact_5_trailing_colon_minute, +MINUTE('123456:') AS compact_6_trailing_colon_minute; +➤ trailing_colon_minute[-6,8,0] ¦ leading_colon_minute[-6,8,0] ¦ trailing_dash_minute[-6,8,0] ¦ trailing_dots_minute[-6,8,0] ¦ compact_3_trailing_colon_minute[-6,8,0] ¦ compact_4_trailing_colon_minute[-6,8,0] ¦ compact_5_trailing_colon_minute[-6,8,0] ¦ compact_6_trailing_colon_minute[-6,8,0] 𝄀 +0 ¦ 34 ¦ 34 ¦ 34 ¦ 1 ¦ 12 ¦ 23 ¦ 34 +SELECT MINUTE(':34:56') AS leading_colon_with_seconds_minute, +MINUTE('12:34:') AS trailing_second_colon_minute, +MINUTE('12::56') AS empty_minute_minute, +MINUTE('12:34:56:') AS fourth_field_colon_minute, +MINUTE('12-.abc') AS compact_prefix_before_fraction_minute; +➤ leading_colon_with_seconds_minute[-6,8,0] ¦ trailing_second_colon_minute[-6,8,0] ¦ empty_minute_minute[-6,8,0] ¦ fourth_field_colon_minute[-6,8,0] ¦ compact_prefix_before_fraction_minute[-6,8,0] 𝄀 +34 ¦ 34 ¦ 0 ¦ 34 ¦ 0 +CREATE TABLE time_extract_prefix_boundaries(v VARCHAR(32), c CHAR(32), t TEXT); +INSERT INTO time_extract_prefix_boundaries VALUES +('12-34', '12-34', '12-34'), +('1234-56', '1234-56', '1234-56'), +('1 02', '1 02', '1 02'), +('1 02:', '1 02:', '1 02:'), +('1 02-34', '1 02-34', '1 02-34'), +('2024-12-20 12', '2024-12-20 12', '2024-12-20 12'), +('2024-12-20 12:', '2024-12-20 12:', '2024-12-20 12:'), +('2024-12-20 12::56', '2024-12-20 12::56', '2024-12-20 12::56'), +('2024-12-20foo', '2024-12-20foo', '2024-12-20foo'), +('2024-12-20 12:34::56', '2024-12-20 12:34::56', '2024-12-20 12:34::56'), +('2024-12-20 12-34', '2024-12-20 12-34', '2024-12-20 12-34'), +('2024-12-20 12/34/56', '2024-12-20 12/34/56', '2024-12-20 12/34/56'), +('2024@12@20 12@34@56', '2024@12@20 12@34@56', '2024@12@20 12@34@56'), +('2024-12-20\t12:34:56', '2024-12-20\t12:34:56', '2024-12-20\t12:34:56'), +('1\t02:34:56', '1\t02:34:56', '1\t02:34:56'), +('12:34 56', '12:34 56', '12:34 56'), +('1 02:34 56', '1 02:34 56', '1 02:34 56'), +('1 02 34', '1 02 34', '1 02 34'), +('2024--12--20 12:34:56', '2024--12--20 12:34:56', '2024--12--20 12:34:56'), +('2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56'), +('2024-12-20 -12:34:56', '2024-12-20 -12:34:56', '2024-12-20 -12:34:56'), +('2024-12-20 +12:34:56', '2024-12-20 +12:34:56', '2024-12-20 +12:34:56'), +('2024-12-20 12:34 56', '2024-12-20 12:34 56', '2024-12-20 12:34 56'), +('2024-12-20 12 34', '2024-12-20 12 34', '2024-12-20 12 34'); +SELECT MINUTE(v), MINUTE(c), MINUTE(t) FROM time_extract_prefix_boundaries; +➤ MINUTE(v)[-6,8,0] ¦ MINUTE(c)[-6,8,0] ¦ MINUTE(t)[-6,8,0] 𝄀 +0 ¦ 0 ¦ 0 𝄀 +12 ¦ 12 ¦ 12 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +56 ¦ 56 ¦ 56 𝄀 +20 ¦ 20 ¦ 20 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +0 ¦ 0 ¦ 0 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +20 ¦ 20 ¦ 20 𝄀 +20 ¦ 20 ¦ 20 +DROP TABLE time_extract_prefix_boundaries; +CREATE TABLE time_extract_ambiguous_prefix_ownership(v VARCHAR(40), c CHAR(40), t TEXT); +INSERT INTO time_extract_ambiguous_prefix_ownership VALUES +('1 2', '1 2', '1 2'), +('1 02', '1 02', '1 02'), +('12:34:56', '12:34:56', '12:34:56'), +('12:34:56 78', '12:34:56 78', '12:34:56 78'), +('1-2-3 4:5:6', '1-2-3 4:5:6', '1-2-3 4:5:6'), +('12345:', '12345:', '12345:'), +('12345-1-1 1:2:3', '12345-1-1 1:2:3', '12345-1-1 1:2:3'), +('-12:34', '-12:34', '-12:34'), +('--12:34', '--12:34', '--12:34'), +('2024-12-20 +12:34', '2024-12-20 +12:34', '2024-12-20 +12:34'), +('2024-12-20 ++12:34', '2024-12-20 ++12:34', '2024-12-20 ++12:34'), +('2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc'), +('2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc'), +('2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc'), +('1 2:3', '1 2:3', '1 2:3'), +('1 2:3:4', '1 2:3:4', '1 2:3:4'), +('12 3', '12 3', '12 3'), +('12 3:4', '12 3:4', '12 3:4'), +('12 3:4:5', '12 3:4:5', '12 3:4:5'), +('12-2-3 4:5:6', '12-2-3 4:5:6', '12-2-3 4:5:6'), +('123-2-3 4:5:6', '123-2-3 4:5:6', '123-2-3 4:5:6'), +('1234-2-3 4:5:6', '1234-2-3 4:5:6', '1234-2-3 4:5:6'), +('1-1-1 1:2:3', '1-1-1 1:2:3', '1-1-1 1:2:3'), +('1-1-1 01:02:03', '1-1-1 01:02:03', '1-1-1 01:02:03'), +('1-2-3 12:34:56', '1-2-3 12:34:56', '1-2-3 12:34:56'), +('0-1-1 12:34:56', '0-1-1 12:34:56', '0-1-1 12:34:56'), +('2024-12-20 12:34:56+', '2024-12-20 12:34:56+', '2024-12-20 12:34:56+'), +('2024-12-20 12:34:56-', '2024-12-20 12:34:56-', '2024-12-20 12:34:56-'); +SELECT MINUTE(v), MINUTE(c), MINUTE(t) FROM time_extract_ambiguous_prefix_ownership; +➤ MINUTE(v)[-6,8,0] ¦ MINUTE(c)[-6,8,0] ¦ MINUTE(t)[-6,8,0] 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +0 ¦ 0 ¦ 0 𝄀 +23 ¦ 23 ¦ 23 𝄀 +null ¦ null ¦ null 𝄀 +34 ¦ 34 ¦ 34 𝄀 +null ¦ null ¦ null 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +20 ¦ 20 ¦ 20 𝄀 +20 ¦ 20 ¦ 20 𝄀 +3 ¦ 3 ¦ 3 𝄀 +3 ¦ 3 ¦ 3 𝄀 +0 ¦ 0 ¦ 0 𝄀 +4 ¦ 4 ¦ 4 𝄀 +4 ¦ 4 ¦ 4 𝄀 +5 ¦ 5 ¦ 5 𝄀 +5 ¦ 5 ¦ 5 𝄀 +5 ¦ 5 ¦ 5 𝄀 +0 ¦ 0 ¦ 0 𝄀 +2 ¦ 2 ¦ 2 𝄀 +34 ¦ 34 ¦ 34 𝄀 +34 ¦ 34 ¦ 34 𝄀 +20 ¦ 20 ¦ 20 𝄀 +20 ¦ 20 ¦ 20 +DROP TABLE time_extract_ambiguous_prefix_ownership; +SELECT MINUTE('123:34:56 78') AS unpadded_123_minute, +MINUTE('123:34:56 78 ') AS padded_123_minute, +MINUTE('\t123:34:56 78\t') AS tab_padded_123_minute, +MINUTE('838:59:59 78') AS unpadded_838_minute, +MINUTE('838:59:59 78 ') AS padded_838_minute, +MINUTE('\t838:59:59 78\t') AS tab_padded_838_minute; +➤ unpadded_123_minute[-6,8,0] ¦ padded_123_minute[-6,8,0] ¦ tab_padded_123_minute[-6,8,0] ¦ unpadded_838_minute[-6,8,0] ¦ padded_838_minute[-6,8,0] ¦ tab_padded_838_minute[-6,8,0] 𝄀 +null ¦ 34 ¦ 34 ¦ null ¦ 59 ¦ 59 diff --git a/test/distributed/cases/function/func_datetime_minute.test b/test/distributed/cases/function/func_datetime_minute.test index 5b8bfcba6d01e..a95885c20ab1b 100644 --- a/test/distributed/cases/function/func_datetime_minute.test +++ b/test/distributed/cases/function/func_datetime_minute.test @@ -40,3 +40,187 @@ DROP TABLE t1; # Range check (should return 0-59) SELECT MINUTE('15:30:45') AS result1, MINUTE('15:00:45') AS result2, MINUTE('15:59:45') AS result3; +-- VARCHAR time-string input +SELECT MINUTE(CAST('12:30:00' AS VARCHAR)) AS varchar_minute, + MINUTE(CAST('272:59:59' AS VARCHAR)) AS varchar_extended_minute, + MINUTE(CAST('2024-12-20 15:30:45' AS VARCHAR)) AS varchar_datetime_minute, + MINUTE(CAST(NULL AS VARCHAR)) AS varchar_null_minute; + +-- Date-only strings use MySQL TIME coercion; complete DATETIME strings use clock fields. +SELECT MINUTE('2024-12-20') AS date_string_minute, + MINUTE(CONCAT(DATE_ADD(CURRENT_DATE, INTERVAL 1 DAY), ' 01:02:03')) AS tomorrow_datetime_minute; + +-- Whitespace-only string values are invalid TIME inputs. +SELECT MINUTE(CAST(' ' AS CHAR)) AS char_spaces_minute, + MINUTE(CAST('\t' AS VARCHAR)) AS varchar_tab_minute, + MINUTE(CAST(' ' AS TEXT)) AS text_spaces_minute; + +-- MySQL str_to_time recognizes YYMMDDHHMMSS and keeps a valid TIME prefix. +SELECT MINUTE('241220153045') AS compact_datetime_minute, + MINUTE('15:30:45abc') AS trailing_time_minute, + MINUTE('2024-12-20 15:30:45abc') AS trailing_datetime_minute, + MINUTE('2024-12-20foo') AS malformed_datetime_minute; + +-- String TIME coercion: fractional/day/compact forms, range clamp, and invalid fields. +SELECT MINUTE('12:34:56.789012') AS fractional_time_minute, + MINUTE('2 03:04:05.9') AS day_fractional_time_minute, + MINUTE('-12:34:56.789012') AS negative_fractional_time_minute, + MINUTE('-2 03:04:05.9') AS negative_day_fractional_time_minute, + MINUTE('839:00:00') AS overflow_time_minute, + MINUTE('-839:00:00') AS negative_overflow_time_minute, + MINUTE('12:60:00') AS invalid_minute_minute, + MINUTE('12:34:60') AS invalid_second_minute, + MINUTE('123045.987654') AS compact_time_minute, + MINUTE('20241220153045.999999') AS compact_datetime_fractional_minute, + MINUTE('20241220156045') AS invalid_compact_datetime_minute, + MINUTE('2024-12-20') AS date_only_time_minute, + MINUTE('2024-12-20T15:30:45.123456') AS t_datetime_minute, + MINUTE(' 12:34:56 ') AS trimmed_time_minute, + MINUTE('foo12:34:56') AS malformed_time_minute; + +-- Signed DATETIME, invalid compact DATETIME, extended day-TIME, and invalid compact seconds. +SELECT MINUTE('-2024-12-20 15:30:45') AS negative_datetime_minute, + MINUTE('-20241220153045') AS negative_compact_datetime_minute, + MINUTE('20240230010203') AS invalid_calendar_compact_datetime_minute, + MINUTE('2 30:00:00') AS extended_day_time_minute, + MINUTE('60') AS invalid_compact_second_minute; + +-- MySQL DATETIME grammar and numeric overflow for string TIME coercion. +SELECT MINUTE('24-12-20 15:30:45') AS two_digit_year_minute, + MINUTE('2000:01:01 12:34:56') AS colon_datetime_minute, + MINUTE('1998-01-01 00:00:009') AS wide_second_datetime_minute, + MINUTE('2024-12-20 15:30:4560') AS invalid_wide_second_datetime_minute, + MINUTE('2001-11-00 01:02:03') AS incomplete_datetime_minute, + MINUTE('99999990000') AS uint32_overflow_minute, + MINUTE('- 12:34:56') AS signed_space_time_minute, + MINUTE('12:34:56.') AS trailing_dot_time_minute; + +-- Zero date components retain the clock; ISO-T uses DATE-prefix TIME coercion. +SELECT MINUTE('0000-01-01 12:34:56') AS zero_year_datetime_minute, + MINUTE('2024-00-01 11:22:33') AS zero_month_datetime_minute, + MINUTE('2024-01-00 10:20:30') AS zero_day_datetime_minute, + MINUTE('0000-00-00 09:08:07') AS zero_date_datetime_minute, + MINUTE('2024-12-20T15:30:45.123456') AS iso_t_datetime_minute; + +-- A DATETIME may omit seconds; zero year still requires a valid nonzero month/day. +SELECT MINUTE('2024-12-20 12:34') AS minute_datetime_minute, + MINUTE('0000-02-28 12:34:56') AS valid_zero_year_february_minute, + MINUTE('0000-02-29 12:34:56') AS invalid_zero_year_leap_day_minute, + MINUTE('0000-02-31 12:34:56') AS invalid_zero_year_february_minute, + MINUTE('0000-04-31 12:34:56') AS invalid_zero_year_april_minute, + MINUTE('1900-02-29 12:34:56') AS invalid_nonleap_datetime_minute, + MINUTE('2000-02-29 01:02:03') AS leap_datetime_minute; + +-- Compact DATETIME only accepts exact 14/12-digit forms or a fractional suffix. +SELECT MINUTE('20241220153045') AS compact_14_minute, + MINUTE('241220153045') AS compact_12_minute, + MINUTE('202412201530451') AS compact_14_extra_digit_minute, + MINUTE('2412201530451') AS compact_12_extra_digit_minute, + MINUTE('20241220153045abc') AS compact_14_alpha_suffix_minute, + MINUTE('241220153045abc') AS compact_12_alpha_suffix_minute; + +-- Compact DATETIME consumes the fractional prefix, including an empty fraction +-- or non-numeric text after the fractional digits. +SELECT MINUTE('20241220153045.123abc') AS compact_14_fraction_text_minute, + MINUTE('20241220153045.') AS compact_14_empty_fraction_minute, + MINUTE('241220153045.123abc') AS compact_12_fraction_text_minute, + MINUTE('241220153045.') AS compact_12_empty_fraction_minute, + MINUTE('20241220153045.-') AS compact_14_negative_fraction_minute, + MINUTE('20241220153045.+') AS compact_14_positive_fraction_minute, + MINUTE('241220153045.-') AS compact_12_negative_fraction_minute, + MINUTE('241220153045.+') AS compact_12_positive_fraction_minute, + MINUTE('20241220153045.123-') AS compact_14_fraction_negative_suffix_minute, + MINUTE('20241220153045.123+') AS compact_14_fraction_positive_suffix_minute, + MINUTE('241220153045.123-') AS compact_12_fraction_negative_suffix_minute, + MINUTE('241220153045.123+') AS compact_12_fraction_positive_suffix_minute, + MINUTE('20241220153045.123abc-') AS compact_14_fraction_text_negative_suffix_minute, + MINUTE('241220153045.123abc+') AS compact_12_fraction_text_positive_suffix_minute; + +-- TIME coercion stops after the valid clock prefix. +SELECT MINUTE('12:') AS trailing_colon_minute, + MINUTE(':34') AS leading_colon_minute, + MINUTE('12:34:56-') AS trailing_dash_minute, + MINUTE('12:34:56..') AS trailing_dots_minute, + MINUTE('123:') AS compact_3_trailing_colon_minute, + MINUTE('1234:') AS compact_4_trailing_colon_minute, + MINUTE('12345:') AS compact_5_trailing_colon_minute, + MINUTE('123456:') AS compact_6_trailing_colon_minute; + +-- MySQL retains every valid field consumed before a partial TIME suffix. +SELECT MINUTE(':34:56') AS leading_colon_with_seconds_minute, + MINUTE('12:34:') AS trailing_second_colon_minute, + MINUTE('12::56') AS empty_minute_minute, + MINUTE('12:34:56:') AS fourth_field_colon_minute, + MINUTE('12-.abc') AS compact_prefix_before_fraction_minute; + +-- Context-aware prefix consumption must agree across all string types. +CREATE TABLE time_extract_prefix_boundaries(v VARCHAR(32), c CHAR(32), t TEXT); +INSERT INTO time_extract_prefix_boundaries VALUES + ('12-34', '12-34', '12-34'), + ('1234-56', '1234-56', '1234-56'), + ('1 02', '1 02', '1 02'), + ('1 02:', '1 02:', '1 02:'), + ('1 02-34', '1 02-34', '1 02-34'), + ('2024-12-20 12', '2024-12-20 12', '2024-12-20 12'), + ('2024-12-20 12:', '2024-12-20 12:', '2024-12-20 12:'), + ('2024-12-20 12::56', '2024-12-20 12::56', '2024-12-20 12::56'), + ('2024-12-20foo', '2024-12-20foo', '2024-12-20foo'), + ('2024-12-20 12:34::56', '2024-12-20 12:34::56', '2024-12-20 12:34::56'), + ('2024-12-20 12-34', '2024-12-20 12-34', '2024-12-20 12-34'), + ('2024-12-20 12/34/56', '2024-12-20 12/34/56', '2024-12-20 12/34/56'), + ('2024@12@20 12@34@56', '2024@12@20 12@34@56', '2024@12@20 12@34@56'), + ('2024-12-20\t12:34:56', '2024-12-20\t12:34:56', '2024-12-20\t12:34:56'), + ('1\t02:34:56', '1\t02:34:56', '1\t02:34:56'), + ('12:34 56', '12:34 56', '12:34 56'), + ('1 02:34 56', '1 02:34 56', '1 02:34 56'), + ('1 02 34', '1 02 34', '1 02 34'), + ('2024--12--20 12:34:56', '2024--12--20 12:34:56', '2024--12--20 12:34:56'), + ('2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56'), + ('2024-12-20 -12:34:56', '2024-12-20 -12:34:56', '2024-12-20 -12:34:56'), + ('2024-12-20 +12:34:56', '2024-12-20 +12:34:56', '2024-12-20 +12:34:56'), + ('2024-12-20 12:34 56', '2024-12-20 12:34 56', '2024-12-20 12:34 56'), + ('2024-12-20 12 34', '2024-12-20 12 34', '2024-12-20 12 34'); +SELECT MINUTE(v), MINUTE(c), MINUTE(t) FROM time_extract_prefix_boundaries; +DROP TABLE time_extract_prefix_boundaries; + +-- DATE/DATETIME/day-TIME/compact-TIME ownership uses one ordered prefix grammar. +CREATE TABLE time_extract_ambiguous_prefix_ownership(v VARCHAR(40), c CHAR(40), t TEXT); +INSERT INTO time_extract_ambiguous_prefix_ownership VALUES + ('1 2', '1 2', '1 2'), + ('1 02', '1 02', '1 02'), + ('12:34:56', '12:34:56', '12:34:56'), + ('12:34:56 78', '12:34:56 78', '12:34:56 78'), + ('1-2-3 4:5:6', '1-2-3 4:5:6', '1-2-3 4:5:6'), + ('12345:', '12345:', '12345:'), + ('12345-1-1 1:2:3', '12345-1-1 1:2:3', '12345-1-1 1:2:3'), + ('-12:34', '-12:34', '-12:34'), + ('--12:34', '--12:34', '--12:34'), + ('2024-12-20 +12:34', '2024-12-20 +12:34', '2024-12-20 +12:34'), + ('2024-12-20 ++12:34', '2024-12-20 ++12:34', '2024-12-20 ++12:34'), + ('2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc'), + ('2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc'), + ('2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc'), + ('1 2:3', '1 2:3', '1 2:3'), + ('1 2:3:4', '1 2:3:4', '1 2:3:4'), + ('12 3', '12 3', '12 3'), + ('12 3:4', '12 3:4', '12 3:4'), + ('12 3:4:5', '12 3:4:5', '12 3:4:5'), + ('12-2-3 4:5:6', '12-2-3 4:5:6', '12-2-3 4:5:6'), + ('123-2-3 4:5:6', '123-2-3 4:5:6', '123-2-3 4:5:6'), + ('1234-2-3 4:5:6', '1234-2-3 4:5:6', '1234-2-3 4:5:6'), + ('1-1-1 1:2:3', '1-1-1 1:2:3', '1-1-1 1:2:3'), + ('1-1-1 01:02:03', '1-1-1 01:02:03', '1-1-1 01:02:03'), + ('1-2-3 12:34:56', '1-2-3 12:34:56', '1-2-3 12:34:56'), + ('0-1-1 12:34:56', '0-1-1 12:34:56', '0-1-1 12:34:56'), + ('2024-12-20 12:34:56+', '2024-12-20 12:34:56+', '2024-12-20 12:34:56+'), + ('2024-12-20 12:34:56-', '2024-12-20 12:34:56-', '2024-12-20 12:34:56-'); +SELECT MINUTE(v), MINUTE(c), MINUTE(t) FROM time_extract_ambiguous_prefix_ownership; +DROP TABLE time_extract_ambiguous_prefix_ownership; + +-- Preserve nonempty trailing whitespace until the ordered parser assigns its owner. +SELECT MINUTE('123:34:56 78') AS unpadded_123_minute, + MINUTE('123:34:56 78 ') AS padded_123_minute, + MINUTE('\t123:34:56 78\t') AS tab_padded_123_minute, + MINUTE('838:59:59 78') AS unpadded_838_minute, + MINUTE('838:59:59 78 ') AS padded_838_minute, + MINUTE('\t838:59:59 78\t') AS tab_padded_838_minute; diff --git a/test/distributed/cases/function/func_datetime_second.result b/test/distributed/cases/function/func_datetime_second.result index f99bd25aef1af..e66a06e1a9354 100644 --- a/test/distributed/cases/function/func_datetime_second.result +++ b/test/distributed/cases/function/func_datetime_second.result @@ -1,52 +1,292 @@ SELECT SECOND('15:30:45') AS result1; -invalid argument parse timestamp, bad value 15:30:45 +➤ result1[-6,8,0] 𝄀 +45 SELECT SECOND('15:30:45') AS time_with_seconds; -invalid argument parse timestamp, bad value 15:30:45 +➤ time_with_seconds[-6,8,0] 𝄀 +45 SELECT SECOND('00:00:00') AS zero_seconds; -invalid argument parse timestamp, bad value 00:00:00 +➤ zero_seconds[-6,8,0] 𝄀 +0 SELECT SECOND('23:59:59') AS max_seconds; -invalid argument parse timestamp, bad value 23:59:59 +➤ max_seconds[-6,8,0] 𝄀 +59 SELECT SECOND('12:34:56') AS various_seconds; -invalid argument parse timestamp, bad value 12:34:56 +➤ various_seconds[-6,8,0] 𝄀 +56 SELECT SECOND('2024-12-20 15:30:45') AS datetime_with_seconds; -datetime_with_seconds +➤ datetime_with_seconds[-6,8,0] 𝄀 45 SELECT SECOND('2024-12-20 00:00:00') AS datetime_zero; -datetime_zero +➤ datetime_zero[-6,8,0] 𝄀 0 SELECT SECOND('2024-12-20 23:59:59') AS datetime_max; -datetime_max +➤ datetime_max[-6,8,0] 𝄀 59 SELECT SECOND(TIMESTAMP('2024-12-20 15:30:45')) AS timestamp_with_seconds; -timestamp_with_seconds +➤ timestamp_with_seconds[-6,8,0] 𝄀 45 SELECT SECOND(NULL) AS null_input; -null_input +➤ null_input[-6,8,0] 𝄀 null CREATE TABLE t1(t TIME); INSERT INTO t1 VALUES ('15:30:45'), ('00:00:00'), ('23:59:59'), ('12:34:56'); SELECT t, SECOND(t) AS second FROM t1; -t second -15:30:45 45 -00:00:00 0 -23:59:59 59 -12:34:56 56 +➤ t[92,64,0] ¦ second[-6,8,0] 𝄀 +15:30:45 ¦ 45 𝄀 +00:00:00 ¦ 0 𝄀 +23:59:59 ¦ 59 𝄀 +12:34:56 ¦ 56 DROP TABLE t1; CREATE TABLE t1(dt DATETIME); INSERT INTO t1 VALUES ('2024-12-20 15:30:45'), ('2024-12-20 00:00:00'), ('2024-12-20 23:59:59'); SELECT dt, SECOND(dt) AS second FROM t1; -dt second -2024-12-20 15:30:45 45 -2024-12-20 00:00:00 0 -2024-12-20 23:59:59 59 +➤ dt[93,64,0] ¦ second[-6,8,0] 𝄀 +2024-12-20 15:30:45 ¦ 45 𝄀 +2024-12-20 00:00:00 ¦ 0 𝄀 +2024-12-20 23:59:59 ¦ 59 DROP TABLE t1; CREATE TABLE t1(t TIME); INSERT INTO t1 VALUES ('15:30:45'), ('00:00:00'), ('23:59:59'), ('12:34:56'); SELECT * FROM t1 WHERE SECOND(t) > 30; -t -15:30:45 -23:59:59 +➤ t[92,64,0] 𝄀 +15:30:45 𝄀 +23:59:59 𝄀 12:34:56 DROP TABLE t1; SELECT SECOND('15:30:45') AS result1, SECOND('15:30:00') AS result2, SECOND('15:30:59') AS result3; -invalid argument parse timestamp, bad value 15:30:45 +➤ result1[-6,8,0] ¦ result2[-6,8,0] ¦ result3[-6,8,0] 𝄀 +45 ¦ 0 ¦ 59 +SELECT SECOND(CAST('12:30:45' AS VARCHAR)) AS varchar_second, +SECOND(CAST('272:59:59' AS VARCHAR)) AS varchar_extended_second, +SECOND(CAST('2024-12-20 15:30:45' AS VARCHAR)) AS varchar_datetime_second, +SECOND(CAST(NULL AS VARCHAR)) AS varchar_null_second; +➤ varchar_second[-6,8,0] ¦ varchar_extended_second[-6,8,0] ¦ varchar_datetime_second[-6,8,0] ¦ varchar_null_second[-6,8,0] 𝄀 +45 ¦ 59 ¦ 45 ¦ null +SELECT SECOND('2024-12-20') AS date_string_second, +SECOND(CONCAT(DATE_ADD(CURRENT_DATE, INTERVAL 1 DAY), ' 01:02:03')) AS tomorrow_datetime_second; +➤ date_string_second[-6,8,0] ¦ tomorrow_datetime_second[-6,8,0] 𝄀 +24 ¦ 3 +SELECT SECOND(CAST(' ' AS CHAR)) AS char_spaces_second, +SECOND(CAST('\t' AS VARCHAR)) AS varchar_tab_second, +SECOND(CAST(' ' AS TEXT)) AS text_spaces_second; +➤ char_spaces_second[-6,8,0] ¦ varchar_tab_second[-6,8,0] ¦ text_spaces_second[-6,8,0] 𝄀 +null ¦ null ¦ null +SELECT SECOND('241220153045') AS compact_datetime_second, +SECOND('15:30:45abc') AS trailing_time_second, +SECOND('2024-12-20 15:30:45abc') AS trailing_datetime_second, +SECOND('2024-12-20foo') AS malformed_datetime_second; +➤ compact_datetime_second[-6,8,0] ¦ trailing_time_second[-6,8,0] ¦ trailing_datetime_second[-6,8,0] ¦ malformed_datetime_second[-6,8,0] 𝄀 +45 ¦ 45 ¦ 45 ¦ 24 +SELECT SECOND('12:34:56.789012') AS fractional_time_second, +SECOND('2 03:04:05.9') AS day_fractional_time_second, +SECOND('-12:34:56.789012') AS negative_fractional_time_second, +SECOND('-2 03:04:05.9') AS negative_day_fractional_time_second, +SECOND('839:00:00') AS overflow_time_second, +SECOND('-839:00:00') AS negative_overflow_time_second, +SECOND('12:60:00') AS invalid_minute_second, +SECOND('12:34:60') AS invalid_second_second, +SECOND('123045.987654') AS compact_time_second, +SECOND('20241220153045.999999') AS compact_datetime_fractional_second, +SECOND('20241220156045') AS invalid_compact_datetime_second, +SECOND('2024-12-20') AS date_only_time_second, +SECOND('2024-12-20T15:30:45.123456') AS t_datetime_second, +SECOND(' 12:34:56 ') AS trimmed_time_second, +SECOND('foo12:34:56') AS malformed_time_second; +➤ fractional_time_second[-6,8,0] ¦ day_fractional_time_second[-6,8,0] ¦ negative_fractional_time_second[-6,8,0] ¦ negative_day_fractional_time_second[-6,8,0] ¦ overflow_time_second[-6,8,0] ¦ negative_overflow_time_second[-6,8,0] ¦ invalid_minute_second[-6,8,0] ¦ invalid_second_second[-6,8,0] ¦ compact_time_second[-6,8,0] ¦ compact_datetime_fractional_second[-6,8,0] ¦ invalid_compact_datetime_second[-6,8,0] ¦ date_only_time_second[-6,8,0] ¦ t_datetime_second[-6,8,0] ¦ trimmed_time_second[-6,8,0] ¦ malformed_time_second[-6,8,0] 𝄀 +56 ¦ 5 ¦ 56 ¦ 5 ¦ 59 ¦ 59 ¦ null ¦ null ¦ 45 ¦ 45 ¦ null ¦ 24 ¦ 24 ¦ 56 ¦ null +SELECT SECOND('-2024-12-20 15:30:45') AS negative_datetime_second, +SECOND('-20241220153045') AS negative_compact_datetime_second, +SECOND('20240230010203') AS invalid_calendar_compact_datetime_second, +SECOND('2 30:00:00') AS extended_day_time_second, +SECOND('60') AS invalid_compact_second_second; +➤ negative_datetime_second[-6,8,0] ¦ negative_compact_datetime_second[-6,8,0] ¦ invalid_calendar_compact_datetime_second[-6,8,0] ¦ extended_day_time_second[-6,8,0] ¦ invalid_compact_second_second[-6,8,0] 𝄀 +45 ¦ 45 ¦ null ¦ 0 ¦ null +SELECT SECOND('24-12-20 15:30:45') AS two_digit_year_second, +SECOND('2000:01:01 12:34:56') AS colon_datetime_second, +SECOND('1998-01-01 00:00:009') AS wide_second_datetime_second, +SECOND('2024-12-20 15:30:4560') AS invalid_wide_second_datetime_second, +SECOND('2001-11-00 01:02:03') AS incomplete_datetime_second, +SECOND('99999990000') AS uint32_overflow_second, +SECOND('- 12:34:56') AS signed_space_time_second, +SECOND('12:34:56.') AS trailing_dot_time_second; +➤ two_digit_year_second[-6,8,0] ¦ colon_datetime_second[-6,8,0] ¦ wide_second_datetime_second[-6,8,0] ¦ invalid_wide_second_datetime_second[-6,8,0] ¦ incomplete_datetime_second[-6,8,0] ¦ uint32_overflow_second[-6,8,0] ¦ signed_space_time_second[-6,8,0] ¦ trailing_dot_time_second[-6,8,0] 𝄀 +45 ¦ 56 ¦ 9 ¦ null ¦ 3 ¦ null ¦ 56 ¦ 56 +SELECT SECOND('0000-01-01 12:34:56') AS zero_year_datetime_second, +SECOND('2024-00-01 11:22:33') AS zero_month_datetime_second, +SECOND('2024-01-00 10:20:30') AS zero_day_datetime_second, +SECOND('0000-00-00 09:08:07') AS zero_date_datetime_second, +SECOND('2024-12-20T15:30:45.123456') AS iso_t_datetime_second; +➤ zero_year_datetime_second[-6,8,0] ¦ zero_month_datetime_second[-6,8,0] ¦ zero_day_datetime_second[-6,8,0] ¦ zero_date_datetime_second[-6,8,0] ¦ iso_t_datetime_second[-6,8,0] 𝄀 +56 ¦ 33 ¦ 30 ¦ 7 ¦ 24 +SELECT SECOND('2024-12-20 12:34') AS minute_datetime_second, +SECOND('0000-02-28 12:34:56') AS valid_zero_year_february_second, +SECOND('0000-02-29 12:34:56') AS invalid_zero_year_leap_day_second, +SECOND('0000-02-31 12:34:56') AS invalid_zero_year_february_second, +SECOND('0000-04-31 12:34:56') AS invalid_zero_year_april_second, +SECOND('1900-02-29 12:34:56') AS invalid_nonleap_datetime_second, +SECOND('2000-02-29 01:02:03') AS leap_datetime_second; +➤ minute_datetime_second[-6,8,0] ¦ valid_zero_year_february_second[-6,8,0] ¦ invalid_zero_year_leap_day_second[-6,8,0] ¦ invalid_zero_year_february_second[-6,8,0] ¦ invalid_zero_year_april_second[-6,8,0] ¦ invalid_nonleap_datetime_second[-6,8,0] ¦ leap_datetime_second[-6,8,0] 𝄀 +0 ¦ 56 ¦ null ¦ null ¦ null ¦ null ¦ 3 +SELECT SECOND('20241220153045') AS compact_14_second, +SECOND('241220153045') AS compact_12_second, +SECOND('202412201530451') AS compact_14_extra_digit_second, +SECOND('2412201530451') AS compact_12_extra_digit_second, +SECOND('20241220153045abc') AS compact_14_alpha_suffix_second, +SECOND('241220153045abc') AS compact_12_alpha_suffix_second; +➤ compact_14_second[-6,8,0] ¦ compact_12_second[-6,8,0] ¦ compact_14_extra_digit_second[-6,8,0] ¦ compact_12_extra_digit_second[-6,8,0] ¦ compact_14_alpha_suffix_second[-6,8,0] ¦ compact_12_alpha_suffix_second[-6,8,0] 𝄀 +45 ¦ 45 ¦ null ¦ null ¦ null ¦ null +SELECT SECOND('20241220153045.123abc') AS compact_14_fraction_text_second, +SECOND('20241220153045.') AS compact_14_empty_fraction_second, +SECOND('241220153045.123abc') AS compact_12_fraction_text_second, +SECOND('241220153045.') AS compact_12_empty_fraction_second, +SECOND('20241220153045.-') AS compact_14_negative_fraction_second, +SECOND('20241220153045.+') AS compact_14_positive_fraction_second, +SECOND('241220153045.-') AS compact_12_negative_fraction_second, +SECOND('241220153045.+') AS compact_12_positive_fraction_second, +SECOND('20241220153045.123-') AS compact_14_fraction_negative_suffix_second, +SECOND('20241220153045.123+') AS compact_14_fraction_positive_suffix_second, +SECOND('241220153045.123-') AS compact_12_fraction_negative_suffix_second, +SECOND('241220153045.123+') AS compact_12_fraction_positive_suffix_second, +SECOND('20241220153045.123abc-') AS compact_14_fraction_text_negative_suffix_second, +SECOND('241220153045.123abc+') AS compact_12_fraction_text_positive_suffix_second; +➤ compact_14_fraction_text_second[-6,8,0] ¦ compact_14_empty_fraction_second[-6,8,0] ¦ compact_12_fraction_text_second[-6,8,0] ¦ compact_12_empty_fraction_second[-6,8,0] ¦ compact_14_negative_fraction_second[-6,8,0] ¦ compact_14_positive_fraction_second[-6,8,0] ¦ compact_12_negative_fraction_second[-6,8,0] ¦ compact_12_positive_fraction_second[-6,8,0] ¦ compact_14_fraction_negative_suffix_second[-6,8,0] ¦ compact_14_fraction_positive_suffix_second[-6,8,0] ¦ compact_12_fraction_negative_suffix_second[-6,8,0] ¦ compact_12_fraction_positive_suffix_second[-6,8,0] ¦ compact_14_fraction_text_negative_suffix_second[-6,8,0] ¦ compact_12_fraction_text_positive_suffix_second[-6,8,0] 𝄀 +45 ¦ 45 ¦ 45 ¦ 45 ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ null ¦ 45 ¦ 45 +SELECT SECOND('12:') AS trailing_colon_second, +SECOND(':34') AS leading_colon_second, +SECOND('12:34:56-') AS trailing_dash_second, +SECOND('12:34:56..') AS trailing_dots_second, +SECOND('123:') AS compact_3_trailing_colon_second, +SECOND('1234:') AS compact_4_trailing_colon_second, +SECOND('12345:') AS compact_5_trailing_colon_second, +SECOND('123456:') AS compact_6_trailing_colon_second; +➤ trailing_colon_second[-6,8,0] ¦ leading_colon_second[-6,8,0] ¦ trailing_dash_second[-6,8,0] ¦ trailing_dots_second[-6,8,0] ¦ compact_3_trailing_colon_second[-6,8,0] ¦ compact_4_trailing_colon_second[-6,8,0] ¦ compact_5_trailing_colon_second[-6,8,0] ¦ compact_6_trailing_colon_second[-6,8,0] 𝄀 +12 ¦ 0 ¦ 56 ¦ 56 ¦ 23 ¦ 34 ¦ 45 ¦ 56 +SELECT SECOND(':34:56') AS leading_colon_with_seconds_second, +SECOND('12:34:') AS trailing_second_colon_second, +SECOND('12::56') AS empty_minute_second, +SECOND('12:34:56:') AS fourth_field_colon_second, +SECOND('12-.abc') AS compact_prefix_before_fraction_second; +➤ leading_colon_with_seconds_second[-6,8,0] ¦ trailing_second_colon_second[-6,8,0] ¦ empty_minute_second[-6,8,0] ¦ fourth_field_colon_second[-6,8,0] ¦ compact_prefix_before_fraction_second[-6,8,0] 𝄀 +56 ¦ 0 ¦ 12 ¦ 56 ¦ 12 +CREATE TABLE time_extract_prefix_boundaries(v VARCHAR(32), c CHAR(32), t TEXT); +INSERT INTO time_extract_prefix_boundaries VALUES +('12-34', '12-34', '12-34'), +('1234-56', '1234-56', '1234-56'), +('1 02', '1 02', '1 02'), +('1 02:', '1 02:', '1 02:'), +('1 02-34', '1 02-34', '1 02-34'), +('2024-12-20 12', '2024-12-20 12', '2024-12-20 12'), +('2024-12-20 12:', '2024-12-20 12:', '2024-12-20 12:'), +('2024-12-20 12::56', '2024-12-20 12::56', '2024-12-20 12::56'), +('2024-12-20foo', '2024-12-20foo', '2024-12-20foo'), +('2024-12-20 12:34::56', '2024-12-20 12:34::56', '2024-12-20 12:34::56'), +('2024-12-20 12-34', '2024-12-20 12-34', '2024-12-20 12-34'), +('2024-12-20 12/34/56', '2024-12-20 12/34/56', '2024-12-20 12/34/56'), +('2024@12@20 12@34@56', '2024@12@20 12@34@56', '2024@12@20 12@34@56'), +('2024-12-20\t12:34:56', '2024-12-20\t12:34:56', '2024-12-20\t12:34:56'), +('1\t02:34:56', '1\t02:34:56', '1\t02:34:56'), +('12:34 56', '12:34 56', '12:34 56'), +('1 02:34 56', '1 02:34 56', '1 02:34 56'), +('1 02 34', '1 02 34', '1 02 34'), +('2024--12--20 12:34:56', '2024--12--20 12:34:56', '2024--12--20 12:34:56'), +('2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56'), +('2024-12-20 -12:34:56', '2024-12-20 -12:34:56', '2024-12-20 -12:34:56'), +('2024-12-20 +12:34:56', '2024-12-20 +12:34:56', '2024-12-20 +12:34:56'), +('2024-12-20 12:34 56', '2024-12-20 12:34 56', '2024-12-20 12:34 56'), +('2024-12-20 12 34', '2024-12-20 12 34', '2024-12-20 12 34'); +SELECT SECOND(v), SECOND(c), SECOND(t) FROM time_extract_prefix_boundaries; +➤ SECOND(v)[-6,8,0] ¦ SECOND(c)[-6,8,0] ¦ SECOND(t)[-6,8,0] 𝄀 +12 ¦ 12 ¦ 12 𝄀 +34 ¦ 34 ¦ 34 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +24 ¦ 24 ¦ 24 𝄀 +56 ¦ 56 ¦ 56 𝄀 +0 ¦ 0 ¦ 0 𝄀 +56 ¦ 56 ¦ 56 𝄀 +56 ¦ 56 ¦ 56 𝄀 +56 ¦ 56 ¦ 56 𝄀 +56 ¦ 56 ¦ 56 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +56 ¦ 56 ¦ 56 𝄀 +56 ¦ 56 ¦ 56 𝄀 +56 ¦ 56 ¦ 56 𝄀 +56 ¦ 56 ¦ 56 𝄀 +24 ¦ 24 ¦ 24 𝄀 +24 ¦ 24 ¦ 24 +DROP TABLE time_extract_prefix_boundaries; +CREATE TABLE time_extract_ambiguous_prefix_ownership(v VARCHAR(40), c CHAR(40), t TEXT); +INSERT INTO time_extract_ambiguous_prefix_ownership VALUES +('1 2', '1 2', '1 2'), +('1 02', '1 02', '1 02'), +('12:34:56', '12:34:56', '12:34:56'), +('12:34:56 78', '12:34:56 78', '12:34:56 78'), +('1-2-3 4:5:6', '1-2-3 4:5:6', '1-2-3 4:5:6'), +('12345:', '12345:', '12345:'), +('12345-1-1 1:2:3', '12345-1-1 1:2:3', '12345-1-1 1:2:3'), +('-12:34', '-12:34', '-12:34'), +('--12:34', '--12:34', '--12:34'), +('2024-12-20 +12:34', '2024-12-20 +12:34', '2024-12-20 +12:34'), +('2024-12-20 ++12:34', '2024-12-20 ++12:34', '2024-12-20 ++12:34'), +('2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc'), +('2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc'), +('2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc'), +('1 2:3', '1 2:3', '1 2:3'), +('1 2:3:4', '1 2:3:4', '1 2:3:4'), +('12 3', '12 3', '12 3'), +('12 3:4', '12 3:4', '12 3:4'), +('12 3:4:5', '12 3:4:5', '12 3:4:5'), +('12-2-3 4:5:6', '12-2-3 4:5:6', '12-2-3 4:5:6'), +('123-2-3 4:5:6', '123-2-3 4:5:6', '123-2-3 4:5:6'), +('1234-2-3 4:5:6', '1234-2-3 4:5:6', '1234-2-3 4:5:6'), +('1-1-1 1:2:3', '1-1-1 1:2:3', '1-1-1 1:2:3'), +('1-1-1 01:02:03', '1-1-1 01:02:03', '1-1-1 01:02:03'), +('1-2-3 12:34:56', '1-2-3 12:34:56', '1-2-3 12:34:56'), +('0-1-1 12:34:56', '0-1-1 12:34:56', '0-1-1 12:34:56'), +('2024-12-20 12:34:56+', '2024-12-20 12:34:56+', '2024-12-20 12:34:56+'), +('2024-12-20 12:34:56-', '2024-12-20 12:34:56-', '2024-12-20 12:34:56-'); +SELECT SECOND(v), SECOND(c), SECOND(t) FROM time_extract_ambiguous_prefix_ownership; +➤ SECOND(v)[-6,8,0] ¦ SECOND(c)[-6,8,0] ¦ SECOND(t)[-6,8,0] 𝄀 +1 ¦ 1 ¦ 1 𝄀 +0 ¦ 0 ¦ 0 𝄀 +56 ¦ 56 ¦ 56 𝄀 +56 ¦ 56 ¦ 56 𝄀 +1 ¦ 1 ¦ 1 𝄀 +45 ¦ 45 ¦ 45 𝄀 +null ¦ null ¦ null 𝄀 +0 ¦ 0 ¦ 0 𝄀 +null ¦ null ¦ null 𝄀 +0 ¦ 0 ¦ 0 𝄀 +0 ¦ 0 ¦ 0 𝄀 +56 ¦ 56 ¦ 56 𝄀 +24 ¦ 24 ¦ 24 𝄀 +24 ¦ 24 ¦ 24 𝄀 +0 ¦ 0 ¦ 0 𝄀 +4 ¦ 4 ¦ 4 𝄀 +12 ¦ 12 ¦ 12 𝄀 +0 ¦ 0 ¦ 0 𝄀 +5 ¦ 5 ¦ 5 𝄀 +6 ¦ 6 ¦ 6 𝄀 +6 ¦ 6 ¦ 6 𝄀 +6 ¦ 6 ¦ 6 𝄀 +1 ¦ 1 ¦ 1 𝄀 +3 ¦ 3 ¦ 3 𝄀 +56 ¦ 56 ¦ 56 𝄀 +56 ¦ 56 ¦ 56 𝄀 +24 ¦ 24 ¦ 24 𝄀 +24 ¦ 24 ¦ 24 +DROP TABLE time_extract_ambiguous_prefix_ownership; +SELECT SECOND('123:34:56 78') AS unpadded_123_second, +SECOND('123:34:56 78 ') AS padded_123_second, +SECOND('\t123:34:56 78\t') AS tab_padded_123_second, +SECOND('838:59:59 78') AS unpadded_838_second, +SECOND('838:59:59 78 ') AS padded_838_second, +SECOND('\t838:59:59 78\t') AS tab_padded_838_second; +➤ unpadded_123_second[-6,8,0] ¦ padded_123_second[-6,8,0] ¦ tab_padded_123_second[-6,8,0] ¦ unpadded_838_second[-6,8,0] ¦ padded_838_second[-6,8,0] ¦ tab_padded_838_second[-6,8,0] 𝄀 +null ¦ 56 ¦ 56 ¦ null ¦ 59 ¦ 59 diff --git a/test/distributed/cases/function/func_datetime_second.test b/test/distributed/cases/function/func_datetime_second.test index 89c2d2871fe55..f8ea2b4e29914 100644 --- a/test/distributed/cases/function/func_datetime_second.test +++ b/test/distributed/cases/function/func_datetime_second.test @@ -40,3 +40,187 @@ DROP TABLE t1; # Range check (should return 0-59) SELECT SECOND('15:30:45') AS result1, SECOND('15:30:00') AS result2, SECOND('15:30:59') AS result3; +-- VARCHAR time-string input +SELECT SECOND(CAST('12:30:45' AS VARCHAR)) AS varchar_second, + SECOND(CAST('272:59:59' AS VARCHAR)) AS varchar_extended_second, + SECOND(CAST('2024-12-20 15:30:45' AS VARCHAR)) AS varchar_datetime_second, + SECOND(CAST(NULL AS VARCHAR)) AS varchar_null_second; + +-- Date-only strings use MySQL TIME coercion; complete DATETIME strings use clock fields. +SELECT SECOND('2024-12-20') AS date_string_second, + SECOND(CONCAT(DATE_ADD(CURRENT_DATE, INTERVAL 1 DAY), ' 01:02:03')) AS tomorrow_datetime_second; + +-- Whitespace-only string values are invalid TIME inputs. +SELECT SECOND(CAST(' ' AS CHAR)) AS char_spaces_second, + SECOND(CAST('\t' AS VARCHAR)) AS varchar_tab_second, + SECOND(CAST(' ' AS TEXT)) AS text_spaces_second; + +-- MySQL str_to_time recognizes YYMMDDHHMMSS and keeps a valid TIME prefix. +SELECT SECOND('241220153045') AS compact_datetime_second, + SECOND('15:30:45abc') AS trailing_time_second, + SECOND('2024-12-20 15:30:45abc') AS trailing_datetime_second, + SECOND('2024-12-20foo') AS malformed_datetime_second; + +-- String TIME coercion: fractional/day/compact forms, range clamp, and invalid fields. +SELECT SECOND('12:34:56.789012') AS fractional_time_second, + SECOND('2 03:04:05.9') AS day_fractional_time_second, + SECOND('-12:34:56.789012') AS negative_fractional_time_second, + SECOND('-2 03:04:05.9') AS negative_day_fractional_time_second, + SECOND('839:00:00') AS overflow_time_second, + SECOND('-839:00:00') AS negative_overflow_time_second, + SECOND('12:60:00') AS invalid_minute_second, + SECOND('12:34:60') AS invalid_second_second, + SECOND('123045.987654') AS compact_time_second, + SECOND('20241220153045.999999') AS compact_datetime_fractional_second, + SECOND('20241220156045') AS invalid_compact_datetime_second, + SECOND('2024-12-20') AS date_only_time_second, + SECOND('2024-12-20T15:30:45.123456') AS t_datetime_second, + SECOND(' 12:34:56 ') AS trimmed_time_second, + SECOND('foo12:34:56') AS malformed_time_second; + +-- Signed DATETIME, invalid compact DATETIME, extended day-TIME, and invalid compact seconds. +SELECT SECOND('-2024-12-20 15:30:45') AS negative_datetime_second, + SECOND('-20241220153045') AS negative_compact_datetime_second, + SECOND('20240230010203') AS invalid_calendar_compact_datetime_second, + SECOND('2 30:00:00') AS extended_day_time_second, + SECOND('60') AS invalid_compact_second_second; + +-- MySQL DATETIME grammar and numeric overflow for string TIME coercion. +SELECT SECOND('24-12-20 15:30:45') AS two_digit_year_second, + SECOND('2000:01:01 12:34:56') AS colon_datetime_second, + SECOND('1998-01-01 00:00:009') AS wide_second_datetime_second, + SECOND('2024-12-20 15:30:4560') AS invalid_wide_second_datetime_second, + SECOND('2001-11-00 01:02:03') AS incomplete_datetime_second, + SECOND('99999990000') AS uint32_overflow_second, + SECOND('- 12:34:56') AS signed_space_time_second, + SECOND('12:34:56.') AS trailing_dot_time_second; + +-- Zero date components retain the clock; ISO-T uses DATE-prefix TIME coercion. +SELECT SECOND('0000-01-01 12:34:56') AS zero_year_datetime_second, + SECOND('2024-00-01 11:22:33') AS zero_month_datetime_second, + SECOND('2024-01-00 10:20:30') AS zero_day_datetime_second, + SECOND('0000-00-00 09:08:07') AS zero_date_datetime_second, + SECOND('2024-12-20T15:30:45.123456') AS iso_t_datetime_second; + +-- A DATETIME may omit seconds; zero year still requires a valid nonzero month/day. +SELECT SECOND('2024-12-20 12:34') AS minute_datetime_second, + SECOND('0000-02-28 12:34:56') AS valid_zero_year_february_second, + SECOND('0000-02-29 12:34:56') AS invalid_zero_year_leap_day_second, + SECOND('0000-02-31 12:34:56') AS invalid_zero_year_february_second, + SECOND('0000-04-31 12:34:56') AS invalid_zero_year_april_second, + SECOND('1900-02-29 12:34:56') AS invalid_nonleap_datetime_second, + SECOND('2000-02-29 01:02:03') AS leap_datetime_second; + +-- Compact DATETIME only accepts exact 14/12-digit forms or a fractional suffix. +SELECT SECOND('20241220153045') AS compact_14_second, + SECOND('241220153045') AS compact_12_second, + SECOND('202412201530451') AS compact_14_extra_digit_second, + SECOND('2412201530451') AS compact_12_extra_digit_second, + SECOND('20241220153045abc') AS compact_14_alpha_suffix_second, + SECOND('241220153045abc') AS compact_12_alpha_suffix_second; + +-- Compact DATETIME consumes the fractional prefix, including an empty fraction +-- or non-numeric text after the fractional digits. +SELECT SECOND('20241220153045.123abc') AS compact_14_fraction_text_second, + SECOND('20241220153045.') AS compact_14_empty_fraction_second, + SECOND('241220153045.123abc') AS compact_12_fraction_text_second, + SECOND('241220153045.') AS compact_12_empty_fraction_second, + SECOND('20241220153045.-') AS compact_14_negative_fraction_second, + SECOND('20241220153045.+') AS compact_14_positive_fraction_second, + SECOND('241220153045.-') AS compact_12_negative_fraction_second, + SECOND('241220153045.+') AS compact_12_positive_fraction_second, + SECOND('20241220153045.123-') AS compact_14_fraction_negative_suffix_second, + SECOND('20241220153045.123+') AS compact_14_fraction_positive_suffix_second, + SECOND('241220153045.123-') AS compact_12_fraction_negative_suffix_second, + SECOND('241220153045.123+') AS compact_12_fraction_positive_suffix_second, + SECOND('20241220153045.123abc-') AS compact_14_fraction_text_negative_suffix_second, + SECOND('241220153045.123abc+') AS compact_12_fraction_text_positive_suffix_second; + +-- TIME coercion stops after the valid clock prefix. +SELECT SECOND('12:') AS trailing_colon_second, + SECOND(':34') AS leading_colon_second, + SECOND('12:34:56-') AS trailing_dash_second, + SECOND('12:34:56..') AS trailing_dots_second, + SECOND('123:') AS compact_3_trailing_colon_second, + SECOND('1234:') AS compact_4_trailing_colon_second, + SECOND('12345:') AS compact_5_trailing_colon_second, + SECOND('123456:') AS compact_6_trailing_colon_second; + +-- MySQL retains every valid field consumed before a partial TIME suffix. +SELECT SECOND(':34:56') AS leading_colon_with_seconds_second, + SECOND('12:34:') AS trailing_second_colon_second, + SECOND('12::56') AS empty_minute_second, + SECOND('12:34:56:') AS fourth_field_colon_second, + SECOND('12-.abc') AS compact_prefix_before_fraction_second; + +-- Context-aware prefix consumption must agree across all string types. +CREATE TABLE time_extract_prefix_boundaries(v VARCHAR(32), c CHAR(32), t TEXT); +INSERT INTO time_extract_prefix_boundaries VALUES + ('12-34', '12-34', '12-34'), + ('1234-56', '1234-56', '1234-56'), + ('1 02', '1 02', '1 02'), + ('1 02:', '1 02:', '1 02:'), + ('1 02-34', '1 02-34', '1 02-34'), + ('2024-12-20 12', '2024-12-20 12', '2024-12-20 12'), + ('2024-12-20 12:', '2024-12-20 12:', '2024-12-20 12:'), + ('2024-12-20 12::56', '2024-12-20 12::56', '2024-12-20 12::56'), + ('2024-12-20foo', '2024-12-20foo', '2024-12-20foo'), + ('2024-12-20 12:34::56', '2024-12-20 12:34::56', '2024-12-20 12:34::56'), + ('2024-12-20 12-34', '2024-12-20 12-34', '2024-12-20 12-34'), + ('2024-12-20 12/34/56', '2024-12-20 12/34/56', '2024-12-20 12/34/56'), + ('2024@12@20 12@34@56', '2024@12@20 12@34@56', '2024@12@20 12@34@56'), + ('2024-12-20\t12:34:56', '2024-12-20\t12:34:56', '2024-12-20\t12:34:56'), + ('1\t02:34:56', '1\t02:34:56', '1\t02:34:56'), + ('12:34 56', '12:34 56', '12:34 56'), + ('1 02:34 56', '1 02:34 56', '1 02:34 56'), + ('1 02 34', '1 02 34', '1 02 34'), + ('2024--12--20 12:34:56', '2024--12--20 12:34:56', '2024--12--20 12:34:56'), + ('2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56', '2024/-12/-20 12:34:56'), + ('2024-12-20 -12:34:56', '2024-12-20 -12:34:56', '2024-12-20 -12:34:56'), + ('2024-12-20 +12:34:56', '2024-12-20 +12:34:56', '2024-12-20 +12:34:56'), + ('2024-12-20 12:34 56', '2024-12-20 12:34 56', '2024-12-20 12:34 56'), + ('2024-12-20 12 34', '2024-12-20 12 34', '2024-12-20 12 34'); +SELECT SECOND(v), SECOND(c), SECOND(t) FROM time_extract_prefix_boundaries; +DROP TABLE time_extract_prefix_boundaries; + +-- DATE/DATETIME/day-TIME/compact-TIME ownership uses one ordered prefix grammar. +CREATE TABLE time_extract_ambiguous_prefix_ownership(v VARCHAR(40), c CHAR(40), t TEXT); +INSERT INTO time_extract_ambiguous_prefix_ownership VALUES + ('1 2', '1 2', '1 2'), + ('1 02', '1 02', '1 02'), + ('12:34:56', '12:34:56', '12:34:56'), + ('12:34:56 78', '12:34:56 78', '12:34:56 78'), + ('1-2-3 4:5:6', '1-2-3 4:5:6', '1-2-3 4:5:6'), + ('12345:', '12345:', '12345:'), + ('12345-1-1 1:2:3', '12345-1-1 1:2:3', '12345-1-1 1:2:3'), + ('-12:34', '-12:34', '-12:34'), + ('--12:34', '--12:34', '--12:34'), + ('2024-12-20 +12:34', '2024-12-20 +12:34', '2024-12-20 +12:34'), + ('2024-12-20 ++12:34', '2024-12-20 ++12:34', '2024-12-20 ++12:34'), + ('2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc', '2024-12-20 12:34:56abc'), + ('2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc', '2024-12-20 12:34:56+abc'), + ('2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc', '2024-12-20 12:34:56-abc'), + ('1 2:3', '1 2:3', '1 2:3'), + ('1 2:3:4', '1 2:3:4', '1 2:3:4'), + ('12 3', '12 3', '12 3'), + ('12 3:4', '12 3:4', '12 3:4'), + ('12 3:4:5', '12 3:4:5', '12 3:4:5'), + ('12-2-3 4:5:6', '12-2-3 4:5:6', '12-2-3 4:5:6'), + ('123-2-3 4:5:6', '123-2-3 4:5:6', '123-2-3 4:5:6'), + ('1234-2-3 4:5:6', '1234-2-3 4:5:6', '1234-2-3 4:5:6'), + ('1-1-1 1:2:3', '1-1-1 1:2:3', '1-1-1 1:2:3'), + ('1-1-1 01:02:03', '1-1-1 01:02:03', '1-1-1 01:02:03'), + ('1-2-3 12:34:56', '1-2-3 12:34:56', '1-2-3 12:34:56'), + ('0-1-1 12:34:56', '0-1-1 12:34:56', '0-1-1 12:34:56'), + ('2024-12-20 12:34:56+', '2024-12-20 12:34:56+', '2024-12-20 12:34:56+'), + ('2024-12-20 12:34:56-', '2024-12-20 12:34:56-', '2024-12-20 12:34:56-'); +SELECT SECOND(v), SECOND(c), SECOND(t) FROM time_extract_ambiguous_prefix_ownership; +DROP TABLE time_extract_ambiguous_prefix_ownership; + +-- Preserve nonempty trailing whitespace until the ordered parser assigns its owner. +SELECT SECOND('123:34:56 78') AS unpadded_123_second, + SECOND('123:34:56 78 ') AS padded_123_second, + SECOND('\t123:34:56 78\t') AS tab_padded_123_second, + SECOND('838:59:59 78') AS unpadded_838_second, + SECOND('838:59:59 78 ') AS padded_838_second, + SECOND('\t838:59:59 78\t') AS tab_padded_838_second;