diff --git a/sql3/planner/expression.go b/sql3/planner/expression.go index 6a284a8d1..aa184b147 100644 --- a/sql3/planner/expression.go +++ b/sql3/planner/expression.go @@ -1579,6 +1579,8 @@ func (n *callPlanExpression) Evaluate(currentRow []interface{}) (interface{}, er // time quantum functions case "RANGEQ": return n.EvaluateRangeQ(currentRow) + case "DATETIMEADD": + return n.EvaluateDatetimeAdd(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 51aaf78d1..d77548eae 100644 --- a/sql3/planner/expressionanalyzercall.go +++ b/sql3/planner/expressionanalyzercall.go @@ -260,6 +260,8 @@ func (p *ExecutionPlanner) analyzeCallExpression(ctx context.Context, call *pars case "RANGEQ": return p.analyzeFunctionRangeQ(call, scope) + case "DATETIMEADD": + return p.analyzeFunctionDatetimeAdd(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 49a531c52..57d56a60a 100644 --- a/sql3/planner/inbuiltfunctionsdate.go +++ b/sql3/planner/inbuiltfunctionsdate.go @@ -19,6 +19,7 @@ const intervalHour = "HH" const intervalMinute = "MI" const intervalSecond = "S" const intervalMillisecond = "MS" +const intervalMicrosecond = "US" const intervalNanosecond = "NS" func (p *ExecutionPlanner) analyzeFunctionDatePart(call *parser.Call, scope parser.Statement) (parser.Expr, error) { @@ -38,35 +39,66 @@ func (p *ExecutionPlanner) analyzeFunctionDatePart(call *parser.Call, scope pars return nil, sql3.NewErrParameterTypeMistmatch(call.Args[1].Pos().Line, call.Args[1].Pos().Column, call.Args[1].DataType().TypeDescription(), dateType.TypeDescription()) } - //return int + // return int call.ResultDataType = parser.NewDataTypeInt() return call, nil } func (p *ExecutionPlanner) analyzeFunctionToTimestamp(call *parser.Call, scope parser.Statement) (parser.Expr, error) { - //param1 is the number to be converted to timestamp. This param is required. - //param2 is the time unit of the numeric value in param 1. This param is optional. - //ToTimestamp can be invoked with just param1. + // param1 is the number to be converted to timestamp. This param is required. + // param2 is the time unit of the numeric value in param 1. This param is optional. + // ToTimestamp can be invoked with just param1. if len(call.Args) != 1 && len(call.Args) != 2 { return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 2, len(call.Args)) } - //param1 is a integer of type int64 + // param1 is a integer of type int64 param1Type := parser.NewDataTypeInt() if !typesAreAssignmentCompatible(param1Type, call.Args[0].DataType()) { return nil, sql3.NewErrParameterTypeMistmatch(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Args[0].DataType().TypeDescription(), param1Type.TypeDescription()) } - //param2 is a string and it should be one of 's', 'ms', 'us', 'ns'. - //param2 is optional, will be defaulted to 's' if not supplied. + // param2 is a string and it should be one of 's', 'ms', 'us', 'ns'. + // param2 is optional, will be defaulted to 's' if not supplied. if len(call.Args) == 2 { param2Type := parser.NewDataTypeString() if !typesAreAssignmentCompatible(param2Type, call.Args[1].DataType()) { return nil, sql3.NewErrParameterTypeMistmatch(call.Args[1].Pos().Line, call.Args[1].Pos().Column, call.Args[1].DataType().TypeDescription(), param2Type.TypeDescription()) } } - //ToTimestamp returns a timestamp calculated from param1 using time unit passed in param 2 + // ToTimestamp returns a timestamp calculated from param1 using time unit passed in param 2 + call.ResultDataType = parser.NewDataTypeTimestamp() + return call, nil +} + +func (p *ExecutionPlanner) analyzeFunctionDatetimeAdd(call *parser.Call, scope parser.Statement) (parser.Expr, error) { + // param1 is the time unit of duration to be added to the target timestamp. + // param2 is the time duration to be added to the target timestamp. + // param3 is the target timestamp to which the time duration to be added. + if len(call.Args) != 3 { + return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 3, len(call.Args)) + } + + // param1- time unit is a string and it should be one of 'yy','m','d','hh','mi','s', 'ms', 'us', 'ns'. + param1Type := parser.NewDataTypeString() + if !typesAreAssignmentCompatible(param1Type, call.Args[0].DataType()) { + return nil, sql3.NewErrParameterTypeMistmatch(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Args[0].DataType().TypeDescription(), param1Type.TypeDescription()) + } + + // param2- time duration is a int + param2Type := parser.NewDataTypeInt() + if !typesAreAssignmentCompatible(param2Type, call.Args[1].DataType()) { + return nil, sql3.NewErrParameterTypeMistmatch(call.Args[1].Pos().Line, call.Args[1].Pos().Column, call.Args[1].DataType().TypeDescription(), param2Type.TypeDescription()) + } + + // param3- target datetime to which the duration to be added to + param3Type := parser.NewDataTypeTimestamp() + if !typesAreAssignmentCompatible(param3Type, call.Args[2].DataType()) { + return nil, sql3.NewErrParameterTypeMistmatch(call.Args[2].Pos().Line, call.Args[2].Pos().Column, call.Args[2].DataType().TypeDescription(), param3Type.TypeDescription()) + } + + // DatetimeAdd returns a timestamp calculated by adding param2 to param3 using time unit passed in param 1 call.ResultDataType = parser.NewDataTypeTimestamp() return call, nil } @@ -87,7 +119,7 @@ func (n *callPlanExpression) EvaluateDatepart(currentRow []interface{}) (interfa return nil, nil } - //get the date value + // 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 @@ -98,7 +130,7 @@ func (n *callPlanExpression) EvaluateDatepart(currentRow []interface{}) (interfa return nil, sql3.NewErrInternalf("unable to convert value") } - //get the interval 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 @@ -139,10 +171,13 @@ func (n *callPlanExpression) EvaluateDatepart(currentRow []interface{}) (interfa return int64(date.Second()), nil case intervalMillisecond: - return int64(date.Nanosecond() * 1000 * 1000), nil + return int64(date.Nanosecond() / 1000000), nil + + case intervalMicrosecond: + return int64((date.Nanosecond() % 1000000) / 1000), nil case intervalNanosecond: - return int64(date.Nanosecond()), nil + return int64(date.Nanosecond() % 1000), nil default: return nil, sql3.NewErrCallParameterValueInvalid(0, 0, interval, "interval") @@ -151,48 +186,147 @@ func (n *callPlanExpression) EvaluateDatepart(currentRow []interface{}) (interfa } func (n *callPlanExpression) EvaluateToTimestamp(currentRow []interface{}) (interface{}, error) { - //retrieve param1, the number to be converted to timestamp + // retrieve param1, the number to be converted to timestamp param1, err := n.args[0].Evaluate(currentRow) if err != nil { return nil, err } else if param1 == nil { - //if the param1 is null silently return null timestamp value + // if the param1 is null silently return null timestamp value return nil, nil } coercedParam1, err := coerceValue(n.args[0].Type(), parser.NewDataTypeInt(), param1, parser.Pos{Line: 0, Column: 0}) if err != nil { - //raise error if param 1 is not an integer. Should we return nil instead of raising error here? see note at return. + // raise error if param 1 is not an integer. Should we return nil instead of raising error here? see note at return. return nil, err } num, ok := coercedParam1.(int64) if !ok { - //raise error if param 1 is not an integer. Should we return nil instead of raising error here? see note at return. + // raise error if param 1 is not an integer. Should we return nil instead of raising error here? see note at return. return nil, sql3.NewErrInternalf("unable to convert value") } - //retrieve param2, time unit for param1, if not supplied default to seconds 's'. + // retrieve param2, time unit for param1, if not supplied default to seconds 's'. var unit string = featurebase.TimeUnitSeconds if len(n.args) == 2 { param2, err := n.args[1].Evaluate(currentRow) if err != nil { - //raise error if unable to retieve the argument for param2 + // raise error if unable to retieve the argument for param2 return nil, err } coercedParam2, err := coerceValue(n.args[1].Type(), parser.NewDataTypeString(), param2, parser.Pos{Line: 0, Column: 0}) if err != nil { - //raise error if param2 is not a string + // raise error if param2 is not a string return nil, err } unit, ok = coercedParam2.(string) if !ok { - //raise error if param2 is not a string + // raise error if param2 is not a string return nil, sql3.NewErrInternalf("unable to convert value") } if !featurebase.IsValidTimeUnit(unit) { - //raise error is param2 is not a valid time unit + // raise error is param2 is not a valid time unit return nil, sql3.NewErrCallParameterValueInvalid(0, 0, unit, "timeunit") } } - //should we throw error or return nil if the conversion fails? what is the desired behaviour when ToTimestamp errors for one bad record in a batch of thousands? + // should we throw error or return nil if the conversion fails? what is the desired behaviour when ToTimestamp errors for one bad record in a batch of thousands? return featurebase.ValToTimestamp(unit, num) } + +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) + if err != nil { + return nil, err + } + coercedParam1, err := coerceValue(n.args[0].Type(), parser.NewDataTypeString(), param1, parser.Pos{Line: 0, Column: 0}) + if err != nil { + // raise error if param 1 is not string. + return nil, err + } + timeunit, ok := coercedParam1.(string) + if !ok { + // raise error if param 1 is not string. + return nil, sql3.NewErrInternalf("unable to convert value") + } + + // retrieve param2, timeduration to be added to the target timestamp. + param2, err := n.args[1].Evaluate(currentRow) + if err != nil { + // raise error if unable to retieve the argument for param2 + return nil, err + } + // retrieve param3, target timestamp to which the timeduration to be added to. + param3, err := n.args[2].Evaluate(currentRow) + if err != nil { + // raise error if unable to retieve the argument for param3 + return nil, err + } + if param2 == nil || param3 == nil { + // if either of timeduration or target datetime is null then return null + return nil, nil + } + coercedParam2, err := coerceValue(n.args[1].Type(), parser.NewDataTypeInt(), param2, parser.Pos{Line: 0, Column: 0}) + if err != nil { + // raise error if param2 is not a string + return nil, err + } + timeduration, ok := coercedParam2.(int64) + if !ok { + // raise error if param2 is not a integer + return nil, sql3.NewErrInternalf("unable to convert value") + } + coercedParam3, err := coerceValue(n.args[2].Type(), parser.NewDataTypeTimestamp(), param3, parser.Pos{Line: 0, Column: 0}) + if err != nil { + // raise error if param3 is not a timestamp + return nil, err + } + target, ok := coercedParam3.(time.Time) + if !ok { + // raise error if param3 is not a datetime + return nil, sql3.NewErrInternalf("unable to convert value") + } + if !isValidTimeInterval(strings.ToUpper(timeunit)) { + // raise error if timeunit value is invalid + return nil, sql3.NewErrCallParameterValueInvalid(0, 0, timeunit, "timeunit") + } else if target.IsZero() { + // return nil if target is nil + return nil, nil + } else if timeduration == 0 { + // return target if duration to add is 0 + return target, nil + } + switch strings.ToUpper(timeunit) { + case intervalYear: + return target.AddDate(int(timeduration), 0, 0), nil + case intervalMonth: + return target.AddDate(0, int(timeduration), 0), nil + case intervalDay: + return target.AddDate(0, 0, int(timeduration)), nil + case intervalHour: + return target.Add(time.Hour * time.Duration(timeduration)), nil + case intervalMinute: + return target.Add(time.Minute * time.Duration(timeduration)), nil + case intervalSecond: + return target.Add(time.Second * time.Duration(timeduration)), nil + case intervalMillisecond: + return target.Add(time.Millisecond * time.Duration(timeduration)), nil + case intervalMicrosecond: + return target.Add(time.Microsecond * time.Duration(timeduration)), nil + case intervalNanosecond: + return target.Add(time.Nanosecond * time.Duration(timeduration)), nil + default: + return nil, sql3.NewErrCallParameterValueInvalid(0, 0, timeunit, "timeunit") + } +} + +// isValidTimeInterval returns true if part is valid. +func isValidTimeInterval(unit string) bool { + switch unit { + case intervalYear, intervalYearDay, intervalMonth, intervalDay, intervalWeeKDay, + intervalWeek, intervalHour, intervalMinute, intervalSecond, intervalMillisecond, + intervalMicrosecond, intervalNanosecond: + return true + default: + return false + } +} diff --git a/sql3/test/defs/defs.go b/sql3/test/defs/defs.go index 18764167d..3db5aec0b 100644 --- a/sql3/test/defs/defs.go +++ b/sql3/test/defs/defs.go @@ -46,6 +46,8 @@ var TableTests []TableTest = []TableTest{ setFunctionTests, setParameterTests, datePartTests, + toTimestampTests, + datetimeAddTests, stringScalarFunctionsTests, insertTest, @@ -210,6 +212,16 @@ func knownTimestamp() time.Time { return tm } +func knownSubSecondTimestamp() time.Time { + tm := knownTimestamp() + duration, err := time.ParseDuration("100200300ns") + 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 f5cb037a9..d61c01626 100644 --- a/sql3/test/defs/defs_date_functions.go +++ b/sql3/test/defs/defs_date_functions.go @@ -11,10 +11,10 @@ var datePartTests = TableTest{ srcHdr("_id", fldTypeID), srcHdr("a", fldTypeInt, "min 0", "max 1000"), srcHdr("b", fldTypeInt, "min 0", "max 1000"), - srcHdr("ts", fldTypeTimestamp), + srcHdr("ts", fldTypeTimestamp, "timeunit 'ns'"), ), srcRows( - srcRow(int64(1), int64(10), int64(100), knownTimestamp()), + srcRow(int64(1), int64(10), int64(100), knownSubSecondTimestamp()), ), ), SQLTests: []SQLTest{ @@ -36,30 +36,6 @@ var datePartTests = TableTest{ ), ExpErr: "invalid value '1' for parameter 'interval'", }, - { - SQLs: sqls( - "select totimestamp()", - ), - ExpErr: "count of formal parameters (2) does not match count of actual parameters (0)", - }, - { - SQLs: sqls( - "select totimestamp('a')", - ), - ExpErr: "an expression of type 'string' cannot be passed to a parameter of type 'int'", - }, - { - SQLs: sqls( - "select totimestamp(1, 2)", - ), - ExpErr: "an expression of type 'int' cannot be passed to a parameter of type 'string'", - }, - { - SQLs: sqls( - "select totimestamp(1, 'x')", - ), - ExpErr: "invalid value 'x' for parameter 'timeunit'", - }, { SQLs: sqls( "select _id, datepart('yy', ts) from dateparttests", @@ -186,7 +162,20 @@ var datePartTests = TableTest{ hdr("", fldTypeInt), ), ExpRows: rows( - row(int64(1), int64(0)), + row(int64(1), int64(100)), + ), + Compare: CompareExactUnordered, + }, + { + SQLs: sqls( + "select _id, datepart('us', ts) from dateparttests", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(200)), ), Compare: CompareExactUnordered, }, @@ -199,7 +188,7 @@ var datePartTests = TableTest{ hdr("", fldTypeInt), ), ExpRows: rows( - row(int64(1), int64(0)), + row(int64(1), int64(300)), ), Compare: CompareExactUnordered, }, @@ -218,6 +207,42 @@ var datePartTests = TableTest{ ), Compare: CompareExactUnordered, }, + }, +} + +// toTimestamp tests +var toTimestampTests = TableTest{ + + Table: tbl( + "", + nil, + nil, + ), + SQLTests: []SQLTest{ + { + SQLs: sqls( + "select totimestamp()", + ), + ExpErr: "count of formal parameters (2) does not match count of actual parameters (0)", + }, + { + SQLs: sqls( + "select totimestamp('a')", + ), + ExpErr: "an expression of type 'string' cannot be passed to a parameter of type 'int'", + }, + { + SQLs: sqls( + "select totimestamp(1, 2)", + ), + ExpErr: "an expression of type 'int' cannot be passed to a parameter of type 'string'", + }, + { + SQLs: sqls( + "select totimestamp(1, 'x')", + ), + ExpErr: "invalid value 'x' for parameter 'timeunit'", + }, { //test ToTimestamp(num, timeunit) for all possible time unit values SQLs: sqls( @@ -243,3 +268,230 @@ var datePartTests = TableTest{ }, }, } + +// datetimeAdd tests +var datetimeAddTests = TableTest{ + + Table: tbl( + "datetimeadd", + srcHdrs( + srcHdr("_id", fldTypeID), + srcHdr("ts", fldTypeTimestamp, "timeunit 'ns'"), + ), + srcRows( + srcRow(int64(1), knownSubSecondTimestamp()), + ), + ), + SQLTests: []SQLTest{ + { + SQLs: sqls( + "select datetimeadd()", + ), + ExpErr: "count of formal parameters (3) does not match count of actual parameters (0)", + }, + { + SQLs: sqls( + "select datetimeadd(1,1,current_timestamp)", + ), + ExpErr: "an expression of type 'int' cannot be passed to a parameter of type 'string'", + }, + { + SQLs: sqls( + "select datetimeadd('yy', '2',current_timestamp)", + ), + ExpErr: "an expression of type 'string' cannot be passed to a parameter of type 'int'", + }, + { + SQLs: sqls( + "select datetimeadd('yy', 2, true)", + ), + ExpErr: "an expression of type 'bool' cannot be passed to a parameter of type 'timestamp'", + }, + { + SQLs: sqls( + "select datetimeadd('x',1,current_timestamp)", + ), + ExpErr: "invalid value 'x' for parameter 'timeunit'", + }, + { + SQLs: sqls( + "select datetimeadd('ms',7000,'YYYY-MM-DDTHH:MM:SS')", + ), + ExpErr: "unable to convert 'YYYY-MM-DDTHH:MM:SS' to type 'timestamp'", + }, + //Test datetimeadd() for all possible time units + { + SQLs: sqls( + "select _id, datepart('YY',datetimeadd('YY', 1, ts)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(2013)), + ), + Compare: CompareExactUnordered, + }, + { + SQLs: sqls( + "select _id, datepart('M',datetimeadd('M', 1, ts)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(12)), + ), + Compare: CompareExactUnordered, + }, + { + SQLs: sqls( + "select _id, datepart('D',datetimeadd('D', 1, ts)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(2)), + ), + Compare: CompareExactUnordered, + }, + { + SQLs: sqls( + "select _id, datepart('HH',datetimeadd('HH', 1, ts)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(23)), + ), + Compare: CompareExactUnordered, + }, + { + SQLs: sqls( + "select _id, datepart('MI',datetimeadd('MI', 1, ts)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(9)), + ), + Compare: CompareExactUnordered, + }, + { + SQLs: sqls( + "select _id, datepart('S',datetimeadd('S', 1, ts)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(42)), + ), + Compare: CompareExactUnordered, + }, + { + SQLs: sqls( + "select _id, datepart('MS',datetimeadd('MS', 1, ts)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(101)), + ), + Compare: CompareExactUnordered, + }, + { + SQLs: sqls( + "select _id, datepart('US',datetimeadd('US', 1, ts)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(201)), + ), + Compare: CompareExactUnordered, + }, + { + SQLs: sqls( + "select _id, datepart('NS',datetimeadd('NS', 1, ts)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(301)), + ), + Compare: CompareExactUnordered, + }, + //test datetimeadd() for subtraction + { + SQLs: sqls( + "select _id, datepart('YY',datetimeadd('YY', -1, ts)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(2011)), + ), + Compare: CompareExactUnordered, + }, + //test datetimeadd() for transition + { + SQLs: sqls( + "select _id, datepart('NS',datetimeadd('NS', 700, ts)) as a, datepart('US',datetimeadd('NS', 700, ts)) as b from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("a", fldTypeInt), + hdr("b", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(0), int64(201)), + ), + Compare: CompareExactUnordered, + }, + //test datetimeadd() for literals + { + SQLs: sqls( + "select _id, datepart('YY',datetimeadd('YY', 1, 0)) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(1971)), + ), + Compare: CompareExactUnordered, + }, + { + SQLs: sqls( + "select _id, datepart('YY',datetimeadd('YY', 1, '2023-03-03T00:00:00Z')) from datetimeadd", + ), + ExpHdrs: hdrs( + hdr("_id", fldTypeID), + hdr("", fldTypeInt), + ), + ExpRows: rows( + row(int64(1), int64(2024)), + ), + Compare: CompareExactUnordered, + }, + }, +} diff --git a/sql3/test/defs/types.go b/sql3/test/defs/types.go index 0ebb57dab..9a1a0486c 100644 --- a/sql3/test/defs/types.go +++ b/sql3/test/defs/types.go @@ -242,7 +242,7 @@ func (sr sourceRows) insertTuples(t *testing.T) string { case nil: sb.WriteString("null") case time.Time: - sb.WriteString("'" + v.Format(time.RFC3339) + "'") + sb.WriteString("'" + v.Format(time.RFC3339Nano) + "'") default: t.Fatalf("unsupported cell type: %T", cell)