mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* Removed support for EPOCH column constraint from TIMESTAMP SQL data type. * Implicit conversion of integers to timestamp will treat the integer value as seconds since unix epoch. * Add new ToTimeStamp(num, timeunit) SQL scalar function to help convert integer values to timestamp.
198 lines
6.5 KiB
Go
198 lines
6.5 KiB
Go
package planner
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
featurebase "github.com/featurebasedb/featurebase/v3"
|
|
"github.com/featurebasedb/featurebase/v3/sql3"
|
|
"github.com/featurebasedb/featurebase/v3/sql3/parser"
|
|
)
|
|
|
|
const intervalYear = "YY"
|
|
const intervalYearDay = "YD"
|
|
const intervalMonth = "M"
|
|
const intervalDay = "D"
|
|
const intervalWeeKDay = "W"
|
|
const intervalWeek = "WK"
|
|
const intervalHour = "HH"
|
|
const intervalMinute = "MI"
|
|
const intervalSecond = "S"
|
|
const intervalMillisecond = "MS"
|
|
const intervalNanosecond = "NS"
|
|
|
|
func (p *ExecutionPlanner) analyzeFunctionDatePart(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.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.
|
|
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
|
|
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.
|
|
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
|
|
call.ResultDataType = parser.NewDataTypeTimestamp()
|
|
return call, nil
|
|
}
|
|
|
|
func (n *callPlanExpression) EvaluateDatepart(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 int64(date.Year()), nil
|
|
|
|
case intervalYearDay:
|
|
return int64(date.YearDay()), nil
|
|
|
|
case intervalMonth:
|
|
return int64(date.Month()), nil
|
|
|
|
case intervalDay:
|
|
return int64(date.Day()), nil
|
|
|
|
case intervalWeeKDay:
|
|
return int64(date.Weekday()), nil
|
|
|
|
case intervalWeek:
|
|
_, isoWeek := date.ISOWeek()
|
|
return int64(isoWeek), nil
|
|
|
|
case intervalHour:
|
|
return int64(date.Hour()), nil
|
|
|
|
case intervalMinute:
|
|
return int64(date.Minute()), nil
|
|
|
|
case intervalSecond:
|
|
return int64(date.Second()), nil
|
|
|
|
case intervalMillisecond:
|
|
return int64(date.Nanosecond() * 1000 * 1000), nil
|
|
|
|
case intervalNanosecond:
|
|
return int64(date.Nanosecond()), nil
|
|
|
|
default:
|
|
return nil, sql3.NewErrCallParameterValueInvalid(0, 0, interval, "interval")
|
|
}
|
|
|
|
}
|
|
|
|
func (n *callPlanExpression) EvaluateToTimestamp(currentRow []interface{}) (interface{}, error) {
|
|
//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
|
|
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.
|
|
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.
|
|
return nil, sql3.NewErrInternalf("unable to convert value")
|
|
}
|
|
|
|
//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
|
|
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
|
|
return nil, err
|
|
}
|
|
unit, ok = coercedParam2.(string)
|
|
if !ok {
|
|
//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
|
|
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?
|
|
return featurebase.ValToTimestamp(unit, num)
|
|
}
|