diff --git a/expression/builtin_compare.go b/expression/builtin_compare.go index 609ee26dc09e1..b0e1670d5cdbc 100644 --- a/expression/builtin_compare.go +++ b/expression/builtin_compare.go @@ -23,6 +23,7 @@ import ( "github.com/pingcap/tidb/parser/opcode" "github.com/pingcap/tidb/parser/terror" "github.com/pingcap/tidb/sessionctx" + "github.com/pingcap/tidb/sessionctx/stmtctx" "github.com/pingcap/tidb/types" "github.com/pingcap/tidb/types/json" "github.com/pingcap/tidb/util/chunk" @@ -416,10 +417,33 @@ func ResolveType4Between(args [3]Expression) types.EvalType { return cmpTp } +// GLCmpStringMode represents Greatest/Least interal string comparison mode +type GLCmpStringMode uint8 + +const ( + // GLCmpStringDirectly Greatest and Least function compares string directly + GLCmpStringDirectly GLCmpStringMode = iota + // GLCmpStringAsDate Greatest/Least function compares string as 'yyyy-mm-dd' format + GLCmpStringAsDate + // GLCmpStringAsDatetime Greatest/Least function compares string as 'yyyy-mm-dd hh:mm:ss' format + GLCmpStringAsDatetime +) + +// GLRetTimeType represents Greatest/Least return time type +type GLRetTimeType uint8 + +const ( + // GLRetNoneTemporal Greatest/Least function returns non temporal time + GLRetNoneTemporal GLRetTimeType = iota + // GLRetDate Greatest/Least function returns date type, 'yyyy-mm-dd' + GLRetDate + // GLRetDatetime Greatest/Least function returns datetime type, 'yyyy-mm-dd hh:mm:ss' + GLRetDatetime +) + // resolveType4Extremum gets compare type for GREATEST and LEAST and BETWEEN (mainly for datetime). -func resolveType4Extremum(args []Expression) (_ types.EvalType, cmpStringAsDatetime bool) { +func resolveType4Extremum(args []Expression) (_ types.EvalType, fieldTimeType GLRetTimeType, cmpStringMode GLCmpStringMode) { aggType := aggregateType(args) - var temporalItem *types.FieldType if aggType.EvalType().IsStringKind() { for i := range args { @@ -432,13 +456,22 @@ func resolveType4Extremum(args []Expression) (_ types.EvalType, cmpStringAsDatet } } - if !types.IsTypeTemporal(aggType.Tp) && temporalItem != nil { - aggType.Tp = temporalItem.Tp - cmpStringAsDatetime = true + if !types.IsTypeTemporal(aggType.Tp) && temporalItem != nil && types.IsTemporalWithDate(temporalItem.Tp) { + if temporalItem.Tp == mysql.TypeDate { + cmpStringMode = GLCmpStringAsDate + } else { + cmpStringMode = GLCmpStringAsDatetime + } } // TODO: String charset, collation checking are needed. } - return aggType.EvalType(), cmpStringAsDatetime + var timeType = GLRetNoneTemporal + if aggType.Tp == mysql.TypeDate { + timeType = GLRetDate + } else if aggType.Tp == mysql.TypeDatetime || aggType.Tp == mysql.TypeTimestamp { + timeType = GLRetDatetime + } + return aggType.EvalType(), timeType, cmpStringMode } // unsupportedJSONComparison reports warnings while there is a JSON type in least/greatest function's arguments @@ -460,23 +493,25 @@ func (c *greatestFunctionClass) getFunction(ctx sessionctx.Context, args []Expre if err = c.verifyArgs(args); err != nil { return nil, err } - tp, cmpStringAsDatetime := resolveType4Extremum(args) - if cmpStringAsDatetime { + tp, fieldTimeType, cmpStringMode := resolveType4Extremum(args) + argTp := tp + if cmpStringMode != GLCmpStringDirectly { // Args are temporal and string mixed, we cast all args as string and parse it to temporal mannualy to compare. - tp = types.ETString + argTp = types.ETString } else if tp == types.ETJson { unsupportedJSONComparison(ctx, args) + argTp = types.ETString tp = types.ETString } argTps := make([]types.EvalType, len(args)) for i := range args { - argTps[i] = tp + argTps[i] = argTp } bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, tp, argTps...) if err != nil { return nil, err } - switch tp { + switch argTp { case types.ETInt: // adjust unsigned flag greastInitUnsignedFlag := false @@ -495,8 +530,11 @@ func (c *greatestFunctionClass) getFunction(ctx sessionctx.Context, args []Expre sig = &builtinGreatestDecimalSig{bf} sig.setPbCode(tipb.ScalarFuncSig_GreatestDecimal) case types.ETString: - if cmpStringAsDatetime { - sig = &builtinGreatestCmpStringAsTimeSig{bf} + if cmpStringMode == GLCmpStringAsDate { + sig = &builtinGreatestCmpStringAsTimeSig{bf, true} + sig.setPbCode(tipb.ScalarFuncSig_GreatestCmpStringAsDate) + } else if cmpStringMode == GLCmpStringAsDatetime { + sig = &builtinGreatestCmpStringAsTimeSig{bf, false} sig.setPbCode(tipb.ScalarFuncSig_GreatestCmpStringAsTime) } else { sig = &builtinGreatestStringSig{bf} @@ -506,8 +544,13 @@ func (c *greatestFunctionClass) getFunction(ctx sessionctx.Context, args []Expre sig = &builtinGreatestDurationSig{bf} sig.setPbCode(tipb.ScalarFuncSig_GreatestDuration) case types.ETDatetime, types.ETTimestamp: - sig = &builtinGreatestTimeSig{bf} - sig.setPbCode(tipb.ScalarFuncSig_GreatestTime) + if fieldTimeType == GLRetDate { + sig = &builtinGreatestTimeSig{bf, true} + sig.setPbCode(tipb.ScalarFuncSig_GreatestDate) + } else { + sig = &builtinGreatestTimeSig{bf, false} + sig.setPbCode(tipb.ScalarFuncSig_GreatestTime) + } } sig.getRetTp().Flen, sig.getRetTp().Decimal = fixFlenAndDecimalForGreatestAndLeast(args) return sig, nil @@ -648,11 +691,13 @@ func (b *builtinGreatestStringSig) evalString(row chunk.Row) (max string, isNull type builtinGreatestCmpStringAsTimeSig struct { baseBuiltinFunc + cmpAsDate bool } func (b *builtinGreatestCmpStringAsTimeSig) Clone() builtinFunc { newSig := &builtinGreatestCmpStringAsTimeSig{} newSig.cloneFrom(&b.baseBuiltinFunc) + newSig.cmpAsDate = b.cmpAsDate return newSig } @@ -665,13 +710,9 @@ func (b *builtinGreatestCmpStringAsTimeSig) evalString(row chunk.Row) (strRes st if isNull || err != nil { return "", true, err } - t, err := types.ParseDatetime(sc, v) + v, err = doTimeConversionForGL(b.cmpAsDate, b.ctx, sc, v) if err != nil { - if err = handleInvalidTimeError(b.ctx, err); err != nil { - return v, true, err - } - } else { - v = t.String() + return v, true, err } // In MySQL, if the compare result is zero, than we will try to use the string comparison result if i == 0 || strings.Compare(v, strRes) > 0 { @@ -681,13 +722,39 @@ func (b *builtinGreatestCmpStringAsTimeSig) evalString(row chunk.Row) (strRes st return strRes, false, nil } +func doTimeConversionForGL(cmpAsDate bool, ctx sessionctx.Context, sc *stmtctx.StatementContext, strVal string) (string, error) { + var t types.Time + var err error + if cmpAsDate { + t, err = types.ParseDate(sc, strVal) + if err == nil { + t, err = t.Convert(sc, mysql.TypeDate) + } + } else { + t, err = types.ParseDatetime(sc, strVal) + if err == nil { + t, err = t.Convert(sc, mysql.TypeDatetime) + } + } + if err != nil { + if err = handleInvalidTimeError(ctx, err); err != nil { + return "", err + } + } else { + strVal = t.String() + } + return strVal, nil +} + type builtinGreatestTimeSig struct { baseBuiltinFunc + cmpAsDate bool } func (b *builtinGreatestTimeSig) Clone() builtinFunc { newSig := &builtinGreatestTimeSig{} newSig.cloneFrom(&b.baseBuiltinFunc) + newSig.cmpAsDate = b.cmpAsDate return newSig } @@ -701,6 +768,12 @@ func (b *builtinGreatestTimeSig) evalTime(row chunk.Row) (res types.Time, isNull res = v } } + // Convert ETType Time value to MySQL actual type, distinguish date and datetime + sc := b.ctx.GetSessionVars().StmtCtx + resTimeTp := getAccurateTimeTypeForGLRet(b.cmpAsDate) + if res, err = res.Convert(sc, resTimeTp); err != nil { + return types.ZeroTime, true, handleInvalidTimeError(b.ctx, err) + } return res, false, nil } @@ -735,17 +808,19 @@ func (c *leastFunctionClass) getFunction(ctx sessionctx.Context, args []Expressi if err = c.verifyArgs(args); err != nil { return nil, err } - tp, cmpStringAsDatetime := resolveType4Extremum(args) - if cmpStringAsDatetime { + tp, fieldTimeType, cmpStringMode := resolveType4Extremum(args) + argTp := tp + if cmpStringMode != GLCmpStringDirectly { // Args are temporal and string mixed, we cast all args as string and parse it to temporal mannualy to compare. - tp = types.ETString + argTp = types.ETString } else if tp == types.ETJson { unsupportedJSONComparison(ctx, args) + argTp = types.ETString tp = types.ETString } argTps := make([]types.EvalType, len(args)) for i := range args { - argTps[i] = tp + argTps[i] = argTp } bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, tp, argTps...) if err != nil { @@ -770,8 +845,11 @@ func (c *leastFunctionClass) getFunction(ctx sessionctx.Context, args []Expressi sig = &builtinLeastDecimalSig{bf} sig.setPbCode(tipb.ScalarFuncSig_LeastDecimal) case types.ETString: - if cmpStringAsDatetime { - sig = &builtinLeastCmpStringAsTimeSig{bf} + if cmpStringMode == GLCmpStringAsDate { + sig = &builtinLeastCmpStringAsTimeSig{bf, true} + sig.setPbCode(tipb.ScalarFuncSig_LeastCmpStringAsDate) + } else if cmpStringMode == GLCmpStringAsDatetime { + sig = &builtinLeastCmpStringAsTimeSig{bf, false} sig.setPbCode(tipb.ScalarFuncSig_LeastCmpStringAsTime) } else { sig = &builtinLeastStringSig{bf} @@ -781,8 +859,13 @@ func (c *leastFunctionClass) getFunction(ctx sessionctx.Context, args []Expressi sig = &builtinLeastDurationSig{bf} sig.setPbCode(tipb.ScalarFuncSig_LeastDuration) case types.ETDatetime, types.ETTimestamp: - sig = &builtinLeastTimeSig{bf} - sig.setPbCode(tipb.ScalarFuncSig_LeastTime) + if fieldTimeType == GLRetDate { + sig = &builtinLeastTimeSig{bf, true} + sig.setPbCode(tipb.ScalarFuncSig_LeastDate) + } else { + sig = &builtinLeastTimeSig{bf, false} + sig.setPbCode(tipb.ScalarFuncSig_LeastTime) + } } sig.getRetTp().Flen, sig.getRetTp().Decimal = fixFlenAndDecimalForGreatestAndLeast(args) return sig, nil @@ -910,11 +993,13 @@ func (b *builtinLeastStringSig) evalString(row chunk.Row) (min string, isNull bo type builtinLeastCmpStringAsTimeSig struct { baseBuiltinFunc + cmpAsDate bool } func (b *builtinLeastCmpStringAsTimeSig) Clone() builtinFunc { newSig := &builtinLeastCmpStringAsTimeSig{} newSig.cloneFrom(&b.baseBuiltinFunc) + newSig.cmpAsDate = b.cmpAsDate return newSig } @@ -927,13 +1012,9 @@ func (b *builtinLeastCmpStringAsTimeSig) evalString(row chunk.Row) (strRes strin if isNull || err != nil { return "", true, err } - t, err := types.ParseDatetime(sc, v) + v, err = doTimeConversionForGL(b.cmpAsDate, b.ctx, sc, v) if err != nil { - if err = handleInvalidTimeError(b.ctx, err); err != nil { - return v, true, err - } - } else { - v = t.String() + return v, true, err } if i == 0 || strings.Compare(v, strRes) < 0 { strRes = v @@ -945,11 +1026,13 @@ func (b *builtinLeastCmpStringAsTimeSig) evalString(row chunk.Row) (strRes strin type builtinLeastTimeSig struct { baseBuiltinFunc + cmpAsDate bool } func (b *builtinLeastTimeSig) Clone() builtinFunc { newSig := &builtinLeastTimeSig{} newSig.cloneFrom(&b.baseBuiltinFunc) + newSig.cmpAsDate = b.cmpAsDate return newSig } @@ -963,9 +1046,25 @@ func (b *builtinLeastTimeSig) evalTime(row chunk.Row) (res types.Time, isNull bo res = v } } + // Convert ETType Time value to MySQL actual type, distinguish date and datetime + sc := b.ctx.GetSessionVars().StmtCtx + resTimeTp := getAccurateTimeTypeForGLRet(b.cmpAsDate) + if res, err = res.Convert(sc, resTimeTp); err != nil { + return types.ZeroTime, true, handleInvalidTimeError(b.ctx, err) + } return res, false, nil } +func getAccurateTimeTypeForGLRet(cmpAsDate bool) byte { + var resTimeTp byte + if cmpAsDate { + resTimeTp = mysql.TypeDate + } else { + resTimeTp = mysql.TypeDatetime + } + return resTimeTp +} + type builtinLeastDurationSig struct { baseBuiltinFunc } diff --git a/expression/builtin_compare_test.go b/expression/builtin_compare_test.go index edc4b7e7ad056..cd204d25a2459 100644 --- a/expression/builtin_compare_test.go +++ b/expression/builtin_compare_test.go @@ -351,6 +351,10 @@ func TestGreatestLeastFunc(t *testing.T) { []interface{}{905969664.0, 4556, "1990-06-16 17:22:56.005534"}, "905969664", "1990-06-16 17:22:56.005534", false, false, }, + { + []interface{}{105969664.0, 120000, types.Duration{Duration: 20*time.Hour + 0*time.Minute + 0*time.Second}}, + "20:00:00", "105969664", false, false, + }, } { f0, err := newFunctionForTest(ctx, ast.Greatest, primitiveValsToConstants(ctx, test.args)...) require.NoError(t, err) diff --git a/expression/builtin_compare_vec.go b/expression/builtin_compare_vec.go index e3bf42864fd94..35663bb8418d6 100644 --- a/expression/builtin_compare_vec.go +++ b/expression/builtin_compare_vec.go @@ -652,14 +652,10 @@ func (b *builtinGreatestCmpStringAsTimeSig) vecEvalString(input *chunk.Chunk, re // NOTE: can't use Column.GetString because it returns an unsafe string, copy the row instead. argTimeStr := string(result.GetBytes(i)) - - argTime, err := types.ParseDatetime(sc, argTimeStr) + var err error + argTimeStr, err = doTimeConversionForGL(b.cmpAsDate, b.ctx, sc, argTimeStr) if err != nil { - if err = handleInvalidTimeError(b.ctx, err); err != nil { - return err - } - } else { - argTimeStr = argTime.String() + return err } if j == 0 || strings.Compare(argTimeStr, dstStrings[i]) > 0 { dstStrings[i] = argTimeStr @@ -737,14 +733,10 @@ func (b *builtinLeastCmpStringAsTimeSig) vecEvalString(input *chunk.Chunk, resul // NOTE: can't use Column.GetString because it returns an unsafe string, copy the row instead. argTimeStr := string(result.GetBytes(i)) - - argTime, err := types.ParseDatetime(sc, argTimeStr) + var err error + argTimeStr, err = doTimeConversionForGL(b.cmpAsDate, b.ctx, sc, argTimeStr) if err != nil { - if err = handleInvalidTimeError(b.ctx, err); err != nil { - return err - } - } else { - argTimeStr = argTime.String() + return err } if j == 0 || strings.Compare(argTimeStr, dstStrings[i]) < 0 { dstStrings[i] = argTimeStr @@ -845,6 +837,15 @@ func (b *builtinGreatestTimeSig) vecEvalTime(input *chunk.Chunk, result *chunk.C } } } + sc := b.ctx.GetSessionVars().StmtCtx + resTimeTp := getAccurateTimeTypeForGLRet(b.cmpAsDate) + for rowIdx := 0; rowIdx < n; rowIdx++ { + resTimes := result.Times() + resTimes[rowIdx], err = resTimes[rowIdx].Convert(sc, resTimeTp) + if err != nil { + return err + } + } return nil } @@ -877,6 +878,15 @@ func (b *builtinLeastTimeSig) vecEvalTime(input *chunk.Chunk, result *chunk.Colu } } } + sc := b.ctx.GetSessionVars().StmtCtx + resTimeTp := getAccurateTimeTypeForGLRet(b.cmpAsDate) + for rowIdx := 0; rowIdx < n; rowIdx++ { + resTimes := result.Times() + resTimes[rowIdx], err = resTimes[rowIdx].Convert(sc, resTimeTp) + if err != nil { + return err + } + } return nil } diff --git a/expression/distsql_builtin.go b/expression/distsql_builtin.go index 47dd46f87b39f..d1f054a9cb1dd 100644 --- a/expression/distsql_builtin.go +++ b/expression/distsql_builtin.go @@ -222,7 +222,13 @@ func getSignatureByPB(ctx sessionctx.Context, sigCode tipb.ScalarFuncSig, tp *ti case tipb.ScalarFuncSig_GreatestString: f = &builtinGreatestStringSig{base} case tipb.ScalarFuncSig_GreatestTime: - f = &builtinGreatestTimeSig{base} + f = &builtinGreatestTimeSig{base, false} + case tipb.ScalarFuncSig_GreatestDate: + f = &builtinGreatestTimeSig{base, true} + case tipb.ScalarFuncSig_GreatestCmpStringAsTime: + f = &builtinGreatestCmpStringAsTimeSig{base, false} + case tipb.ScalarFuncSig_GreatestCmpStringAsDate: + f = &builtinGreatestCmpStringAsTimeSig{base, true} case tipb.ScalarFuncSig_LeastInt: f = &builtinLeastIntSig{base} case tipb.ScalarFuncSig_LeastReal: @@ -232,7 +238,13 @@ func getSignatureByPB(ctx sessionctx.Context, sigCode tipb.ScalarFuncSig, tp *ti case tipb.ScalarFuncSig_LeastString: f = &builtinLeastStringSig{base} case tipb.ScalarFuncSig_LeastTime: - f = &builtinLeastTimeSig{base} + f = &builtinLeastTimeSig{base, false} + case tipb.ScalarFuncSig_LeastDate: + f = &builtinLeastTimeSig{base, true} + case tipb.ScalarFuncSig_LeastCmpStringAsTime: + f = &builtinLeastCmpStringAsTimeSig{base, false} + case tipb.ScalarFuncSig_LeastCmpStringAsDate: + f = &builtinLeastCmpStringAsTimeSig{base, true} case tipb.ScalarFuncSig_IntervalInt: f = &builtinIntervalIntSig{base, false} // Since interval function won't be pushed down to TiKV, therefore it doesn't matter what value we give to hasNullable case tipb.ScalarFuncSig_IntervalReal: diff --git a/expression/integration_test.go b/expression/integration_test.go index 95dc3157507ad..9477c5524d0c6 100644 --- a/expression/integration_test.go +++ b/expression/integration_test.go @@ -6927,3 +6927,65 @@ func TestIssue30174(t *testing.T) { tk.MustQuery("select * from t2 where c3 in (select c2 from t1);").Check(testkit.Rows()) tk.MustQuery("select * from t2 where c2 in (select c2 from t1);").Check(testkit.Rows()) } + +func TestIssue30264(t *testing.T) { + store, clean := testkit.CreateMockStore(t) + defer clean() + + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + // compare Time/Int/Int as string type, return string type + tk.MustQuery("select greatest(time '21:00', year(date'20220101'), 23);").Check(testkit.Rows("23")) + // compare Time/Date/Int as string type, return string type + tk.MustQuery("select greatest(time '21:00', date'891001', 120000);").Check(testkit.Rows("21:00:00")) + // compare Time/Date/Int as string type, return string type + tk.MustQuery("select greatest(time '20:00', date'101001', 120101);").Check(testkit.Rows("20:00:00")) + // compare Date/String/Int as Date type, return string type + tk.MustQuery("select greatest(date'101001', '19990329', 120101);").Check(testkit.Rows("2012-01-01")) + // compare Time/Date as DateTime type, return DateTime type + tk.MustQuery("select greatest(time '20:00', date'691231');").Check(testkit.Rows("2069-12-31 00:00:00")) + // compare Date/Date as Date type, return Date type + tk.MustQuery("select greatest(date '120301', date'691231');").Check(testkit.Rows("2069-12-31")) + // compare Time/Time as Time type, return Time type + tk.MustQuery("select greatest(time '203001', time '2230');").Check(testkit.Rows("20:30:01")) + // compare DateTime/DateTime as DateTime type, return DateTime type + tk.MustQuery("select greatest(timestamp '2021-01-31 00:00:01', timestamp '2021-12-31 12:00:00');").Check(testkit.Rows("2021-12-31 12:00:00")) + // compare Time/DateTime as DateTime type, return DateTime type + tk.MustQuery("select greatest(time '00:00:01', timestamp '2069-12-31 12:00:00');").Check(testkit.Rows("2069-12-31 12:00:00")) + // compare Date/DateTime as DateTime type, return DateTime type + tk.MustQuery("select greatest(date '21000101', timestamp '2069-12-31 12:00:00');").Check(testkit.Rows("2100-01-01 00:00:00")) + // compare JSON/JSON, return JSON type + tk.MustQuery("select greatest(cast('1' as JSON), cast('2' as JSON));").Check(testkit.Rows("2")) + //Original 30264 Issue: + tk.MustQuery("select greatest(time '20:00:00', 120000);").Check(testkit.Rows("20:00:00")) + tk.MustQuery("select greatest(date '2005-05-05', 20010101, 20040404, 20030303);").Check(testkit.Rows("2005-05-05")) + tk.MustQuery("select greatest(date '1995-05-05', 19910101, 20050505, 19930303);").Check(testkit.Rows("2005-05-05")) + + tk.MustExec("use test") + tk.MustExec("drop table if exists t1,t2;") + tk.MustExec("CREATE TABLE `t1` (a datetime, b date, c time)") + tk.MustExec("insert into t1 values(timestamp'2021-01-31 00:00:01', '2069-12-31', '20:00:01');") + tk.MustExec("set tidb_enable_vectorized_expression = on;") + // compare Time/Int/Int as string type, return string type + tk.MustQuery("select greatest(c, year(date'20220101'), 23) from t1;").Check(testkit.Rows("23")) + // compare Time/Date/Int as string type, return string type + tk.MustQuery("select greatest(c, date'891001', 120000) from t1;").Check(testkit.Rows("20:00:01")) + // compare Time/Date/Int as string type, return string type + tk.MustQuery("select greatest(c, date'101001', 120101) from t1;").Check(testkit.Rows("20:00:01")) + // compare Date/String/Int as Date type, return string type + tk.MustQuery("select greatest(b, '19990329', 120101) from t1;").Check(testkit.Rows("2069-12-31")) + // compare Time/Date as DateTime type, return DateTime type + tk.MustQuery("select greatest(time '20:00', b) from t1;").Check(testkit.Rows("2069-12-31 00:00:00")) + // compare Date/Date as Date type, return Date type + tk.MustQuery("select greatest(date '120301', b) from t1;").Check(testkit.Rows("2069-12-31")) + // compare Time/Time as Time type, return Time type + tk.MustQuery("select greatest(c, time '2230') from t1;").Check(testkit.Rows("20:00:01")) + // compare DateTime/DateTime as DateTime type, return DateTime type + tk.MustQuery("select greatest(a, timestamp '2021-12-31 12:00:00') from t1;").Check(testkit.Rows("2021-12-31 12:00:00")) + // compare Time/DateTime as DateTime type, return DateTime type + tk.MustQuery("select greatest(c, timestamp '2069-12-31 12:00:00') from t1;").Check(testkit.Rows("2069-12-31 12:00:00")) + // compare Date/DateTime as DateTime type, return DateTime type + tk.MustQuery("select greatest(date '21000101', a) from t1;").Check(testkit.Rows("2100-01-01 00:00:00")) + // compare JSON/JSON, return JSON type + tk.MustQuery("select greatest(cast(a as JSON), cast('3' as JSON)) from t1;").Check(testkit.Rows("3")) +}