From 909c62d44ecf2cb4fc1fc06c84cba3605a3b565f Mon Sep 17 00:00:00 2001 From: rachithrr Date: Tue, 7 Mar 2023 16:47:07 -0600 Subject: [PATCH] FB-1895: Implement DateTimeFromParts (#2296) --- sql3/errors.go | 8 +++ sql3/planner/expression.go | 2 + sql3/planner/expressionanalyzercall.go | 2 + sql3/planner/inbuiltfunctionsdate.go | 47 +++++++++++- sql3/test/defs/defs_date_functions.go | 99 +++++++++++++++++++++++++- 5 files changed, 155 insertions(+), 3 deletions(-) diff --git a/sql3/errors.go b/sql3/errors.go index fa3e69fe2..3e2e2983f 100644 --- a/sql3/errors.go +++ b/sql3/errors.go @@ -138,6 +138,7 @@ const ( // time quantum function eval ErrQRangeFromAndToTimeCannotBeBothNull errors.Code = "ErrQRangeFromAndToTimeCannotBeBothNull" ErrQRangeInvalidUse errors.Code = "ErrQRangeInvalidUse" + ErrYearOutOfRange errors.Code = "ErrYearOutOfRange" ) func NewErrDuplicateColumn(line int, col int, column string) error { @@ -852,3 +853,10 @@ func NewErrQRangeInvalidUse(line, col int) error { fmt.Sprintf("[%d:%d] calling ranqeq() usage invalid", line, col), ) } + +func NewErrYearOutOfRange(line, col int, year int) error { + return errors.New( + ErrYearOutOfRange, + fmt.Sprintf("[%d:%d] year '%d' out of range [0,9999]", line, col, year), + ) +} diff --git a/sql3/planner/expression.go b/sql3/planner/expression.go index bc91d6769..b884f5935 100644 --- a/sql3/planner/expression.go +++ b/sql3/planner/expression.go @@ -1580,6 +1580,8 @@ func (n *callPlanExpression) Evaluate(currentRow []interface{}) (interface{}, er // time quantum functions case "RANGEQ": return n.EvaluateRangeQ(currentRow) + case "DATETIMEFROMPARTS": + return n.EvaluateDateTimeFromParts(currentRow) case "DATETIMEADD": return n.EvaluateDatetimeAdd(currentRow) default: diff --git a/sql3/planner/expressionanalyzercall.go b/sql3/planner/expressionanalyzercall.go index 8565ca4be..185727a27 100644 --- a/sql3/planner/expressionanalyzercall.go +++ b/sql3/planner/expressionanalyzercall.go @@ -261,6 +261,8 @@ func (p *ExecutionPlanner) analyzeCallExpression(ctx context.Context, call *pars // time quantum funtions case "RANGEQ": return p.analyzeFunctionRangeQ(call, scope) + case "DATETIMEFROMPARTS": + return p.analyzeFunctionDateTimeFromParts(call, scope) case "DATETIMEADD": return p.analyzeFunctionDatetimeAdd(call, scope) diff --git a/sql3/planner/inbuiltfunctionsdate.go b/sql3/planner/inbuiltfunctionsdate.go index d39efe0af..a46b1f467 100644 --- a/sql3/planner/inbuiltfunctionsdate.go +++ b/sql3/planner/inbuiltfunctionsdate.go @@ -101,6 +101,24 @@ func (p *ExecutionPlanner) analyzeFunctionDatetimeAdd(call *parser.Call, scope p // DatetimeAdd returns a timestamp calculated by adding param2 to param3 using time unit passed in param 1 call.ResultDataType = parser.NewDataTypeTimestamp() + + return call, nil +} + +func (p *ExecutionPlanner) analyzeFunctionDateTimeFromParts(call *parser.Call, scope parser.Statement) (parser.Expr, error) { + if len(call.Args) != 7 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 7, len(call.Args)) + } + + intType := parser.NewDataTypeInt() + for _, part := range call.Args { + if !typesAreAssignmentCompatible(intType, part.DataType()) { + return nil, sql3.NewErrParameterTypeMistmatch(part.Pos().Line, part.Pos().Column, part.DataType().TypeDescription(), intType.TypeDescription()) + } + } + + call.ResultDataType = parser.NewDataTypeTimestamp() + return call, nil } @@ -123,7 +141,6 @@ func (p *ExecutionPlanner) analyzeFunctionDateTimeName(call *parser.Call, scope //return int call.ResultDataType = parser.NewDataTypeString() - return call, nil } @@ -209,6 +226,34 @@ func (n *callPlanExpression) EvaluateDatepart(currentRow []interface{}) (interfa } +// EvaluateDateTimeFromParts evaluates the call to date_time_from_parts. This uses the base time.Date() function. +func (n *callPlanExpression) EvaluateDateTimeFromParts(currentRow []interface{}) (interface{}, error) { + timestamps := make([]int, len(n.args)) + for i, arg := range n.args { + param, err := arg.Evaluate(currentRow) + if err != nil { + return nil, err + } else if param == nil { + return nil, nil + } + coercedValue, err := coerceValue(arg.Type(), parser.NewDataTypeInt(), param, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + val, ok := coercedValue.(int64) + if !ok { + return nil, sql3.NewErrInternalf("unable to convert value") + } + timestamps[i] = int(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 dt, nil +} + func (n *callPlanExpression) EvaluateToTimestamp(currentRow []interface{}) (interface{}, error) { // retrieve param1, the number to be converted to timestamp param1, err := n.args[0].Evaluate(currentRow) diff --git a/sql3/test/defs/defs_date_functions.go b/sql3/test/defs/defs_date_functions.go index 8fbaa2345..088cff101 100644 --- a/sql3/test/defs/defs_date_functions.go +++ b/sql3/test/defs/defs_date_functions.go @@ -1,6 +1,9 @@ package defs -import "time" +import ( + "fmt" + "time" +) // datepart tests var datePartTests = TableTest{ @@ -19,24 +22,56 @@ var datePartTests = TableTest{ ), SQLTests: []SQLTest{ { + name: "DatePartIncorrectParamsCount", SQLs: sqls( "select datepart()", ), ExpErr: "count of formal parameters (2) does not match count of actual parameters (0)", }, { + name: "DatePartIntError", SQLs: sqls( "select datepart(1, 2)", ), ExpErr: "an expression of type 'int' cannot be passed to a parameter of type 'string'", }, { + name: "DatePartInvalidParam", SQLs: sqls( "select datepart('1', current_timestamp)", ), ExpErr: "invalid value '1' for parameter 'interval'", }, { + name: "ToTimestampWrongParamsCount", + SQLs: sqls( + "select totimestamp()", + ), + ExpErr: "count of formal parameters (2) does not match count of actual parameters (0)", + }, + { + name: "ToTimestampStringError", + SQLs: sqls( + "select totimestamp('a')", + ), + ExpErr: "an expression of type 'string' cannot be passed to a parameter of type 'int'", + }, + { + name: "ToTimestampIntError", + SQLs: sqls( + "select totimestamp(1, 2)", + ), + ExpErr: "an expression of type 'int' cannot be passed to a parameter of type 'string'", + }, + { + name: "ToTimestampInvalid", + SQLs: sqls( + "select totimestamp(1, 'x')", + ), + ExpErr: "invalid value 'x' for parameter 'timeunit'", + }, + { + name: "DATEPARTYY", SQLs: sqls( "select _id, datepart('yy', ts) from dateparttests", ), @@ -50,6 +85,7 @@ var datePartTests = TableTest{ Compare: CompareExactUnordered, }, { + name: "DATEPARTYD", SQLs: sqls( "select _id, datepart('yd', ts) from dateparttests", ), @@ -63,6 +99,7 @@ var datePartTests = TableTest{ Compare: CompareExactUnordered, }, { + name: "DATEPARTM", SQLs: sqls( "select _id, datepart('m', ts) from dateparttests", ), @@ -76,6 +113,7 @@ var datePartTests = TableTest{ Compare: CompareExactUnordered, }, { + name: "DATEPARTD", SQLs: sqls( "select _id, datepart('d', ts) from dateparttests", ), @@ -89,6 +127,7 @@ var datePartTests = TableTest{ Compare: CompareExactUnordered, }, { + name: "DATEPARTW", SQLs: sqls( "select _id, datepart('w', ts) from dateparttests", ), @@ -102,6 +141,7 @@ var datePartTests = TableTest{ Compare: CompareExactUnordered, }, { + name: "DATEPARTWK", SQLs: sqls( "select _id, datepart('wk', ts) from dateparttests", ), @@ -115,6 +155,7 @@ var datePartTests = TableTest{ Compare: CompareExactUnordered, }, { + name: "DATEPARTHH", SQLs: sqls( "select _id, datepart('hh', ts) from dateparttests", ), @@ -128,6 +169,7 @@ var datePartTests = TableTest{ Compare: CompareExactUnordered, }, { + name: "DatePartMI", SQLs: sqls( "select _id, datepart('mi', ts) from dateparttests", ), @@ -141,6 +183,7 @@ var datePartTests = TableTest{ Compare: CompareExactUnordered, }, { + name: "DatePartS", SQLs: sqls( "select _id, datepart('s', ts) from dateparttests", ), @@ -154,6 +197,7 @@ var datePartTests = TableTest{ Compare: CompareExactUnordered, }, { + name: "DatePartMS", SQLs: sqls( "select _id, datepart('ms', ts) from dateparttests", ), @@ -180,6 +224,7 @@ var datePartTests = TableTest{ Compare: CompareExactUnordered, }, { + name: "DatePartNS", SQLs: sqls( "select _id, datepart('ns', ts) from dateparttests", ), @@ -194,6 +239,7 @@ var datePartTests = TableTest{ }, { //test datepart(timestamp, part) for implicit conversion of integer value passed as argument to timestamp param + name: "DatePartImplicitIntConversion", SQLs: sqls( "select datepart('yy', 0) as \"yy\", datepart('m', 0) as \"m\", datepart('d', 0) as \"d\"", ), @@ -244,7 +290,7 @@ var toTimestampTests = TableTest{ ExpErr: "invalid value 'x' for parameter 'timeunit'", }, { - //test ToTimestamp(num, timeunit) for all possible time unit values + name: "ToTimestampAllPossibleValues", SQLs: sqls( "select totimestamp(1000) as \"default\", totimestamp(1000, 's') as \"s\", totimestamp(1000000, 'ms') as \"ms\", totimestamp(1000000000, 'us') as \"us\", totimestamp(1000000000, 'µs') as \"µs\", totimestamp(1000000000000, 'ns') as \"ns\"", ), @@ -266,6 +312,55 @@ var toTimestampTests = TableTest{ ), Compare: CompareExactUnordered, }, + { + name: "DateTimeFromPartsParamsCountMismatch", + SQLs: sqls( + "select datetimefromparts(12,32,43,34,34,34)", + ), + ExpErr: "count of formal parameters (7) does not match count of actual parameters (6)", + }, + { + name: "DateTimeFromPartsParamsTypeMismatch", + SQLs: sqls( + "select datetimefromparts(12,32,43,34,34,34,'foo')", + ), + ExpErr: "an expression of type 'string' cannot be passed to a parameter of type 'int'", + }, + { + name: "DateTimeFromPartsYearOutOfRange", + SQLs: sqls( + "select datetimefromparts(10000,1,1,1,1,1,1)", + ), + ExpErr: "[0:0] year '10000' out of range [0,9999]", + }, + { + name: "DateTimeFromPartsKnownTimestamp", + SQLs: sqls( + fmt.Sprintf("select datetimefromparts(%d,%d,%d,%d,%d,%d,%d) as datetime", knownTimestamp().Year(), + knownTimestamp().Month(), knownTimestamp().Day(), knownTimestamp().Hour(), knownTimestamp().Minute(), + knownTimestamp().Second(), knownTimestamp().Nanosecond()/(1000*1000)), + ), + ExpHdrs: hdrs( + hdr("datetime", fldTypeTimestamp), + ), + ExpRows: rows( + row(knownTimestamp()), + ), + Compare: CompareExactUnordered, + }, + { + name: "DateTimeFromPartsAllZeros", + SQLs: sqls( + fmt.Sprintf("select datetimefromparts(%d,%d,%d,%d,%d,%d,%d) as datetime", 0, 1, 1, 0, 0, 0, 0), + ), + ExpHdrs: hdrs( + hdr("datetime", fldTypeTimestamp), + ), + ExpRows: rows( + row(time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC)), + ), + Compare: CompareExactUnordered, + }, }, }