mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-11 23:31:03 +00:00
FB-1894: Implement DateTimeDiff() (#2307)
This commit is contained in:
parent
b17582110f
commit
ef14f3a560
6 changed files with 357 additions and 11 deletions
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue