Sfoglia il codice sorgente

feat(func): add `convert_tz` for timezone conversion (#2069)

* feat: add `convert_tz` for timezone conversion

Fixes:

- #986
- #2065

Signed-off-by: xjasonlyu <xjasonlyu@gmail.com>

* add convert test cases

Signed-off-by: xjasonlyu <xjasonlyu@gmail.com>

* add error test cases

Signed-off-by: xjasonlyu <xjasonlyu@gmail.com>

* add validation test cases

Signed-off-by: xjasonlyu <xjasonlyu@gmail.com>

---------

Signed-off-by: xjasonlyu <xjasonlyu@gmail.com>
Jason Lyu 1 anno fa
parent
commit
aa7ebe3d3c

+ 8 - 0
docs/en_US/sqls/functions/transform_functions.md

@@ -21,6 +21,14 @@ When casting to a datetime type, the supported column type and casting rule are:
 3. If column is string, it will try to automatically detect the format and convert it to datetime type.
 3. If column is string, it will try to automatically detect the format and convert it to datetime type.
 4. Other types are not supported.
 4. Other types are not supported.
 
 
+## CONVERT_TZ
+
+```text
+convert_tz(col, "Asia/Shanghai")
+```
+
+Convert a time value to a time in the corresponding time zone. The time zone parameter format refers to [IANA Time Zone Database](https://www.iana.org/time-zones), the default value is `UTC`. Set to `Local` to use the system time zone.
+
 ## ENCODE
 ## ENCODE
 
 
 ```text
 ```text

+ 9 - 1
docs/zh_CN/sqls/functions/transform_functions.md

@@ -8,7 +8,7 @@
 cast(col,  "bigint")
 cast(col,  "bigint")
 ```
 ```
 
 
-将值从一种数据类型转换为另一种数据类型。 支持的类型包括:bigint,float,string,boolean,bytea 和 datetime。
+将值从一种数据类型转换为另一种数据类型。支持的类型包括:bigint,float,string,boolean,bytea 和 datetime。
 
 
 ### 转换为 datetime 类型
 ### 转换为 datetime 类型
 
 
@@ -19,6 +19,14 @@ cast(col,  "bigint")
 3. 如果参数为 string 类型,则会尝试自动识别格式并将其转换为 datetime类型。
 3. 如果参数为 string 类型,则会尝试自动识别格式并将其转换为 datetime类型。
 4. 其他类型的参数均不支持转换。
 4. 其他类型的参数均不支持转换。
 
 
+## CONVERT_TZ
+
+```text
+convert_tz(col, "Asia/Shanghai")
+```
+
+将时间数值转换成对应时区的时间。时区参数格式参照 [IANA 时区数据库](https://www.iana.org/time-zones),默认值为 `UTC`,设置为 `Local` 则使用系统时区。
+
 ## CHR
 ## CHR
 
 
 ```text
 ```text

+ 28 - 0
internal/binder/function/funcs_misc.go

@@ -63,6 +63,34 @@ func registerMiscFunc() {
 		},
 		},
 		check: returnNilIfHasAnyNil,
 		check: returnNilIfHasAnyNil,
 	}
 	}
+	builtins["convert_tz"] = builtinFunc{
+		fType: ast.FuncTypeScalar,
+		exec: func(ctx api.FunctionContext, args []interface{}) (interface{}, bool) {
+			arg0, err := cast.InterfaceToTime(args[0], "")
+			if err != nil {
+				return err, false
+			}
+			arg1 := cast.ToStringAlways(args[1])
+			loc, err := time.LoadLocation(arg1)
+			if err != nil {
+				return err, false
+			}
+			return arg0.In(loc), true
+		},
+		val: func(_ api.FunctionContext, args []ast.Expr) error {
+			if err := ValidateLen(2, len(args)); err != nil {
+				return err
+			}
+			if ast.IsNumericArg(args[0]) || ast.IsStringArg(args[0]) || ast.IsBooleanArg(args[0]) {
+				return ProduceErrInfo(0, "datetime")
+			}
+			if ast.IsNumericArg(args[1]) || ast.IsTimeArg(args[1]) || ast.IsBooleanArg(args[1]) {
+				return ProduceErrInfo(1, "string")
+			}
+			return nil
+		},
+		check: returnNilIfHasAnyNil,
+	}
 	builtins["to_json"] = builtinFunc{
 	builtins["to_json"] = builtinFunc{
 		fType: ast.FuncTypeScalar,
 		fType: ast.FuncTypeScalar,
 		exec: func(ctx api.FunctionContext, args []interface{}) (interface{}, bool) {
 		exec: func(ctx api.FunctionContext, args []interface{}) (interface{}, bool) {

+ 93 - 0
internal/binder/function/funcs_misc_test.go

@@ -15,10 +15,13 @@
 package function
 package function
 
 
 import (
 import (
+	"errors"
 	"fmt"
 	"fmt"
 	"reflect"
 	"reflect"
 	"testing"
 	"testing"
+	"time"
 
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/require"
 
 
 	"github.com/lf-edge/ekuiper/internal/conf"
 	"github.com/lf-edge/ekuiper/internal/conf"
@@ -241,6 +244,96 @@ func TestFromJson(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestConvertTZ(t *testing.T) {
+	f, ok := builtins["convert_tz"]
+	if !ok {
+		t.Fatal("builtin not found")
+	}
+	contextLogger := conf.Log.WithField("rule", "testExec")
+	ctx := kctx.WithValue(kctx.Background(), kctx.LoggerKey, contextLogger)
+	tempStore, _ := state.CreateStore("mockRule0", api.AtMostOnce)
+	fctx := kctx.NewDefaultFuncContext(ctx.WithMeta("mockRule0", "test", tempStore), 2)
+
+	loc, _ := time.LoadLocation("Asia/Shanghai")
+
+	tests := []struct {
+		args   []interface{}
+		result interface{}
+	}{
+		{ // 0
+			args: []interface{}{
+				time.Date(2022, time.April, 13, 6, 22, 32, 233000000, time.UTC),
+				"UTC",
+			},
+			result: time.Date(2022, time.April, 13, 6, 22, 32, 233000000, time.UTC),
+		}, { // 1
+			args: []interface{}{
+				time.Date(2022, time.April, 13, 6, 22, 32, 233000000, time.UTC),
+				"Asia/Shanghai",
+			},
+			result: time.Date(2022, time.April, 13, 14, 22, 32, 233000000, loc),
+		}, { // 2
+			args: []interface{}{
+				time.Date(2022, time.April, 13, 6, 22, 32, 233000000, time.UTC),
+				"Unknown",
+			},
+			result: errors.New("unknown time zone Unknown"),
+		}, { // 3
+			args: []interface{}{
+				true,
+				"UTC",
+			},
+			result: errors.New("unsupported type to convert to timestamp true"),
+		},
+	}
+	for _, tt := range tests {
+		result, _ := f.exec(fctx, tt.args)
+		assert.Equal(t, tt.result, result)
+	}
+
+	vtests := []struct {
+		args    []ast.Expr
+		wantErr bool
+	}{
+		{
+			[]ast.Expr{&ast.TimeLiteral{Val: 0}, &ast.StringLiteral{Val: "0"}},
+			false,
+		},
+		{
+			[]ast.Expr{&ast.StringLiteral{Val: "0"}},
+			true,
+		},
+		{
+			[]ast.Expr{&ast.NumberLiteral{Val: 0}, &ast.NumberLiteral{Val: 0}},
+			true,
+		},
+		{
+			[]ast.Expr{&ast.NumberLiteral{Val: 0}, &ast.TimeLiteral{Val: 0}},
+			true,
+		},
+		{
+			[]ast.Expr{&ast.NumberLiteral{Val: 0}, &ast.BooleanLiteral{Val: true}},
+			true,
+		},
+		{
+			[]ast.Expr{&ast.StringLiteral{Val: "0"}, &ast.NumberLiteral{Val: 0}},
+			true,
+		},
+		{
+			[]ast.Expr{&ast.BooleanLiteral{Val: true}, &ast.NumberLiteral{Val: 0}},
+			true,
+		},
+	}
+	for _, vtt := range vtests {
+		err := f.val(fctx, vtt.args)
+		if vtt.wantErr {
+			assert.Error(t, err)
+		} else {
+			assert.NoError(t, err)
+		}
+	}
+}
+
 func TestDelay(t *testing.T) {
 func TestDelay(t *testing.T) {
 	f, ok := builtins["delay"]
 	f, ok := builtins["delay"]
 	if !ok {
 	if !ok {