diff --git a/expression/builtin_time.go b/expression/builtin_time.go index 62a8107a48da1..45ef659976f09 100644 --- a/expression/builtin_time.go +++ b/expression/builtin_time.go @@ -1572,26 +1572,30 @@ func (c *fromUnixTimeFunctionClass) getFunction(ctx sessionctx.Context, args []E _, isArg0Con := args[0].(*Constant) isArg0Str := args[0].GetType().EvalType() == types.ETString bf := newBaseBuiltinFuncWithTp(ctx, args, retTp, argTps...) - if len(args) == 1 { - if isArg0Str { - bf.tp.Decimal = types.MaxFsp - } else if isArg0Con { - arg0, _, err1 := args[0].EvalDecimal(ctx, chunk.Row{}) - if err1 != nil { - return sig, err1 - } + + if len(args) > 1 { + bf.tp.Flen = args[1].GetType().Flen + return &builtinFromUnixTime2ArgSig{bf}, nil + } + + // Calculate the time fsp. + switch { + case isArg0Str: + bf.tp.Decimal = int(types.MaxFsp) + case isArg0Con: + arg0, arg0IsNull, err0 := args[0].EvalDecimal(ctx, chunk.Row{}) + if err0 != nil { + return nil, err0 + } + + bf.tp.Decimal = int(types.MaxFsp) + if !arg0IsNull { fsp := int(arg0.GetDigitsFrac()) - if fsp > types.MaxFsp { - fsp = types.MaxFsp - } - bf.tp.Decimal = fsp + bf.tp.Decimal = mathutil.Min(fsp, int(types.MaxFsp)) } - sig = &builtinFromUnixTime1ArgSig{bf} - } else { - bf.tp.Flen = args[1].GetType().Flen - sig = &builtinFromUnixTime2ArgSig{bf} } - return sig, nil + + return &builtinFromUnixTime1ArgSig{bf}, nil } func evalFromUnixTime(ctx sessionctx.Context, fsp int, row chunk.Row, arg Expression) (res types.Time, isNull bool, err error) { diff --git a/planner/core/integration_test.go b/planner/core/integration_test.go index ff7ca01e6dd92..21f2ad151f7c5 100644 --- a/planner/core/integration_test.go +++ b/planner/core/integration_test.go @@ -15,22 +15,44 @@ package core_test import ( . "github.com/pingcap/check" + "github.com/pingcap/tidb/domain" + "github.com/pingcap/tidb/kv" "github.com/pingcap/tidb/util/testkit" + "github.com/pingcap/tidb/util/testutil" ) var _ = Suite(&testIntegrationSuite{}) type testIntegrationSuite struct { + testData testutil.TestData + store kv.Storage + dom *domain.Domain } -func (s *testIntegrationSuite) TestShowSubquery(c *C) { - store, dom, err := newStoreWithBootstrap() +func (s *testIntegrationSuite) SetUpSuite(c *C) { + var err error + s.testData, err = testutil.LoadTestSuiteData("testdata", "integration_suite") + c.Assert(err, IsNil) +} + +func (s *testIntegrationSuite) TearDownSuite(c *C) { + c.Assert(s.testData.GenerateOutputIfNeeded(), IsNil) +} + +func (s *testIntegrationSuite) SetUpTest(c *C) { + var err error + s.store, s.dom, err = newStoreWithBootstrap() + c.Assert(err, IsNil) +} + +func (s *testIntegrationSuite) TearDownTest(c *C) { + s.dom.Close() + err := s.store.Close() c.Assert(err, IsNil) - tk := testkit.NewTestKit(c, store) - defer func() { - dom.Close() - store.Close() - }() +} + +func (s *testIntegrationSuite) TestShowSubquery(c *C) { + tk := testkit.NewTestKit(c, s.store) tk.MustExec("use test") tk.MustExec("drop table if exists t") tk.MustExec("create table t(a varchar(10), b int, c int)") @@ -59,3 +81,27 @@ func (s *testIntegrationSuite) TestShowSubquery(c *C) { "a varchar(10) YES ", )) } + +func (s *testIntegrationSuite) TestIsFromUnixtimeNullRejective(c *C) { + tk := testkit.NewTestKit(c, s.store) + tk.MustExec("use test") + tk.MustExec(`drop table if exists t;`) + tk.MustExec(`create table t(a bigint, b bigint);`) + s.runTestsWithTestData("TestIsFromUnixtimeNullRejective", tk, c) +} + +func (s *testIntegrationSuite) runTestsWithTestData(caseName string, tk *testkit.TestKit, c *C) { + var input []string + var output []struct { + SQL string + Plan []string + } + s.testData.GetTestCasesByName(caseName, c, &input, &output) + for i, tt := range input { + s.testData.OnRecord(func() { + output[i].SQL = tt + output[i].Plan = s.testData.ConvertRowsToStrings(tk.MustQuery(tt).Rows()) + }) + tk.MustQuery(tt).Check(testkit.Rows(output[i].Plan...)) + } +} diff --git a/planner/core/testdata/integration_suite_in.json b/planner/core/testdata/integration_suite_in.json new file mode 100644 index 0000000000000..a4722af040a71 --- /dev/null +++ b/planner/core/testdata/integration_suite_in.json @@ -0,0 +1,22 @@ +[ + { + "name": "TestPushLimitDownIndexLookUpReader", + "cases": [ + // Limit should be pushed down into IndexLookUpReader, row count of IndexLookUpReader and TableScan should be 1.00. + "explain select * from tbl use index(idx_b_c) where b > 1 limit 2,1", + // Projection atop IndexLookUpReader, Limit should be pushed down into IndexLookUpReader, and Projection should have row count 1.00 as well. + "explain select * from tbl use index(idx_b_c) where b > 1 order by b desc limit 2,1", + // Limit should be pushed down into IndexLookUpReader when Selection on top of IndexScan. + "explain select * from tbl use index(idx_b_c) where b > 1 and c > 1 limit 2,1", + // Limit should NOT be pushed down into IndexLookUpReader when Selection on top of TableScan. + "explain select * from tbl use index(idx_b_c) where b > 1 and a > 1 limit 2,1" + ] + }, + { + "name": "TestIsFromUnixtimeNullRejective", + "cases": [ + // fix #12385 + "explain select * from t t1 left join t t2 on t1.a=t2.a where from_unixtime(t2.b);" + ] + } +] diff --git a/planner/core/testdata/integration_suite_out.json b/planner/core/testdata/integration_suite_out.json new file mode 100644 index 0000000000000..0b256fd7aa314 --- /dev/null +++ b/planner/core/testdata/integration_suite_out.json @@ -0,0 +1,24 @@ +[ + { + "Name": "TestPushLimitDownIndexLookUpReader", + "Cases": null + }, + { + "Name": "TestIsFromUnixtimeNullRejective", + "Cases": [ + { + "SQL": "explain select * from t t1 left join t t2 on t1.a=t2.a where from_unixtime(t2.b);", + "Plan": [ + "HashLeftJoin_8 9990.00 root inner join, inner:Selection_13, equal:[eq(test.t1.a, test.t2.a)]", + "├─TableReader_12 9990.00 root data:Selection_11", + "│ └─Selection_11 9990.00 cop not(isnull(test.t1.a))", + "│ └─TableScan_10 10000.00 cop table:t1, range:[-inf,+inf], keep order:false, stats:pseudo", + "└─Selection_13 7992.00 root from_unixtime(cast(test.t2.b))", + " └─TableReader_16 9990.00 root data:Selection_15", + " └─Selection_15 9990.00 cop not(isnull(test.t2.a))", + " └─TableScan_14 10000.00 cop table:t2, range:[-inf,+inf], keep order:false, stats:pseudo" + ] + } + ] + } +] diff --git a/util/testutil/testutil.go b/util/testutil/testutil.go index c89d841a2b2c5..9e4928c6df696 100644 --- a/util/testutil/testutil.go +++ b/util/testutil/testutil.go @@ -193,6 +193,26 @@ func loadTestSuiteCases(filePath string) (res []testCases, err error) { return res, err } +// GetTestCasesByName gets the test cases for a test function by its name. +func (t *TestData) GetTestCasesByName(caseName string, c *check.C, in interface{}, out interface{}) { + casesIdx, ok := t.funcMap[caseName] + c.Assert(ok, check.IsTrue, check.Commentf("Must get test %s", caseName)) + err := json.Unmarshal(*t.input[casesIdx].Cases, in) + c.Assert(err, check.IsNil) + if !record { + err = json.Unmarshal(*t.output[casesIdx].Cases, out) + c.Assert(err, check.IsNil) + } else { + // Init for generate output file. + inputLen := reflect.ValueOf(in).Elem().Len() + v := reflect.ValueOf(out).Elem() + if v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), inputLen, inputLen)) + } + } + t.output[casesIdx].decodedOut = out +} + // GetTestCases gets the test cases for a test function. func (t *TestData) GetTestCases(c *check.C, in interface{}, out interface{}) { // Extract caller's name.