From 5dae1a3135e9cfb2fbba82add592aabb86fb0210 Mon Sep 17 00:00:00 2001 From: tangenta Date: Fri, 6 Sep 2024 15:03:37 +0800 Subject: [PATCH] expression: support tidb encode key function (#51678) close pingcap/tidb#51683 --- pkg/expression/BUILD.bazel | 3 + pkg/expression/builtin.go | 5 +- pkg/expression/builtin_info.go | 227 +++++++++ pkg/expression/builtin_info_vec.go | 24 + pkg/expression/integration_test/BUILD.bazel | 2 +- .../integration_test/integration_test.go | 39 ++ pkg/parser/ast/functions.go | 3 + pkg/planner/core/BUILD.bazel | 1 + pkg/planner/core/core_init.go | 5 +- pkg/planner/core/expression_codec_fn.go | 462 ++++++++++++++++++ pkg/planner/core/expression_rewriter.go | 234 --------- tests/integrationtest/r/executor/show.result | 3 + .../addindextest3/functional_test.go | 27 + 13 files changed, 798 insertions(+), 237 deletions(-) create mode 100644 pkg/planner/core/expression_codec_fn.go diff --git a/pkg/expression/BUILD.bazel b/pkg/expression/BUILD.bazel index b92787e83f494..bfb562f84bd2c 100644 --- a/pkg/expression/BUILD.bazel +++ b/pkg/expression/BUILD.bazel @@ -93,6 +93,8 @@ go_library( "//pkg/planner/cascades/base", "//pkg/sessionctx/stmtctx", "//pkg/sessionctx/variable", + "//pkg/store/helper", + "//pkg/tablecodec", "//pkg/types", "//pkg/types/parser_driver", "//pkg/util", @@ -126,6 +128,7 @@ go_library( "@com_github_google_uuid//:uuid", "@com_github_pingcap_errors//:errors", "@com_github_pingcap_failpoint//:failpoint", + "@com_github_pingcap_kvproto//pkg/kvrpcpb", "@com_github_pingcap_tipb//go-tipb", "@com_github_qri_io_jsonschema//:jsonschema", "@com_github_tikv_client_go_v2//oracle", diff --git a/pkg/expression/builtin.go b/pkg/expression/builtin.go index 50d81b1a8f825..6f0943cc8990a 100644 --- a/pkg/expression/builtin.go +++ b/pkg/expression/builtin.go @@ -941,7 +941,10 @@ var funcs = map[string]functionClass{ ast.VecAsText: &vecAsTextFunctionClass{baseFunctionClass{ast.VecAsText, 1, 1}}, // TiDB internal function. - ast.TiDBDecodeKey: &tidbDecodeKeyFunctionClass{baseFunctionClass{ast.TiDBDecodeKey, 1, 1}}, + ast.TiDBDecodeKey: &tidbDecodeKeyFunctionClass{baseFunctionClass{ast.TiDBDecodeKey, 1, 1}}, + ast.TiDBMVCCInfo: &tidbMVCCInfoFunctionClass{baseFunctionClass: baseFunctionClass{ast.TiDBMVCCInfo, 1, 1}}, + ast.TiDBEncodeRecordKey: &tidbEncodeRecordKeyClass{baseFunctionClass{ast.TiDBEncodeRecordKey, 3, -1}}, + ast.TiDBEncodeIndexKey: &tidbEncodeIndexKeyClass{baseFunctionClass{ast.TiDBEncodeIndexKey, 4, -1}}, // This function is used to show tidb-server version info. ast.TiDBVersion: &tidbVersionFunctionClass{baseFunctionClass{ast.TiDBVersion, 0, 0}}, ast.TiDBIsDDLOwner: &tidbIsDDLOwnerFunctionClass{baseFunctionClass{ast.TiDBIsDDLOwner, 0, 0}}, diff --git a/pkg/expression/builtin_info.go b/pkg/expression/builtin_info.go index 98f1780273518..176c342339026 100644 --- a/pkg/expression/builtin_info.go +++ b/pkg/expression/builtin_info.go @@ -20,20 +20,25 @@ package expression import ( "context" + "encoding/hex" "encoding/json" "slices" "strings" "time" "github.com/pingcap/errors" + "github.com/pingcap/kvproto/pkg/kvrpcpb" "github.com/pingcap/tidb/pkg/expression/contextopt" infoschema "github.com/pingcap/tidb/pkg/infoschema/context" "github.com/pingcap/tidb/pkg/parser" "github.com/pingcap/tidb/pkg/parser/ast" "github.com/pingcap/tidb/pkg/parser/mysql" "github.com/pingcap/tidb/pkg/sessionctx/variable" + "github.com/pingcap/tidb/pkg/store/helper" + "github.com/pingcap/tidb/pkg/tablecodec" "github.com/pingcap/tidb/pkg/types" "github.com/pingcap/tidb/pkg/util/chunk" + "github.com/pingcap/tidb/pkg/util/dbterror/plannererrors" "github.com/pingcap/tidb/pkg/util/plancodec" "github.com/pingcap/tidb/pkg/util/printer" "github.com/pingcap/tipb/go-tipb" @@ -57,6 +62,9 @@ var ( _ functionClass = &tidbVersionFunctionClass{} _ functionClass = &tidbIsDDLOwnerFunctionClass{} _ functionClass = &tidbDecodePlanFunctionClass{} + _ functionClass = &tidbMVCCInfoFunctionClass{} + _ functionClass = &tidbEncodeRecordKeyClass{} + _ functionClass = &tidbEncodeIndexKeyClass{} _ functionClass = &tidbDecodeKeyFunctionClass{} _ functionClass = &tidbDecodeSQLDigestsFunctionClass{} _ functionClass = &nextValFunctionClass{} @@ -78,6 +86,9 @@ var ( _ builtinFunc = &builtinVersionSig{} _ builtinFunc = &builtinTiDBVersionSig{} _ builtinFunc = &builtinRowCountSig{} + _ builtinFunc = &builtinTiDBMVCCInfoSig{} + _ builtinFunc = &builtinTiDBEncodeRecordKeySig{} + _ builtinFunc = &builtinTiDBEncodeIndexKeySig{} _ builtinFunc = &builtinTiDBDecodeKeySig{} _ builtinFunc = &builtinTiDBDecodeSQLDigestsSig{} _ builtinFunc = &builtinNextValSig{} @@ -892,6 +903,216 @@ func (b *builtinRowCountSig) evalInt(ctx EvalContext, row chunk.Row) (res int64, return res, false, nil } +type tidbMVCCInfoFunctionClass struct { + baseFunctionClass +} + +func (c *tidbMVCCInfoFunctionClass) getFunction(ctx BuildContext, args []Expression) (builtinFunc, error) { + bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETString, types.ETString) + if err != nil { + return nil, err + } + sig := &builtinTiDBMVCCInfoSig{baseBuiltinFunc: bf} + return sig, nil +} + +type builtinTiDBMVCCInfoSig struct { + baseBuiltinFunc + contextopt.KVStorePropReader +} + +// RequiredOptionalEvalProps implements the RequireOptionalEvalProps interface. +func (b *builtinTiDBMVCCInfoSig) RequiredOptionalEvalProps() OptionalEvalPropKeySet { + return b.KVStorePropReader.RequiredOptionalEvalProps() +} + +func (b *builtinTiDBMVCCInfoSig) Clone() builtinFunc { + newSig := &builtinTiDBMVCCInfoSig{} + newSig.cloneFrom(&b.baseBuiltinFunc) + return newSig +} + +// evalString evals a builtinTiDBMVCCInfoSig. +func (b *builtinTiDBMVCCInfoSig) evalString(ctx EvalContext, row chunk.Row) (string, bool, error) { + if !ctx.RequestVerification("", "", "", mysql.SuperPriv) { + return "", false, plannererrors.ErrSpecificAccessDenied.FastGenByArgs("SUPER") + } + s, isNull, err := b.args[0].EvalString(ctx, row) + if isNull || err != nil { + return "", isNull, err + } + encodedKey, err := hex.DecodeString(s) + if err != nil { + return "", false, err + } + store, err := b.GetKVStore(ctx) + if err != nil { + return "", isNull, err + } + hStore, ok := store.(helper.Storage) + if !ok { + return "", isNull, errors.New("storage is not a helper.Storage") + } + h := helper.NewHelper(hStore) + resp, err := h.GetMvccByEncodedKey(encodedKey) + if err != nil { + return "", false, err + } + type mvccInfoResult struct { + Key string `json:"key"` + Resp *kvrpcpb.MvccGetByKeyResponse `json:"mvcc"` + } + mvccInfo := []*mvccInfoResult{{s, resp}} + if tablecodec.IsIndexKey(encodedKey) && !tablecodec.IsTempIndexKey(encodedKey) { + tablecodec.IndexKey2TempIndexKey(encodedKey) + hexStr := hex.EncodeToString(encodedKey) + res, err := h.GetMvccByEncodedKey(encodedKey) + if err != nil { + return "", false, err + } + if res.Info != nil && (len(res.Info.Writes) > 0 || len(res.Info.Values) > 0 || res.Info.Lock != nil) { + mvccInfo = append(mvccInfo, &mvccInfoResult{hexStr, res}) + } + } + js, err := json.Marshal(mvccInfo) + if err != nil { + return "", false, err + } + return string(js), false, nil +} + +type tidbEncodeRecordKeyClass struct { + baseFunctionClass +} + +func (c *tidbEncodeRecordKeyClass) getFunction(ctx BuildContext, args []Expression) (builtinFunc, error) { + if err := c.verifyArgs(args); err != nil { + return nil, err + } + evalTps := make([]types.EvalType, 0, len(args)) + evalTps = append(evalTps, types.ETString, types.ETString) + for _, arg := range args[2:] { + evalTps = append(evalTps, arg.GetType(ctx.GetEvalCtx()).EvalType()) + } + bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETString, evalTps...) + if err != nil { + return nil, err + } + sig := &builtinTiDBEncodeRecordKeySig{baseBuiltinFunc: bf} + return sig, nil +} + +type builtinTiDBEncodeRecordKeySig struct { + baseBuiltinFunc + contextopt.InfoSchemaPropReader + contextopt.SessionVarsPropReader +} + +// RequiredOptionalEvalProps implements the RequireOptionalEvalProps interface. +func (b *builtinTiDBEncodeRecordKeySig) RequiredOptionalEvalProps() OptionalEvalPropKeySet { + return b.InfoSchemaPropReader.RequiredOptionalEvalProps() | + b.SessionVarsPropReader.RequiredOptionalEvalProps() +} + +func (b *builtinTiDBEncodeRecordKeySig) Clone() builtinFunc { + newSig := &builtinTiDBEncodeRecordKeySig{} + newSig.cloneFrom(&b.baseBuiltinFunc) + return newSig +} + +// evalString evals a builtinTiDBEncodeRecordKeySig. +func (b *builtinTiDBEncodeRecordKeySig) evalString(ctx EvalContext, row chunk.Row) (string, bool, error) { + is, err := b.GetDomainInfoSchema(ctx) + if err != nil { + return "", true, err + } + if EncodeRecordKeyFromRow == nil { + return "", false, errors.New("EncodeRecordKeyFromRow is not initialized") + } + recordKey, isNull, err := EncodeRecordKeyFromRow(ctx, is, b.args, row) + if isNull || err != nil { + if errors.ErrorEqual(err, plannererrors.ErrSpecificAccessDenied) { + sv, err2 := b.GetSessionVars(ctx) + if err2 != nil { + return "", isNull, err + } + tblName, isNull, err2 := b.args[1].EvalString(ctx, row) + if err2 != nil || isNull { + return "", isNull, err + } + return "", isNull, plannererrors.ErrTableaccessDenied.FastGenByArgs("SELECT", sv.User.AuthUsername, sv.User.AuthHostname, tblName) + } + return "", isNull, err + } + return hex.EncodeToString(recordKey), false, nil +} + +type tidbEncodeIndexKeyClass struct { + baseFunctionClass +} + +func (c *tidbEncodeIndexKeyClass) getFunction(ctx BuildContext, args []Expression) (builtinFunc, error) { + if err := c.verifyArgs(args); err != nil { + return nil, err + } + evalTps := make([]types.EvalType, 0, len(args)) + evalTps = append(evalTps, types.ETString, types.ETString, types.ETString) + for _, arg := range args[3:] { + evalTps = append(evalTps, arg.GetType(ctx.GetEvalCtx()).EvalType()) + } + bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETString, evalTps...) + if err != nil { + return nil, err + } + sig := &builtinTiDBEncodeIndexKeySig{baseBuiltinFunc: bf} + return sig, nil +} + +type builtinTiDBEncodeIndexKeySig struct { + baseBuiltinFunc + contextopt.InfoSchemaPropReader + contextopt.SessionVarsPropReader +} + +// RequiredOptionalEvalProps implements the RequireOptionalEvalProps interface. +func (b *builtinTiDBEncodeIndexKeySig) RequiredOptionalEvalProps() OptionalEvalPropKeySet { + return b.InfoSchemaPropReader.RequiredOptionalEvalProps() | + b.SessionVarsPropReader.RequiredOptionalEvalProps() +} + +func (b *builtinTiDBEncodeIndexKeySig) Clone() builtinFunc { + newSig := &builtinTiDBEncodeIndexKeySig{} + newSig.cloneFrom(&b.baseBuiltinFunc) + return newSig +} + +// evalString evals a builtinTiDBEncodeIndexKeySig. +func (b *builtinTiDBEncodeIndexKeySig) evalString(ctx EvalContext, row chunk.Row) (string, bool, error) { + is, err := b.GetDomainInfoSchema(ctx) + if err != nil { + return "", true, err + } + if EncodeIndexKeyFromRow == nil { + return "", false, errors.New("EncodeIndexKeyFromRow is not initialized") + } + idxKey, isNull, err := EncodeIndexKeyFromRow(ctx, is, b.args, row) + if isNull || err != nil { + if errors.ErrorEqual(err, plannererrors.ErrSpecificAccessDenied) { + sv, err2 := b.GetSessionVars(ctx) + if err2 != nil { + return "", isNull, err + } + tblName, isNull, err2 := b.args[1].EvalString(ctx, row) + if err2 != nil || isNull { + return "", isNull, err + } + return "", isNull, plannererrors.ErrTableaccessDenied.FastGenByArgs("SELECT", sv.User.AuthUsername, sv.User.AuthHostname, tblName) + } + return "", isNull, err + } + return hex.EncodeToString(idxKey), false, nil +} + type tidbDecodeKeyFunctionClass struct { baseFunctionClass } @@ -911,6 +1132,12 @@ func (c *tidbDecodeKeyFunctionClass) getFunction(ctx BuildContext, args []Expres // DecodeKeyFromString is used to decode key by expressions var DecodeKeyFromString func(types.Context, infoschema.MetaOnlyInfoSchema, string) string +// EncodeRecordKeyFromRow is used to encode record key by expressions. +var EncodeRecordKeyFromRow func(ctx EvalContext, is infoschema.MetaOnlyInfoSchema, args []Expression, row chunk.Row) ([]byte, bool, error) + +// EncodeIndexKeyFromRow is used to encode index key by expressions. +var EncodeIndexKeyFromRow func(ctx EvalContext, is infoschema.MetaOnlyInfoSchema, args []Expression, row chunk.Row) ([]byte, bool, error) + type builtinTiDBDecodeKeySig struct { baseBuiltinFunc contextopt.InfoSchemaPropReader diff --git a/pkg/expression/builtin_info_vec.go b/pkg/expression/builtin_info_vec.go index 5887ad6475b6c..34c576bc5a659 100644 --- a/pkg/expression/builtin_info_vec.go +++ b/pkg/expression/builtin_info_vec.go @@ -385,6 +385,30 @@ func (b *builtinVersionSig) vecEvalString(ctx EvalContext, input *chunk.Chunk, r return nil } +func (b *builtinTiDBMVCCInfoSig) vectorized() bool { + return false +} + +func (b *builtinTiDBMVCCInfoSig) vecEvalString(ctx EvalContext, input *chunk.Chunk, result *chunk.Column) error { + return errors.Errorf("not implemented") +} + +func (b *builtinTiDBEncodeRecordKeySig) vectorized() bool { + return false +} + +func (b *builtinTiDBEncodeRecordKeySig) vecEvalString(ctx EvalContext, input *chunk.Chunk, result *chunk.Column) error { + return errors.Errorf("not implemented") +} + +func (b *builtinTiDBEncodeIndexKeySig) vectorized() bool { + return false +} + +func (b *builtinTiDBEncodeIndexKeySig) vecEvalString(ctx EvalContext, input *chunk.Chunk, result *chunk.Column) error { + return errors.Errorf("not implemented") +} + func (b *builtinTiDBDecodeKeySig) vectorized() bool { return true } diff --git a/pkg/expression/integration_test/BUILD.bazel b/pkg/expression/integration_test/BUILD.bazel index 42f4b40dd89df..b99f7815f8fd2 100644 --- a/pkg/expression/integration_test/BUILD.bazel +++ b/pkg/expression/integration_test/BUILD.bazel @@ -8,7 +8,7 @@ go_test( "main_test.go", ], flaky = True, - shard_count = 42, + shard_count = 43, deps = [ "//pkg/config", "//pkg/domain", diff --git a/pkg/expression/integration_test/integration_test.go b/pkg/expression/integration_test/integration_test.go index 3e81cdf8bed58..a1cd13635bb9d 100644 --- a/pkg/expression/integration_test/integration_test.go +++ b/pkg/expression/integration_test/integration_test.go @@ -1107,6 +1107,45 @@ func TestTiDBDecodeKeyFunc(t *testing.T) { tk.MustQuery(sql).Check(testkit.Rows(rs)) } +func TestTiDBEncodeKey(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + tk.MustExec("create table t (a int primary key, b int);") + tk.MustExec("insert into t values (1, 1);") + err := tk.QueryToErr("select tidb_encode_record_key('test', 't1', 0);") + require.ErrorContains(t, err, "doesn't exist") + tk.MustQuery("select tidb_encode_record_key('test', 't', 1);"). + Check(testkit.Rows("7480000000000000685f728000000000000001")) + + tk.MustExec("alter table t add index i(b);") + err = tk.QueryToErr("select tidb_encode_index_key('test', 't', 'i1', 1);") + require.ErrorContains(t, err, "index not found") + tk.MustQuery("select tidb_encode_index_key('test', 't', 'i', 1, 1);"). + Check(testkit.Rows("7480000000000000685f698000000000000001038000000000000001038000000000000001")) + + tk.MustExec("create table t1 (a int primary key, b int) partition by hash(a) partitions 4;") + tk.MustExec("insert into t1 values (1, 1);") + tk.MustQuery("select tidb_encode_record_key('test', 't1(p1)', 1);").Check(testkit.Rows("74800000000000006d5f728000000000000001")) + rs := tk.MustQuery("select tidb_mvcc_info('74800000000000006d5f728000000000000001');") + mvccInfo := rs.Rows()[0][0].(string) + require.NotEqual(t, mvccInfo, `{"info":{}}`) + + tk.MustExec("create user 'alice'@'%';") + tk.MustExec("flush privileges;") + tk2 := testkit.NewTestKit(t, store) + err = tk2.Session().Auth(&auth.UserIdentity{Username: "alice", Hostname: "localhost"}, nil, nil, nil) + require.NoError(t, err) + err = tk2.QueryToErr("select tidb_mvcc_info('74800000000000006d5f728000000000000001');") + require.ErrorContains(t, err, "Access denied") + err = tk2.QueryToErr("select tidb_encode_record_key('test', 't1(p1)', 1);") + require.ErrorContains(t, err, "SELECT command denied") + err = tk2.QueryToErr("select tidb_encode_index_key('test', 't', 'i1', 1);") + require.ErrorContains(t, err, "SELECT command denied") + tk.MustExec("grant select on test.t1 to 'alice'@'%';") + tk2.MustQuery("select tidb_encode_record_key('test', 't1(p1)', 1);").Check(testkit.Rows("74800000000000006d5f728000000000000001")) +} + func TestIssue9710(t *testing.T) { store := testkit.CreateMockStore(t) diff --git a/pkg/parser/ast/functions.go b/pkg/parser/ast/functions.go index 173018e6babea..68e736b20ced9 100644 --- a/pkg/parser/ast/functions.go +++ b/pkg/parser/ast/functions.go @@ -368,6 +368,9 @@ const ( // TiDB internal function. TiDBDecodeKey = "tidb_decode_key" + TiDBMVCCInfo = "tidb_mvcc_info" + TiDBEncodeRecordKey = "tidb_encode_record_key" + TiDBEncodeIndexKey = "tidb_encode_index_key" TiDBDecodeBase64Key = "tidb_decode_base64_key" // MVCC information fetching function. diff --git a/pkg/planner/core/BUILD.bazel b/pkg/planner/core/BUILD.bazel index 960ad2f493817..6a7fa154f4ae5 100644 --- a/pkg/planner/core/BUILD.bazel +++ b/pkg/planner/core/BUILD.bazel @@ -11,6 +11,7 @@ go_library( "encode.go", "exhaust_physical_plans.go", "explain.go", + "expression_codec_fn.go", "expression_rewriter.go", "find_best_task.go", "flat_plan.go", diff --git a/pkg/planner/core/core_init.go b/pkg/planner/core/core_init.go index b191d90f3a85c..a0af6bad0cccb 100644 --- a/pkg/planner/core/core_init.go +++ b/pkg/planner/core/core_init.go @@ -77,7 +77,10 @@ func init() { base.InvalidTask = &RootTask{} // invalid if p is nil expression.EvalSimpleAst = evalAstExpr expression.BuildSimpleExpr = buildSimpleExpr - expression.DecodeKeyFromString = decodeKeyFromString + helper := tidbCodecFuncHelper{} + expression.DecodeKeyFromString = helper.decodeKeyFromString + expression.EncodeRecordKeyFromRow = helper.encodeHandleFromRow + expression.EncodeIndexKeyFromRow = helper.encodeIndexKeyFromRow plannerutil.EvalAstExprWithPlanCtx = evalAstExprWithPlanCtx plannerutil.RewriteAstExprWithPlanCtx = rewriteAstExprWithPlanCtx DefaultDisabledLogicalRulesList = new(atomic.Value) diff --git a/pkg/planner/core/expression_codec_fn.go b/pkg/planner/core/expression_codec_fn.go new file mode 100644 index 0000000000000..461aca426e321 --- /dev/null +++ b/pkg/planner/core/expression_codec_fn.go @@ -0,0 +1,462 @@ +// Copyright 2024 PingCAP, 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 core + +import ( + "context" + "encoding/hex" + "encoding/json" + "strconv" + "strings" + "time" + + "github.com/pingcap/errors" + "github.com/pingcap/tidb/pkg/expression" + "github.com/pingcap/tidb/pkg/infoschema" + infoschemactx "github.com/pingcap/tidb/pkg/infoschema/context" + "github.com/pingcap/tidb/pkg/kv" + "github.com/pingcap/tidb/pkg/meta/model" + pmodel "github.com/pingcap/tidb/pkg/parser/model" + "github.com/pingcap/tidb/pkg/parser/mysql" + "github.com/pingcap/tidb/pkg/table" + "github.com/pingcap/tidb/pkg/table/tables" + "github.com/pingcap/tidb/pkg/tablecodec" + "github.com/pingcap/tidb/pkg/types" + "github.com/pingcap/tidb/pkg/util/chunk" + "github.com/pingcap/tidb/pkg/util/codec" + "github.com/pingcap/tidb/pkg/util/dbterror/plannererrors" +) + +// tidbCodecFuncHelper contains some utililty functions for +// - tidb_decode_key(hex_string) +// - tidb_encode_record_key(database_name, table_name, handle/pk columns...) +// - tidb_encode_index_key(database_name, table_name, index_name, index columns..., handle/pk columns...) +// +// define an individual struct instead of a bunch of un-exported functions +// to avoid polluting the global scope of current package. +type tidbCodecFuncHelper struct{} + +func (h tidbCodecFuncHelper) encodeHandleFromRow( + ctx expression.EvalContext, + isVer infoschemactx.MetaOnlyInfoSchema, + args []expression.Expression, + row chunk.Row, +) ([]byte, bool, error) { + dbName, isNull, err := args[0].EvalString(ctx, row) + if err != nil || isNull { + return nil, isNull, err + } + tblName, isNull, err := args[1].EvalString(ctx, row) + if err != nil || isNull { + return nil, isNull, err + } + is := isVer.(infoschema.InfoSchema) + tbl, _, err := h.findCommonOrPartitionedTable(ctx, is, dbName, tblName) + if err != nil { + return nil, false, err + } + recordID, err := h.buildHandle(ctx, tbl.Meta(), args[2:], row) + if err != nil { + return nil, false, err + } + key := tablecodec.EncodeRecordKey(tbl.RecordPrefix(), recordID) + return key, false, nil +} + +func (h tidbCodecFuncHelper) findCommonOrPartitionedTable( + ctx expression.EvalContext, + is infoschema.InfoSchema, + dbName string, + tblName string, +) (table.Table, int64, error) { + tblName, partName := h.extractTablePartition(tblName) + tbl, err := is.TableByName(context.Background(), pmodel.NewCIStr(dbName), pmodel.NewCIStr(tblName)) + if err != nil { + return nil, 0, err + } + if !ctx.RequestVerification(dbName, tblName, "", mysql.AllPrivMask) { + // The arguments will be filled by caller. + return nil, 0, plannererrors.ErrSpecificAccessDenied + } + if len(partName) > 0 { + if part := tbl.GetPartitionedTable(); part != nil { + pid, err := tables.FindPartitionByName(tbl.Meta(), partName) + if err != nil { + return nil, 0, errors.Trace(err) + } + tbl = part.GetPartition(pid) + return tbl, pid, nil + } + return nil, 0, errors.New("not a partitioned table") + } + return tbl, tbl.Meta().ID, nil +} + +func (tidbCodecFuncHelper) extractTablePartition(str string) (table, partition string) { + start := strings.IndexByte(str, '(') + if start == -1 { + return str, "" + } + end := strings.IndexByte(str, ')') + if end == -1 { + return str, "" + } + return str[:start], str[start+1 : end] +} + +func (tidbCodecFuncHelper) buildHandle( + ctx expression.EvalContext, + tblInfo *model.TableInfo, + pkArgs []expression.Expression, + row chunk.Row, +) (kv.Handle, error) { + var recordID kv.Handle + if !tblInfo.IsCommonHandle { + h, _, err := pkArgs[0].EvalInt(ctx, row) + if err != nil { + return nil, err + } + recordID = kv.IntHandle(h) + } else { + pkIdx := tables.FindPrimaryIndex(tblInfo) + if len(pkIdx.Columns) != len(pkArgs) { + return nil, errors.Errorf("pk column count mismatch, expected %d, got %d", len(pkIdx.Columns), pkArgs) + } + pkDts := make([]types.Datum, 0, len(pkIdx.Columns)) + for i, idxCol := range pkIdx.Columns { + dt, err := pkArgs[i].Eval(ctx, row) + if err != nil { + return nil, err + } + ft := tblInfo.Columns[idxCol.Offset].FieldType + pkDt, err := dt.ConvertTo(ctx.TypeCtx(), &ft) + if err != nil { + return nil, err + } + pkDts = append(pkDts, pkDt) + } + tablecodec.TruncateIndexValues(tblInfo, pkIdx, pkDts) + var handleBytes []byte + handleBytes, err := codec.EncodeKey(ctx.Location(), nil, pkDts...) + ec := ctx.ErrCtx() + err = ec.HandleError(err) + if err != nil { + return nil, err + } + recordID, err = kv.NewCommonHandle(handleBytes) + if err != nil { + return nil, err + } + } + return recordID, nil +} + +func (h tidbCodecFuncHelper) encodeIndexKeyFromRow( + ctx expression.EvalContext, + isVer infoschemactx.MetaOnlyInfoSchema, + args []expression.Expression, + row chunk.Row, +) ([]byte, bool, error) { + dbName, isNull, err := args[0].EvalString(ctx, row) + if err != nil || isNull { + return nil, isNull, err + } + tblName, isNull, err := args[1].EvalString(ctx, row) + if err != nil || isNull { + return nil, isNull, err + } + idxName, isNull, err := args[2].EvalString(ctx, row) + if err != nil || isNull { + return nil, isNull, err + } + is := isVer.(infoschema.InfoSchema) + tbl, physicalID, err := h.findCommonOrPartitionedTable(ctx, is, dbName, tblName) + if err != nil { + return nil, false, err + } + tblInfo := tbl.Meta() + idxInfo := tblInfo.FindIndexByName(strings.ToLower(idxName)) + if idxInfo == nil { + return nil, false, errors.New("index not found") + } + + pkLen := 1 + var pkIdx *model.IndexInfo + if tblInfo.IsCommonHandle { + pkIdx = tables.FindPrimaryIndex(tblInfo) + pkLen = len(pkIdx.Columns) + } + + if len(idxInfo.Columns)+pkLen != len(args)-3 { + return nil, false, errors.Errorf( + "column count mismatch, expected %d (index length + pk/rowid length), got %d", + len(idxInfo.Columns)+pkLen, len(args)-3) + } + + handle, err := h.buildHandle(ctx, tblInfo, args[3+len(idxInfo.Columns):], row) + if err != nil { + return nil, false, err + } + + idxDts := make([]types.Datum, 0, len(idxInfo.Columns)) + for i, idxCol := range idxInfo.Columns { + dt, err := args[i+3].Eval(ctx, row) + if err != nil { + return nil, false, err + } + ft := tblInfo.Columns[idxCol.Offset].FieldType + idxDt, err := dt.ConvertTo(ctx.TypeCtx(), &ft) + if err != nil { + return nil, false, err + } + idxDts = append(idxDts, idxDt) + } + tablecodec.TruncateIndexValues(tblInfo, idxInfo, idxDts) + // Use physicalID instead of tblInfo.ID here to handle the partition case. + idx := tables.NewIndex(physicalID, tblInfo, idxInfo) + + idxKey, _, err := idx.GenIndexKey(ctx.ErrCtx(), ctx.Location(), idxDts, handle, nil) + return idxKey, false, err +} + +func (h tidbCodecFuncHelper) decodeKeyFromString( + tc types.Context, isVer infoschemactx.MetaOnlyInfoSchema, s string) string { + key, err := hex.DecodeString(s) + if err != nil { + tc.AppendWarning(errors.NewNoStackErrorf("invalid key: %X", key)) + return s + } + // Auto decode byte if needed. + _, bs, err := codec.DecodeBytes(key, nil) + if err == nil { + key = bs + } + tableID := tablecodec.DecodeTableID(key) + if tableID <= 0 { + tc.AppendWarning(errors.NewNoStackErrorf("invalid key: %X", key)) + return s + } + + is, ok := isVer.(infoschema.InfoSchema) + if !ok { + tc.AppendWarning(errors.NewNoStackErrorf("infoschema not found when decoding key: %X", key)) + return s + } + tbl, _ := infoschema.FindTableByTblOrPartID(is, tableID) + loc := tc.Location() + if tablecodec.IsRecordKey(key) { + ret, err := h.decodeRecordKey(key, tableID, tbl, loc) + if err != nil { + tc.AppendWarning(err) + return s + } + return ret + } else if tablecodec.IsIndexKey(key) { + ret, err := h.decodeIndexKey(key, tableID, tbl, loc) + if err != nil { + tc.AppendWarning(err) + return s + } + return ret + } else if tablecodec.IsTableKey(key) { + ret, err := h.decodeTableKey(key, tableID, tbl) + if err != nil { + tc.AppendWarning(err) + return s + } + return ret + } + tc.AppendWarning(errors.NewNoStackErrorf("invalid key: %X", key)) + return s +} + +func (h tidbCodecFuncHelper) decodeRecordKey( + key []byte, tableID int64, tbl table.Table, loc *time.Location) (string, error) { + _, handle, err := tablecodec.DecodeRecordKey(key) + if err != nil { + return "", errors.Trace(err) + } + if handle.IsInt() { + ret := make(map[string]any) + if tbl != nil && tbl.Meta().Partition != nil { + ret["partition_id"] = tableID + tableID = tbl.Meta().ID + } + ret["table_id"] = strconv.FormatInt(tableID, 10) + // When the clustered index is enabled, we should show the PK name. + if tbl != nil && tbl.Meta().HasClusteredIndex() { + ret[tbl.Meta().GetPkName().String()] = handle.IntValue() + } else { + ret["_tidb_rowid"] = handle.IntValue() + } + retStr, err := json.Marshal(ret) + if err != nil { + return "", errors.Trace(err) + } + return string(retStr), nil + } + if tbl != nil { + tblInfo := tbl.Meta() + idxInfo := tables.FindPrimaryIndex(tblInfo) + if idxInfo == nil { + return "", errors.Trace(errors.Errorf("primary key not found when decoding record key: %X", key)) + } + cols := make(map[int64]*types.FieldType, len(tblInfo.Columns)) + for _, col := range tblInfo.Columns { + cols[col.ID] = &(col.FieldType) + } + handleColIDs := make([]int64, 0, len(idxInfo.Columns)) + for _, col := range idxInfo.Columns { + handleColIDs = append(handleColIDs, tblInfo.Columns[col.Offset].ID) + } + + if len(handleColIDs) != handle.NumCols() { + return "", errors.Trace(errors.Errorf("primary key length not match handle columns number in key")) + } + datumMap, err := tablecodec.DecodeHandleToDatumMap(handle, handleColIDs, cols, loc, nil) + if err != nil { + return "", errors.Trace(err) + } + ret := make(map[string]any) + if tbl.Meta().Partition != nil { + ret["partition_id"] = tableID + tableID = tbl.Meta().ID + } + ret["table_id"] = tableID + handleRet := make(map[string]any) + for colID := range datumMap { + dt := datumMap[colID] + dtStr, err := h.datumToJSONObject(&dt) + if err != nil { + return "", errors.Trace(err) + } + found := false + for _, colInfo := range tblInfo.Columns { + if colInfo.ID == colID { + found = true + handleRet[colInfo.Name.L] = dtStr + break + } + } + if !found { + return "", errors.Trace(errors.Errorf("column not found when decoding record key: %X", key)) + } + } + ret["handle"] = handleRet + retStr, err := json.Marshal(ret) + if err != nil { + return "", errors.Trace(err) + } + return string(retStr), nil + } + ret := make(map[string]any) + ret["table_id"] = tableID + ret["handle"] = handle.String() + retStr, err := json.Marshal(ret) + if err != nil { + return "", errors.Trace(err) + } + return string(retStr), nil +} + +func (h tidbCodecFuncHelper) decodeIndexKey( + key []byte, tableID int64, tbl table.Table, loc *time.Location) (string, error) { + if tbl != nil { + _, indexID, _, err := tablecodec.DecodeKeyHead(key) + if err != nil { + return "", errors.Trace(errors.Errorf("invalid record/index key: %X", key)) + } + tblInfo := tbl.Meta() + var targetIndex *model.IndexInfo + for _, idx := range tblInfo.Indices { + if idx.ID == indexID { + targetIndex = idx + break + } + } + if targetIndex == nil { + return "", errors.Trace(errors.Errorf("index not found when decoding index key: %X", key)) + } + colInfos := tables.BuildRowcodecColInfoForIndexColumns(targetIndex, tblInfo) + tps := tables.BuildFieldTypesForIndexColumns(targetIndex, tblInfo) + values, err := tablecodec.DecodeIndexKV(key, []byte{0}, len(colInfos), tablecodec.HandleNotNeeded, colInfos) + if err != nil { + return "", errors.Trace(err) + } + ds := make([]types.Datum, 0, len(colInfos)) + for i := 0; i < len(colInfos); i++ { + d, err := tablecodec.DecodeColumnValue(values[i], tps[i], loc) + if err != nil { + return "", errors.Trace(err) + } + ds = append(ds, d) + } + ret := make(map[string]any) + if tbl.Meta().Partition != nil { + ret["partition_id"] = tableID + tableID = tbl.Meta().ID + } + ret["table_id"] = tableID + ret["index_id"] = indexID + idxValMap := make(map[string]any, len(targetIndex.Columns)) + for i := 0; i < len(targetIndex.Columns); i++ { + dtStr, err := h.datumToJSONObject(&ds[i]) + if err != nil { + return "", errors.Trace(err) + } + idxValMap[targetIndex.Columns[i].Name.L] = dtStr + } + ret["index_vals"] = idxValMap + retStr, err := json.Marshal(ret) + if err != nil { + return "", errors.Trace(err) + } + return string(retStr), nil + } + _, indexID, indexValues, err := tablecodec.DecodeIndexKey(key) + if err != nil { + return "", errors.Trace(errors.Errorf("invalid index key: %X", key)) + } + ret := make(map[string]any) + ret["table_id"] = tableID + ret["index_id"] = indexID + ret["index_vals"] = strings.Join(indexValues, ", ") + retStr, err := json.Marshal(ret) + if err != nil { + return "", errors.Trace(err) + } + return string(retStr), nil +} + +func (tidbCodecFuncHelper) decodeTableKey(_ []byte, tableID int64, tbl table.Table) (string, error) { + ret := map[string]int64{} + if tbl != nil && tbl.Meta().GetPartitionInfo() != nil { + ret["partition_id"] = tableID + tableID = tbl.Meta().ID + } + ret["table_id"] = tableID + retStr, err := json.Marshal(ret) + if err != nil { + return "", errors.Trace(err) + } + return string(retStr), nil +} + +func (tidbCodecFuncHelper) datumToJSONObject(d *types.Datum) (any, error) { + if d.IsNull() { + return nil, nil + } + return d.ToString() +} diff --git a/pkg/planner/core/expression_rewriter.go b/pkg/planner/core/expression_rewriter.go index 7c75b71e28ef3..68607389911f9 100644 --- a/pkg/planner/core/expression_rewriter.go +++ b/pkg/planner/core/expression_rewriter.go @@ -16,12 +16,9 @@ package core import ( "context" - "encoding/hex" - "encoding/json" "fmt" "strconv" "strings" - "time" "github.com/pingcap/errors" "github.com/pingcap/tidb/pkg/expression" @@ -29,7 +26,6 @@ import ( exprctx "github.com/pingcap/tidb/pkg/expression/context" "github.com/pingcap/tidb/pkg/expression/contextopt" "github.com/pingcap/tidb/pkg/infoschema" - infoschemactx "github.com/pingcap/tidb/pkg/infoschema/context" "github.com/pingcap/tidb/pkg/meta/model" "github.com/pingcap/tidb/pkg/parser/ast" "github.com/pingcap/tidb/pkg/parser/charset" @@ -42,12 +38,9 @@ import ( "github.com/pingcap/tidb/pkg/planner/util/coreusage" "github.com/pingcap/tidb/pkg/sessionctx/variable" "github.com/pingcap/tidb/pkg/table" - "github.com/pingcap/tidb/pkg/table/tables" - "github.com/pingcap/tidb/pkg/tablecodec" "github.com/pingcap/tidb/pkg/types" driver "github.com/pingcap/tidb/pkg/types/parser_driver" "github.com/pingcap/tidb/pkg/util/chunk" - "github.com/pingcap/tidb/pkg/util/codec" "github.com/pingcap/tidb/pkg/util/collate" "github.com/pingcap/tidb/pkg/util/dbterror/plannererrors" "github.com/pingcap/tidb/pkg/util/hint" @@ -2640,230 +2633,3 @@ func hasCurrentDatetimeDefault(col *model.ColumnInfo) bool { } return strings.ToLower(x) == ast.CurrentTimestamp } - -func decodeKeyFromString(tc types.Context, isVer infoschemactx.MetaOnlyInfoSchema, s string) string { - key, err := hex.DecodeString(s) - if err != nil { - tc.AppendWarning(errors.NewNoStackErrorf("invalid key: %X", key)) - return s - } - // Auto decode byte if needed. - _, bs, err := codec.DecodeBytes(key, nil) - if err == nil { - key = bs - } - tableID := tablecodec.DecodeTableID(key) - if tableID <= 0 { - tc.AppendWarning(errors.NewNoStackErrorf("invalid key: %X", key)) - return s - } - - is, ok := isVer.(infoschema.InfoSchema) - if !ok { - tc.AppendWarning(errors.NewNoStackErrorf("infoschema not found when decoding key: %X", key)) - return s - } - tbl, _ := infoschema.FindTableByTblOrPartID(is, tableID) - loc := tc.Location() - if tablecodec.IsRecordKey(key) { - ret, err := decodeRecordKey(key, tableID, tbl, loc) - if err != nil { - tc.AppendWarning(err) - return s - } - return ret - } else if tablecodec.IsIndexKey(key) { - ret, err := decodeIndexKey(key, tableID, tbl, loc) - if err != nil { - tc.AppendWarning(err) - return s - } - return ret - } else if tablecodec.IsTableKey(key) { - ret, err := decodeTableKey(key, tableID, tbl) - if err != nil { - tc.AppendWarning(err) - return s - } - return ret - } - tc.AppendWarning(errors.NewNoStackErrorf("invalid key: %X", key)) - return s -} - -func decodeRecordKey(key []byte, tableID int64, tbl table.Table, loc *time.Location) (string, error) { - _, handle, err := tablecodec.DecodeRecordKey(key) - if err != nil { - return "", errors.Trace(err) - } - if handle.IsInt() { - ret := make(map[string]any) - if tbl != nil && tbl.Meta().Partition != nil { - ret["partition_id"] = tableID - tableID = tbl.Meta().ID - } - ret["table_id"] = strconv.FormatInt(tableID, 10) - // When the clustered index is enabled, we should show the PK name. - if tbl != nil && tbl.Meta().HasClusteredIndex() { - ret[tbl.Meta().GetPkName().String()] = handle.IntValue() - } else { - ret["_tidb_rowid"] = handle.IntValue() - } - retStr, err := json.Marshal(ret) - if err != nil { - return "", errors.Trace(err) - } - return string(retStr), nil - } - if tbl != nil { - tblInfo := tbl.Meta() - idxInfo := tables.FindPrimaryIndex(tblInfo) - if idxInfo == nil { - return "", errors.Trace(errors.Errorf("primary key not found when decoding record key: %X", key)) - } - cols := make(map[int64]*types.FieldType, len(tblInfo.Columns)) - for _, col := range tblInfo.Columns { - cols[col.ID] = &(col.FieldType) - } - handleColIDs := make([]int64, 0, len(idxInfo.Columns)) - for _, col := range idxInfo.Columns { - handleColIDs = append(handleColIDs, tblInfo.Columns[col.Offset].ID) - } - - if len(handleColIDs) != handle.NumCols() { - return "", errors.Trace(errors.Errorf("primary key length not match handle columns number in key")) - } - datumMap, err := tablecodec.DecodeHandleToDatumMap(handle, handleColIDs, cols, loc, nil) - if err != nil { - return "", errors.Trace(err) - } - ret := make(map[string]any) - if tbl.Meta().Partition != nil { - ret["partition_id"] = tableID - tableID = tbl.Meta().ID - } - ret["table_id"] = tableID - handleRet := make(map[string]any) - for colID := range datumMap { - dt := datumMap[colID] - dtStr, err := datumToJSONObject(&dt) - if err != nil { - return "", errors.Trace(err) - } - found := false - for _, colInfo := range tblInfo.Columns { - if colInfo.ID == colID { - found = true - handleRet[colInfo.Name.L] = dtStr - break - } - } - if !found { - return "", errors.Trace(errors.Errorf("column not found when decoding record key: %X", key)) - } - } - ret["handle"] = handleRet - retStr, err := json.Marshal(ret) - if err != nil { - return "", errors.Trace(err) - } - return string(retStr), nil - } - ret := make(map[string]any) - ret["table_id"] = tableID - ret["handle"] = handle.String() - retStr, err := json.Marshal(ret) - if err != nil { - return "", errors.Trace(err) - } - return string(retStr), nil -} - -func decodeIndexKey(key []byte, tableID int64, tbl table.Table, loc *time.Location) (string, error) { - if tbl != nil { - _, indexID, _, err := tablecodec.DecodeKeyHead(key) - if err != nil { - return "", errors.Trace(errors.Errorf("invalid record/index key: %X", key)) - } - tblInfo := tbl.Meta() - var targetIndex *model.IndexInfo - for _, idx := range tblInfo.Indices { - if idx.ID == indexID { - targetIndex = idx - break - } - } - if targetIndex == nil { - return "", errors.Trace(errors.Errorf("index not found when decoding index key: %X", key)) - } - colInfos := tables.BuildRowcodecColInfoForIndexColumns(targetIndex, tblInfo) - tps := tables.BuildFieldTypesForIndexColumns(targetIndex, tblInfo) - values, err := tablecodec.DecodeIndexKV(key, []byte{0}, len(colInfos), tablecodec.HandleNotNeeded, colInfos) - if err != nil { - return "", errors.Trace(err) - } - ds := make([]types.Datum, 0, len(colInfos)) - for i := 0; i < len(colInfos); i++ { - d, err := tablecodec.DecodeColumnValue(values[i], tps[i], loc) - if err != nil { - return "", errors.Trace(err) - } - ds = append(ds, d) - } - ret := make(map[string]any) - if tbl.Meta().Partition != nil { - ret["partition_id"] = tableID - tableID = tbl.Meta().ID - } - ret["table_id"] = tableID - ret["index_id"] = indexID - idxValMap := make(map[string]any, len(targetIndex.Columns)) - for i := 0; i < len(targetIndex.Columns); i++ { - dtStr, err := datumToJSONObject(&ds[i]) - if err != nil { - return "", errors.Trace(err) - } - idxValMap[targetIndex.Columns[i].Name.L] = dtStr - } - ret["index_vals"] = idxValMap - retStr, err := json.Marshal(ret) - if err != nil { - return "", errors.Trace(err) - } - return string(retStr), nil - } - _, indexID, indexValues, err := tablecodec.DecodeIndexKey(key) - if err != nil { - return "", errors.Trace(errors.Errorf("invalid index key: %X", key)) - } - ret := make(map[string]any) - ret["table_id"] = tableID - ret["index_id"] = indexID - ret["index_vals"] = strings.Join(indexValues, ", ") - retStr, err := json.Marshal(ret) - if err != nil { - return "", errors.Trace(err) - } - return string(retStr), nil -} - -func decodeTableKey(_ []byte, tableID int64, tbl table.Table) (string, error) { - ret := map[string]int64{} - if tbl != nil && tbl.Meta().GetPartitionInfo() != nil { - ret["partition_id"] = tableID - tableID = tbl.Meta().ID - } - ret["table_id"] = tableID - retStr, err := json.Marshal(ret) - if err != nil { - return "", errors.Trace(err) - } - return string(retStr), nil -} - -func datumToJSONObject(d *types.Datum) (any, error) { - if d.IsNull() { - return nil, nil - } - return d.ToString() -} diff --git a/tests/integrationtest/r/executor/show.result b/tests/integrationtest/r/executor/show.result index cfd90be7fc7c5..4c0627d302622 100644 --- a/tests/integrationtest/r/executor/show.result +++ b/tests/integrationtest/r/executor/show.result @@ -839,8 +839,11 @@ tidb_decode_binary_plan tidb_decode_key tidb_decode_plan tidb_decode_sql_digests +tidb_encode_index_key +tidb_encode_record_key tidb_encode_sql_digest tidb_is_ddl_owner +tidb_mvcc_info tidb_parse_tso tidb_parse_tso_logical tidb_row_checksum diff --git a/tests/realtikvtest/addindextest3/functional_test.go b/tests/realtikvtest/addindextest3/functional_test.go index 51de47d165be4..aaa6eb4e29764 100644 --- a/tests/realtikvtest/addindextest3/functional_test.go +++ b/tests/realtikvtest/addindextest3/functional_test.go @@ -17,6 +17,7 @@ package addindextest import ( "context" "fmt" + "strings" "sync" "testing" @@ -116,3 +117,29 @@ func TestMockMemoryUsedUp(t *testing.T) { tk.MustExec("insert into t values (1,1,1,1), (2,2,2,2), (3,3,3,3);") tk.MustGetErrMsg("alter table t add index i(c), add index i2(c2);", "[ddl:8247]Ingest failed: memory used up") } + +func TestTiDBEncodeKeyTempIndexKey(t *testing.T) { + store := realtikvtest.CreateMockStoreAndSetup(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + tk.MustExec("create table t (a int primary key, b int);") + tk.MustExec("insert into t values (1, 1);") + runDML := false + testfailpoint.EnableCall(t, "github.com/pingcap/tidb/pkg/ddl/onJobRunBefore", func(job *model.Job) { + if !runDML && job.Type == model.ActionAddIndex && job.SchemaState == model.StateWriteOnly { + tk2 := testkit.NewTestKit(t, store) + tk2.MustExec("use test") + tk2.MustExec("insert into t values (2, 2);") + runDML = true + } + }) + tk.MustExec("create index idx on t(b);") + require.True(t, runDML) + + rows := tk.MustQuery("select tidb_mvcc_info(tidb_encode_index_key('test', 't', 'idx', 1, 1));").Rows() + rs := rows[0][0].(string) + require.Equal(t, 1, strings.Count(rs, "writes"), rs) + rows = tk.MustQuery("select tidb_mvcc_info(tidb_encode_index_key('test', 't', 'idx', 2, 2));").Rows() + rs = rows[0][0].(string) + require.Equal(t, 2, strings.Count(rs, "writes"), rs) +}