From 7ea4135ecf1d446a42b92f4ade160c3d053e1537 Mon Sep 17 00:00:00 2001 From: Lory Cloutier <118481783+lorycloutier@users.noreply.github.com> Date: Tue, 7 Mar 2023 12:58:44 -0600 Subject: [PATCH] Add datetimename function to SQL3 (#2293) FB-1896 Added the datetimename function, which returns parts of a timestamp as strings. Month and day of the week are named ("January", "Monday") while others are returned as a string of digits ("2023"). Added tests to the test definitions. --- sql3/planner/expression.go | 2 + sql3/planner/expressionanalyzercall.go | 2 + sql3/planner/inbuiltfunctionsdate.go | 107 ++++++++++++++++++++++++- sql3/test/defs/defs.go | 2 + sql3/test/defs/defs_date_functions.go | 70 ++++++++++++++-- 5 files changed, 176 insertions(+), 7 deletions(-) diff --git a/sql3/planner/expression.go b/sql3/planner/expression.go index aa184b147..b8d11e6e6 100644 --- a/sql3/planner/expression.go +++ b/sql3/planner/expression.go @@ -1576,6 +1576,8 @@ func (n *callPlanExpression) Evaluate(currentRow []interface{}) (interface{}, er return n.EvaluateToTimestamp(currentRow) case "STR": return n.EvaluateStr(currentRow) + case "DATETIMENAME": + return n.EvaluateDateTimeName(currentRow) // time quantum functions case "RANGEQ": return n.EvaluateRangeQ(currentRow) diff --git a/sql3/planner/expressionanalyzercall.go b/sql3/planner/expressionanalyzercall.go index d77548eae..8565ca4be 100644 --- a/sql3/planner/expressionanalyzercall.go +++ b/sql3/planner/expressionanalyzercall.go @@ -256,6 +256,8 @@ func (p *ExecutionPlanner) analyzeCallExpression(ctx context.Context, call *pars return p.analyzeFunctionToTimestamp(call, scope) case "STR": return p.analyseFunctionStr(call, scope) + case "DATETIMENAME": + return p.analyzeFunctionDateTimeName(call, scope) // time quantum funtions case "RANGEQ": return p.analyzeFunctionRangeQ(call, scope) diff --git a/sql3/planner/inbuiltfunctionsdate.go b/sql3/planner/inbuiltfunctionsdate.go index 57d56a60a..d39efe0af 100644 --- a/sql3/planner/inbuiltfunctionsdate.go +++ b/sql3/planner/inbuiltfunctionsdate.go @@ -1,6 +1,7 @@ package planner import ( + "fmt" "strings" "time" @@ -103,6 +104,29 @@ func (p *ExecutionPlanner) analyzeFunctionDatetimeAdd(call *parser.Call, scope p return call, nil } +func (p *ExecutionPlanner) analyzeFunctionDateTimeName(call *parser.Call, scope parser.Statement) (parser.Expr, error) { + + if len(call.Args) != 2 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 2, 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()) + } + + // date + 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()) + } + + //return int + call.ResultDataType = parser.NewDataTypeString() + + return call, nil +} + func (n *callPlanExpression) EvaluateDatepart(currentRow []interface{}) (interface{}, error) { intervalEval, err := n.args[0].Evaluate(currentRow) if err != nil { @@ -174,10 +198,10 @@ func (n *callPlanExpression) EvaluateDatepart(currentRow []interface{}) (interfa return int64(date.Nanosecond() / 1000000), nil case intervalMicrosecond: - return int64((date.Nanosecond() % 1000000) / 1000), nil + return int64(date.Nanosecond() / 1000), nil case intervalNanosecond: - return int64(date.Nanosecond() % 1000), nil + return int64(date.Nanosecond()), nil default: return nil, sql3.NewErrCallParameterValueInvalid(0, 0, interval, "interval") @@ -232,6 +256,85 @@ func (n *callPlanExpression) EvaluateToTimestamp(currentRow []interface{}) (inte return featurebase.ValToTimestamp(unit, num) } +func (n *callPlanExpression) EvaluateDateTimeName(currentRow []interface{}) (interface{}, error) { + intervalEval, err := n.args[0].Evaluate(currentRow) + if err != nil { + return nil, err + } + + dateEval, err := n.args[1].Evaluate(currentRow) + if err != nil { + return nil, err + } + + // nil if anything is nil + if intervalEval == nil || dateEval == nil { + return nil, nil + } + + //get the date value + coercedDate, err := coerceValue(n.args[1].Type(), parser.NewDataTypeTimestamp(), dateEval, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + date, dateOk := coercedDate.(time.Time) + if !dateOk { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + //get the interval value + coercedInterval, err := coerceValue(n.args[0].Type(), parser.NewDataTypeString(), intervalEval, parser.Pos{Line: 0, Column: 0}) + if err != nil { + return nil, err + } + + interval, intervalOk := coercedInterval.(string) + if !intervalOk { + return nil, sql3.NewErrInternalf("unable to convert value") + } + + switch strings.ToUpper(interval) { + case intervalYear: + return fmt.Sprint(date.Year()), nil + + case intervalYearDay: + return fmt.Sprint(date.YearDay()), nil + + case intervalMonth: + return fmt.Sprint(date.Month()), nil + + case intervalDay: + return fmt.Sprint(date.Day()), nil + + case intervalWeeKDay: + return fmt.Sprint(date.Weekday()), nil + + case intervalWeek: + _, isoWeek := date.ISOWeek() + return fmt.Sprint(isoWeek), nil + + case intervalHour: + return fmt.Sprint(date.Hour()), nil + + case intervalMinute: + return fmt.Sprint(date.Minute()), nil + + case intervalSecond: + return fmt.Sprint(date.Second()), nil + + case intervalMillisecond: + return fmt.Sprint(date.Nanosecond() / 1000000), nil + case intervalMicrosecond: + return fmt.Sprint(date.Nanosecond() / 1000), nil + case intervalNanosecond: + return fmt.Sprint(date.Nanosecond()), nil + + default: + return nil, sql3.NewErrCallParameterValueInvalid(0, 0, interval, "interval") + } +} + func (n *callPlanExpression) EvaluateDatetimeAdd(currentRow []interface{}) (interface{}, error) { // retrieve param1, timeunit of the value to be added to the target timestamp param1, err := n.args[0].Evaluate(currentRow) diff --git a/sql3/test/defs/defs.go b/sql3/test/defs/defs.go index 3db5aec0b..82221d766 100644 --- a/sql3/test/defs/defs.go +++ b/sql3/test/defs/defs.go @@ -46,8 +46,10 @@ var TableTests []TableTest = []TableTest{ setFunctionTests, setParameterTests, datePartTests, + dateTimeNameTests, toTimestampTests, datetimeAddTests, + stringScalarFunctionsTests, insertTest, diff --git a/sql3/test/defs/defs_date_functions.go b/sql3/test/defs/defs_date_functions.go index d61c01626..8fbaa2345 100644 --- a/sql3/test/defs/defs_date_functions.go +++ b/sql3/test/defs/defs_date_functions.go @@ -175,7 +175,7 @@ var datePartTests = TableTest{ hdr("", fldTypeInt), ), ExpRows: rows( - row(int64(1), int64(200)), + row(int64(1), int64(100200)), ), Compare: CompareExactUnordered, }, @@ -188,7 +188,7 @@ var datePartTests = TableTest{ hdr("", fldTypeInt), ), ExpRows: rows( - row(int64(1), int64(300)), + row(int64(1), int64(100200300)), ), Compare: CompareExactUnordered, }, @@ -269,6 +269,66 @@ var toTimestampTests = TableTest{ }, } +var dateTimeNameTests = TableTest{ + + Table: tbl( + "datetimenametests", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("ts", fldTypeTimestamp), + ), + srcRows( + srcRow(int64(1), knownTimestamp()), + ), + ), + SQLTests: []SQLTest{ + // dateTimeName tests + // make sure returning a string as the year still works + // if this works then the other parts converted to strings of digits should also work + { + SQLs: sqls( + "select _id, datetimename('yy', ts) from dateparttests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "2012"), + ), + Compare: CompareExactUnordered, + }, + //check to make sure it gets a month name correctly + { + SQLs: sqls( + "select _id, datetimename('m', ts) from dateparttests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "November"), + ), + Compare: CompareExactUnordered, + }, + // check to make sure it gets a day of the week correctly + { + SQLs: sqls( + "select _id, datetimename('w', ts) from dateparttests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "Thursday"), + ), + Compare: CompareExactUnordered, + }, + }, +} + // datetimeAdd tests var datetimeAddTests = TableTest{ @@ -420,7 +480,7 @@ var datetimeAddTests = TableTest{ hdr("", fldTypeInt), ), ExpRows: rows( - row(int64(1), int64(201)), + row(int64(1), int64(100201)), ), Compare: CompareExactUnordered, }, @@ -433,7 +493,7 @@ var datetimeAddTests = TableTest{ hdr("", fldTypeInt), ), ExpRows: rows( - row(int64(1), int64(301)), + row(int64(1), int64(100200301)), ), Compare: CompareExactUnordered, }, @@ -462,7 +522,7 @@ var datetimeAddTests = TableTest{ hdr("b", fldTypeInt), ), ExpRows: rows( - row(int64(1), int64(0), int64(201)), + row(int64(1), int64(100201000), int64(100201)), ), Compare: CompareExactUnordered, },