From 6de130fe39dc5c453648e9bde92b87c2cf06f168 Mon Sep 17 00:00:00 2001 From: Lory Cloutier <118481783+lorycloutier@users.noreply.github.com> Date: Fri, 10 Mar 2023 14:49:47 -0600 Subject: [PATCH] Added date_trunc time/date scalar function (#2312) FB-1961 Added function and test coverage. --- sql3/planner/expression.go | 2 + sql3/planner/expressionanalyzercall.go | 2 + sql3/planner/inbuiltfunctionsdate.go | 83 ++++++++++++ sql3/test/defs/defs.go | 1 + sql3/test/defs/defs_date_functions.go | 178 +++++++++++++++++++++++++ 5 files changed, 266 insertions(+) diff --git a/sql3/planner/expression.go b/sql3/planner/expression.go index 058b92a14..1c6c6a92a 100644 --- a/sql3/planner/expression.go +++ b/sql3/planner/expression.go @@ -1581,6 +1581,8 @@ func (n *callPlanExpression) Evaluate(currentRow []interface{}) (interface{}, er return n.EvaluateStr(currentRow) case "DATETIMENAME": return n.EvaluateDateTimeName(currentRow) + case "DATE_TRUNC": + return n.EvaluateDateTrunc(currentRow) // time quantum functions case "RANGEQ": return n.EvaluateRangeQ(currentRow) diff --git a/sql3/planner/expressionanalyzercall.go b/sql3/planner/expressionanalyzercall.go index dac0451d7..597fbd1ba 100644 --- a/sql3/planner/expressionanalyzercall.go +++ b/sql3/planner/expressionanalyzercall.go @@ -258,6 +258,8 @@ func (p *ExecutionPlanner) analyzeCallExpression(ctx context.Context, call *pars return p.analyseFunctionStr(call, scope) case "DATETIMENAME": return p.analyzeFunctionDateTimeName(call, scope) + case "DATE_TRUNC": + return p.analyzeFunctionDateTrunc(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 6e2a8d7ea..961c4cf4d 100644 --- a/sql3/planner/inbuiltfunctionsdate.go +++ b/sql3/planner/inbuiltfunctionsdate.go @@ -144,6 +144,28 @@ func (p *ExecutionPlanner) analyzeFunctionDateTimeName(call *parser.Call, scope return call, nil } +func (p *ExecutionPlanner) analyzeFunctionDateTrunc(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 +} + // 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 { @@ -539,6 +561,67 @@ func (n *callPlanExpression) EvaluateDatetimeAdd(currentRow []interface{}) (inte return nil, sql3.NewErrCallParameterValueInvalid(0, 0, timeunit, "timeunit") } } +func (n *callPlanExpression) EvaluateDateTrunc(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 date.Format("2006"), nil + case intervalMonth: + return date.Format("2006-01"), nil + case intervalDay: + return date.Format("2006-01-02"), nil + case intervalHour: + return date.Format("2006-01-02T15"), nil + case intervalMinute: + return date.Format("2006-01-02T15:04"), nil + case intervalSecond: + return date.Format("2006-01-02T15:04:05"), nil + case intervalMillisecond: + return date.Format("2006-01-02T15:04:05.000"), nil + case intervalMicrosecond: + return date.Format("2006-01-02T15:04:05.000000"), nil + case intervalNanosecond: + return date.Format("2006-01-02T15:04:05.000000000"), nil + default: + return nil, sql3.NewErrCallParameterValueInvalid(0, 0, interval, "interval") + } +} // isValidTimeInterval returns true if part is valid. func isValidTimeInterval(unit string) bool { diff --git a/sql3/test/defs/defs.go b/sql3/test/defs/defs.go index 3cc449b47..b348960df 100644 --- a/sql3/test/defs/defs.go +++ b/sql3/test/defs/defs.go @@ -49,6 +49,7 @@ var TableTests []TableTest = []TableTest{ dateTimeNameTests, toTimestampTests, datetimeAddTests, + dateTruncTests, datetimedifftests, stringScalarFunctionsTests, diff --git a/sql3/test/defs/defs_date_functions.go b/sql3/test/defs/defs_date_functions.go index 94e2c9b6b..70f2d7865 100644 --- a/sql3/test/defs/defs_date_functions.go +++ b/sql3/test/defs/defs_date_functions.go @@ -659,6 +659,184 @@ var datetimeAddTests = TableTest{ }, } +var dateTruncTests = TableTest{ + + Table: tbl( + "datetrunctests", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("ts", fldTypeTimestamp, "timeunit 'ns'"), + ), + srcRows( + srcRow(int64(1), knownSubSecondTimestamp()), + ), + ), + + SQLTests: []SQLTest{ + { + name: "DateTruncIncorrectParamsCount", + SQLs: sqls( + "select date_trunc()", + ), + ExpErr: "count of formal parameters (2) does not match count of actual parameters (0)", + }, + { + name: "DateTruncTypeError", + SQLs: sqls( + "select date_trunc(1, 2)", + ), + ExpErr: "an expression of type 'int' cannot be passed to a parameter of type 'string'", + }, + { + name: "DateTruncInvalidParam", + SQLs: sqls( + "select date_trunc('1', current_timestamp)", + ), + ExpErr: "invalid value '1' for parameter 'interval'", + }, + { + name: "DateTruncOnYear", + SQLs: sqls( + "select _id, date_trunc('yy', ts) from datetrunctests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "2012"), + ), + Compare: CompareExactUnordered, + }, + { + name: "DateTruncOnMonth", + SQLs: sqls( + "select _id, date_trunc('m', ts) from datetrunctests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "2012-11"), + ), + Compare: CompareExactUnordered, + }, + { + name: "DateTruncOnDay", + SQLs: sqls( + "select _id, date_trunc('d', ts) from datetrunctests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "2012-11-01"), + ), + Compare: CompareExactUnordered, + }, + { + name: "DateTruncOnHour", + SQLs: sqls( + "select _id, date_trunc('hh', ts) from datetrunctests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "2012-11-01T22"), + ), + Compare: CompareExactUnordered, + }, + { + name: "DateTruncOnMinute", + SQLs: sqls( + "select _id, date_trunc('mi', ts) from datetrunctests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "2012-11-01T22:08"), + ), + Compare: CompareExactUnordered, + }, + { + name: "DateTruncOnSecond", + SQLs: sqls( + "select _id, date_trunc('s', ts) from datetrunctests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "2012-11-01T22:08:41"), + ), + Compare: CompareExactUnordered, + }, + { + name: "DateTruncOnMilliS", + SQLs: sqls( + "select _id, date_trunc('ms', ts) from datetrunctests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "2012-11-01T22:08:41.100"), + ), + Compare: CompareExactUnordered, + }, + { + name: "DateTruncOnMicroS", + SQLs: sqls( + "select _id, date_trunc('us', ts) from datetrunctests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "2012-11-01T22:08:41.100200"), + ), + Compare: CompareExactUnordered, + }, + { + name: "DateTruncOnNanoS", + SQLs: sqls( + "select _id, date_trunc('ns', ts) from datetrunctests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "2012-11-01T22:08:41.100200300"), + ), + Compare: CompareExactUnordered, + }, + { + name: "VerifyTimeStamp", + SQLs: sqls( + "select _id, datetimename('ns', ts) from datetrunctests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeString), + ), + ExpRows: rows( + row(int64(1), "100200300"), + ), + Compare: CompareExactUnordered, + }, + }, +} + var datetimedifftests = TableTest{ name: "DatetimeDiff", Table: tbl("dttable",