diff --git a/pkg/sql/plan/base_binder.go b/pkg/sql/plan/base_binder.go index 2c78e0269b479..0947a156f3dfa 100644 --- a/pkg/sql/plan/base_binder.go +++ b/pkg/sql/plan/base_binder.go @@ -2691,6 +2691,16 @@ func BindFuncExprImplByPlanExpr(ctx context.Context, name string, args []*Expr) } } + case "maketime": + // Hex and bit literals are represented as VARCHAR literals carrying + // IsBin. They are integral seconds, so they retain TIME(0) metadata even + // though the VARCHAR seconds overload normally advertises TIME(6). + if len(args) == 3 { + if literal := args[2].GetLit(); literal != nil && literal.IsBin { + returnType.Scale = 0 + } + } + case "timestampadd": // For TIMESTAMPADD with DATE input, check if unit is constant and adjust return type // MySQL behavior: DATE input + date unit → DATE output, DATE input + time unit → DATETIME output @@ -2741,6 +2751,15 @@ func BindFuncExprImplByPlanExpr(ctx context.Context, name string, args []*Expr) } for idx, castType := range argsCastType { if !argsType[idx].Eq(castType) && castType.Oid != types.T_any { + // MAKETIME uses the scale on its VARCHAR seconds target only to + // derive the TIME return scale. Recasting an already-VARCHAR + // argument solely for that metadata clears Literal.IsBin, changing + // X'..'/B'..' from a binary number into ordinary text. + if name == "maketime" && idx == 2 && + argsType[idx].Oid == types.T_varchar && castType.Oid == types.T_varchar && + argsType[idx].Width == castType.Width { + continue + } if argsType[idx].Oid == castType.Oid && castType.Oid.IsDecimal() && argsType[idx].Scale == castType.Scale { continue } diff --git a/pkg/sql/plan/build_expr_test.go b/pkg/sql/plan/build_expr_test.go index 4e80bba5243b5..e32976bb7a8ae 100644 --- a/pkg/sql/plan/build_expr_test.go +++ b/pkg/sql/plan/build_expr_test.go @@ -15,6 +15,7 @@ package plan import ( + "strings" "testing" "time" @@ -402,6 +403,110 @@ func runOneExprStmt(opt Optimizer, t *testing.T, sql string) (*plan.Plan, error) return pl, nil } +func TestMakeTimeBinaryLiteralBindAndExecute(t *testing.T) { + tests := []struct { + name string + sql string + want string + wantNull bool + }{ + {name: "hex hour", sql: "select cast(maketime(X'0102', 0, 0) as varchar)", want: "258:00:00"}, + {name: "hex hour empty", sql: "select cast(maketime(X'', 0, 0) as varchar)", want: "00:00:00"}, + {name: "hex hour max int64", sql: "select cast(maketime(X'7FFFFFFFFFFFFFFF', 0, 0) as varchar)", want: "838:59:59"}, + {name: "hex hour max int64 plus one", sql: "select cast(maketime(X'8000000000000000', 0, 0) as varchar)", want: "838:59:59"}, + {name: "hex hour uint64 overflow", sql: "select cast(maketime(X'FFFFFFFFFFFFFFFF', 0, 0) as varchar)", want: "838:59:59"}, + {name: "hex hour wider than uint64", sql: "select cast(maketime(X'FFFFFFFFFFFFFFFFFF', 0, 0) as varchar)", want: "838:59:59"}, + {name: "hex hour wide leading zeros", sql: "select cast(maketime(X'000000000000000001', 0, 0) as varchar)", want: "01:00:00"}, + {name: "hex minute", sql: "select cast(maketime(12, X'01', 0) as varchar)", want: "12:01:00"}, + {name: "hex minute overflow", sql: "select cast(maketime(12, X'FFFFFFFFFFFFFFFFFF', 0) as varchar)", wantNull: true}, + {name: "hex minute wide leading zeros", sql: "select cast(maketime(12, X'000000000000000001', 0) as varchar)", want: "12:01:00"}, + {name: "hex second", sql: "select cast(maketime(12, 0, X'01') as varchar)", want: "12:00:01"}, + {name: "hex second wide leading zeros", sql: "select cast(maketime(12, 0, X'000000000000000001') as varchar)", want: "12:00:01"}, + {name: "hex second wide leading zero max", sql: "select cast(maketime(12, 0, X'00000000000000003B') as varchar)", want: "12:00:59"}, + {name: "hex second wider overflow", sql: "select cast(maketime(12, 0, X'010000000000000000') as varchar)", wantNull: true}, + {name: "bit second", sql: "select cast(maketime(12, 0, B'00000001') as varchar)", want: "12:00:01"}, + {name: "binary string second", sql: "select cast(maketime(12, 0, cast('01' as binary(2))) as varchar)", want: "12:00:01"}, + {name: "empty string second coerces to zero", sql: "select cast(maketime(12, 34, '') as varchar)", want: "12:34:00.000000"}, + {name: "nonnumeric string second coerces to zero", sql: "select cast(maketime(12, 34, 'foo') as varchar)", want: "12:34:00.000000"}, + {name: "plain strings", sql: "select cast(maketime('12.7', '15.8', '30.9') as varchar)", want: "12:15:30.900000"}, + {name: "decimal second", sql: "select cast(maketime(12, 34, cast('56.789012' as decimal(20, 6))) as varchar)", want: "12:34:56.789012"}, + {name: "decimal minute below half", sql: "select cast(maketime(12, cast('59.49999999999999999999' as decimal(30, 20)), cast('0' as decimal(2, 1))) as varchar)", want: "12:59:00.0"}, + {name: "decimal minute at half", sql: "select cast(maketime(12, cast('58.5' as decimal(3, 1)), 0) as varchar)", want: "12:59:00"}, + {name: "decimal64 minute below half", sql: "select cast(maketime(12, cast('58.499999999999999' as decimal(17, 15)), 0) as varchar)", want: "12:58:00"}, + {name: "decimal minute rounds out of range", sql: "select maketime(12, cast('59.5' as decimal(3, 1)), 0)", wantNull: true}, + {name: "decimal hour below half", sql: "select cast(maketime(cast('12.49999999999999999999' as decimal(30, 20)), 0, 0) as varchar)", want: "12:00:00"}, + {name: "negative decimal hour at half", sql: "select cast(maketime(cast('-12.5' as decimal(3, 1)), 0, 0) as varchar)", want: "-13:00:00"}, + {name: "decimal hour positive overflow", sql: "select cast(maketime(cast('99999999999999999999999999999999999999' as decimal(38, 0)), 0, 0) as varchar)", want: "838:59:59"}, + {name: "decimal hour negative overflow", sql: "select cast(maketime(cast('-99999999999999999999999999999999999999' as decimal(38, 0)), 0, 0) as varchar)", want: "-838:59:59"}, + {name: "decimal256 hour below half", sql: "select cast(maketime(cast('12.499999999999999999999999999999' as decimal(65, 30)), 0, 0) as varchar)", want: "12:00:00"}, + {name: "decimal256 hour at half", sql: "select cast(maketime(cast('12.500000000000000000000000000000' as decimal(65, 30)), 0, 0) as varchar)", want: "13:00:00"}, + {name: "decimal256 minute below half", sql: "select cast(maketime(12, cast('58.499999999999999999999999999999' as decimal(65, 30)), 0) as varchar)", want: "12:58:00"}, + {name: "decimal256 minute at half", sql: "select cast(maketime(12, cast('58.500000000000000000000000000000' as decimal(65, 30)), 0) as varchar)", want: "12:59:00"}, + {name: "decimal256 hour positive overflow", sql: "select cast(maketime(cast('99999999999999999999999999999999999999999999999999999999999999999' as decimal(65, 0)), 0, 0) as varchar)", want: "838:59:59"}, + {name: "decimal256 hour negative overflow", sql: "select cast(maketime(cast('-99999999999999999999999999999999999999999999999999999999999999999' as decimal(65, 0)), 0, 0) as varchar)", want: "-838:59:59"}, + {name: "safe exponent underflow", sql: "select cast(maketime(12, 34, '1e-5000') as varchar)", want: "12:34:00.000000"}, + {name: "zero mantissa huge exponent", sql: "select cast(maketime(12, 34, '0e5000') as varchar)", want: "12:34:00.000000"}, + {name: "wide zero mantissa", sql: "select cast(maketime(12, 34, '" + strings.Repeat("0", 4097) + "') as varchar)", want: "12:34:00.000000"}, + {name: "wide leading zero second", sql: "select cast(maketime(12, 34, '" + strings.Repeat("0", 4096) + "1') as varchar)", want: "12:34:01.000000"}, + {name: "wide trailing fractional zero second", sql: "select cast(maketime(12, 34, '1." + strings.Repeat("0", 4097) + "') as varchar)", want: "12:34:01.000000"}, + {name: "wide zero padding canceled by exponent", sql: "select cast(maketime(12, 34, '0." + strings.Repeat("0", 4096) + "1e4097') as varchar)", want: "12:34:01.000000"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mock := NewMockOptimizer(false) + pl, err := runOneExprStmt(mock, t, test.sql) + require.NoError(t, err) + + query := pl.GetQuery() + require.NotNil(t, query) + expr := query.Nodes[1].ProjectList[0] + proc := testutil.NewProc(t) + defer proc.Free() + executor, err := colexec.NewExpressionExecutor(proc, expr) + require.NoError(t, err) + defer executor.Free() + + result, err := executor.Eval(proc, nil, nil) + require.NoError(t, err) + if test.wantNull { + require.True(t, result.GetNulls().Contains(0)) + return + } + require.False(t, result.GetNulls().Contains(0)) + require.Equal(t, test.want, result.GetStringAt(0)) + }) + } +} + +func TestMakeTimeExtremeExactSecondBindAndExecute(t *testing.T) { + tests := []struct { + name string + sql string + }{ + {"exponent", "select maketime(12, 34, '1e" + strings.Repeat("9", 8192) + "')"}, + {"mantissa", "select maketime(12, 34, '" + strings.Repeat("9", 4097) + "')"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mock := NewMockOptimizer(false) + pl, err := runOneExprStmt(mock, t, test.sql) + require.NoError(t, err) + + expr := pl.GetQuery().Nodes[1].ProjectList[0] + proc := testutil.NewProc(t) + defer proc.Free() + executor, err := colexec.NewExpressionExecutor(proc, expr) + require.NoError(t, err) + defer executor.Free() + result, err := executor.Eval(proc, nil, nil) + require.NoError(t, err) + require.True(t, result.GetNulls().Contains(0)) + }) + } +} + func makeTimeExpr(s string, p int32) *plan.Expr { dt, _ := types.ParseTime(s, 0) return &plan.Expr{ diff --git a/pkg/sql/plan/function/func_binary.go b/pkg/sql/plan/function/func_binary.go index d16e7dcf5cd01..d1a2c21062c78 100644 --- a/pkg/sql/plan/function/func_binary.go +++ b/pkg/sql/plan/function/func_binary.go @@ -4019,11 +4019,12 @@ func TimeFormat(ivecs []*vector.Vector, result vector.FunctionResultWrapper, pro times := vector.GenerateFunctionFixedTypeParameter[types.Time](ivecs[0]) formats := vector.GenerateFunctionStrParameter(ivecs[1]) fmt, null2 := formats.GetStrValue(0) + emptyFormat := len(fmt) == 0 var buf bytes.Buffer for i := uint64(0); i < uint64(length); i++ { t, null1 := times.GetValue(i) - if null1 || null2 { + if null1 || null2 || emptyFormat { if err = rs.AppendBytes(nil, true); err != nil { return err } @@ -4043,7 +4044,10 @@ func TimeFormat(ivecs []*vector.Vector, result vector.FunctionResultWrapper, pro // timeFormat: Get the format string corresponding to the time according to format specifiers // Only supports time-related format specifiers: %H, %h, %I, %i, %k, %l, %S, %s, %f, %p, %r, %T func timeFormat(ctx context.Context, t types.Time, format string, buf *bytes.Buffer) error { - hour, minute, sec, msec, _ := t.ClockFormat() + hour, minute, sec, msec, isNeg := t.ClockFormat() + if isNeg && len(format) > 0 { + buf.WriteByte('-') + } inPatternMatch := false for _, b := range format { if inPatternMatch { @@ -4112,7 +4116,7 @@ func makeTimeFormat(ctx context.Context, hour uint64, minute, sec uint8, msec ui case 'S', 's': FormatInt2BufByWidth(int(sec), 2, buf) case 'T': - fmt.Fprintf(buf, "%02d:%02d:%02d", hour%24, minute, sec) + fmt.Fprintf(buf, "%02d:%02d:%02d", hour, minute, sec) default: // For unsupported format specifiers, just write the character as-is // This matches MySQL behavior where non-time format specifiers are ignored @@ -7270,180 +7274,440 @@ func MakeDateString( return nil } -// makeTimeFromInt64: Helper function to create Time from int64 values -func makeTimeFromInt64(hour, minute, second int64, rs *vector.FunctionResult[types.Time], i uint64) error { - // MySQL allows hour to be in range [0, 838] (TIME type range) - // minute and second should be in range [0, 59] - // If values are out of range, MySQL returns NULL - if hour < 0 || hour > 838 { - return rs.Append(types.Time(0), true) +func makeTimeIntegerSecond(value int64, null bool) (int64, uint32, bool) { + if null || value < 0 || value >= 60 { + return 0, 0, true } + return value, 0, false +} - if minute < 0 || minute > 59 || second < 0 || second > 59 { +// makeTimeFromInt64: Helper function to create Time from int64 values +func makeTimeFromInt64(hour, minute, second int64, microsecond uint32, rs *vector.FunctionResult[types.Time]) error { + if minute < 0 || minute > 59 || second < 0 || second > 60 || microsecond >= types.MicroSecsPerSec { return rs.Append(types.Time(0), true) } - // Create Time value using TimeFromClock - // hour can be up to 838, so we use uint64 for hour - timeValue := types.TimeFromClock(false, uint64(hour), uint8(minute), uint8(second), 0) + maxTime := types.TimeFromClock(false, 838, 59, 59, 0) + if hour > 838 { + return rs.Append(maxTime, false) + } + if hour < -838 { + return rs.Append(-maxTime, false) + } - // Validate the resulting time - h := timeValue.Hour() - if h < 0 { - h = -h + isNegative := hour < 0 + if isNegative { + hour = -hour } - if !types.ValidTime(uint64(h), 0, 0) { - return rs.Append(types.Time(0), true) + timeValue := types.TimeFromClock(isNegative, uint64(hour), uint8(minute), uint8(second), microsecond) + if timeValue > maxTime { + timeValue = maxTime + } else if timeValue < -maxTime { + timeValue = -maxTime } return rs.Append(timeValue, false) } -// MakeTime: MAKETIME(hour, minute, second) - Returns a time value calculated from the hour, minute, and second arguments. -func MakeTime(ivecs []*vector.Vector, result vector.FunctionResultWrapper, _ *process.Process, length int, selectList *FunctionSelectList) error { - rs := vector.MustFunctionResult[types.Time](result) - - // Check the types of input vectors and create appropriate parameter wrappers - hourType := ivecs[0].GetType().Oid - minuteType := ivecs[1].GetType().Oid - secondType := ivecs[2].GetType().Oid +func makeTimeSignedIntegerGetter[T constraints.Signed](vec *vector.Vector) func(uint64) (int64, bool) { + param := vector.GenerateFunctionFixedTypeParameter[T](vec) + return func(i uint64) (int64, bool) { + value, null := param.GetValue(i) + return int64(value), null + } +} - // Create parameter wrappers based on types (these can be reused for all rows) - var getHourValue func(uint64) (int64, bool) - var getMinuteValue func(uint64) (int64, bool) - var getSecondValue func(uint64) (int64, bool) +func makeTimeUnsignedIntegerGetter[T constraints.Unsigned](vec *vector.Vector) func(uint64) (int64, bool) { + param := vector.GenerateFunctionFixedTypeParameter[T](vec) + return func(i uint64) (int64, bool) { + value, null := param.GetValue(i) + if uint64(value) > math.MaxInt64 { + return math.MaxInt64, null + } + return int64(value), null + } +} - // Setup hour parameter extractor - switch hourType { +func makeTimeIntegerGetter(vec *vector.Vector) (func(uint64) (int64, bool), bool) { + switch vec.GetType().Oid { case types.T_int8: - hourParam := vector.GenerateFunctionFixedTypeParameter[int8](ivecs[0]) - getHourValue = func(i uint64) (int64, bool) { - val, null := hourParam.GetValue(i) - return int64(val), null - } + return makeTimeSignedIntegerGetter[int8](vec), true case types.T_int16: - hourParam := vector.GenerateFunctionFixedTypeParameter[int16](ivecs[0]) - getHourValue = func(i uint64) (int64, bool) { - val, null := hourParam.GetValue(i) - return int64(val), null - } + return makeTimeSignedIntegerGetter[int16](vec), true case types.T_int32: - hourParam := vector.GenerateFunctionFixedTypeParameter[int32](ivecs[0]) - getHourValue = func(i uint64) (int64, bool) { - val, null := hourParam.GetValue(i) - return int64(val), null - } + return makeTimeSignedIntegerGetter[int32](vec), true case types.T_int64: - hourParam := vector.GenerateFunctionFixedTypeParameter[int64](ivecs[0]) - getHourValue = func(i uint64) (int64, bool) { - val, null := hourParam.GetValue(i) - return val, null + return makeTimeSignedIntegerGetter[int64](vec), true + case types.T_uint8: + return makeTimeUnsignedIntegerGetter[uint8](vec), true + case types.T_uint16: + return makeTimeUnsignedIntegerGetter[uint16](vec), true + case types.T_uint32: + return makeTimeUnsignedIntegerGetter[uint32](vec), true + case types.T_uint64: + return makeTimeUnsignedIntegerGetter[uint64](vec), true + default: + return nil, false + } +} + +func makeTimeBinaryInteger(value []byte) int64 { + for len(value) > 0 && value[0] == 0 { + value = value[1:] + } + if len(value) > 8 { + return math.MaxInt64 + } + + var result uint64 + for _, b := range value { + result = result<<8 | uint64(b) + } + if result > math.MaxInt64 { + return math.MaxInt64 + } + return int64(result) +} + +func makeTimeFloatGetter[T constraints.Float](vec *vector.Vector) func(uint64) (float64, bool) { + param := vector.GenerateFunctionFixedTypeParameter[T](vec) + return func(i uint64) (float64, bool) { + value, null := param.GetValue(i) + return float64(value), null + } +} + +func makeTimeExactInteger(value string) (int64, bool) { + exact, ok := new(big.Rat).SetString(value) + if !ok { + return 0, true + } + negative := exact.Sign() < 0 + numerator := new(big.Int).Abs(exact.Num()) + rounded, remainder := new(big.Int), new(big.Int) + rounded.QuoRem(numerator, exact.Denom(), remainder) + if new(big.Int).Lsh(remainder, 1).Cmp(exact.Denom()) >= 0 { + rounded.Add(rounded, big.NewInt(1)) + } + if negative { + rounded.Neg(rounded) + } + if rounded.IsInt64() { + return rounded.Int64(), false + } + if negative { + return math.MinInt64, false + } + return math.MaxInt64, false +} + +func makeTimeDecimal128IntegerGetter(vec *vector.Vector) func(uint64) (int64, bool) { + param := vector.GenerateFunctionFixedTypeParameter[types.Decimal128](vec) + scale := vec.GetType().Scale + return func(i uint64) (int64, bool) { + value, null := param.GetValue(i) + if null { + return 0, true } - case types.T_uint8, types.T_uint16, types.T_uint32, types.T_uint64: - hourParam := vector.GenerateFunctionFixedTypeParameter[uint64](ivecs[0]) - getHourValue = func(i uint64) (int64, bool) { - val, null := hourParam.GetValue(i) - return int64(val), null + return makeTimeExactInteger(value.Format(scale)) + } +} + +func makeTimeDecimal256IntegerGetter(vec *vector.Vector) func(uint64) (int64, bool) { + param := vector.GenerateFunctionFixedTypeParameter[types.Decimal256](vec) + scale := vec.GetType().Scale + return func(i uint64) (int64, bool) { + value, null := param.GetValue(i) + if null { + return 0, true } - case types.T_float32, types.T_float64: - hourParam := vector.GenerateFunctionFixedTypeParameter[float64](ivecs[0]) - getHourValue = func(i uint64) (int64, bool) { - val, null := hourParam.GetValue(i) - return int64(val), null // Truncate decimal part + return makeTimeExactInteger(value.Format(scale)) + } +} + +func makeTimeStringIntegerGetter(vec *vector.Vector) func(uint64) (int64, bool) { + param := vector.GenerateFunctionStrParameter(vec) + isBinary := vec.GetIsBin() + return func(i uint64) (int64, bool) { + value, null := param.GetStrValue(i) + if null { + return 0, true } - default: - return moerr.NewInvalidArgNoCtx("MAKETIME hour parameter", hourType) + if isBinary { + return makeTimeBinaryInteger(value), false + } + result, _ := parseLeadingInteger(strings.TrimSpace(functionUtil.QuickBytesToStr(value))) + return result, false } +} - // Setup minute parameter extractor - switch minuteType { - case types.T_int8: - minuteParam := vector.GenerateFunctionFixedTypeParameter[int8](ivecs[1]) - getMinuteValue = func(i uint64) (int64, bool) { - val, null := minuteParam.GetValue(i) - return int64(val), null +func makeTimeExactSecond(value string) (int64, uint32, bool) { + const maxExactSecondDigits = 4096 + const maxExactSecondExponent = maxExactSecondDigits + 7 + + value = strings.TrimSpace(value) + if len(value) == 0 { + return 0, 0, false + } + + end := 0 + negative := false + if value[end] == '+' || value[end] == '-' { + negative = value[end] == '-' + end++ + } + + totalDigits := 0 + firstNonzeroDigit := -1 + lastNonzeroDigit := -1 + integerStart := end + for end < len(value) && value[end] >= '0' && value[end] <= '9' { + if value[end] != '0' { + if firstNonzeroDigit == -1 { + firstNonzeroDigit = totalDigits + } + lastNonzeroDigit = totalDigits } - case types.T_int16: - minuteParam := vector.GenerateFunctionFixedTypeParameter[int16](ivecs[1]) - getMinuteValue = func(i uint64) (int64, bool) { - val, null := minuteParam.GetValue(i) - return int64(val), null + end++ + totalDigits++ + } + integerEnd := end + fractionStart := end + fractionEnd := end + if end < len(value) && value[end] == '.' { + end++ + fractionStart = end + for end < len(value) && value[end] >= '0' && value[end] <= '9' { + if value[end] != '0' { + if firstNonzeroDigit == -1 { + firstNonzeroDigit = totalDigits + } + lastNonzeroDigit = totalDigits + } + end++ + totalDigits++ + } + fractionEnd = end + } + if totalDigits == 0 { + return 0, 0, false + } + if firstNonzeroDigit == -1 { + return 0, 0, false + } + significantDigits := lastNonzeroDigit - firstNonzeroDigit + 1 + if significantDigits > maxExactSecondDigits { + return 0, 0, true + } + + exponent := 0 + if end < len(value) && (value[end] == 'e' || value[end] == 'E') { + exponentStart := end + end++ + negativeExponent := false + if end < len(value) && (value[end] == '+' || value[end] == '-') { + negativeExponent = value[end] == '-' + end++ + } + exponentDigits := end + exponentMagnitude := 0 + exponentLimit := maxExactSecondExponent + totalDigits + exponentOverflow := false + for end < len(value) && value[end] >= '0' && value[end] <= '9' { + digit := int(value[end] - '0') + if !exponentOverflow { + if exponentMagnitude > (exponentLimit-digit)/10 { + exponentOverflow = true + } else { + exponentMagnitude = exponentMagnitude*10 + digit + } + } + end++ } - case types.T_int32: - minuteParam := vector.GenerateFunctionFixedTypeParameter[int32](ivecs[1]) - getMinuteValue = func(i uint64) (int64, bool) { - val, null := minuteParam.GetValue(i) - return int64(val), null + if end == exponentDigits { + end = exponentStart + } else if exponentOverflow { + if negativeExponent { + return 0, 0, false + } + return 0, 0, true + } else if negativeExponent { + exponent = -exponentMagnitude + } else { + exponent = exponentMagnitude } - case types.T_int64: - minuteParam := vector.GenerateFunctionFixedTypeParameter[int64](ivecs[1]) - getMinuteValue = func(i uint64) (int64, bool) { - val, null := minuteParam.GetValue(i) - return val, null + } + + fractionDigits := fractionEnd - fractionStart + trailingZeroDigits := totalDigits - lastNonzeroDigit - 1 + exponent += trailingZeroDigits - fractionDigits + if exponent < -maxExactSecondExponent { + return 0, 0, false + } + if exponent > maxExactSecondExponent { + return 0, 0, true + } + + var normalized strings.Builder + normalized.Grow(significantDigits + 16) + if negative { + normalized.WriteByte('-') + } + digitIndex := 0 + appendSignificantDigits := func(part string) { + for i := range part { + if digitIndex >= firstNonzeroDigit && digitIndex <= lastNonzeroDigit { + normalized.WriteByte(part[i]) + } + digitIndex++ } - case types.T_uint8, types.T_uint16, types.T_uint32, types.T_uint64: - minuteParam := vector.GenerateFunctionFixedTypeParameter[uint64](ivecs[1]) - getMinuteValue = func(i uint64) (int64, bool) { - val, null := minuteParam.GetValue(i) - return int64(val), null + } + appendSignificantDigits(value[integerStart:integerEnd]) + appendSignificantDigits(value[fractionStart:fractionEnd]) + if exponent != 0 { + normalized.WriteByte('e') + normalized.WriteString(strconv.Itoa(exponent)) + } + + second, ok := new(big.Rat).SetString(normalized.String()) + if !ok || second.Sign() < 0 || second.Cmp(big.NewRat(60, 1)) >= 0 { + return 0, 0, true + } + + scaledNumerator := new(big.Int).Mul(second.Num(), big.NewInt(types.MicroSecsPerSec)) + totalMicroseconds, remainder := new(big.Int), new(big.Int) + totalMicroseconds.QuoRem(scaledNumerator, second.Denom(), remainder) + twiceRemainder := new(big.Int).Lsh(remainder, 1) + if twiceRemainder.Cmp(second.Denom()) >= 0 { + totalMicroseconds.Add(totalMicroseconds, big.NewInt(1)) + } + if !totalMicroseconds.IsInt64() { + return 0, 0, true + } + + total := totalMicroseconds.Int64() + return total / types.MicroSecsPerSec, uint32(total % types.MicroSecsPerSec), false +} + +func makeTimeStringSecondGetter(vec *vector.Vector) func(uint64) (int64, uint32, bool) { + param := vector.GenerateFunctionStrParameter(vec) + isBinary := vec.GetIsBin() + return func(i uint64) (int64, uint32, bool) { + value, null := param.GetStrValue(i) + if null { + return 0, 0, true } - case types.T_float32, types.T_float64: - minuteParam := vector.GenerateFunctionFixedTypeParameter[float64](ivecs[1]) - getMinuteValue = func(i uint64) (int64, bool) { - val, null := minuteParam.GetValue(i) - return int64(val), null // Truncate decimal part + if isBinary { + return makeTimeIntegerSecond(makeTimeBinaryInteger(value), false) } - default: - return moerr.NewInvalidArgNoCtx("MAKETIME minute parameter", minuteType) + return makeTimeExactSecond(functionUtil.QuickBytesToStr(value)) } +} - // Setup second parameter extractor - switch secondType { - case types.T_int8: - secondParam := vector.GenerateFunctionFixedTypeParameter[int8](ivecs[2]) - getSecondValue = func(i uint64) (int64, bool) { - val, null := secondParam.GetValue(i) - return int64(val), null +// MakeTime: MAKETIME(hour, minute, second) - Returns a time value calculated from the hour, minute, and second arguments. +func MakeTime(ivecs []*vector.Vector, result vector.FunctionResultWrapper, _ *process.Process, length int, selectList *FunctionSelectList) error { + rs := vector.MustFunctionResult[types.Time](result) + + var getHourValue func(uint64) (int64, bool) + var getMinuteValue func(uint64) (int64, bool) + var getSecondValue func(uint64) (int64, uint32, bool) + + if ivecs[0].GetType().Oid.IsMySQLString() { + getHourValue = makeTimeStringIntegerGetter(ivecs[0]) + } else if ivecs[0].GetType().Oid == types.T_decimal128 { + getHourValue = makeTimeDecimal128IntegerGetter(ivecs[0]) + } else if ivecs[0].GetType().Oid == types.T_decimal256 { + getHourValue = makeTimeDecimal256IntegerGetter(ivecs[0]) + } else if getter, ok := makeTimeIntegerGetter(ivecs[0]); ok { + getHourValue = getter + } else { + var getFloat func(uint64) (float64, bool) + switch ivecs[0].GetType().Oid { + case types.T_float32: + getFloat = makeTimeFloatGetter[float32](ivecs[0]) + case types.T_float64: + getFloat = makeTimeFloatGetter[float64](ivecs[0]) + default: + return moerr.NewInvalidArgNoCtx("MAKETIME hour parameter", ivecs[0].GetType().Oid) } - case types.T_int16: - secondParam := vector.GenerateFunctionFixedTypeParameter[int16](ivecs[2]) - getSecondValue = func(i uint64) (int64, bool) { - val, null := secondParam.GetValue(i) - return int64(val), null + getHourValue = func(i uint64) (int64, bool) { + value, null := getFloat(i) + if null || math.IsNaN(value) || math.IsInf(value, 0) { + return 0, true + } + rounded := math.Round(value) + if rounded >= float64(math.MaxInt64) { + return math.MaxInt64, false + } + if rounded <= float64(math.MinInt64) { + return math.MinInt64, false + } + return int64(rounded), false } - case types.T_int32: - secondParam := vector.GenerateFunctionFixedTypeParameter[int32](ivecs[2]) - getSecondValue = func(i uint64) (int64, bool) { - val, null := secondParam.GetValue(i) - return int64(val), null + } + + if ivecs[1].GetType().Oid.IsMySQLString() { + getMinuteValue = makeTimeStringIntegerGetter(ivecs[1]) + } else if ivecs[1].GetType().Oid == types.T_decimal128 { + getMinuteValue = makeTimeDecimal128IntegerGetter(ivecs[1]) + } else if ivecs[1].GetType().Oid == types.T_decimal256 { + getMinuteValue = makeTimeDecimal256IntegerGetter(ivecs[1]) + } else if getter, ok := makeTimeIntegerGetter(ivecs[1]); ok { + getMinuteValue = getter + } else { + var getFloat func(uint64) (float64, bool) + switch ivecs[1].GetType().Oid { + case types.T_float32: + getFloat = makeTimeFloatGetter[float32](ivecs[1]) + case types.T_float64: + getFloat = makeTimeFloatGetter[float64](ivecs[1]) + default: + return moerr.NewInvalidArgNoCtx("MAKETIME minute parameter", ivecs[1].GetType().Oid) } - case types.T_int64: - secondParam := vector.GenerateFunctionFixedTypeParameter[int64](ivecs[2]) - getSecondValue = func(i uint64) (int64, bool) { - val, null := secondParam.GetValue(i) - return val, null + getMinuteValue = func(i uint64) (int64, bool) { + value, null := getFloat(i) + if null || math.IsNaN(value) || math.IsInf(value, 0) { + return 0, true + } + rounded := math.Round(value) + if rounded < 0 || rounded >= 60 { + return 0, true + } + return int64(rounded), false } - case types.T_uint8, types.T_uint16, types.T_uint32, types.T_uint64: - secondParam := vector.GenerateFunctionFixedTypeParameter[uint64](ivecs[2]) - getSecondValue = func(i uint64) (int64, bool) { - val, null := secondParam.GetValue(i) - return int64(val), null + } + + if ivecs[2].GetType().Oid.IsMySQLString() { + getSecondValue = makeTimeStringSecondGetter(ivecs[2]) + } else if getter, ok := makeTimeIntegerGetter(ivecs[2]); ok { + getSecondValue = func(i uint64) (int64, uint32, bool) { + value, null := getter(i) + return makeTimeIntegerSecond(value, null) } - case types.T_float32, types.T_float64: - secondParam := vector.GenerateFunctionFixedTypeParameter[float64](ivecs[2]) - getSecondValue = func(i uint64) (int64, bool) { - val, null := secondParam.GetValue(i) - return int64(val), null // Truncate decimal part + } else { + var getFloat func(uint64) (float64, bool) + switch ivecs[2].GetType().Oid { + case types.T_float32: + getFloat = makeTimeFloatGetter[float32](ivecs[2]) + case types.T_float64: + getFloat = makeTimeFloatGetter[float64](ivecs[2]) + default: + return moerr.NewInvalidArgNoCtx("MAKETIME second parameter", ivecs[2].GetType().Oid) + } + getSecondValue = func(i uint64) (int64, uint32, bool) { + value, null := getFloat(i) + if null || math.IsNaN(value) || math.IsInf(value, 0) || value < 0 || value >= 60 { + return 0, 0, true + } + total := int64(math.Round(value * float64(types.MicroSecsPerSec))) + return total / types.MicroSecsPerSec, uint32(total % types.MicroSecsPerSec), false } - default: - return moerr.NewInvalidArgNoCtx("MAKETIME second parameter", secondType) } - // Process all rows for i := uint64(0); i < uint64(length); i++ { hourInt, null1 := getHourValue(i) minuteInt, null2 := getMinuteValue(i) - secondInt, null3 := getSecondValue(i) + secondInt, microsecond, null3 := getSecondValue(i) if null1 || null2 || null3 { if err := rs.Append(types.Time(0), true); err != nil { @@ -7452,7 +7716,7 @@ func MakeTime(ivecs []*vector.Vector, result vector.FunctionResultWrapper, _ *pr continue } - if err := makeTimeFromInt64(hourInt, minuteInt, secondInt, rs, i); err != nil { + if err := makeTimeFromInt64(hourInt, minuteInt, secondInt, microsecond, rs); err != nil { return err } } diff --git a/pkg/sql/plan/function/func_binary_test.go b/pkg/sql/plan/function/func_binary_test.go index 5043f95639b40..0471c9beaf21f 100644 --- a/pkg/sql/plan/function/func_binary_test.go +++ b/pkg/sql/plan/function/func_binary_test.go @@ -9399,6 +9399,8 @@ func initTimeFormatTestCase() []tcTemp { t2, _ := types.ParseTime("00:00:00", 6) t3, _ := types.ParseTime("23:59:59.123456", 6) t4, _ := types.ParseTime("12:34:56.789012", 6) + t5, _ := types.ParseTime("123:45:06", 6) + t6, _ := types.ParseTime("-123:45:06", 6) return []tcTemp{ { @@ -9417,14 +9419,38 @@ func initTimeFormatTestCase() []tcTemp { info: "test time_format - %T", inputs: []FunctionTestInput{ NewFunctionTestInput(types.T_time.ToType(), - []types.Time{t1}, - []bool{false}), + []types.Time{t1, t5, t6}, + []bool{false, false, false}), NewFunctionTestConstInput(types.T_varchar.ToType(), []string{"%T"}, []bool{false}), }, expect: NewFunctionTestResult(types.T_varchar.ToType(), false, - []string{"15:30:45"}, + []string{"15:30:45", "123:45:06", "-123:45:06"}, + []bool{false, false, false}), + }, + { + info: "test time_format - negative time prefixes complete result", + inputs: []FunctionTestInput{ + NewFunctionTestInput(types.T_time.ToType(), + []types.Time{t6}, + []bool{false}), + NewFunctionTestConstInput(types.T_varchar.ToType(), []string{"elapsed=%H:%i:%s"}, []bool{false}), + }, + expect: NewFunctionTestResult(types.T_varchar.ToType(), false, + []string{"-elapsed=123:45:06"}, []bool{false}), }, + { + info: "test time_format - empty format returns null", + inputs: []FunctionTestInput{ + NewFunctionTestInput(types.T_time.ToType(), + []types.Time{t1, t6}, + []bool{false, false}), + NewFunctionTestConstInput(types.T_varchar.ToType(), []string{""}, []bool{false}), + }, + expect: NewFunctionTestResult(types.T_varchar.ToType(), false, + []string{"", ""}, + []bool{true, true}), + }, { info: "test time_format - %h:%i:%s %p", inputs: []FunctionTestInput{ @@ -9487,6 +9513,362 @@ func TestTimeFormat(t *testing.T) { } } +func TestMakeTimeFractionAndSign(t *testing.T) { + proc := testutil.NewProcess(t) + floatWithMicrosecondScale := types.T_float64.ToTypeWithScale(6) + fcTC := NewFunctionTestCase(proc, + []FunctionTestInput{ + NewFunctionTestInput(floatWithMicrosecondScale, + []float64{12, -12, 12, 838, -838, 839, -839, 12, 12, 12, math.MaxFloat64, -math.MaxFloat64, 838.9, -838.9, math.NaN(), math.Inf(1), math.Inf(-1), 0}, + []bool{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true}), + NewFunctionTestInput(floatWithMicrosecondScale, + []float64{34, 34, 59, 59, 59, 0, 0, 60, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0}, + []bool{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}), + NewFunctionTestInput(floatWithMicrosecondScale, + []float64{56.789012, 56.789012, 59.9999996, 59.9999996, 59.9999996, 0, 0, 0, math.NaN(), math.Inf(1), 0, 0, 0, 0, 0, 0, 0, 0}, + []bool{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}), + }, + NewFunctionTestResult(types.T_time.ToTypeWithScale(6), false, + []types.Time{ + types.TimeFromClock(false, 12, 34, 56, 789012), + types.TimeFromClock(true, 12, 34, 56, 789012), + types.TimeFromClock(false, 13, 0, 0, 0), + types.TimeFromClock(false, 838, 59, 59, 0), + types.TimeFromClock(true, 838, 59, 59, 0), + types.TimeFromClock(false, 838, 59, 59, 0), + types.TimeFromClock(true, 838, 59, 59, 0), + 0, + 0, + 0, + types.TimeFromClock(false, 838, 59, 59, 0), + types.TimeFromClock(true, 838, 59, 59, 0), + types.TimeFromClock(false, 838, 59, 59, 0), + types.TimeFromClock(true, 838, 59, 59, 0), + 0, + 0, + 0, + 0, + }, + []bool{false, false, false, false, false, false, false, true, true, true, false, false, false, false, true, true, true, true}), + MakeTime) + + s, info := fcTC.Run() + require.True(t, s, "MAKETIME fractional/sign case failed: %s", info) +} + +func TestMakeTimeUnsignedHourOverflow(t *testing.T) { + proc := testutil.NewProcess(t) + fcTC := NewFunctionTestCase(proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_uint64.ToType(), + []uint64{838, math.MaxUint64}, + []bool{false, false}), + NewFunctionTestInput(types.T_uint64.ToType(), + []uint64{34, 34}, + []bool{false, false}), + NewFunctionTestInput(types.T_uint64.ToType(), + []uint64{56, 56}, + []bool{false, false}), + }, + NewFunctionTestResult(types.T_time.ToType(), false, + []types.Time{ + types.TimeFromClock(false, 838, 34, 56, 0), + types.TimeFromClock(false, 838, 59, 59, 0), + }, + []bool{false, false}), + MakeTime) + + s, info := fcTC.Run() + require.True(t, s, "MAKETIME unsigned hour overflow case failed: %s", info) +} + +func TestMakeTimeBinaryIntegerBoundaries(t *testing.T) { + tests := []struct { + name string + value []byte + want int64 + }{ + {name: "empty", value: nil, want: 0}, + {name: "zero", value: []byte{0}, want: 0}, + {name: "max int64", value: []byte{0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, want: math.MaxInt64}, + {name: "max int64 plus one", value: []byte{0x80, 0, 0, 0, 0, 0, 0, 0}, want: math.MaxInt64}, + {name: "max uint64", value: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, want: math.MaxInt64}, + {name: "wider than uint64", value: []byte{1, 0, 0, 0, 0, 0, 0, 0, 0}, want: math.MaxInt64}, + {name: "wide leading zeros", value: []byte{0, 0, 0, 0, 0, 0, 0, 0, 1}, want: 1}, + {name: "wide leading zero max int64", value: []byte{0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, want: math.MaxInt64}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.want, makeTimeBinaryInteger(test.value)) + }) + } +} + +func TestMakeTimeSignedHourOverflow(t *testing.T) { + proc := testutil.NewProcess(t) + fcTC := NewFunctionTestCase(proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_int64.ToType(), + []int64{math.MaxInt64, math.MinInt64}, + []bool{false, false}), + NewFunctionTestInput(types.T_int64.ToType(), + []int64{0, 0}, + []bool{false, false}), + NewFunctionTestInput(types.T_int64.ToType(), + []int64{0, 0}, + []bool{false, false}), + }, + NewFunctionTestResult(types.T_time.ToType(), false, + []types.Time{ + types.TimeFromClock(false, 838, 59, 59, 0), + types.TimeFromClock(true, 838, 59, 59, 0), + }, + []bool{false, false}), + MakeTime) + + s, info := fcTC.Run() + require.True(t, s, "MAKETIME signed hour overflow case failed: %s", info) +} + +func TestMakeTimeUint32Overload(t *testing.T) { + proc := testutil.NewProcess(t) + fcTC := NewFunctionTestCase(proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_uint32.ToType(), []uint32{12}, []bool{false}), + NewFunctionTestInput(types.T_uint32.ToType(), []uint32{34}, []bool{false}), + NewFunctionTestInput(types.T_uint32.ToType(), []uint32{56}, []bool{false}), + }, + NewFunctionTestResult(types.T_time.ToType(), false, + []types.Time{types.TimeFromClock(false, 12, 34, 56, 0)}, + []bool{false}), + MakeTime) + + s, info := fcTC.Run() + require.True(t, s, "MAKETIME uint32 overload failed: %s", info) +} + +func TestMakeTimeFloatHourRounding(t *testing.T) { + proc := testutil.NewProcess(t) + fcTC := NewFunctionTestCase(proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_float64.ToType(), + []float64{12.7, 12.5, 13.5, -12.5, -13.5, 838.9}, + []bool{false, false, false, false, false, false}), + NewFunctionTestInput(types.T_int64.ToType(), + []int64{0, 0, 0, 0, 0, 0}, + []bool{false, false, false, false, false, false}), + NewFunctionTestInput(types.T_int64.ToType(), + []int64{0, 0, 0, 0, 0, 0}, + []bool{false, false, false, false, false, false}), + }, + NewFunctionTestResult(types.T_time.ToType(), false, + []types.Time{ + types.TimeFromClock(false, 13, 0, 0, 0), + types.TimeFromClock(false, 13, 0, 0, 0), + types.TimeFromClock(false, 14, 0, 0, 0), + types.TimeFromClock(true, 13, 0, 0, 0), + types.TimeFromClock(true, 14, 0, 0, 0), + types.TimeFromClock(false, 838, 59, 59, 0), + }, + []bool{false, false, false, false, false, false}), + MakeTime) + + s, info := fcTC.Run() + require.True(t, s, "MAKETIME float hour rounding failed: %s", info) +} + +func TestMakeTimeFloatMinuteRange(t *testing.T) { + proc := testutil.NewProcess(t) + fcTC := NewFunctionTestCase(proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_int64.ToType(), + []int64{12, 12, 12, 12, 12, 12, 12, 12, 12}, + []bool{false, false, false, false, false, false, false, false, false}), + NewFunctionTestInput(types.T_float64.ToType(), + []float64{15.8, 58.5, 59.5, 59.9, -0.5, -0.9, math.NaN(), math.Inf(1), math.Inf(-1)}, + []bool{false, false, false, false, false, false, false, false, false}), + NewFunctionTestInput(types.T_int64.ToType(), + []int64{0, 0, 0, 0, 0, 0, 0, 0, 0}, + []bool{false, false, false, false, false, false, false, false, false}), + }, + NewFunctionTestResult(types.T_time.ToType(), false, + []types.Time{ + types.TimeFromClock(false, 12, 16, 0, 0), + types.TimeFromClock(false, 12, 59, 0, 0), + 0, + 0, + 0, + 0, + 0, + 0, + 0, + }, + []bool{false, false, true, true, true, true, true, true, true}), + MakeTime) + + s, info := fcTC.Run() + require.True(t, s, "MAKETIME float minute range failed: %s", info) +} + +func TestMakeTimeExactStringSecondRounding(t *testing.T) { + proc := testutil.NewProcess(t) + fcTC := NewFunctionTestCase(proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_int64.ToType(), []int64{12, 12, 12, 12, 12, 12}, []bool{false, false, false, false, false, false}), + NewFunctionTestInput(types.T_int64.ToType(), []int64{59, 59, 59, 0, 34, 34}, []bool{false, false, false, false, false, false}), + NewFunctionTestInput(types.T_varchar.ToType(), []string{ + "59.99999949999999999", + "59.9999995", + "59.99999950000000001", + "5.9e1", + "", + "foo", + }, []bool{false, false, false, false, false, false}), + }, + NewFunctionTestResult(types.T_time.ToTypeWithScale(6), false, + []types.Time{ + types.TimeFromClock(false, 12, 59, 59, 999999), + types.TimeFromClock(false, 13, 0, 0, 0), + types.TimeFromClock(false, 13, 0, 0, 0), + types.TimeFromClock(false, 12, 0, 59, 0), + types.TimeFromClock(false, 12, 34, 0, 0), + types.TimeFromClock(false, 12, 34, 0, 0), + }, + []bool{false, false, false, false, false, false}), + MakeTime) + + s, info := fcTC.Run() + require.True(t, s, "MAKETIME exact string-second rounding failed: %s", info) + + _, _, null := makeTimeExactSecond("1e999999999") + require.True(t, null, "MAKETIME must reject an unbounded exponent without allocating it") + + second, microsecond, null := makeTimeExactSecond("1e-5000") + require.False(t, null) + require.Zero(t, second) + require.Zero(t, microsecond) + + second, microsecond, null = makeTimeExactSecond("0e5000") + require.False(t, null) + require.Zero(t, second) + require.Zero(t, microsecond) + + second, microsecond, null = makeTimeExactSecond(strings.Repeat("0", 4097)) + require.False(t, null) + require.Zero(t, second) + require.Zero(t, microsecond) + + for _, test := range []struct { + name string + value string + }{ + {name: "wide leading zeroes", value: strings.Repeat("0", 4096) + "1"}, + {name: "wide trailing fractional zeroes", value: "1." + strings.Repeat("0", 4097)}, + {name: "wide fractional leading zeroes canceled by exponent", value: "0." + strings.Repeat("0", 4096) + "1e4097"}, + {name: "wide integer trailing zeroes canceled by exponent", value: "1" + strings.Repeat("0", 4096) + "e-4096"}, + } { + second, microsecond, null = makeTimeExactSecond(test.value) + require.False(t, null, test.name) + require.Equal(t, int64(1), second, test.name) + require.Zero(t, microsecond, test.name) + } + + for _, value := range []string{"1e-4103", "1e-4104"} { + second, microsecond, null = makeTimeExactSecond(value) + require.False(t, null, value) + require.Zero(t, second, value) + require.Zero(t, microsecond, value) + } +} + +func TestMakeTimeStringHourMinuteSemantics(t *testing.T) { + proc := testutil.NewProcess(t) + + tests := []struct { + name string + inputs []FunctionTestInput + expect FunctionTestResult + }{ + { + name: "string hour and minute truncate while fractional second is preserved", + inputs: []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{"12.7"}, []bool{false}), + NewFunctionTestInput(types.T_varchar.ToType(), []string{"15.8"}, []bool{false}), + NewFunctionTestInput(types.T_float64.ToTypeWithScale(6), []float64{56.789012}, []bool{false}), + }, + expect: NewFunctionTestResult(types.T_time.ToTypeWithScale(6), false, + []types.Time{types.TimeFromClock(false, 12, 15, 56, 789012)}, []bool{false}), + }, + { + name: "only string hour truncates", + inputs: []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{"12.7"}, []bool{false}), + NewFunctionTestInput(types.T_float64.ToTypeWithScale(1), []float64{15.8}, []bool{false}), + NewFunctionTestInput(types.T_float64.ToTypeWithScale(1), []float64{30.9}, []bool{false}), + }, + expect: NewFunctionTestResult(types.T_time.ToTypeWithScale(1), false, + []types.Time{types.TimeFromClock(false, 12, 16, 30, 900000)}, []bool{false}), + }, + { + name: "only string minute truncates", + inputs: []FunctionTestInput{ + NewFunctionTestInput(types.T_float64.ToTypeWithScale(1), []float64{12.7}, []bool{false}), + NewFunctionTestInput(types.T_varchar.ToType(), []string{"15.8"}, []bool{false}), + NewFunctionTestInput(types.T_float64.ToTypeWithScale(1), []float64{30.9}, []bool{false}), + }, + expect: NewFunctionTestResult(types.T_time.ToTypeWithScale(1), false, + []types.Time{types.TimeFromClock(false, 13, 15, 30, 900000)}, []bool{false}), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fcTC := NewFunctionTestCase(proc, test.inputs, test.expect, MakeTime) + s, info := fcTC.Run() + require.True(t, s, "MAKETIME string source semantics failed: %s", info) + }) + } +} + +func TestMakeTimeIntegerSecondRange(t *testing.T) { + proc := testutil.NewProcess(t) + expected := NewFunctionTestResult(types.T_time.ToType(), false, + []types.Time{ + types.TimeFromClock(false, 12, 34, 59, 0), + 0, + }, + []bool{false, true}) + + t.Run("signed", func(t *testing.T) { + fcTC := NewFunctionTestCase(proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_int64.ToType(), []int64{12, 12}, []bool{false, false}), + NewFunctionTestInput(types.T_int64.ToType(), []int64{34, 34}, []bool{false, false}), + NewFunctionTestInput(types.T_int64.ToType(), []int64{59, 60}, []bool{false, false}), + }, + expected, + MakeTime) + + s, info := fcTC.Run() + require.True(t, s, "MAKETIME signed integer second range failed: %s", info) + }) + + t.Run("unsigned", func(t *testing.T) { + fcTC := NewFunctionTestCase(proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_uint64.ToType(), []uint64{12, 12}, []bool{false, false}), + NewFunctionTestInput(types.T_uint64.ToType(), []uint64{34, 34}, []bool{false, false}), + NewFunctionTestInput(types.T_uint64.ToType(), []uint64{59, 60}, []bool{false, false}), + }, + expected, + MakeTime) + + s, info := fcTC.Run() + require.True(t, s, "MAKETIME unsigned integer second range failed: %s", info) + }) +} + // TestTimestampDiffDateString tests TIMESTAMPDIFF with DATE and string arguments // This tests the new overload that handles mixed DATE and string types func TestTimestampDiffDateString(t *testing.T) { diff --git a/pkg/sql/plan/function/func_unary.go b/pkg/sql/plan/function/func_unary.go index 88cdded0fc352..fd24a43b7d1cf 100644 --- a/pkg/sql/plan/function/func_unary.go +++ b/pkg/sql/plan/function/func_unary.go @@ -4545,10 +4545,9 @@ func DatetimeToHour(ivecs []*vector.Vector, result vector.FunctionResultWrapper, } func TimeToHour(ivecs []*vector.Vector, result vector.FunctionResultWrapper, proc *process.Process, length int, selectList *FunctionSelectList) error { - return opUnaryFixedToFixed[types.Time, uint8](ivecs, result, proc, length, func(v types.Time) uint8 { + return opUnaryFixedToFixed[types.Time, uint32](ivecs, result, proc, length, func(v types.Time) uint32 { hour, _, _, _, _ := v.ClockFormat() - // HOUR function returns 0-23, so we need to take modulo 24 - return uint8(hour % 24) + return uint32(hour) }, selectList) } diff --git a/pkg/sql/plan/function/func_unary_test.go b/pkg/sql/plan/function/func_unary_test.go index 211eda10e5d2c..ada0ed349f758 100644 --- a/pkg/sql/plan/function/func_unary_test.go +++ b/pkg/sql/plan/function/func_unary_test.go @@ -3859,6 +3859,9 @@ func initHourTestCase() []tcTemp { t1, _ := types.ParseTime("15:30:45", 6) t2, _ := types.ParseTime("00:00:00", 6) t3, _ := types.ParseTime("23:59:59", 6) + t4, _ := types.ParseTime("272:59:59", 6) + t5, _ := types.ParseTime("-272:59:59", 6) + t6 := types.TimeFromClock(false, types.MaxHourInTime, 59, 59, 0) return []tcTemp{ { @@ -3890,12 +3893,12 @@ func initHourTestCase() []tcTemp { typ: types.T_time, inputs: []FunctionTestInput{ NewFunctionTestInput(types.T_time.ToType(), - []types.Time{t1, t2, t3}, - []bool{false, false, false}), + []types.Time{t1, t2, t3, t4, t5, t6}, + []bool{false, false, false, false, false, false}), }, - expect: NewFunctionTestResult(types.T_uint8.ToType(), false, - []uint8{15, 0, 23}, - []bool{false, false, false}), + expect: NewFunctionTestResult(types.T_uint32.ToType(), false, + []uint32{15, 0, 23, 272, 272, uint32(types.MaxHourInTime)}, + []bool{false, false, false, false, false, false}), }, } } diff --git a/pkg/sql/plan/function/function_test.go b/pkg/sql/plan/function/function_test.go index 137363baee5c3..e2e77a489b845 100644 --- a/pkg/sql/plan/function/function_test.go +++ b/pkg/sql/plan/function/function_test.go @@ -354,6 +354,214 @@ func Test_GetFunctionByName(t *testing.T) { } } +func TestMakeTimeReturnScale(t *testing.T) { + proc := testutil.NewProcess(t) + + integerResult, err := GetFunctionByName(proc.Ctx, "maketime", []types.Type{ + types.T_int64.ToType(), + types.T_int64.ToType(), + types.T_int64.ToType(), + }) + require.NoError(t, err) + require.Equal(t, types.T_time.ToType(), integerResult.retType) + + fractionalResult, err := GetFunctionByName(proc.Ctx, "maketime", []types.Type{ + types.T_int64.ToType(), + types.T_int64.ToType(), + types.New(types.T_decimal128, 20, 6), + }) + require.NoError(t, err) + require.True(t, fractionalResult.needCast) + require.Equal(t, types.T_varchar, fractionalResult.targetTypes[2].Oid) + require.Equal(t, int32(6), fractionalResult.targetTypes[2].Scale) + require.Equal(t, types.T_time.ToTypeWithScale(6), fractionalResult.retType) + + defaultFloatResult, err := GetFunctionByName(proc.Ctx, "maketime", []types.Type{ + types.T_int64.ToType(), + types.T_int64.ToType(), + {Oid: types.T_float64, Size: 8, Scale: -1}, + }) + require.NoError(t, err) + require.Equal(t, types.T_time.ToTypeWithScale(6), defaultFloatResult.retType) +} + +func TestMakeTimeDecimalHourMinuteUseExactOverloads(t *testing.T) { + proc := testutil.NewProcess(t) + decimalType := types.New(types.T_decimal128, 30, 20) + decimal256Type := types.New(types.T_decimal256, 65, 30) + + tests := []struct { + inputs []types.Type + args []types.T + }{ + {[]types.Type{decimalType, types.T_int64.ToType(), types.T_int64.ToType()}, []types.T{types.T_decimal128, types.T_float64, types.T_float64}}, + {[]types.Type{decimalType, types.T_varchar.ToType(), types.T_int64.ToType()}, []types.T{types.T_decimal128, types.T_varchar, types.T_float64}}, + {[]types.Type{decimalType, decimalType, types.T_int64.ToType()}, []types.T{types.T_decimal128, types.T_decimal128, types.T_float64}}, + {[]types.Type{decimalType, types.T_int64.ToType(), types.T_varchar.ToType()}, []types.T{types.T_decimal128, types.T_float64, types.T_varchar}}, + {[]types.Type{decimalType, types.T_varchar.ToType(), types.T_varchar.ToType()}, []types.T{types.T_decimal128, types.T_varchar, types.T_varchar}}, + {[]types.Type{types.T_int64.ToType(), decimalType, types.T_int64.ToType()}, []types.T{types.T_float64, types.T_decimal128, types.T_float64}}, + {[]types.Type{types.T_varchar.ToType(), decimalType, types.T_int64.ToType()}, []types.T{types.T_varchar, types.T_decimal128, types.T_float64}}, + {[]types.Type{types.T_int64.ToType(), decimalType, types.T_varchar.ToType()}, []types.T{types.T_float64, types.T_decimal128, types.T_varchar}}, + {[]types.Type{types.T_varchar.ToType(), decimalType, types.T_varchar.ToType()}, []types.T{types.T_varchar, types.T_decimal128, types.T_varchar}}, + {[]types.Type{decimalType, decimalType, types.New(types.T_decimal128, 20, 6)}, []types.T{types.T_decimal128, types.T_decimal128, types.T_varchar}}, + {[]types.Type{decimal256Type, types.T_int64.ToType(), types.T_int64.ToType()}, []types.T{types.T_decimal256, types.T_float64, types.T_float64}}, + {[]types.Type{types.T_int64.ToType(), decimal256Type, types.T_int64.ToType()}, []types.T{types.T_float64, types.T_decimal256, types.T_float64}}, + {[]types.Type{decimal256Type, decimalType, types.T_varchar.ToType()}, []types.T{types.T_decimal256, types.T_decimal128, types.T_varchar}}, + {[]types.Type{decimalType, decimal256Type, types.T_varchar.ToType()}, []types.T{types.T_decimal128, types.T_decimal256, types.T_varchar}}, + {[]types.Type{decimal256Type, decimal256Type, types.T_varchar.ToType()}, []types.T{types.T_decimal256, types.T_decimal256, types.T_varchar}}, + } + + for _, test := range tests { + result, err := GetFunctionByName(proc.Ctx, "maketime", test.inputs) + require.NoError(t, err) + require.True(t, result.needCast) + selected, err := GetFunctionById(proc.Ctx, result.GetEncodedOverloadID()) + require.NoError(t, err) + require.Equal(t, test.args, selected.args) + } +} + +func TestMakeTimeDecimal256OverloadMatrix(t *testing.T) { + proc := testutil.NewProcess(t) + decimal128Type := types.New(types.T_decimal128, 30, 20) + decimal256Type := types.New(types.T_decimal256, 65, 30) + type typeChoice struct { + input types.Type + target types.T + } + hourMinuteChoices := []typeChoice{ + {types.T_int64.ToType(), types.T_float64}, + {types.T_varchar.ToType(), types.T_varchar}, + {decimal128Type, types.T_decimal128}, + {decimal256Type, types.T_decimal256}, + } + secondChoices := []typeChoice{ + {types.T_int64.ToType(), types.T_float64}, + {types.T_varchar.ToType(), types.T_varchar}, + } + + for _, hour := range hourMinuteChoices { + for _, minute := range hourMinuteChoices { + if hour.target != types.T_decimal256 && minute.target != types.T_decimal256 { + continue + } + for _, second := range secondChoices { + result, err := GetFunctionByName(proc.Ctx, "maketime", []types.Type{hour.input, minute.input, second.input}) + require.NoError(t, err) + selected, err := GetFunctionById(proc.Ctx, result.GetEncodedOverloadID()) + require.NoError(t, err) + require.Equal(t, []types.T{hour.target, minute.target, second.target}, selected.args) + } + } + } +} + +func TestMakeTimeStringSecondUsesExactOverload(t *testing.T) { + proc := testutil.NewProcess(t) + + result, err := GetFunctionByName(proc.Ctx, "maketime", []types.Type{ + types.T_int64.ToType(), + types.T_int64.ToType(), + types.T_varchar.ToType(), + }) + require.NoError(t, err) + require.True(t, result.needCast) + require.Len(t, result.targetTypes, 3) + require.Equal(t, types.T_float64, result.targetTypes[0].Oid) + require.Equal(t, types.T_float64, result.targetTypes[1].Oid) + require.Equal(t, types.T_varchar, result.targetTypes[2].Oid) + require.Equal(t, int32(-1), result.targetTypes[2].Scale) + require.Equal(t, types.T_time.ToTypeWithScale(6), result.retType) +} + +func TestMakeTimeStringArgumentTargets(t *testing.T) { + proc := testutil.NewProcess(t) + defaultFloat := types.T_float64.ToType() + defaultFloat.Scale = -1 + scaledFloat := types.T_float64.ToTypeWithScale(1) + + tests := []struct { + name string + inputs []types.Type + overloadArgs []types.T + needCast bool + targets []types.Type + returnType types.Type + }{ + { + name: "varchar hour and minute with double second", + inputs: []types.Type{ + types.T_varchar.ToType(), types.T_varchar.ToType(), defaultFloat, + }, + overloadArgs: []types.T{types.T_varchar, types.T_varchar, types.T_float64}, + returnType: types.T_time.ToTypeWithScale(6), + }, + { + name: "all varchar", + inputs: []types.Type{ + types.T_varchar.ToType(), types.T_varchar.ToType(), types.T_varchar.ToType(), + }, + overloadArgs: []types.T{types.T_varchar, types.T_varchar, types.T_varchar}, + needCast: true, + targets: []types.Type{ + types.T_varchar.ToType(), types.T_varchar.ToType(), types.T_varchar.ToTypeWithScale(-1), + }, + returnType: types.T_time.ToTypeWithScale(6), + }, + { + name: "only hour is varchar", + inputs: []types.Type{ + types.T_varchar.ToType(), scaledFloat, scaledFloat, + }, + overloadArgs: []types.T{types.T_varchar, types.T_float64, types.T_float64}, + returnType: types.T_time.ToTypeWithScale(1), + }, + { + name: "only minute is varchar", + inputs: []types.Type{ + scaledFloat, types.T_varchar.ToType(), scaledFloat, + }, + overloadArgs: []types.T{types.T_float64, types.T_varchar, types.T_float64}, + returnType: types.T_time.ToTypeWithScale(1), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := GetFunctionByName(proc.Ctx, "maketime", test.inputs) + require.NoError(t, err) + require.Equal(t, test.needCast, result.needCast) + require.Equal(t, test.targets, result.targetTypes) + require.Equal(t, test.returnType, result.retType) + + selected, err := GetFunctionById(proc.Ctx, result.GetEncodedOverloadID()) + require.NoError(t, err) + require.Equal(t, test.overloadArgs, selected.args) + }) + } +} + +func TestMakeTimeBinaryArgumentsUseNumericOverloads(t *testing.T) { + proc := testutil.NewProcess(t) + binaryTypes := []types.T{types.T_binary, types.T_varbinary, types.T_blob} + + for _, binaryType := range binaryTypes { + for position := range 3 { + inputs := []types.Type{ + types.T_int64.ToType(), + types.T_int64.ToType(), + types.T_int64.ToType(), + } + inputs[position] = binaryType.ToType() + + result, err := GetFunctionByName(proc.Ctx, "maketime", inputs) + require.NoError(t, err) + require.True(t, result.needCast) + require.Equal(t, types.T_int64, result.targetTypes[position].Oid) + } + } +} + func TestGetFunctionByNameAESDecryptReturnsBlob(t *testing.T) { proc := testutil.NewProcess(t) tests := []struct { diff --git a/pkg/sql/plan/function/list_builtIn.go b/pkg/sql/plan/function/list_builtIn.go index 805266c3024c1..bd99b14a70a2b 100644 --- a/pkg/sql/plan/function/list_builtIn.go +++ b/pkg/sql/plan/function/list_builtIn.go @@ -9154,7 +9154,7 @@ var supportedDateAndTimeBuiltIns = []FuncNew{ overloadId: 2, args: []types.T{types.T_time}, retType: func(parameters []types.Type) types.Type { - return types.T_uint8.ToType() + return types.T_uint32.ToType() }, newOp: func() executeLogicOfOverload { return TimeToHour @@ -10777,6 +10777,93 @@ var supportedDateAndTimeBuiltIns = []FuncNew{ }, } +func makeTimeReturnType(parameters []types.Type) types.Type { + scale := parameters[2].Scale + if scale < 0 { + scale = 6 + } else if scale > 6 { + scale = 6 + } + return types.T_time.ToTypeWithScale(scale) +} + +func isMakeTimeTextType(oid types.T) bool { + // Binary inputs must take the numeric cast path so hex/bit literal byte + // semantics are consumed before function-expression evaluation clears IsBin. + switch oid { + case types.T_binary, types.T_varbinary, types.T_blob: + return false + default: + return oid.IsMySQLString() + } +} + +func makeTimeCheck(overloads []overload, inputs []types.Type) checkResult { + if len(inputs) != 3 { + return newCheckResultWithFailure(failedFunctionParametersWrong) + } + exactSecond := isMakeTimeTextType(inputs[2].Oid) || inputs[2].Oid.IsDecimal() + exactHour := inputs[0].Oid.IsDecimal() + exactMinute := inputs[1].Oid.IsDecimal() + if !isMakeTimeTextType(inputs[0].Oid) && !isMakeTimeTextType(inputs[1].Oid) && !exactHour && !exactMinute && !exactSecond { + return fixedTypeMatch(overloads, inputs) + } + + targetOids := []types.T{types.T_float64, types.T_float64, types.T_float64} + if isMakeTimeTextType(inputs[0].Oid) { + targetOids[0] = types.T_varchar + } else if exactHour { + if inputs[0].Oid == types.T_decimal256 { + targetOids[0] = types.T_decimal256 + } else { + targetOids[0] = types.T_decimal128 + } + } + if isMakeTimeTextType(inputs[1].Oid) { + targetOids[1] = types.T_varchar + } else if exactMinute { + if inputs[1].Oid == types.T_decimal256 { + targetOids[1] = types.T_decimal256 + } else { + targetOids[1] = types.T_decimal128 + } + } + if exactSecond { + targetOids[2] = types.T_varchar + } + status, _ := tryToMatch(inputs, targetOids) + if status == matchFailed { + return fixedTypeMatch(overloads, inputs) + } + + for i, ov := range overloads { + if len(ov.args) != len(targetOids) || ov.args[0] != targetOids[0] || ov.args[1] != targetOids[1] || ov.args[2] != targetOids[2] { + continue + } + if status == matchDirectly && !exactSecond { + return newCheckResultWithSuccess(i) + } + targets := make([]types.Type, len(inputs)) + for j := range targets { + if inputs[j].Oid == targetOids[j] { + targets[j] = inputs[j] + } else { + targets[j] = targetOids[j].ToType() + SetTargetScaleFromSource(&inputs[j], &targets[j]) + } + } + if exactSecond { + if inputs[2].Oid.IsDecimal() { + targets[2].Scale = inputs[2].Scale + } else { + targets[2].Scale = -1 + } + } + return newCheckResultWithCast(i, targets) + } + return newCheckResultWithFailure(failedFunctionParametersWrong) +} + var supportedControlBuiltIns = []FuncNew{ // function `add_fault_point` { @@ -11227,14 +11314,12 @@ var supportedControlBuiltIns = []FuncNew{ functionId: MAKETIME, class: plan.Function_STRICT, layout: STANDARD_FUNCTION, - checkFn: fixedTypeMatch, + checkFn: makeTimeCheck, Overloads: []overload{ { overloadId: 0, args: []types.T{types.T_int64, types.T_int64, types.T_int64}, - retType: func(parameters []types.Type) types.Type { - return types.T_time.ToType() - }, + retType: makeTimeReturnType, newOp: func() executeLogicOfOverload { return MakeTime }, @@ -11242,9 +11327,7 @@ var supportedControlBuiltIns = []FuncNew{ { overloadId: 1, args: []types.T{types.T_uint64, types.T_uint64, types.T_uint64}, - retType: func(parameters []types.Type) types.Type { - return types.T_time.ToType() - }, + retType: makeTimeReturnType, newOp: func() executeLogicOfOverload { return MakeTime }, @@ -11252,9 +11335,7 @@ var supportedControlBuiltIns = []FuncNew{ { overloadId: 2, args: []types.T{types.T_float64, types.T_float64, types.T_float64}, - retType: func(parameters []types.Type) types.Type { - return types.T_time.ToType() - }, + retType: makeTimeReturnType, newOp: func() executeLogicOfOverload { return MakeTime }, @@ -11262,9 +11343,7 @@ var supportedControlBuiltIns = []FuncNew{ { overloadId: 3, args: []types.T{types.T_int32, types.T_int32, types.T_int32}, - retType: func(parameters []types.Type) types.Type { - return types.T_time.ToType() - }, + retType: makeTimeReturnType, newOp: func() executeLogicOfOverload { return MakeTime }, @@ -11272,13 +11351,211 @@ var supportedControlBuiltIns = []FuncNew{ { overloadId: 4, args: []types.T{types.T_uint32, types.T_uint32, types.T_uint32}, - retType: func(parameters []types.Type) types.Type { - return types.T_time.ToType() + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { + return MakeTime }, + }, + { + overloadId: 5, + args: []types.T{types.T_varchar, types.T_float64, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { + return MakeTime + }, + }, + { + overloadId: 6, + args: []types.T{types.T_float64, types.T_varchar, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { + return MakeTime + }, + }, + { + overloadId: 7, + args: []types.T{types.T_varchar, types.T_varchar, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { + return MakeTime + }, + }, + { + overloadId: 8, + args: []types.T{types.T_float64, types.T_float64, types.T_varchar}, + retType: makeTimeReturnType, newOp: func() executeLogicOfOverload { return MakeTime }, }, + { + overloadId: 9, + args: []types.T{types.T_varchar, types.T_float64, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { + return MakeTime + }, + }, + { + overloadId: 10, + args: []types.T{types.T_float64, types.T_varchar, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { + return MakeTime + }, + }, + { + overloadId: 11, + args: []types.T{types.T_varchar, types.T_varchar, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { + return MakeTime + }, + }, + { + overloadId: 12, + args: []types.T{types.T_decimal128, types.T_float64, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 13, + args: []types.T{types.T_decimal128, types.T_varchar, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 14, + args: []types.T{types.T_decimal128, types.T_decimal128, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 15, + args: []types.T{types.T_decimal128, types.T_float64, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 16, + args: []types.T{types.T_decimal128, types.T_varchar, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 17, + args: []types.T{types.T_decimal128, types.T_decimal128, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 18, + args: []types.T{types.T_float64, types.T_decimal128, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 19, + args: []types.T{types.T_varchar, types.T_decimal128, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 20, + args: []types.T{types.T_float64, types.T_decimal128, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 21, + args: []types.T{types.T_varchar, types.T_decimal128, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 22, + args: []types.T{types.T_decimal256, types.T_float64, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 23, + args: []types.T{types.T_decimal256, types.T_varchar, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 24, + args: []types.T{types.T_decimal256, types.T_decimal128, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 25, + args: []types.T{types.T_decimal256, types.T_decimal256, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 26, + args: []types.T{types.T_decimal256, types.T_float64, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 27, + args: []types.T{types.T_decimal256, types.T_varchar, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 28, + args: []types.T{types.T_decimal256, types.T_decimal128, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 29, + args: []types.T{types.T_decimal256, types.T_decimal256, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 30, + args: []types.T{types.T_float64, types.T_decimal256, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 31, + args: []types.T{types.T_varchar, types.T_decimal256, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 32, + args: []types.T{types.T_decimal128, types.T_decimal256, types.T_float64}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 33, + args: []types.T{types.T_float64, types.T_decimal256, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 34, + args: []types.T{types.T_varchar, types.T_decimal256, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, + { + overloadId: 35, + args: []types.T{types.T_decimal128, types.T_decimal256, types.T_varchar}, + retType: makeTimeReturnType, + newOp: func() executeLogicOfOverload { return MakeTime }, + }, }, }, diff --git a/test/distributed/cases/function/func_datetime_hour.result b/test/distributed/cases/function/func_datetime_hour.result index 487baff41f9ea..d59d208c3ec4c 100644 --- a/test/distributed/cases/function/func_datetime_hour.result +++ b/test/distributed/cases/function/func_datetime_hour.result @@ -15,6 +15,12 @@ midnight SELECT HOUR(CAST('23:59:59' AS TIME)) AS end_of_day; end_of_day 23 +SELECT HOUR(CAST('272:59:59' AS TIME)) AS extended_positive; +extended_positive +272 +SELECT HOUR(CAST('-272:59:59' AS TIME)) AS extended_negative; +extended_negative +272 SELECT HOUR(CAST('2024-12-20 10:20:30' AS DATETIME)) AS datetime_hour; datetime_hour 10 diff --git a/test/distributed/cases/function/func_datetime_hour.test b/test/distributed/cases/function/func_datetime_hour.test index 50d612d77e730..43b575079e80b 100644 --- a/test/distributed/cases/function/func_datetime_hour.test +++ b/test/distributed/cases/function/func_datetime_hour.test @@ -9,6 +9,8 @@ SELECT HOUR(NOW()) AS result3; SELECT HOUR(CAST('15:30:45' AS TIME)) AS time_cast; SELECT HOUR(CAST('00:00:00' AS TIME)) AS midnight; SELECT HOUR(CAST('23:59:59' AS TIME)) AS end_of_day; +SELECT HOUR(CAST('272:59:59' AS TIME)) AS extended_positive; +SELECT HOUR(CAST('-272:59:59' AS TIME)) AS extended_negative; # Additional test cases with DATETIME type SELECT HOUR(CAST('2024-12-20 10:20:30' AS DATETIME)) AS datetime_hour; diff --git a/test/distributed/cases/function/func_datetime_maketime.result b/test/distributed/cases/function/func_datetime_maketime.result index 5beaddbaf6a12..7f8f25be009dd 100644 --- a/test/distributed/cases/function/func_datetime_maketime.result +++ b/test/distributed/cases/function/func_datetime_maketime.result @@ -25,18 +25,21 @@ time7 SELECT MAKETIME(100, 0, 0) AS time8; time8 100:00:00 -SELECT MAKETIME(-1, 15, 30) AS invalid_hour; -invalid_hour -null +SELECT MAKETIME(839, 0, 0) AS oversized_positive_hour; +oversized_positive_hour +838:59:59 +SELECT MAKETIME(-839, 0, 0) AS oversized_negative_hour; +oversized_negative_hour +-838:59:59 +SELECT CAST(MAKETIME(-1, 15, 30) AS VARCHAR) AS negative_hour; +negative_hour +-01:15:30 SELECT MAKETIME(12, 60, 30) AS invalid_minute; invalid_minute null SELECT MAKETIME(12, 15, 60) AS invalid_second; invalid_second null -SELECT MAKETIME(839, 0, 0) AS invalid_hour_too_large; -invalid_hour_too_large -null SELECT MAKETIME(NULL, 15, 30) AS null_hour; null_hour null @@ -49,9 +52,90 @@ null SELECT MAKETIME(NULL, NULL, NULL) AS null_all; null_all null -SELECT MAKETIME(12.7, 15.8, 30.9) AS float_values; +SELECT CAST(MAKETIME(12.7, 15.8, 30.9) AS VARCHAR) AS float_values; float_values -12:15:30 +13:16:30.9 +SELECT CAST(MAKETIME(12.5, 58.5, 0) AS VARCHAR) AS positive_half_values; +positive_half_values +13:59:00 +SELECT CAST(MAKETIME(-12.5, 0, 0) AS VARCHAR) AS negative_half_hour; +negative_half_hour +-13:00:00 +SELECT MAKETIME(12, -0.5, 0) AS negative_half_minute; +negative_half_minute +null +SELECT CAST(MAKETIME(12, 34, 56.789012) AS VARCHAR) AS fractional_seconds; +fractional_seconds +12:34:56.789012 +SELECT CAST(MAKETIME(12, 34, '56.789012') AS VARCHAR) AS string_fractional_seconds; +string_fractional_seconds +12:34:56.789012 +SELECT CAST(MAKETIME('12.7', '15.8', '30.9') AS VARCHAR) AS all_string_values; +all_string_values +12:15:30.900000 +SELECT CAST(MAKETIME('12.7', 15.8, 30.9) AS VARCHAR) AS string_hour_float_minute; +string_hour_float_minute +12:16:30.9 +SELECT CAST(MAKETIME(12.7, '15.8', 30.9) AS VARCHAR) AS float_hour_string_minute; +float_hour_string_minute +13:15:30.9 +SELECT CAST(MAKETIME(-12, 34, 56.789012) AS VARCHAR) AS negative_fractional; +negative_fractional +-12:34:56.789012 +SELECT CAST(MAKETIME(12, 59, 59.9999996) AS VARCHAR) AS fractional_carry; +fractional_carry +13:00:00.000000 +SELECT CAST(MAKETIME(838, 59, 59.9999996) AS VARCHAR) AS positive_rounded_endpoint; +positive_rounded_endpoint +838:59:59.000000 +SELECT CAST(MAKETIME(-838, 59, 59.9999996) AS VARCHAR) AS negative_rounded_endpoint; +negative_rounded_endpoint +-838:59:59.000000 +SELECT CAST(MAKETIME(12, 59, '59.99999949999999999') AS VARCHAR) AS varchar_below_half_microsecond; +varchar_below_half_microsecond +12:59:59.999999 +SELECT CAST(MAKETIME(12, 59, '59.9999995') AS VARCHAR) AS varchar_at_half_microsecond; +varchar_at_half_microsecond +13:00:00.000000 +SELECT CAST(MAKETIME(12, 59, CAST('59.99999949999999999' AS DECIMAL(30,20))) AS VARCHAR) AS decimal_below_half_microsecond; +decimal_below_half_microsecond +12:59:59.999999 +SELECT CAST(MAKETIME(12, CAST('59.49999999999999999999' AS DECIMAL(30,20)), CAST('0' AS DECIMAL(2,1))) AS VARCHAR) AS decimal_minute_below_half; +decimal_minute_below_half +12:59:00.0 +SELECT CAST(MAKETIME(X'0102', 0, 0) AS VARCHAR) AS binary_hour; +binary_hour +258:00:00 +SELECT CAST(MAKETIME(12, 0, X'01') AS VARCHAR) AS binary_second; +binary_second +12:00:01 +SELECT CAST(MAKETIME(12, 0, X'000000000000000001') AS VARCHAR) AS binary_second_wide_leading_zeros; +binary_second_wide_leading_zeros +12:00:01 +SELECT CAST(MAKETIME(12, 34, '') AS VARCHAR) AS empty_string_second; +empty_string_second +12:34:00.000000 +SELECT CAST(MAKETIME(12, 34, 'foo') AS VARCHAR) AS nonnumeric_string_second; +nonnumeric_string_second +12:34:00.000000 +CREATE TABLE maketime_double_seconds(h INT, m INT, s DOUBLE); +INSERT INTO maketime_double_seconds VALUES (12, 34, 56.789012); +SELECT CAST(MAKETIME(h, m, s) AS VARCHAR) AS double_column_microseconds FROM maketime_double_seconds; +double_column_microseconds +12:34:56.789012 +DROP TABLE maketime_double_seconds; +CREATE TABLE maketime_varchar_seconds(h INT, m INT, s VARCHAR(20)); +INSERT INTO maketime_varchar_seconds VALUES (12, 34, '56.789012'); +SELECT CAST(MAKETIME(h, m, s) AS VARCHAR) AS varchar_column_microseconds FROM maketime_varchar_seconds; +varchar_column_microseconds +12:34:56.789012 +DROP TABLE maketime_varchar_seconds; +CREATE TABLE maketime_varchar_parts_double_second(h VARCHAR(20), m VARCHAR(20), s DOUBLE); +INSERT INTO maketime_varchar_parts_double_second VALUES ('12', '34', 56.789012); +SELECT CAST(MAKETIME(h, m, s) AS VARCHAR) AS varchar_parts_double_second FROM maketime_varchar_parts_double_second; +varchar_parts_double_second +12:34:56.789012 +DROP TABLE maketime_varchar_parts_double_second; CREATE TABLE t1(h INT, m INT, s INT); INSERT INTO t1 VALUES (12, 15, 30), (0, 0, 0), (23, 59, 59); SELECT MAKETIME(h, m, s) AS time_value FROM t1; @@ -67,9 +151,9 @@ h m s 12 15 30 20 0 0 DROP TABLE t1; -SELECT MAKETIME(12.0, 15.0, 30.0) AS float_input; +SELECT CAST(MAKETIME(12.0, 15.0, 30.0) AS VARCHAR) AS float_input; float_input -12:15:30 +12:15:30.0 SELECT MAKETIME(CAST(12 AS UNSIGNED), CAST(15 AS UNSIGNED), CAST(30 AS UNSIGNED)) AS unsigned_input; unsigned_input 12:15:30 diff --git a/test/distributed/cases/function/func_datetime_maketime.test b/test/distributed/cases/function/func_datetime_maketime.test index 1a752353f892e..994f84556017f 100644 --- a/test/distributed/cases/function/func_datetime_maketime.test +++ b/test/distributed/cases/function/func_datetime_maketime.test @@ -12,15 +12,16 @@ SELECT MAKETIME(0, 30, 45) AS time4; SELECT MAKETIME(12, 0, 0) AS time5; SELECT MAKETIME(12, 15, 0) AS time6; -# Large hour values (MySQL TIME allows up to 838 hours) +# Large hour values clamp to the MySQL TIME endpoint SELECT MAKETIME(838, 59, 59) AS time7; SELECT MAKETIME(100, 0, 0) AS time8; +SELECT MAKETIME(839, 0, 0) AS oversized_positive_hour; +SELECT MAKETIME(-839, 0, 0) AS oversized_negative_hour; -# Invalid values (should return NULL) -SELECT MAKETIME(-1, 15, 30) AS invalid_hour; +# Negative hours are valid; other invalid values return NULL +SELECT CAST(MAKETIME(-1, 15, 30) AS VARCHAR) AS negative_hour; SELECT MAKETIME(12, 60, 30) AS invalid_minute; SELECT MAKETIME(12, 15, 60) AS invalid_second; -SELECT MAKETIME(839, 0, 0) AS invalid_hour_too_large; # NULL handling SELECT MAKETIME(NULL, 15, 30) AS null_hour; @@ -28,8 +29,55 @@ SELECT MAKETIME(12, NULL, 30) AS null_minute; SELECT MAKETIME(12, 15, NULL) AS null_second; SELECT MAKETIME(NULL, NULL, NULL) AS null_all; -# Float values (should truncate) -SELECT MAKETIME(12.7, 15.8, 30.9) AS float_values; +# Float hour and minute values round half away from zero; fractional seconds are preserved +SELECT CAST(MAKETIME(12.7, 15.8, 30.9) AS VARCHAR) AS float_values; +SELECT CAST(MAKETIME(12.5, 58.5, 0) AS VARCHAR) AS positive_half_values; +SELECT CAST(MAKETIME(-12.5, 0, 0) AS VARCHAR) AS negative_half_hour; +SELECT MAKETIME(12, -0.5, 0) AS negative_half_minute; +SELECT CAST(MAKETIME(12, 34, 56.789012) AS VARCHAR) AS fractional_seconds; +SELECT CAST(MAKETIME(12, 34, '56.789012') AS VARCHAR) AS string_fractional_seconds; +SELECT CAST(MAKETIME('12.7', '15.8', '30.9') AS VARCHAR) AS all_string_values; +SELECT CAST(MAKETIME('12.7', 15.8, 30.9) AS VARCHAR) AS string_hour_float_minute; +SELECT CAST(MAKETIME(12.7, '15.8', 30.9) AS VARCHAR) AS float_hour_string_minute; +SELECT CAST(MAKETIME(-12, 34, 56.789012) AS VARCHAR) AS negative_fractional; +SELECT CAST(MAKETIME(12, 59, 59.9999996) AS VARCHAR) AS fractional_carry; +SELECT CAST(MAKETIME(838, 59, 59.9999996) AS VARCHAR) AS positive_rounded_endpoint; +SELECT CAST(MAKETIME(-838, 59, 59.9999996) AS VARCHAR) AS negative_rounded_endpoint; + +# Exact VARCHAR/DECIMAL seconds do not cross the microsecond boundary through FLOAT64 +SELECT CAST(MAKETIME(12, 59, '59.99999949999999999') AS VARCHAR) AS varchar_below_half_microsecond; +SELECT CAST(MAKETIME(12, 59, '59.9999995') AS VARCHAR) AS varchar_at_half_microsecond; +SELECT CAST(MAKETIME(12, 59, CAST('59.99999949999999999' AS DECIMAL(30,20))) AS VARCHAR) AS decimal_below_half_microsecond; + +# DECIMAL hour/minute round exactly without crossing a half boundary through FLOAT64 +SELECT CAST(MAKETIME(12, CAST('59.49999999999999999999' AS DECIMAL(30,20)), CAST('0' AS DECIMAL(2,1))) AS VARCHAR) AS decimal_minute_below_half; + +# Binary literals retain numeric byte semantics instead of being parsed as text +SELECT CAST(MAKETIME(X'0102', 0, 0) AS VARCHAR) AS binary_hour; +SELECT CAST(MAKETIME(12, 0, X'01') AS VARCHAR) AS binary_second; +SELECT CAST(MAKETIME(12, 0, X'000000000000000001') AS VARCHAR) AS binary_second_wide_leading_zeros; + +# Exact VARCHAR seconds use MySQL numeric coercion for empty and nonnumeric text +SELECT CAST(MAKETIME(12, 34, '') AS VARCHAR) AS empty_string_second; +SELECT CAST(MAKETIME(12, 34, 'foo') AS VARCHAR) AS nonnumeric_string_second; + +# Default DOUBLE column retains microseconds +CREATE TABLE maketime_double_seconds(h INT, m INT, s DOUBLE); +INSERT INTO maketime_double_seconds VALUES (12, 34, 56.789012); +SELECT CAST(MAKETIME(h, m, s) AS VARCHAR) AS double_column_microseconds FROM maketime_double_seconds; +DROP TABLE maketime_double_seconds; + +# VARCHAR second values retain fractional microseconds +CREATE TABLE maketime_varchar_seconds(h INT, m INT, s VARCHAR(20)); +INSERT INTO maketime_varchar_seconds VALUES (12, 34, '56.789012'); +SELECT CAST(MAKETIME(h, m, s) AS VARCHAR) AS varchar_column_microseconds FROM maketime_varchar_seconds; +DROP TABLE maketime_varchar_seconds; + +# VARCHAR hour/minute retain val_int semantics with a DOUBLE second +CREATE TABLE maketime_varchar_parts_double_second(h VARCHAR(20), m VARCHAR(20), s DOUBLE); +INSERT INTO maketime_varchar_parts_double_second VALUES ('12', '34', 56.789012); +SELECT CAST(MAKETIME(h, m, s) AS VARCHAR) AS varchar_parts_double_second FROM maketime_varchar_parts_double_second; +DROP TABLE maketime_varchar_parts_double_second; # Table usage CREATE TABLE t1(h INT, m INT, s INT); @@ -44,6 +92,5 @@ SELECT * FROM t1 WHERE MAKETIME(h, m, s) > '12:00:00'; DROP TABLE t1; # Different numeric types -SELECT MAKETIME(12.0, 15.0, 30.0) AS float_input; +SELECT CAST(MAKETIME(12.0, 15.0, 30.0) AS VARCHAR) AS float_input; SELECT MAKETIME(CAST(12 AS UNSIGNED), CAST(15 AS UNSIGNED), CAST(30 AS UNSIGNED)) AS unsigned_input; - diff --git a/test/distributed/cases/function/func_datetime_time_format.result b/test/distributed/cases/function/func_datetime_time_format.result index a28105870aaff..9d085e457fff0 100644 --- a/test/distributed/cases/function/func_datetime_time_format.result +++ b/test/distributed/cases/function/func_datetime_time_format.result @@ -7,6 +7,12 @@ basic_format SELECT TIME_FORMAT('15:30:45', '%T') AS t_format; t_format 15:30:45 +SELECT TIME_FORMAT('123:45:06', '%T') AS extended_t_format; +extended_t_format +123:45:06 +SELECT TIME_FORMAT('-123:45:06', 'elapsed=%H:%i:%s') AS negative_with_text; +negative_with_text +-elapsed=123:45:06 SELECT TIME_FORMAT('00:00:00', '%H:%i:%s') AS zero_time; zero_time 00:00:00 @@ -70,6 +76,12 @@ null SELECT TIME_FORMAT('15:30:45', NULL) AS null_format; null_format null +SELECT TIME_FORMAT('15:30:45', '') AS empty_positive_format; +empty_positive_format +null +SELECT TIME_FORMAT('-15:30:45', '') AS empty_negative_format; +empty_negative_format +null CREATE TABLE t1(t TIME); INSERT INTO t1 VALUES ('15:30:45'), ('00:00:00'), ('23:59:59'), ('12:34:56'); SELECT t, TIME_FORMAT(t, '%H:%i:%s') AS formatted FROM t1; diff --git a/test/distributed/cases/function/func_datetime_time_format.test b/test/distributed/cases/function/func_datetime_time_format.test index b80e9ce5c63d5..98e99771486fe 100644 --- a/test/distributed/cases/function/func_datetime_time_format.test +++ b/test/distributed/cases/function/func_datetime_time_format.test @@ -5,6 +5,8 @@ SELECT TIME_FORMAT('15:30:45', '%H:%i:%s') AS result1; # Basic time formats SELECT TIME_FORMAT('15:30:45', '%H:%i:%s') AS basic_format; SELECT TIME_FORMAT('15:30:45', '%T') AS t_format; +SELECT TIME_FORMAT('123:45:06', '%T') AS extended_t_format; +SELECT TIME_FORMAT('-123:45:06', 'elapsed=%H:%i:%s') AS negative_with_text; SELECT TIME_FORMAT('00:00:00', '%H:%i:%s') AS zero_time; SELECT TIME_FORMAT('23:59:59', '%H:%i:%s') AS max_time; @@ -38,6 +40,8 @@ SELECT TIME_FORMAT('15:30:45', '%s') AS second_alt; # NULL handling SELECT TIME_FORMAT(NULL, '%H:%i:%s') AS null_time; SELECT TIME_FORMAT('15:30:45', NULL) AS null_format; +SELECT TIME_FORMAT('15:30:45', '') AS empty_positive_format; +SELECT TIME_FORMAT('-15:30:45', '') AS empty_negative_format; # Table usage CREATE TABLE t1(t TIME); @@ -54,4 +58,3 @@ DROP TABLE t1; # Complex format strings SELECT TIME_FORMAT('15:30:45.123456', '%H:%i:%s.%f') AS complex_format; SELECT TIME_FORMAT('15:30:45', 'Time is %H:%i:%s') AS with_text; -