mirror of
https://github.com/minio/minio.git
synced 2025-01-12 07:23:23 -05:00
Add support for Timestamp data type in SQL Select (#7185)
This change adds support for casting strings to Timestamp via CAST: `CAST('2010T' AS TIMESTAMP)` It also implements the following date-time functions: - UTCNOW() - DATE_ADD() - DATE_DIFF() - EXTRACT() For values passed to these functions, date-types are automatically inferred.
This commit is contained in:
parent
ea6d61ab1f
commit
f04f8bbc78
@ -103,6 +103,6 @@ For a more detailed SELECT SQL reference, please see [here](https://docs.aws.ama
|
|||||||
- All [operators](https://docs.aws.amazon.com/AmazonS3/latest/dev/s3-glacier-select-sql-reference-operators.html) are supported.
|
- All [operators](https://docs.aws.amazon.com/AmazonS3/latest/dev/s3-glacier-select-sql-reference-operators.html) are supported.
|
||||||
- All aggregation, conditional, type-conversion and string functions are supported.
|
- All aggregation, conditional, type-conversion and string functions are supported.
|
||||||
- JSON path expressions such as `FROM S3Object[*].path` are not yet evaluated.
|
- JSON path expressions such as `FROM S3Object[*].path` are not yet evaluated.
|
||||||
- Large numbers (more than 64-bit) are not yet supported.
|
- Large numbers (outside of the signed 64-bit range) are not yet supported.
|
||||||
- Date [functions](https://docs.aws.amazon.com/AmazonS3/latest/dev/s3-glacier-select-sql-reference-date.html) are not yet supported (EXTRACT, DATE_DIFF, etc).
|
- The Date [functions](https://docs.aws.amazon.com/AmazonS3/latest/dev/s3-glacier-select-sql-reference-date.html) `DATE_ADD`, `DATE_DIFF`, `EXTRACT` and `UTCNOW` along with type conversion using `CAST` to the `TIMESTAMP` data type are currently supported.
|
||||||
- AWS S3's [reserved keywords](https://docs.aws.amazon.com/AmazonS3/latest/dev/s3-glacier-select-sql-reference-keyword-list.html) list is not yet respected.
|
- AWS S3's [reserved keywords](https://docs.aws.amazon.com/AmazonS3/latest/dev/s3-glacier-select-sql-reference-keyword-list.html) list is not yet respected.
|
||||||
|
@ -60,6 +60,8 @@ func (r *Record) Set(name string, value *sql.Value) (err error) {
|
|||||||
v = f
|
v = f
|
||||||
} else if i, ok := value.ToInt(); ok {
|
} else if i, ok := value.ToInt(); ok {
|
||||||
v = i
|
v = i
|
||||||
|
} else if t, ok := value.ToTimestamp(); ok {
|
||||||
|
v = sql.FormatSQLTimestamp(t)
|
||||||
} else if s, ok := value.ToString(); ok {
|
} else if s, ok := value.ToString(); ok {
|
||||||
v = s
|
v = s
|
||||||
} else if value.IsNull() {
|
} else if value.IsNull() {
|
||||||
|
@ -201,6 +201,16 @@ func (e *FuncExpr) analyze(s *Select) (result qProp) {
|
|||||||
case sqlFnExtract:
|
case sqlFnExtract:
|
||||||
return e.Extract.From.analyze(s)
|
return e.Extract.From.analyze(s)
|
||||||
|
|
||||||
|
case sqlFnDateAdd:
|
||||||
|
result.combine(e.DateAdd.Quantity.analyze(s))
|
||||||
|
result.combine(e.DateAdd.Timestamp.analyze(s))
|
||||||
|
return result
|
||||||
|
|
||||||
|
case sqlFnDateDiff:
|
||||||
|
result.combine(e.DateDiff.Timestamp1.analyze(s))
|
||||||
|
result.combine(e.DateDiff.Timestamp2.analyze(s))
|
||||||
|
return result
|
||||||
|
|
||||||
// Handle aggregation function calls
|
// Handle aggregation function calls
|
||||||
case aggFnAvg, aggFnMax, aggFnMin, aggFnSum, aggFnCount:
|
case aggFnAvg, aggFnMax, aggFnMin, aggFnSum, aggFnCount:
|
||||||
// Initialize accumulator
|
// Initialize accumulator
|
||||||
@ -283,6 +293,11 @@ func (e *FuncExpr) analyze(s *Select) (result qProp) {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
case sqlFnUTCNow:
|
||||||
|
if len(e.SFunc.ArgsList) != 0 {
|
||||||
|
result.err = fmt.Errorf("%s() takes no arguments", string(funcName))
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement other functions
|
// TODO: implement other functions
|
||||||
|
@ -305,10 +305,9 @@ func (e *UnaryTerm) evalNode(r Record) (*Value, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inferTypeForArithOp(v)
|
inferTypeForArithOp(v)
|
||||||
if ival, ok := v.ToInt(); ok {
|
v.negate()
|
||||||
return FromInt(-ival), nil
|
if v.isNumeric() {
|
||||||
} else if fval, ok := v.ToFloat(); ok {
|
return v, nil
|
||||||
return FromFloat(-fval), nil
|
|
||||||
}
|
}
|
||||||
return nil, errArithMismatchedTypes
|
return nil, errArithMismatchedTypes
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FuncName - SQL function name.
|
// FuncName - SQL function name.
|
||||||
@ -52,21 +53,10 @@ const (
|
|||||||
sqlFnUpper FuncName = "UPPER"
|
sqlFnUpper FuncName = "UPPER"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Allowed cast types
|
|
||||||
const (
|
|
||||||
castBool = "BOOL"
|
|
||||||
castInt = "INT"
|
|
||||||
castInteger = "INTEGER"
|
|
||||||
castString = "STRING"
|
|
||||||
castFloat = "FLOAT"
|
|
||||||
castDecimal = "DECIMAL"
|
|
||||||
castNumeric = "NUMERIC"
|
|
||||||
castTimestamp = "TIMESTAMP"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errUnimplementedCast = errors.New("This cast not yet implemented")
|
errUnimplementedCast = errors.New("This cast not yet implemented")
|
||||||
errNonStringTrimArg = errors.New("TRIM() received a non-string argument")
|
errNonStringTrimArg = errors.New("TRIM() received a non-string argument")
|
||||||
|
errNonTimestampArg = errors.New("Expected a timestamp argument")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *FuncExpr) getFunctionName() FuncName {
|
func (e *FuncExpr) getFunctionName() FuncName {
|
||||||
@ -83,6 +73,10 @@ func (e *FuncExpr) getFunctionName() FuncName {
|
|||||||
return sqlFnExtract
|
return sqlFnExtract
|
||||||
case e.Trim != nil:
|
case e.Trim != nil:
|
||||||
return sqlFnTrim
|
return sqlFnTrim
|
||||||
|
case e.DateAdd != nil:
|
||||||
|
return sqlFnDateAdd
|
||||||
|
case e.DateDiff != nil:
|
||||||
|
return sqlFnDateDiff
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -102,10 +96,17 @@ func (e *FuncExpr) evalSQLFnNode(r Record) (res *Value, err error) {
|
|||||||
return handleSQLSubstring(r, e.Substring)
|
return handleSQLSubstring(r, e.Substring)
|
||||||
|
|
||||||
case sqlFnExtract:
|
case sqlFnExtract:
|
||||||
return nil, errNotImplemented
|
return handleSQLExtract(r, e.Extract)
|
||||||
|
|
||||||
case sqlFnTrim:
|
case sqlFnTrim:
|
||||||
return handleSQLTrim(r, e.Trim)
|
return handleSQLTrim(r, e.Trim)
|
||||||
|
|
||||||
|
case sqlFnDateAdd:
|
||||||
|
return handleDateAdd(r, e.DateAdd)
|
||||||
|
|
||||||
|
case sqlFnDateDiff:
|
||||||
|
return handleDateDiff(r, e.DateDiff)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For all simple argument functions, we evaluate the arguments here
|
// For all simple argument functions, we evaluate the arguments here
|
||||||
@ -119,30 +120,33 @@ func (e *FuncExpr) evalSQLFnNode(r Record) (res *Value, err error) {
|
|||||||
|
|
||||||
switch e.getFunctionName() {
|
switch e.getFunctionName() {
|
||||||
case sqlFnCoalesce:
|
case sqlFnCoalesce:
|
||||||
return coalesce(r, argVals)
|
return coalesce(argVals)
|
||||||
|
|
||||||
case sqlFnNullIf:
|
case sqlFnNullIf:
|
||||||
return nullif(r, argVals[0], argVals[1])
|
return nullif(argVals[0], argVals[1])
|
||||||
|
|
||||||
case sqlFnCharLength, sqlFnCharacterLength:
|
case sqlFnCharLength, sqlFnCharacterLength:
|
||||||
return charlen(r, argVals[0])
|
return charlen(argVals[0])
|
||||||
|
|
||||||
case sqlFnLower:
|
case sqlFnLower:
|
||||||
return lowerCase(r, argVals[0])
|
return lowerCase(argVals[0])
|
||||||
|
|
||||||
case sqlFnUpper:
|
case sqlFnUpper:
|
||||||
return upperCase(r, argVals[0])
|
return upperCase(argVals[0])
|
||||||
|
|
||||||
case sqlFnDateAdd, sqlFnDateDiff, sqlFnToString, sqlFnToTimestamp, sqlFnUTCNow:
|
case sqlFnUTCNow:
|
||||||
|
return handleUTCNow()
|
||||||
|
|
||||||
|
case sqlFnToString, sqlFnToTimestamp:
|
||||||
// TODO: implement
|
// TODO: implement
|
||||||
fallthrough
|
fallthrough
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, errInvalidASTNode
|
return nil, errNotImplemented
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func coalesce(r Record, args []*Value) (res *Value, err error) {
|
func coalesce(args []*Value) (res *Value, err error) {
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
if arg.IsNull() {
|
if arg.IsNull() {
|
||||||
continue
|
continue
|
||||||
@ -152,7 +156,7 @@ func coalesce(r Record, args []*Value) (res *Value, err error) {
|
|||||||
return FromNull(), nil
|
return FromNull(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func nullif(r Record, v1, v2 *Value) (res *Value, err error) {
|
func nullif(v1, v2 *Value) (res *Value, err error) {
|
||||||
// Handle Null cases
|
// Handle Null cases
|
||||||
if v1.IsNull() || v2.IsNull() {
|
if v1.IsNull() || v2.IsNull() {
|
||||||
return v1, nil
|
return v1, nil
|
||||||
@ -185,7 +189,7 @@ func nullif(r Record, v1, v2 *Value) (res *Value, err error) {
|
|||||||
return v1, nil
|
return v1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func charlen(r Record, v *Value) (*Value, error) {
|
func charlen(v *Value) (*Value, error) {
|
||||||
inferTypeAsString(v)
|
inferTypeAsString(v)
|
||||||
s, ok := v.ToString()
|
s, ok := v.ToString()
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -195,7 +199,7 @@ func charlen(r Record, v *Value) (*Value, error) {
|
|||||||
return FromInt(int64(len(s))), nil
|
return FromInt(int64(len(s))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func lowerCase(r Record, v *Value) (*Value, error) {
|
func lowerCase(v *Value) (*Value, error) {
|
||||||
inferTypeAsString(v)
|
inferTypeAsString(v)
|
||||||
s, ok := v.ToString()
|
s, ok := v.ToString()
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -205,7 +209,7 @@ func lowerCase(r Record, v *Value) (*Value, error) {
|
|||||||
return FromString(strings.ToLower(s)), nil
|
return FromString(strings.ToLower(s)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func upperCase(r Record, v *Value) (*Value, error) {
|
func upperCase(v *Value) (*Value, error) {
|
||||||
inferTypeAsString(v)
|
inferTypeAsString(v)
|
||||||
s, ok := v.ToString()
|
s, ok := v.ToString()
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -215,6 +219,58 @@ func upperCase(r Record, v *Value) (*Value, error) {
|
|||||||
return FromString(strings.ToUpper(s)), nil
|
return FromString(strings.ToUpper(s)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleDateAdd(r Record, d *DateAddFunc) (*Value, error) {
|
||||||
|
q, err := d.Quantity.evalNode(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inferTypeForArithOp(q)
|
||||||
|
qty, ok := q.ToFloat()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("QUANTITY must be a numeric argument to %s()", sqlFnDateAdd)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, err := d.Timestamp.evalNode(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inferTypeAsTimestamp(ts)
|
||||||
|
t, ok := ts.ToTimestamp()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s() expects a timestamp argument", sqlFnDateAdd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dateAdd(strings.ToUpper(d.DatePart), qty, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDateDiff(r Record, d *DateDiffFunc) (*Value, error) {
|
||||||
|
tval1, err := d.Timestamp1.evalNode(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inferTypeAsTimestamp(tval1)
|
||||||
|
ts1, ok := tval1.ToTimestamp()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s() expects two timestamp arguments", sqlFnDateDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
tval2, err := d.Timestamp2.evalNode(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inferTypeAsTimestamp(tval2)
|
||||||
|
ts2, ok := tval2.ToTimestamp()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s() expects two timestamp arguments", sqlFnDateDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dateDiff(strings.ToUpper(d.DatePart), ts1, ts2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUTCNow() (*Value, error) {
|
||||||
|
return FromTimestamp(time.Now().UTC()), nil
|
||||||
|
}
|
||||||
|
|
||||||
func handleSQLSubstring(r Record, e *SubstringFunc) (val *Value, err error) {
|
func handleSQLSubstring(r Record, e *SubstringFunc) (val *Value, err error) {
|
||||||
// Both forms `SUBSTRING('abc' FROM 2 FOR 1)` and
|
// Both forms `SUBSTRING('abc' FROM 2 FOR 1)` and
|
||||||
// SUBSTRING('abc', 2, 1) are supported.
|
// SUBSTRING('abc', 2, 1) are supported.
|
||||||
@ -301,6 +357,22 @@ func handleSQLTrim(r Record, e *TrimFunc) (res *Value, err error) {
|
|||||||
return FromString(result), nil
|
return FromString(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleSQLExtract(r Record, e *ExtractFunc) (res *Value, err error) {
|
||||||
|
timeVal, verr := e.From.evalNode(r)
|
||||||
|
if verr != nil {
|
||||||
|
return nil, verr
|
||||||
|
}
|
||||||
|
|
||||||
|
inferTypeAsTimestamp(timeVal)
|
||||||
|
|
||||||
|
t, ok := timeVal.ToTimestamp()
|
||||||
|
if !ok {
|
||||||
|
return nil, errNonTimestampArg
|
||||||
|
}
|
||||||
|
|
||||||
|
return extract(strings.ToUpper(e.Timeword), t)
|
||||||
|
}
|
||||||
|
|
||||||
func errUnsupportedCast(fromType, toType string) error {
|
func errUnsupportedCast(fromType, toType string) error {
|
||||||
return fmt.Errorf("Cannot cast from %v to %v", fromType, toType)
|
return fmt.Errorf("Cannot cast from %v to %v", fromType, toType)
|
||||||
}
|
}
|
||||||
@ -309,12 +381,23 @@ func errCastFailure(msg string) error {
|
|||||||
return fmt.Errorf("Error casting: %s", msg)
|
return fmt.Errorf("Error casting: %s", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allowed cast types
|
||||||
|
const (
|
||||||
|
castBool = "BOOL"
|
||||||
|
castInt = "INT"
|
||||||
|
castInteger = "INTEGER"
|
||||||
|
castString = "STRING"
|
||||||
|
castFloat = "FLOAT"
|
||||||
|
castDecimal = "DECIMAL"
|
||||||
|
castNumeric = "NUMERIC"
|
||||||
|
castTimestamp = "TIMESTAMP"
|
||||||
|
)
|
||||||
|
|
||||||
func (e *Expression) castTo(r Record, castType string) (res *Value, err error) {
|
func (e *Expression) castTo(r Record, castType string) (res *Value, err error) {
|
||||||
v, err := e.evalNode(r)
|
v, err := e.evalNode(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fmt.Println("Cast to ", castType)
|
|
||||||
|
|
||||||
switch castType {
|
switch castType {
|
||||||
case castInt, castInteger:
|
case castInt, castInteger:
|
||||||
@ -329,7 +412,15 @@ func (e *Expression) castTo(r Record, castType string) (res *Value, err error) {
|
|||||||
s, err := stringCast(v)
|
s, err := stringCast(v)
|
||||||
return FromString(s), err
|
return FromString(s), err
|
||||||
|
|
||||||
case castBool, castDecimal, castNumeric, castTimestamp:
|
case castTimestamp:
|
||||||
|
t, err := timestampCast(v)
|
||||||
|
return FromTimestamp(t), err
|
||||||
|
|
||||||
|
case castBool:
|
||||||
|
b, err := boolCast(v)
|
||||||
|
return FromBool(b), err
|
||||||
|
|
||||||
|
case castDecimal, castNumeric:
|
||||||
fallthrough
|
fallthrough
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -431,3 +522,44 @@ func stringCast(v *Value) (string, error) {
|
|||||||
// This does not happen
|
// This does not happen
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func timestampCast(v *Value) (t time.Time, _ error) {
|
||||||
|
switch v.vType {
|
||||||
|
case typeString:
|
||||||
|
s, _ := v.ToString()
|
||||||
|
return parseSQLTimestamp(s)
|
||||||
|
case typeBytes:
|
||||||
|
b, _ := v.ToBytes()
|
||||||
|
return parseSQLTimestamp(string(b))
|
||||||
|
case typeTimestamp:
|
||||||
|
t, _ = v.ToTimestamp()
|
||||||
|
return t, nil
|
||||||
|
default:
|
||||||
|
return t, errCastFailure(fmt.Sprintf("cannot cast %v to Timestamp type", v.GetTypeString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolCast(v *Value) (b bool, _ error) {
|
||||||
|
sToB := func(s string) (bool, error) {
|
||||||
|
if s == "true" {
|
||||||
|
return true, nil
|
||||||
|
} else if s == "false" {
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
return false, errCastFailure("cannot cast to Bool")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch v.vType {
|
||||||
|
case typeBool:
|
||||||
|
b, _ := v.ToBool()
|
||||||
|
return b, nil
|
||||||
|
case typeString:
|
||||||
|
s, _ := v.ToString()
|
||||||
|
return sToB(strings.ToLower(s))
|
||||||
|
case typeBytes:
|
||||||
|
b, _ := v.ToBytes()
|
||||||
|
return sToB(strings.ToLower(string(b)))
|
||||||
|
default:
|
||||||
|
return false, errCastFailure("cannot cast %v to Bool")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -247,6 +247,8 @@ type FuncExpr struct {
|
|||||||
Substring *SubstringFunc `parser:"| @@"`
|
Substring *SubstringFunc `parser:"| @@"`
|
||||||
Extract *ExtractFunc `parser:"| @@"`
|
Extract *ExtractFunc `parser:"| @@"`
|
||||||
Trim *TrimFunc `parser:"| @@"`
|
Trim *TrimFunc `parser:"| @@"`
|
||||||
|
DateAdd *DateAddFunc `parser:"| @@"`
|
||||||
|
DateDiff *DateDiffFunc `parser:"| @@"`
|
||||||
|
|
||||||
// Used during evaluation for aggregation funcs
|
// Used during evaluation for aggregation funcs
|
||||||
aggregate *aggVal
|
aggregate *aggVal
|
||||||
@ -255,7 +257,7 @@ type FuncExpr struct {
|
|||||||
// SimpleArgFunc represents functions with simple expression
|
// SimpleArgFunc represents functions with simple expression
|
||||||
// arguments.
|
// arguments.
|
||||||
type SimpleArgFunc struct {
|
type SimpleArgFunc struct {
|
||||||
FunctionName string `parser:" @(\"AVG\" | \"MAX\" | \"MIN\" | \"SUM\" | \"COALESCE\" | \"NULLIF\" | \"DATE_ADD\" | \"DATE_DIFF\" | \"TO_STRING\" | \"TO_TIMESTAMP\" | \"UTCNOW\" | \"CHAR_LENGTH\" | \"CHARACTER_LENGTH\" | \"LOWER\" | \"UPPER\") "`
|
FunctionName string `parser:" @(\"AVG\" | \"MAX\" | \"MIN\" | \"SUM\" | \"COALESCE\" | \"NULLIF\" | \"TO_STRING\" | \"TO_TIMESTAMP\" | \"UTCNOW\" | \"CHAR_LENGTH\" | \"CHARACTER_LENGTH\" | \"LOWER\" | \"UPPER\") "`
|
||||||
|
|
||||||
ArgsList []*Expression `parser:"\"(\" (@@ (\",\" @@)*)?\")\""`
|
ArgsList []*Expression `parser:"\"(\" (@@ (\",\" @@)*)?\")\""`
|
||||||
}
|
}
|
||||||
@ -294,6 +296,20 @@ type TrimFunc struct {
|
|||||||
TrimFrom *PrimaryTerm `parser:" \"FROM\" )? @@ \")\" "`
|
TrimFrom *PrimaryTerm `parser:" \"FROM\" )? @@ \")\" "`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DateAddFunc represents the DATE_ADD function
|
||||||
|
type DateAddFunc struct {
|
||||||
|
DatePart string `parser:" \"DATE_ADD\" \"(\" @( \"YEAR\":Timeword | \"MONTH\":Timeword | \"DAY\":Timeword | \"HOUR\":Timeword | \"MINUTE\":Timeword | \"SECOND\":Timeword ) \",\""`
|
||||||
|
Quantity *Operand `parser:" @@ \",\""`
|
||||||
|
Timestamp *PrimaryTerm `parser:" @@ \")\""`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DateDiffFunc represents the DATE_DIFF function
|
||||||
|
type DateDiffFunc struct {
|
||||||
|
DatePart string `parser:" \"DATE_DIFF\" \"(\" @( \"YEAR\":Timeword | \"MONTH\":Timeword | \"DAY\":Timeword | \"HOUR\":Timeword | \"MINUTE\":Timeword | \"SECOND\":Timeword ) \",\" "`
|
||||||
|
Timestamp1 *PrimaryTerm `parser:" @@ \",\" "`
|
||||||
|
Timestamp2 *PrimaryTerm `parser:" @@ \")\" "`
|
||||||
|
}
|
||||||
|
|
||||||
// LitValue represents a literal value parsed from the sql
|
// LitValue represents a literal value parsed from the sql
|
||||||
type LitValue struct {
|
type LitValue struct {
|
||||||
Number *float64 `parser:"( @Number"`
|
Number *float64 `parser:"( @Number"`
|
||||||
|
@ -84,7 +84,6 @@ func ParseSelectStatement(s string) (stmt SelectStatement, err error) {
|
|||||||
stmt.selectQProp = selectAST.Expression.analyze(&selectAST)
|
stmt.selectQProp = selectAST.Expression.analyze(&selectAST)
|
||||||
err = stmt.selectQProp.err
|
err = stmt.selectQProp.err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Got Analysis err:", err)
|
|
||||||
err = errQueryAnalysisFailure(err)
|
err = errQueryAnalysisFailure(err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
199
pkg/s3select/sql/timestampfuncs.go
Normal file
199
pkg/s3select/sql/timestampfuncs.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2019 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
layoutYear = "2006T"
|
||||||
|
layoutMonth = "2006-01T"
|
||||||
|
layoutDay = "2006-01-02T"
|
||||||
|
layoutMinute = "2006-01-02T15:04Z07:00"
|
||||||
|
layoutSecond = "2006-01-02T15:04:05Z07:00"
|
||||||
|
layoutNanosecond = "2006-01-02T15:04:05.999999999Z07:00"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tformats = []string{
|
||||||
|
layoutYear,
|
||||||
|
layoutMonth,
|
||||||
|
layoutDay,
|
||||||
|
layoutMinute,
|
||||||
|
layoutSecond,
|
||||||
|
layoutNanosecond,
|
||||||
|
}
|
||||||
|
oneNanoSecond = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseSQLTimestamp(s string) (t time.Time, err error) {
|
||||||
|
for _, f := range tformats {
|
||||||
|
t, err = time.Parse(f, s)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatSQLTimestamp - returns the a string representation of the
|
||||||
|
// timestamp as used in S3 Select
|
||||||
|
func FormatSQLTimestamp(t time.Time) string {
|
||||||
|
_, zoneOffset := t.Zone()
|
||||||
|
hasZone := zoneOffset != 0
|
||||||
|
hasFracSecond := t.Nanosecond() != 0
|
||||||
|
hasSecond := t.Second() != 0
|
||||||
|
hasTime := t.Hour() != 0 || t.Minute() != 0
|
||||||
|
hasDay := t.Day() != 1
|
||||||
|
hasMonth := t.Month() != 1
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case hasFracSecond:
|
||||||
|
return t.Format(layoutNanosecond)
|
||||||
|
case hasSecond:
|
||||||
|
return t.Format(layoutSecond)
|
||||||
|
case hasTime || hasZone:
|
||||||
|
return t.Format(layoutMinute)
|
||||||
|
case hasDay:
|
||||||
|
return t.Format(layoutDay)
|
||||||
|
case hasMonth:
|
||||||
|
return t.Format(layoutMonth)
|
||||||
|
default:
|
||||||
|
return t.Format(layoutYear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
timePartYear = "YEAR"
|
||||||
|
timePartMonth = "MONTH"
|
||||||
|
timePartDay = "DAY"
|
||||||
|
timePartHour = "HOUR"
|
||||||
|
timePartMinute = "MINUTE"
|
||||||
|
timePartSecond = "SECOND"
|
||||||
|
timePartTimezoneHour = "TIMEZONE_HOUR"
|
||||||
|
timePartTimezoneMinute = "TIMEZONE_MINUTE"
|
||||||
|
)
|
||||||
|
|
||||||
|
func extract(what string, t time.Time) (v *Value, err error) {
|
||||||
|
switch what {
|
||||||
|
case timePartYear:
|
||||||
|
return FromInt(int64(t.Year())), nil
|
||||||
|
case timePartMonth:
|
||||||
|
return FromInt(int64(t.Month())), nil
|
||||||
|
case timePartDay:
|
||||||
|
return FromInt(int64(t.Day())), nil
|
||||||
|
case timePartHour:
|
||||||
|
return FromInt(int64(t.Hour())), nil
|
||||||
|
case timePartMinute:
|
||||||
|
return FromInt(int64(t.Minute())), nil
|
||||||
|
case timePartSecond:
|
||||||
|
return FromInt(int64(t.Second())), nil
|
||||||
|
case timePartTimezoneHour:
|
||||||
|
_, zoneOffset := t.Zone()
|
||||||
|
return FromInt(int64(zoneOffset / 3600)), nil
|
||||||
|
case timePartTimezoneMinute:
|
||||||
|
_, zoneOffset := t.Zone()
|
||||||
|
return FromInt(int64((zoneOffset % 3600) / 60)), nil
|
||||||
|
default:
|
||||||
|
// This does not happen
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dateAdd(timePart string, qty float64, t time.Time) (*Value, error) {
|
||||||
|
var duration time.Duration
|
||||||
|
switch timePart {
|
||||||
|
case timePartYear:
|
||||||
|
return FromTimestamp(t.AddDate(int(qty), 0, 0)), nil
|
||||||
|
case timePartMonth:
|
||||||
|
return FromTimestamp(t.AddDate(0, int(qty), 0)), nil
|
||||||
|
case timePartDay:
|
||||||
|
return FromTimestamp(t.AddDate(0, 0, int(qty))), nil
|
||||||
|
case timePartHour:
|
||||||
|
duration = time.Duration(qty) * time.Hour
|
||||||
|
case timePartMinute:
|
||||||
|
duration = time.Duration(qty) * time.Minute
|
||||||
|
case timePartSecond:
|
||||||
|
duration = time.Duration(qty) * time.Second
|
||||||
|
default:
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
return FromTimestamp(t.Add(duration)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
dayInNanoseconds = time.Hour * 24
|
||||||
|
)
|
||||||
|
|
||||||
|
// dateDiff computes the difference between two times in terms of the
|
||||||
|
// `timePart` which can be years, months, days, hours, minutes or
|
||||||
|
// seconds. For difference in years, months or days, the time part,
|
||||||
|
// including timezone is ignored.
|
||||||
|
func dateDiff(timePart string, ts1, ts2 time.Time) (*Value, error) {
|
||||||
|
if ts2.Before(ts1) {
|
||||||
|
v, err := dateDiff(timePart, ts2, ts1)
|
||||||
|
v.negate()
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := ts2.Sub(ts1)
|
||||||
|
y1, m1, d1 := ts1.Date()
|
||||||
|
y2, m2, d2 := ts2.Date()
|
||||||
|
dy, dm := int64(y2-y1), int64(m2-m1)
|
||||||
|
|
||||||
|
switch timePart {
|
||||||
|
case timePartYear:
|
||||||
|
if m2 > m1 || (m2 == m1 && d2 >= d1) {
|
||||||
|
return FromInt(dy), nil
|
||||||
|
}
|
||||||
|
return FromInt(dy - 1), nil
|
||||||
|
case timePartMonth:
|
||||||
|
months := 12 * dy
|
||||||
|
if m2 >= m1 {
|
||||||
|
months += dm
|
||||||
|
} else {
|
||||||
|
months += 12 + dm
|
||||||
|
}
|
||||||
|
if d2 < d1 {
|
||||||
|
months--
|
||||||
|
}
|
||||||
|
return FromInt(months), nil
|
||||||
|
case timePartDay:
|
||||||
|
// To compute the number of days between two times
|
||||||
|
// using the time package, zero out the time portions
|
||||||
|
// of the timestamps, compute the difference duration
|
||||||
|
// and then divide by the length of a day.
|
||||||
|
d1 := time.Date(y1, m1, d1, 0, 0, 0, 0, time.UTC)
|
||||||
|
d2 := time.Date(y2, m2, d2, 0, 0, 0, 0, time.UTC)
|
||||||
|
diff := d2.Sub(d1)
|
||||||
|
days := diff / dayInNanoseconds
|
||||||
|
return FromInt(int64(days)), nil
|
||||||
|
case timePartHour:
|
||||||
|
hours := duration / time.Hour
|
||||||
|
return FromInt(int64(hours)), nil
|
||||||
|
case timePartMinute:
|
||||||
|
minutes := duration / time.Minute
|
||||||
|
return FromInt(int64(minutes)), nil
|
||||||
|
case timePartSecond:
|
||||||
|
seconds := duration / time.Second
|
||||||
|
return FromInt(int64(seconds)), nil
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
60
pkg/s3select/sql/timestampfuncs_test.go
Normal file
60
pkg/s3select/sql/timestampfuncs_test.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2019 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseAndDisplaySQLTimestamp(t *testing.T) {
|
||||||
|
beijing := time.FixedZone("", int((8 * time.Hour).Seconds()))
|
||||||
|
fakeLosAngeles := time.FixedZone("", -int((8 * time.Hour).Seconds()))
|
||||||
|
cases := []struct {
|
||||||
|
s string
|
||||||
|
t time.Time
|
||||||
|
}{
|
||||||
|
{"2010T", time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||||
|
{"2010-02T", time.Date(2010, 2, 1, 0, 0, 0, 0, time.UTC)},
|
||||||
|
{"2010-02-03T", time.Date(2010, 2, 3, 0, 0, 0, 0, time.UTC)},
|
||||||
|
{"2010-02-03T04:11Z", time.Date(2010, 2, 3, 4, 11, 0, 0, time.UTC)},
|
||||||
|
{"2010-02-03T04:11:30Z", time.Date(2010, 2, 3, 4, 11, 30, 0, time.UTC)},
|
||||||
|
{"2010-02-03T04:11:30.23Z", time.Date(2010, 2, 3, 4, 11, 30, 230000000, time.UTC)},
|
||||||
|
{"2010-02-03T04:11+08:00", time.Date(2010, 2, 3, 4, 11, 0, 0, beijing)},
|
||||||
|
{"2010-02-03T04:11:30+08:00", time.Date(2010, 2, 3, 4, 11, 30, 0, beijing)},
|
||||||
|
{"2010-02-03T04:11:30.23+08:00", time.Date(2010, 2, 3, 4, 11, 30, 230000000, beijing)},
|
||||||
|
{"2010-02-03T04:11:30-08:00", time.Date(2010, 2, 3, 4, 11, 30, 0, fakeLosAngeles)},
|
||||||
|
{"2010-02-03T04:11:30.23-08:00", time.Date(2010, 2, 3, 4, 11, 30, 230000000, fakeLosAngeles)},
|
||||||
|
}
|
||||||
|
for i, tc := range cases {
|
||||||
|
tval, err := parseSQLTimestamp(tc.s)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: Unexpected error: %v", i+1, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !tval.Equal(tc.t) {
|
||||||
|
t.Errorf("Case %d: Expected %v got %v", i+1, tc.t, tval)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tstr := FormatSQLTimestamp(tc.t)
|
||||||
|
if tstr != tc.s {
|
||||||
|
t.Errorf("Case %d: Expected %s got %s", i+1, tc.s, tstr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -48,6 +49,9 @@ const (
|
|||||||
// 64-bit floating point
|
// 64-bit floating point
|
||||||
typeFloat
|
typeFloat
|
||||||
|
|
||||||
|
// timestamp type
|
||||||
|
typeTimestamp
|
||||||
|
|
||||||
// This type refers to untyped values, e.g. as read from CSV
|
// This type refers to untyped values, e.g. as read from CSV
|
||||||
typeBytes
|
typeBytes
|
||||||
)
|
)
|
||||||
@ -77,6 +81,8 @@ func (v *Value) GetTypeString() string {
|
|||||||
return "INT"
|
return "INT"
|
||||||
case typeFloat:
|
case typeFloat:
|
||||||
return "FLOAT"
|
return "FLOAT"
|
||||||
|
case typeTimestamp:
|
||||||
|
return "TIMESTAMP"
|
||||||
case typeBytes:
|
case typeBytes:
|
||||||
return "BYTES"
|
return "BYTES"
|
||||||
}
|
}
|
||||||
@ -90,6 +96,8 @@ func (v *Value) Repr() string {
|
|||||||
return ":NULL"
|
return ":NULL"
|
||||||
case typeBool, typeInt, typeFloat:
|
case typeBool, typeInt, typeFloat:
|
||||||
return fmt.Sprintf("%v:%s", v.value, v.GetTypeString())
|
return fmt.Sprintf("%v:%s", v.value, v.GetTypeString())
|
||||||
|
case typeTimestamp:
|
||||||
|
return fmt.Sprintf("%s:TIMESTAMP", v.value.(*time.Time))
|
||||||
case typeString:
|
case typeString:
|
||||||
return fmt.Sprintf("\"%s\":%s", v.value.(string), v.GetTypeString())
|
return fmt.Sprintf("\"%s\":%s", v.value.(string), v.GetTypeString())
|
||||||
case typeBytes:
|
case typeBytes:
|
||||||
@ -119,6 +127,11 @@ func FromBool(b bool) *Value {
|
|||||||
return &Value{value: b, vType: typeBool}
|
return &Value{value: b, vType: typeBool}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromTimestamp creates a Value from a timestamp
|
||||||
|
func FromTimestamp(t time.Time) *Value {
|
||||||
|
return &Value{value: t, vType: typeTimestamp}
|
||||||
|
}
|
||||||
|
|
||||||
// FromNull creates a Value with Null value
|
// FromNull creates a Value with Null value
|
||||||
func FromNull() *Value {
|
func FromNull() *Value {
|
||||||
return &Value{vType: typeNull}
|
return &Value{vType: typeNull}
|
||||||
@ -173,6 +186,15 @@ func (v *Value) ToBool() (val bool, ok bool) {
|
|||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToTimestamp returns the timestamp value if present.
|
||||||
|
func (v *Value) ToTimestamp() (t time.Time, ok bool) {
|
||||||
|
switch v.vType {
|
||||||
|
case typeTimestamp:
|
||||||
|
return v.value.(time.Time), true
|
||||||
|
}
|
||||||
|
return t, false
|
||||||
|
}
|
||||||
|
|
||||||
// ToBytes converts Value to byte-slice.
|
// ToBytes converts Value to byte-slice.
|
||||||
func (v *Value) ToBytes() ([]byte, bool) {
|
func (v *Value) ToBytes() ([]byte, bool) {
|
||||||
switch v.vType {
|
switch v.vType {
|
||||||
@ -213,6 +235,11 @@ func (v *Value) setBool(b bool) {
|
|||||||
v.value = b
|
v.value = b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Value) setTimestamp(t time.Time) {
|
||||||
|
v.vType = typeTimestamp
|
||||||
|
v.value = t
|
||||||
|
}
|
||||||
|
|
||||||
// CSVString - convert to string for CSV serialization
|
// CSVString - convert to string for CSV serialization
|
||||||
func (v *Value) CSVString() string {
|
func (v *Value) CSVString() string {
|
||||||
switch v.vType {
|
switch v.vType {
|
||||||
@ -226,6 +253,8 @@ func (v *Value) CSVString() string {
|
|||||||
return fmt.Sprintf("%v", v.value.(int64))
|
return fmt.Sprintf("%v", v.value.(int64))
|
||||||
case typeFloat:
|
case typeFloat:
|
||||||
return fmt.Sprintf("%v", v.value.(float64))
|
return fmt.Sprintf("%v", v.value.(float64))
|
||||||
|
case typeTimestamp:
|
||||||
|
return FormatSQLTimestamp(v.value.(time.Time))
|
||||||
case typeBytes:
|
case typeBytes:
|
||||||
return fmt.Sprintf("%v", string(v.value.([]byte)))
|
return fmt.Sprintf("%v", string(v.value.([]byte)))
|
||||||
default:
|
default:
|
||||||
@ -242,6 +271,16 @@ func floatToValue(f float64) *Value {
|
|||||||
return FromFloat(f)
|
return FromFloat(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// negate negates a numeric value
|
||||||
|
func (v *Value) negate() {
|
||||||
|
switch v.vType {
|
||||||
|
case typeFloat:
|
||||||
|
v.value = -(v.value.(float64))
|
||||||
|
case typeInt:
|
||||||
|
v.value = -(v.value.(int64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Value comparison functions: we do not expose them outside the
|
// Value comparison functions: we do not expose them outside the
|
||||||
// module. Logical operators "<", ">", ">=", "<=" work on strings and
|
// module. Logical operators "<", ">", ">=", "<=" work on strings and
|
||||||
// numbers. Equality operators "=", "!=" work on strings,
|
// numbers. Equality operators "=", "!=" work on strings,
|
||||||
@ -571,6 +610,24 @@ func (v *Value) minmax(a *Value, isMax, isFirstRow bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inferTypeAsTimestamp(v *Value) {
|
||||||
|
if s, ok := v.ToString(); ok {
|
||||||
|
t, err := parseSQLTimestamp(s)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v.setTimestamp(t)
|
||||||
|
} else if b, ok := v.ToBytes(); ok {
|
||||||
|
s := string(b)
|
||||||
|
t, err := parseSQLTimestamp(s)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v.setTimestamp(t)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// inferTypeAsString is used to convert untyped values to string - it
|
// inferTypeAsString is used to convert untyped values to string - it
|
||||||
// is called when the caller requires a string context to proceed.
|
// is called when the caller requires a string context to proceed.
|
||||||
func inferTypeAsString(v *Value) {
|
func inferTypeAsString(v *Value) {
|
||||||
|
Loading…
Reference in New Issue
Block a user