Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions pkg/container/types/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,31 @@ func (dt Datetime) ToTime(scale int32) Time {
return Time(base * scaleVal[scale])
}

// TruncateToScale truncates a datetime to the given scale (0-6).
// Scale represents fractional seconds precision:
// - 0: seconds (no fractional part)
// - 1-5: fractional seconds with corresponding precision
// - 6: microseconds (full precision, no truncation)
func (dt Datetime) TruncateToScale(scale int32) Datetime {
if scale == 6 {
return dt
}

// Get the date part (seconds since epoch)
secPart := (dt / MicroSecsPerSec) * MicroSecsPerSec
// Get the fractional part (microseconds within the second)
microPart := dt % MicroSecsPerSec

divisor := scaleVal[scale]
base := microPart / divisor
// Round up if the next digit >= 5
if microPart%divisor/scaleVal[scale+1] >= 5 {
base += 1
}

return secPart + base*divisor
}

func (dt Datetime) Clock() (hour, minute, sec int8) {
t := dt.sec() % SecsPerDay
hour = int8(t / SecsPerHour)
Expand Down
73 changes: 73 additions & 0 deletions pkg/container/types/datetime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,76 @@ func TestDatetime_DayOfYear(t *testing.T) {
})
}
}

func TestDatetime_TruncateToScale(t *testing.T) {
// Test datetime with full microsecond precision: 2024-01-15 10:20:30.123456
dt := DatetimeFromClock(2024, 1, 15, 10, 20, 30, 123456)

// Test scale 0 (seconds, no fractional part)
truncated := dt.TruncateToScale(0)
expected := DatetimeFromClock(2024, 1, 15, 10, 20, 30, 0)
require.Equal(t, expected, truncated, "scale 0 should truncate to seconds")

// Test scale 3 (milliseconds) - should truncate to .123000
truncated = dt.TruncateToScale(3)
expected = DatetimeFromClock(2024, 1, 15, 10, 20, 30, 123000)
require.Equal(t, expected, truncated, "scale 3 should truncate to milliseconds")

// Test scale 6 (microseconds) - should not change
truncated = dt.TruncateToScale(6)
require.Equal(t, dt, truncated, "scale 6 should not change")

// Test rounding up: .123500 with scale 3 should round to .124000
dt2 := DatetimeFromClock(2024, 1, 15, 10, 20, 30, 123500)
truncated = dt2.TruncateToScale(3)
expected = DatetimeFromClock(2024, 1, 15, 10, 20, 30, 124000)
require.Equal(t, expected, truncated, "scale 3 should round .123500 up to .124000")

// Test rounding up to next second: .999999 with scale 0 should round to next second
dt3 := DatetimeFromClock(2024, 1, 15, 10, 20, 30, 999999)
truncated = dt3.TruncateToScale(0)
expected = DatetimeFromClock(2024, 1, 15, 10, 20, 31, 0)
require.Equal(t, expected, truncated, "scale 0 should round .999999 to next second")

// Test scale 1 (0.1 seconds): .156789 should round to .200000
dt4 := DatetimeFromClock(2024, 1, 15, 10, 20, 30, 156789)
truncated = dt4.TruncateToScale(1)
expected = DatetimeFromClock(2024, 1, 15, 10, 20, 30, 200000)
require.Equal(t, expected, truncated, "scale 1 should round .156789 to .200000")

// Test scale 2 (0.01 seconds): .125678 should round to .130000
dt5 := DatetimeFromClock(2024, 1, 15, 10, 20, 30, 125678)
truncated = dt5.TruncateToScale(2)
expected = DatetimeFromClock(2024, 1, 15, 10, 20, 30, 130000)
require.Equal(t, expected, truncated, "scale 2 should round .125678 to .130000")

// Test scale 4: .123456 should truncate to .123500 (rounds up from .123456)
dt6 := DatetimeFromClock(2024, 1, 15, 10, 20, 30, 123456)
truncated = dt6.TruncateToScale(4)
expected = DatetimeFromClock(2024, 1, 15, 10, 20, 30, 123500)
require.Equal(t, expected, truncated, "scale 4 should round .123456 to .123500")

// Test scale 5: .123456 should truncate to .123460
dt7 := DatetimeFromClock(2024, 1, 15, 10, 20, 30, 123456)
truncated = dt7.TruncateToScale(5)
expected = DatetimeFromClock(2024, 1, 15, 10, 20, 30, 123460)
require.Equal(t, expected, truncated, "scale 5 should round .123456 to .123460")

// Edge case: Test rounding at minute boundary
dt8 := DatetimeFromClock(2024, 1, 15, 10, 20, 59, 999999)
truncated = dt8.TruncateToScale(0)
expected = DatetimeFromClock(2024, 1, 15, 10, 21, 0, 0)
require.Equal(t, expected, truncated, "scale 0 should round 59.999999 to next minute")

// Edge case: Test rounding at hour boundary
dt9 := DatetimeFromClock(2024, 1, 15, 10, 59, 59, 999999)
truncated = dt9.TruncateToScale(0)
expected = DatetimeFromClock(2024, 1, 15, 11, 0, 0, 0)
require.Equal(t, expected, truncated, "scale 0 should round to next hour")

// Edge case: Test rounding at day boundary
dt10 := DatetimeFromClock(2024, 1, 15, 23, 59, 59, 999999)
truncated = dt10.TruncateToScale(0)
expected = DatetimeFromClock(2024, 1, 16, 0, 0, 0, 0)
require.Equal(t, expected, truncated, "scale 0 should round to next day")
}
40 changes: 40 additions & 0 deletions pkg/container/types/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,44 @@ func (t Time) ToInt64() int64 {
return trans
}

// TruncateToScale truncates a time to the given scale (0-6).
// Scale represents fractional seconds precision:
// - 0: seconds (no fractional part)
// - 1-5: fractional seconds with corresponding precision
// - 6: microseconds (full precision, no truncation)
func (t Time) TruncateToScale(scale int32) Time {
if scale == 6 {
return t
}

isNeg := t < 0
absTime := t
if isNeg {
absTime = -t
}

// Time is stored in microseconds (like Timestamp)
// Extract second part and microsecond part
secPart := (absTime / MicroSecsPerSec) * MicroSecsPerSec
microPart := absTime % MicroSecsPerSec

// Use same scale table as Timestamp (from timestamp.go)
scaleTable := [...]int64{1000000, 100000, 10000, 1000, 100, 10, 1}
divisor := scaleTable[scale]
base := int64(microPart) / divisor

// Round up if the next digit >= 5
if int64(microPart)%divisor/(divisor/10) >= 5 {
base += 1
}

result := secPart + Time(base*divisor)
if isNeg {
return -result
}
return result
}

func (t Time) ToDecimal64(ctx context.Context, width, scale int32) (Decimal64, error) {
tToStr := t.NumericString(scale)
ret, err := ParseDecimal64(tToStr, width, scale)
Expand Down Expand Up @@ -353,6 +391,8 @@ func (t Time) ToDatetime(scale int32) Datetime {
// return type bool means the if the time is valid
func (t Time) AddInterval(nums int64, its IntervalType) (Time, bool) {
switch its {
case MicroSecond:
// nums is already in microseconds, no conversion needed
case Second:
nums *= MicroSecsPerSec
case Minute:
Expand Down
18 changes: 18 additions & 0 deletions pkg/container/types/timestamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,24 @@ func (ts Timestamp) ToDatetime(loc *time.Location) Datetime {
return Datetime(ts) + Datetime(offset)*MicroSecsPerSec
}

// TruncateToScale truncates a timestamp to the given scale (0-6).
// Scale represents fractional seconds precision:
// - 0: seconds (no fractional part)
// - 1-5: fractional seconds with corresponding precision
// - 6: microseconds (full precision, no truncation)
func (ts Timestamp) TruncateToScale(scale int32) Timestamp {
if scale == 6 {
return ts
}
divisor := int64(scaleTable[scale])
base := int64(ts) / divisor
// Round up if the next digit >= 5
if int64(ts)%divisor/(divisor/10) >= 5 {
base += 1
}
return Timestamp(base * divisor)
}

// FromClockUTC gets the utc time value in Timestamp
func FromClockUTC(year int32, month, day, hour, minute, sec uint8, msec uint32) Timestamp {
days := DateFromCalendar(year, month, day)
Expand Down
37 changes: 37 additions & 0 deletions pkg/container/types/timestamp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,40 @@ func TestLocation(t *testing.T) {
locPtr = (*unsafeLoc)(unsafe.Pointer(loc))
require.Greater(t, len(locPtr.zone), 1)
}

func TestTimestamp_TruncateToScale(t *testing.T) {
// Test timestamp with full microsecond precision: 1970-01-01 00:00:01.123456
ts := TimestampMinValue + 123456

// Test scale 0 (seconds, no fractional part)
truncated := ts.TruncateToScale(0)
require.Equal(t, int64(TimestampMinValue), int64(truncated))

// Test scale 3 (milliseconds) - should truncate to .123000
truncated = ts.TruncateToScale(3)
require.Equal(t, int64(TimestampMinValue+123000), int64(truncated))

// Test scale 6 (microseconds) - should not change
truncated = ts.TruncateToScale(6)
require.Equal(t, int64(ts), int64(truncated))

// Test rounding up: 1970-01-01 00:00:01.123500 with scale 3 should round to .124000
ts2 := TimestampMinValue + 123500
truncated = ts2.TruncateToScale(3)
require.Equal(t, int64(TimestampMinValue+124000), int64(truncated))

// Test rounding up to next second: 1970-01-01 00:00:01.999999 with scale 0 should round to 2 seconds
ts3 := TimestampMinValue + 999999
truncated = ts3.TruncateToScale(0)
require.Equal(t, int64(TimestampMinValue+MicroSecsPerSec), int64(truncated))

// Test scale 1 (0.1 seconds)
ts4 := TimestampMinValue + 156789
truncated = ts4.TruncateToScale(1)
require.Equal(t, int64(TimestampMinValue+200000), int64(truncated)) // rounds up to .2

// Test scale 2 (0.01 seconds)
ts5 := TimestampMinValue + 125678
truncated = ts5.TruncateToScale(2)
require.Equal(t, int64(TimestampMinValue+130000), int64(truncated)) // rounds up to .13
}
90 changes: 74 additions & 16 deletions pkg/sql/plan/function/func_binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"github.com/matrixorigin/matrixone/pkg/util/fault"
"go.uber.org/zap"
"math"
"strconv"
"strings"
"time"

"github.com/matrixorigin/matrixone/pkg/util/fault"
"go.uber.org/zap"

"github.com/matrixorigin/matrixone/pkg/clusterservice"
"github.com/matrixorigin/matrixone/pkg/common/moerr"
"github.com/matrixorigin/matrixone/pkg/container/nulls"
Expand Down Expand Up @@ -677,7 +678,7 @@ func coalesceCheck(overloads []overload, inputs []types.Type) checkResult {
castType[i] = inputs[i]
} else {
castType[i] = minOid.ToType()
setTargetScaleFromSource(&inputs[i], &castType[i])
SetTargetScaleFromSource(&inputs[i], &castType[i])
}
}

Expand Down Expand Up @@ -1720,18 +1721,18 @@ func doDateSub(start types.Date, diff int64, iTyp types.IntervalType) (types.Dat
}
}

//func doTimeSub(start types.Time, diff int64, unit int64) (types.Time, error) {
// err := types.JudgeIntervalNumOverflow(diff, types.IntervalType(unit))
// if err != nil {
// return 0, err
// }
// t, success := start.AddInterval(-diff, types.IntervalType(unit))
// if success {
// return t, nil
// } else {
// return 0, moerr.NewOutOfRangeNoCtx("time", "")
// }
//}
func doTimeSub(start types.Time, diff int64, iTyp types.IntervalType) (types.Time, error) {
err := types.JudgeIntervalNumOverflow(diff, iTyp)
if err != nil {
return 0, err
}
t, success := start.AddInterval(-diff, iTyp)
if success {
return t, nil
} else {
return 0, moerr.NewOutOfRangeNoCtx("time", "")
}
}

func doDatetimeSub(start types.Datetime, diff int64, iTyp types.IntervalType) (types.Datetime, error) {
err := types.JudgeIntervalNumOverflow(diff, iTyp)
Expand Down Expand Up @@ -1835,6 +1836,9 @@ func TimestampSub(ivecs []*vector.Vector, result vector.FunctionResultWrapper, p
iTyp := types.IntervalType(unit)

scale := ivecs[0].GetType().Scale
if iTyp == types.MicroSecond {
scale = 6
}
rs := vector.MustFunctionResult[types.Timestamp](result)
rs.TempSetType(types.New(types.T_timestamp, 0, scale))

Expand All @@ -1843,6 +1847,21 @@ func TimestampSub(ivecs []*vector.Vector, result vector.FunctionResultWrapper, p
}, selectList)
}

func TimeSub(ivecs []*vector.Vector, result vector.FunctionResultWrapper, proc *process.Process, length int, selectList *FunctionSelectList) (err error) {
rs := vector.MustFunctionResult[types.Time](result)
unit, _ := vector.GenerateFunctionFixedTypeParameter[int64](ivecs[2]).GetValue(0)
scale := ivecs[0].GetType().Scale
iTyp := types.IntervalType(unit)
if iTyp == types.MicroSecond {
scale = 6
}
rs.TempSetType(types.New(types.T_time, 0, scale))

return opBinaryFixedFixedToFixedWithErrorCheck[types.Time, int64, types.Time](ivecs, result, proc, length, func(v1 types.Time, v2 int64) (types.Time, error) {
return doTimeSub(v1, v2, iTyp)
}, selectList)
}

type number interface {
constraints.Unsigned | constraints.Signed | constraints.Float
}
Expand Down Expand Up @@ -2693,7 +2712,7 @@ func extractFromTime(unit string, t types.Time) (string, error) {
var value string
switch unit {
case "microsecond":
value = fmt.Sprintf("%d", int(t))
value = fmt.Sprintf("%d", int(t.MicroSec()))
case "second":
value = fmt.Sprintf("%02d", int(t.Sec()))
case "minute":
Expand All @@ -2719,6 +2738,45 @@ func extractFromTime(unit string, t types.Time) (string, error) {
return value, nil
}

func ExtractFromTimestamp(ivecs []*vector.Vector, result vector.FunctionResultWrapper, proc *process.Process, length int, selectList *FunctionSelectList) (err error) {
if !ivecs[0].IsConst() {
return moerr.NewInternalError(proc.Ctx, "invalid input for extract")
}

p1 := vector.GenerateFunctionStrParameter(ivecs[0])
p2 := vector.GenerateFunctionFixedTypeParameter[types.Timestamp](ivecs[1])
rs := vector.MustFunctionResult[types.Varlena](result)

v1, null1 := p1.GetStrValue(0)
if null1 {
for i := uint64(0); i < uint64(length); i++ {
if err = rs.AppendBytes(nil, true); err != nil {
return err
}
}
return nil
}
unit := functionUtil.QuickBytesToStr(v1)
zone := proc.GetSessionInfo().TimeZone
for i := uint64(0); i < uint64(length); i++ {
v2, null2 := p2.GetValue(i)
if null2 {
if err = rs.AppendBytes(nil, true); err != nil {
return err
}
} else {
// Convert TIMESTAMP to DATETIME with full precision (scale=6) to preserve microseconds
dt := v2.ToDatetime(zone)
res, _ := extractFromDatetime(unit, dt)
if err = rs.AppendBytes(functionUtil.QuickStrToBytes(res), false); err != nil {
return err
}
}
}

return nil
}

func ExtractFromVarchar(ivecs []*vector.Vector, result vector.FunctionResultWrapper, proc *process.Process, length int, selectList *FunctionSelectList) (err error) {
if !ivecs[0].IsConst() {
return moerr.NewInternalError(proc.Ctx, "invalid input for extract")
Expand Down
4 changes: 2 additions & 2 deletions pkg/sql/plan/function/func_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func builtInCurrentTimestamp(ivecs []*vector.Vector, result vector.FunctionResul
}
rs.TempSetType(types.New(types.T_timestamp, 0, scale))

resultValue := types.UnixNanoToTimestamp(proc.GetUnixTime())
resultValue := types.UnixNanoToTimestamp(proc.GetUnixTime()).TruncateToScale(scale)
for i := uint64(0); i < uint64(length); i++ {
if err := rs.Append(resultValue, false); err != nil {
return err
Expand All @@ -98,7 +98,7 @@ func builtInSysdate(ivecs []*vector.Vector, result vector.FunctionResultWrapper,
}
rs.TempSetType(types.New(types.T_timestamp, 0, scale))

resultValue := types.UnixNanoToTimestamp(time.Now().UnixNano())
resultValue := types.UnixNanoToTimestamp(time.Now().UnixNano()).TruncateToScale(scale)
for i := uint64(0); i < uint64(length); i++ {
if err := rs.Append(resultValue, false); err != nil {
return err
Expand Down
Loading
Loading