343 lines
11 KiB
Go
343 lines
11 KiB
Go
// Copyright 2021 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 tables
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"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/sessionctx/variable"
|
|
"github.com/pingcap/tidb/pkg/table"
|
|
"github.com/pingcap/tidb/pkg/tablecodec"
|
|
"github.com/pingcap/tidb/pkg/types"
|
|
"github.com/pingcap/tidb/pkg/util/codec"
|
|
"github.com/pingcap/tidb/pkg/util/collate"
|
|
"github.com/pingcap/tidb/pkg/util/rowcodec"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCompareIndexData(t *testing.T) {
|
|
// dimensions of the domain of compareIndexData
|
|
// 1. table structure, where we only care about column types that influence truncating values
|
|
// 2. comparison of row data & index data
|
|
|
|
type caseData struct {
|
|
indexData []types.Datum
|
|
inputData []types.Datum
|
|
fts []*types.FieldType
|
|
indexLength []int
|
|
correct bool
|
|
}
|
|
|
|
// assume the index is on all columns
|
|
testData := []caseData{
|
|
{
|
|
[]types.Datum{types.NewIntDatum(1), types.NewStringDatum("some string")},
|
|
[]types.Datum{types.NewIntDatum(1), types.NewStringDatum("some string")},
|
|
[]*types.FieldType{types.NewFieldType(mysql.TypeShort), types.NewFieldType(mysql.TypeString)},
|
|
[]int{types.UnspecifiedLength, types.UnspecifiedLength},
|
|
true,
|
|
},
|
|
{
|
|
[]types.Datum{types.NewIntDatum(1), types.NewStringDatum("some string")},
|
|
[]types.Datum{types.NewIntDatum(1), types.NewStringDatum("some string2")},
|
|
[]*types.FieldType{types.NewFieldType(mysql.TypeShort), types.NewFieldType(mysql.TypeString)},
|
|
[]int{types.UnspecifiedLength, types.UnspecifiedLength},
|
|
false,
|
|
},
|
|
{
|
|
[]types.Datum{types.NewIntDatum(1), types.NewStringDatum("some string")},
|
|
[]types.Datum{types.NewIntDatum(1), types.NewStringDatum("some string2")},
|
|
[]*types.FieldType{types.NewFieldType(mysql.TypeShort), types.NewFieldType(mysql.TypeString)},
|
|
[]int{types.UnspecifiedLength, 11},
|
|
true,
|
|
},
|
|
}
|
|
|
|
for caseID, data := range testData {
|
|
tc := types.DefaultStmtNoWarningContext
|
|
cols := make([]*table.Column, 0)
|
|
indexCols := make([]*model.IndexColumn, 0)
|
|
for i, ft := range data.fts {
|
|
cols = append(cols, &table.Column{ColumnInfo: &model.ColumnInfo{Name: pmodel.NewCIStr(fmt.Sprintf("c%d", i)), FieldType: *ft}})
|
|
indexCols = append(indexCols, &model.IndexColumn{Offset: i, Length: data.indexLength[i]})
|
|
}
|
|
indexInfo := &model.IndexInfo{Name: pmodel.NewCIStr("i0"), Columns: indexCols}
|
|
|
|
err := compareIndexData(tc, cols, data.indexData, data.inputData, indexInfo, &model.TableInfo{Name: pmodel.NewCIStr("t")})
|
|
require.Equal(t, data.correct, err == nil, "case id = %v", caseID)
|
|
}
|
|
}
|
|
|
|
func TestCheckRowInsertionConsistency(t *testing.T) {
|
|
sessVars := variable.NewSessionVars(nil)
|
|
rd := rowcodec.Encoder{Enable: true}
|
|
|
|
// mocked data
|
|
mockRowKey233 := tablecodec.EncodeRowKeyWithHandle(1, kv.IntHandle(233))
|
|
mockValue233, err := tablecodec.EncodeRow(sessVars.StmtCtx.TimeZone(), []types.Datum{types.NewIntDatum(233)}, []int64{101}, nil, nil, nil, &rd)
|
|
require.Nil(t, err)
|
|
fakeRowInsertion := mutation{key: []byte{1, 1}, value: []byte{1, 1, 1}}
|
|
|
|
type caseData struct {
|
|
columnIDToInfo map[int64]*model.ColumnInfo
|
|
columnIDToFieldType map[int64]*types.FieldType
|
|
rowToInsert []types.Datum
|
|
rowInsertion mutation
|
|
correct bool
|
|
}
|
|
|
|
testData := []caseData{
|
|
{
|
|
// expected correct behavior
|
|
map[int64]*model.ColumnInfo{
|
|
101: {
|
|
ID: 101,
|
|
Offset: 0,
|
|
FieldType: *types.NewFieldType(mysql.TypeShort),
|
|
},
|
|
},
|
|
map[int64]*types.FieldType{
|
|
101: types.NewFieldType(mysql.TypeShort),
|
|
},
|
|
[]types.Datum{types.NewIntDatum(233)},
|
|
mutation{key: mockRowKey233, value: mockValue233},
|
|
true,
|
|
},
|
|
{
|
|
// mismatching mutation
|
|
map[int64]*model.ColumnInfo{
|
|
101: {
|
|
ID: 101,
|
|
Offset: 0,
|
|
FieldType: *types.NewFieldType(mysql.TypeShort),
|
|
},
|
|
},
|
|
map[int64]*types.FieldType{
|
|
101: types.NewFieldType(mysql.TypeShort),
|
|
},
|
|
[]types.Datum{types.NewIntDatum(1)},
|
|
fakeRowInsertion,
|
|
false,
|
|
},
|
|
{
|
|
// no input row
|
|
map[int64]*model.ColumnInfo{},
|
|
map[int64]*types.FieldType{},
|
|
nil,
|
|
fakeRowInsertion,
|
|
true,
|
|
},
|
|
{
|
|
// invalid value
|
|
map[int64]*model.ColumnInfo{
|
|
101: {
|
|
ID: 101,
|
|
Offset: 0,
|
|
FieldType: *types.NewFieldType(mysql.TypeShort),
|
|
},
|
|
},
|
|
map[int64]*types.FieldType{
|
|
101: types.NewFieldType(mysql.TypeShort),
|
|
},
|
|
[]types.Datum{types.NewIntDatum(233)},
|
|
mutation{key: mockRowKey233, value: []byte{0, 1, 2, 3}},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for caseID, data := range testData {
|
|
err := checkRowInsertionConsistency(
|
|
sessVars, data.rowToInsert, data.rowInsertion, data.columnIDToInfo, data.columnIDToFieldType, "t",
|
|
)
|
|
require.Equal(t, data.correct, err == nil, "case id = %v", caseID)
|
|
}
|
|
}
|
|
|
|
func TestCheckIndexKeysAndCheckHandleConsistency(t *testing.T) {
|
|
// dimensions of the domain of checkIndexKeys:
|
|
// 1. location *2
|
|
// 2. table structure
|
|
// (1) unique index/non-unique index *2
|
|
// (2) clustered index *2
|
|
// (3) string collation *2
|
|
// We don't test primary clustered index and int handle, since they should not have index mutations.
|
|
// Assume PK is always the first column (string).
|
|
|
|
// cases
|
|
locations := []*time.Location{time.UTC, time.Local}
|
|
indexInfos := []*model.IndexInfo{
|
|
{
|
|
ID: 1,
|
|
State: model.StatePublic,
|
|
Primary: false,
|
|
Unique: true,
|
|
Columns: []*model.IndexColumn{
|
|
{
|
|
Offset: 1,
|
|
Length: types.UnspecifiedLength,
|
|
},
|
|
{
|
|
Offset: 0,
|
|
Length: types.UnspecifiedLength,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: 2,
|
|
State: model.StatePublic,
|
|
Primary: false,
|
|
Unique: false,
|
|
Columns: []*model.IndexColumn{
|
|
{
|
|
Offset: 1,
|
|
Length: types.UnspecifiedLength,
|
|
},
|
|
{
|
|
Offset: 0,
|
|
Length: types.UnspecifiedLength,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
columnInfoSets := [][]*model.ColumnInfo{
|
|
{
|
|
{ID: 1, Offset: 0, FieldType: *types.NewFieldType(mysql.TypeString)},
|
|
{ID: 2, Offset: 1, FieldType: *types.NewFieldType(mysql.TypeDatetime)},
|
|
},
|
|
{
|
|
{ID: 1, Offset: 0, FieldType: *types.NewFieldTypeWithCollation(mysql.TypeString, "utf8_unicode_ci",
|
|
types.UnspecifiedLength)},
|
|
{ID: 2, Offset: 1, FieldType: *types.NewFieldType(mysql.TypeDatetime)},
|
|
},
|
|
}
|
|
tc := types.DefaultStmtNoWarningContext
|
|
rd := rowcodec.Encoder{Enable: true}
|
|
|
|
now := types.CurrentTime(mysql.TypeDatetime)
|
|
rowToInsert := []types.Datum{
|
|
types.NewStringDatum("some string"),
|
|
types.NewTimeDatum(now),
|
|
}
|
|
anotherTime, err := now.Add(tc, types.NewDuration(24, 0, 0, 0, 0))
|
|
require.Nil(t, err)
|
|
rowToRemove := []types.Datum{
|
|
types.NewStringDatum("old string"),
|
|
types.NewTimeDatum(anotherTime),
|
|
}
|
|
|
|
getter := func() (map[int64]columnMaps, bool) {
|
|
return nil, false
|
|
}
|
|
setter := func(maps map[int64]columnMaps) {}
|
|
|
|
// test
|
|
collate.SetNewCollationEnabledForTest(true)
|
|
defer collate.SetNewCollationEnabledForTest(false)
|
|
for _, isCommonHandle := range []bool{true, false} {
|
|
for _, lc := range locations {
|
|
for _, columnInfos := range columnInfoSets {
|
|
tc = tc.WithLocation(lc)
|
|
tableInfo := model.TableInfo{
|
|
ID: 1,
|
|
Name: pmodel.NewCIStr("t"),
|
|
Columns: columnInfos,
|
|
Indices: indexInfos,
|
|
PKIsHandle: false,
|
|
IsCommonHandle: isCommonHandle,
|
|
}
|
|
table := MockTableFromMeta(&tableInfo).(*TableCommon)
|
|
var handle, corruptedHandle kv.Handle
|
|
if isCommonHandle {
|
|
encoded, err := codec.EncodeKey(tc.Location(), nil, rowToInsert[0])
|
|
require.Nil(t, err)
|
|
corrupted := make([]byte, len(encoded))
|
|
copy(corrupted, encoded)
|
|
corrupted[len(corrupted)-1] ^= 1
|
|
handle, err = kv.NewCommonHandle(encoded)
|
|
require.Nil(t, err)
|
|
corruptedHandle, err = kv.NewCommonHandle(corrupted)
|
|
require.Nil(t, err)
|
|
} else {
|
|
handle = kv.IntHandle(1)
|
|
corruptedHandle = kv.IntHandle(2)
|
|
}
|
|
|
|
for i, indexInfo := range indexInfos {
|
|
index := table.indices[i]
|
|
maps := getOrBuildColumnMaps(getter, setter, table)
|
|
|
|
// test checkIndexKeys
|
|
insertionKey, insertionValue, err := buildIndexKeyValue(index, rowToInsert, tc.Location(), tableInfo,
|
|
indexInfo, table, handle)
|
|
require.Nil(t, err)
|
|
deletionKey, _, err := buildIndexKeyValue(index, rowToRemove, tc.Location(), tableInfo, indexInfo, table,
|
|
handle)
|
|
require.Nil(t, err)
|
|
indexMutations := []mutation{
|
|
{key: insertionKey, value: insertionValue, indexID: indexInfo.ID},
|
|
{key: deletionKey, indexID: indexInfo.ID},
|
|
}
|
|
err = checkIndexKeys(
|
|
tc, table, rowToInsert, rowToRemove, indexMutations, maps.IndexIDToInfo,
|
|
maps.IndexIDToRowColInfos,
|
|
)
|
|
require.Nil(t, err)
|
|
|
|
// test checkHandleConsistency
|
|
rowKey := tablecodec.EncodeRowKeyWithHandle(table.tableID, handle)
|
|
corruptedRowKey := tablecodec.EncodeRowKeyWithHandle(table.tableID, corruptedHandle)
|
|
rowValue, err := tablecodec.EncodeRow(tc.Location(), rowToInsert, []int64{1, 2}, nil, nil, nil, &rd)
|
|
require.Nil(t, err)
|
|
rowMutation := mutation{key: rowKey, value: rowValue}
|
|
corruptedRowMutation := mutation{key: corruptedRowKey, value: rowValue}
|
|
err = checkHandleConsistency(rowMutation, indexMutations, maps.IndexIDToInfo, &tableInfo)
|
|
require.Nil(t, err)
|
|
err = checkHandleConsistency(corruptedRowMutation, indexMutations, maps.IndexIDToInfo, &tableInfo)
|
|
require.NotNil(t, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildIndexKeyValue(index table.Index, rowToInsert []types.Datum, loc *time.Location,
|
|
tableInfo model.TableInfo, indexInfo *model.IndexInfo, table *TableCommon, handle kv.Handle) ([]byte, []byte, error) {
|
|
indexedValues, err := index.FetchValues(rowToInsert, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
key, distinct, err := tablecodec.GenIndexKey(
|
|
loc, &tableInfo, indexInfo, 1, indexedValues, handle, nil,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
rsData := TryGetHandleRestoredDataWrapper(table.meta, rowToInsert, nil, indexInfo)
|
|
value, err := tablecodec.GenIndexValuePortal(
|
|
loc, &tableInfo, indexInfo, NeedRestoredData(indexInfo.Columns, tableInfo.Columns),
|
|
distinct, false, indexedValues, handle, 0, rsData, nil,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return key, value, nil
|
|
}
|