diff --git a/sql3/errors.go b/sql3/errors.go index 620abcf47..44cd761c8 100644 --- a/sql3/errors.go +++ b/sql3/errors.go @@ -138,10 +138,9 @@ const ( // time quantum function eval ErrQRangeFromAndToTimeCannotBeBothNull errors.Code = "ErrQRangeFromAndToTimeCannotBeBothNull" ErrQRangeInvalidUse errors.Code = "ErrQRangeInvalidUse" - ErrYearOutOfRange errors.Code = "ErrYearOutOfRange" - - //evaluate errors - ErrDivideByZero errors.Code = "ErrDivideByZero" + ErrInvalidDatetimePart errors.Code = "ErrInvalidDatetimePart" + ErrOutputValueOutOfRange errors.Code = "ErrOutputValueOutOfRange" + ErrDivideByZero errors.Code = "ErrDivideByZero" ) func NewErrDuplicateColumn(line int, col int, column string) error { @@ -857,10 +856,17 @@ func NewErrQRangeInvalidUse(line, col int) error { ) } -func NewErrYearOutOfRange(line, col int, year int) error { +func NewErrInvalidDatetimePart(line, col int, datetimepart int) error { return errors.New( - ErrYearOutOfRange, - fmt.Sprintf("[%d:%d] year '%d' out of range [0,9999]", line, col, year), + ErrInvalidDatetimePart, + fmt.Sprintf("[%d:%d] not a valid datetimepart %d", line, col, datetimepart), + ) +} + +func NewErrOutputValueOutOfRange(line, col int) error { + return errors.New( + ErrOutputValueOutOfRange, + fmt.Sprintf("[%d:%d] output value out of range", line, col), ) } diff --git a/sql3/planner/expression.go b/sql3/planner/expression.go index 6518575ec..058b92a14 100644 --- a/sql3/planner/expression.go +++ b/sql3/planner/expression.go @@ -1588,6 +1588,8 @@ func (n *callPlanExpression) Evaluate(currentRow []interface{}) (interface{}, er return n.EvaluateDateTimeFromParts(currentRow) case "DATETIMEADD": return n.EvaluateDatetimeAdd(currentRow) + case "DATETIMEDIFF": + return n.EvaluateDatetimeDiff(currentRow) default: return nil, sql3.NewErrInternalf("unhandled function name '%s'", n.name) } diff --git a/sql3/planner/expressionanalyzercall.go b/sql3/planner/expressionanalyzercall.go index 2b6745d1b..dac0451d7 100644 --- a/sql3/planner/expressionanalyzercall.go +++ b/sql3/planner/expressionanalyzercall.go @@ -263,9 +263,10 @@ func (p *ExecutionPlanner) analyzeCallExpression(ctx context.Context, call *pars return p.analyzeFunctionRangeQ(call, scope) case "DATETIMEFROMPARTS": return p.analyzeFunctionDateTimeFromParts(call, scope) - case "DATETIMEADD": return p.analyzeFunctionDatetimeAdd(call, scope) + case "DATETIMEDIFF": + return p.analyzeFunctionDateTimeDiff(call, scope) default: return nil, sql3.NewErrCallUnknownFunction(call.Name.NamePos.Line, call.Name.NamePos.Column, call.Name.Name) } diff --git a/sql3/planner/inbuiltfunctionsdate.go b/sql3/planner/inbuiltfunctionsdate.go index 1bdc69c88..6e2a8d7ea 100644 --- a/sql3/planner/inbuiltfunctionsdate.go +++ b/sql3/planner/inbuiltfunctionsdate.go @@ -144,6 +144,31 @@ func (p *ExecutionPlanner) analyzeFunctionDateTimeName(call *parser.Call, scope return call, nil } +// analyzeFunctionDateTimeDiff ensures a timeunit and start and end timestamps. +func (p *ExecutionPlanner) analyzeFunctionDateTimeDiff(call *parser.Call, scope parser.Statement) (parser.Expr, error) { + if len(call.Args) != 3 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 3, len(call.Args)) + } + + // interval + intervalType := parser.NewDataTypeString() + if !typesAreAssignmentCompatible(intervalType, call.Args[0].DataType()) { + return nil, sql3.NewErrParameterTypeMistmatch(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Args[0].DataType().TypeDescription(), intervalType.TypeDescription()) + } + + dateType := parser.NewDataTypeTimestamp() + if !typesAreAssignmentCompatible(dateType, call.Args[1].DataType()) { + return nil, sql3.NewErrParameterTypeMistmatch(call.Args[1].Pos().Line, call.Args[1].Pos().Column, call.Args[1].DataType().TypeDescription(), dateType.TypeDescription()) + } + + if !typesAreAssignmentCompatible(dateType, call.Args[2].DataType()) { + return nil, sql3.NewErrParameterTypeMistmatch(call.Args[2].Pos().Line, call.Args[2].Pos().Column, call.Args[2].DataType().TypeDescription(), dateType.TypeDescription()) + } + + call.ResultDataType = parser.NewDataTypeInt() + return call, nil +} + func (n *callPlanExpression) EvaluateDateTimePart(currentRow []interface{}) (interface{}, error) { intervalEval, err := n.args[0].Evaluate(currentRow) if err != nil { @@ -247,13 +272,61 @@ func (n *callPlanExpression) EvaluateDateTimeFromParts(currentRow []interface{}) timestamps[i] = int(val) } + if val, ok := isValidDateTimeParts(timestamps); !ok { + return nil, sql3.NewErrInvalidDatetimePart(0, 0, val) + } + dt := time.Date(timestamps[0], time.Month(timestamps[1]), timestamps[2], timestamps[3], timestamps[4], timestamps[5], timestamps[6]*1000*1000, time.UTC) if dt.Year() < 0 || dt.Year() > 9999 { - return nil, sql3.NewErrYearOutOfRange(0, 0, dt.Year()) + return nil, sql3.NewErrInvalidDatetimePart(0, 0, dt.Year()) } return dt, nil } +// isValidDateTimeParts returns true if the year is between 0 and 9999 and the date and time exists(think of leap year). +// the argument is a slice of ints representing the year, month, day, hour, minutes, seconds, and milliseconds. +// If any value in the slice falls outside its range, the value and false are returned. +func isValidDateTimeParts(timestamps []int) (value int, ok bool) { + if timestamps[0] < 0 || timestamps[0] > 9999 { + return timestamps[0], false + } + if timestamps[1] < 1 || timestamps[1] > 12 { + return timestamps[1], false + } + switch timestamps[1] { + case 1, 3, 5, 7, 8, 10, 12: + if timestamps[2] < 1 || timestamps[2] > 31 { + return timestamps[2], false + } + case 4, 6, 9, 11: + if timestamps[2] < 1 || timestamps[2] > 30 { + return timestamps[2], false + } + case 2: + if timestamps[2] < 1 || timestamps[2] > 29 { + return timestamps[2], false + } + if !(timestamps[0]%4 == 0 && timestamps[0]%100 != 0 || timestamps[0]%400 == 0) { + if timestamps[2] == 29 { + return timestamps[2], false + } + } + } + if timestamps[3] < 0 || timestamps[3] > 23 { + return timestamps[3], false + } + if timestamps[4] < 0 || timestamps[4] > 59 { + return timestamps[4], false + } + if timestamps[5] < 0 || timestamps[5] > 59 { + return timestamps[5], false + } + if timestamps[6] < 0 || timestamps[6] > 999 { + return timestamps[6], false + } + return 0, true +} + func (n *callPlanExpression) EvaluateToTimestamp(currentRow []interface{}) (interface{}, error) { // retrieve param1, the number to be converted to timestamp param1, err := n.args[0].Evaluate(currentRow) @@ -478,3 +551,87 @@ func isValidTimeInterval(unit string) bool { return false } } + +// EvaluateDatetimeDiff takes three arguments: +// 1. param1, timeunit of the value to be subtracted from the target timestamp +// 2. param2, starttime to be subtracted from the endtime timestamp +// 3. param3, endtime timestamp to which the starttime to be subtracted from. +// It returns the difference between the two timestamps. +func (n *callPlanExpression) EvaluateDatetimeDiff(currentRow []interface{}) (interface{}, error) { + param1, err := n.args[0].Evaluate(currentRow) + if err != nil { + return nil, err + } + if param1 == nil { + return nil, nil + } + cp, err := coerceValue(n.args[0].Type(), parser.NewDataTypeString(), param1, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + timeunit, ok := cp.(string) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + param2, err := n.args[1].Evaluate(currentRow) + if err != nil { + return nil, err + } + coercedParam, err := coerceValue(n.args[1].Type(), parser.NewDataTypeTimestamp(), param2, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + if coercedParam == nil { + return nil, nil + } + startDate, ok := coercedParam.(time.Time) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + if coercedParam == nil { + return nil, nil + } + param3, err := n.args[2].Evaluate(currentRow) + if err != nil { + return nil, err + } + if param3 == nil { + return nil, nil + } + coercedParam, err = coerceValue(n.args[2].Type(), parser.NewDataTypeTimestamp(), param3, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + endDate, ok := coercedParam.(time.Time) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + var diff int64 + switch strings.ToUpper(timeunit) { + case intervalYear: + diff = int64(endDate.Year() - startDate.Year()) + case intervalMonth: + diff = int64((endDate.Year()-startDate.Year())*12 + int(endDate.Month()-startDate.Month())) + case intervalDay: + diff = int64(endDate.Sub(startDate).Hours() / 24) + case intervalHour: + diff = int64(endDate.Sub(startDate).Hours()) + case intervalMinute: + diff = int64(endDate.Sub(startDate).Minutes()) + case intervalSecond: + diff = int64(endDate.Sub(startDate).Seconds()) + case intervalMillisecond: + diff = endDate.Sub(startDate).Milliseconds() + case intervalMicrosecond: + diff = endDate.Sub(startDate).Microseconds() + case intervalNanosecond: + diff = endDate.Sub(startDate).Nanoseconds() + default: + return nil, sql3.NewErrCallParameterValueInvalid(0, 0, timeunit, "timeunit") + } + if diff == (-1<<63) || diff == ((1<<63)-1) { + return nil, sql3.NewErrOutputValueOutOfRange(0, 0) + } + return diff, nil +} diff --git a/sql3/test/defs/defs.go b/sql3/test/defs/defs.go index 75c88dd70..3cc449b47 100644 --- a/sql3/test/defs/defs.go +++ b/sql3/test/defs/defs.go @@ -49,6 +49,7 @@ var TableTests []TableTest = []TableTest{ dateTimeNameTests, toTimestampTests, datetimeAddTests, + datetimedifftests, stringScalarFunctionsTests, @@ -224,6 +225,19 @@ func knownSubSecondTimestamp() time.Time { return tm } +func knownSubSecondTimestamp2() time.Time { + tm, err := time.ParseInLocation(time.RFC3339, "2022-12-09T18:04:54+00:00", time.UTC) + if err != nil { + panic(err.Error()) + } + duration, err := time.ParseDuration("300500800ns") + if err != nil { + panic(err.Error()) + } + tm = tm.Add(duration) + return tm +} + func timestampFromString(s string) time.Time { tm, err := time.ParseInLocation(time.RFC3339, s, time.UTC) if err != nil { diff --git a/sql3/test/defs/defs_date_functions.go b/sql3/test/defs/defs_date_functions.go index 3db762ecc..94e2c9b6b 100644 --- a/sql3/test/defs/defs_date_functions.go +++ b/sql3/test/defs/defs_date_functions.go @@ -328,11 +328,18 @@ var toTimestampTests = TableTest{ ExpErr: "an expression of type 'string' cannot be passed to a parameter of type 'int'", }, { - name: "DateTimeFromPartsYearOutOfRange", + name: "DateTimeFromPartsInvalidDatetimePart", SQLs: sqls( "select datetimefromparts(10000,1,1,1,1,1,1)", ), - ExpErr: "[0:0] year '10000' out of range [0,9999]", + ExpErr: "[0:0] not a valid datetimepart 10000", + }, + { + name: "DateTimeFromPartsInvalidDatetimePart2", + SQLs: sqls( + "select datetimefromparts(2023,2,29,1,1,1,1)", + ), + ExpErr: "[0:0] not a valid datetimepart 29", }, { name: "DateTimeFromPartsKnownTimestamp", @@ -651,3 +658,162 @@ var datetimeAddTests = TableTest{ }, }, } + +var datetimedifftests = TableTest{ + name: "DatetimeDiff", + Table: tbl("dttable", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("startTime", fldTypeTimestamp, "timeunit 'ns'"), + srcHdr("endTime", fldTypeTimestamp, "timeunit 'ns'"), + ), + srcRows( + srcRow(int64(1), knownSubSecondTimestamp(), knownSubSecondTimestamp2()), + )), + SQLTests: []SQLTest{ + { + name: "DatetimeDiffWrongParamCount", + SQLs: sqls( + "select datetimediff(startTime, endTime) from dttable;", + ), + ExpErr: "count of formal parameters (3) does not match count of actual parameters (2)", + }, + { + name: "DatetimeDiffInvalidType", + SQLs: sqls( + "select datetimediff('yy','nope', endTime) from dttable;", + ), + ExpErr: "[0:0] unable to convert 'nope' to type 'timestamp'", + }, + { + name: "DatetimeDiffNull", + SQLs: sqls( + "select datetimediff(null, startTime, endTime) from dttable;", + ), + ExpHdrs: hdrs( + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(nil), + ), + Compare: CompareExactUnordered, + }, + { + name: "DatetimeDiffYY", + SQLs: sqls( + "select datetimediff('yy', startTime, endTime) from dttable;", + ), + ExpHdrs: hdrs( + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(10)), + ), + Compare: CompareExactUnordered, + }, + { + name: "DatetimeDiffM", + SQLs: sqls( + "select datetimediff('m', startTime, endTime) from dttable;", + ), + ExpHdrs: hdrs( + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(121)), + ), + Compare: CompareExactUnordered, + }, + { + name: "DatetimeDiffD", + SQLs: sqls( + "select datetimediff('d', startTime, endTime) from dttable;", + ), + ExpHdrs: hdrs( + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(3689)), + ), + Compare: CompareExactUnordered, + }, + { + name: "DatetimeDiffHH", + SQLs: sqls( + "select datetimediff('hh', startTime, endTime) from dttable;", + ), + ExpHdrs: hdrs( + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(88555)), + ), + Compare: CompareExactUnordered, + }, + { + name: "DatetimeDiffMI", + SQLs: sqls( + "select datetimediff('mi', startTime, endTime) from dttable;", + ), + ExpHdrs: hdrs( + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(5313356)), + ), + Compare: CompareExactUnordered, + }, + { + name: "DatetimeDiffS", + SQLs: sqls( + "select datetimediff('s', startTime, endTime) from dttable;", + ), + ExpHdrs: hdrs( + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(318801373)), + ), + Compare: CompareExactUnordered, + }, + { + name: "DatetimeDiffMS", + SQLs: sqls( + "select datetimediff('ms', startTime, endTime) from dttable;", + ), + ExpHdrs: hdrs( + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(318801373200)), + ), + Compare: CompareExactUnordered, + }, + { + name: "DatetimeDiffUS", + SQLs: sqls( + "select datetimediff('us', startTime, endTime) from dttable;", + ), + ExpHdrs: hdrs( + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(318801373200300)), + ), + Compare: CompareExactUnordered, + }, + { + name: "DatetimeDiffNS", + SQLs: sqls( + "select datetimediff('ns', startTime, endTime) from dttable;", + ), + ExpHdrs: hdrs( + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(318801373200300500)), + ), + Compare: CompareExactUnordered, + }, + }, +}