// Copyright 2019 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 rowcodec_test import ( "encoding/binary" "hash/crc32" "math" "sort" "strings" "testing" "time" "github.com/pingcap/tidb/pkg/kv" "github.com/pingcap/tidb/pkg/meta/model" "github.com/pingcap/tidb/pkg/parser/mysql" "github.com/pingcap/tidb/pkg/sessionctx/stmtctx" "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/collate" "github.com/pingcap/tidb/pkg/util/rowcodec" "github.com/stretchr/testify/require" ) type testData struct { id int64 ft *types.FieldType input types.Datum output types.Datum def *types.Datum handle bool } func TestEncodeLargeSmallReuseBug(t *testing.T) { // reuse one rowcodec.Encoder. var encoder rowcodec.Encoder colFt := types.NewFieldType(mysql.TypeString) largeColID := int64(300) b, err := encoder.Encode(nil, []int64{largeColID}, []types.Datum{types.NewBytesDatum([]byte(""))}, nil, nil) require.NoError(t, err) bDecoder := rowcodec.NewDatumMapDecoder([]rowcodec.ColInfo{ { ID: largeColID, Ft: colFt, IsPKHandle: false, }, }, nil) _, err = bDecoder.DecodeToDatumMap(b, nil) require.NoError(t, err) colFt = types.NewFieldType(mysql.TypeLonglong) smallColID := int64(1) b, err = encoder.Encode(nil, []int64{smallColID}, []types.Datum{types.NewIntDatum(2)}, nil, nil) require.NoError(t, err) bDecoder = rowcodec.NewDatumMapDecoder([]rowcodec.ColInfo{ { ID: smallColID, Ft: colFt, IsPKHandle: false, }, }, nil) m, err := bDecoder.DecodeToDatumMap(b, nil) require.NoError(t, err) v := m[smallColID] require.Equal(t, int64(2), v.GetInt64()) } func TestDecodeRowWithHandle(t *testing.T) { handleID := int64(-1) handleValue := int64(10000) tests := []struct { name string testData []testData }{ { "signed int", []testData{ { handleID, types.NewFieldType(mysql.TypeLonglong), types.NewIntDatum(handleValue), types.NewIntDatum(handleValue), nil, true, }, { 10, types.NewFieldType(mysql.TypeLonglong), types.NewIntDatum(1), types.NewIntDatum(1), nil, false, }, }, }, { "unsigned int", []testData{ { handleID, withUnsigned(types.NewFieldType(mysql.TypeLonglong)), types.NewUintDatum(uint64(handleValue)), types.NewUintDatum(uint64(handleValue)), // decode as bytes will uint if unsigned. nil, true, }, { 10, types.NewFieldType(mysql.TypeLonglong), types.NewIntDatum(1), types.NewIntDatum(1), nil, false, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { td := test.testData // transform test data into input. colIDs := make([]int64, 0, len(td)) dts := make([]types.Datum, 0, len(td)) fts := make([]*types.FieldType, 0, len(td)) cols := make([]rowcodec.ColInfo, 0, len(td)) handleColFtMap := make(map[int64]*types.FieldType) for _, d := range td { if d.handle { handleColFtMap[handleID] = d.ft } else { colIDs = append(colIDs, d.id) dts = append(dts, d.input) } fts = append(fts, d.ft) cols = append(cols, rowcodec.ColInfo{ ID: d.id, IsPKHandle: d.handle, Ft: d.ft, }) } // test encode input. var encoder rowcodec.Encoder newRow, err := encoder.Encode(time.UTC, colIDs, dts, nil, nil) require.NoError(t, err) // decode to datum map. mDecoder := rowcodec.NewDatumMapDecoder(cols, time.UTC) dm, err := mDecoder.DecodeToDatumMap(newRow, nil) require.NoError(t, err) dm, err = tablecodec.DecodeHandleToDatumMap(kv.IntHandle(handleValue), []int64{handleID}, handleColFtMap, time.UTC, dm) require.NoError(t, err) for _, d := range td { dat, exists := dm[d.id] require.True(t, exists) require.Equal(t, d.input, dat) } // decode to chunk. cDecoder := rowcodec.NewChunkDecoder(cols, []int64{-1}, nil, time.UTC) chk := chunk.New(fts, 1, 1) err = cDecoder.DecodeToChunk(newRow, 0, kv.IntHandle(handleValue), chk) require.NoError(t, err) chkRow := chk.GetRow(0) cdt := chkRow.GetDatumRow(fts) for i, d := range td { dat := cdt[i] if dat.Kind() == types.KindMysqlDecimal { require.Equal(t, d.output.GetMysqlDecimal(), dat.GetMysqlDecimal()) } else { require.Equal(t, d.output, dat) } } // decode to old row bytes. colOffset := make(map[int64]int) for i, t := range td { colOffset[t.id] = i } bDecoder := rowcodec.NewByteDecoder(cols, []int64{-1}, nil, nil) oldRow, err := bDecoder.DecodeToBytes(colOffset, kv.IntHandle(handleValue), newRow, nil) require.NoError(t, err) for i, d := range td { remain, dat, err := codec.DecodeOne(oldRow[i]) require.NoError(t, err) require.Len(t, remain, 0) if dat.Kind() == types.KindMysqlDecimal { require.Equal(t, d.output.GetMysqlDecimal(), dat.GetMysqlDecimal()) } else { require.Equal(t, d.output, dat) } } }) } } func TestEncodeKindNullDatum(t *testing.T) { var encoder rowcodec.Encoder colIDs := []int64{1, 2} var nilDt types.Datum nilDt.SetNull() dts := []types.Datum{nilDt, types.NewIntDatum(2)} ft := types.NewFieldType(mysql.TypeLonglong) fts := []*types.FieldType{ft, ft} newRow, err := encoder.Encode(time.UTC, colIDs, dts, nil, nil) require.NoError(t, err) cols := []rowcodec.ColInfo{{ID: 1, Ft: ft}, {ID: 2, Ft: ft}} cDecoder := rowcodec.NewChunkDecoder(cols, []int64{-1}, nil, time.UTC) chk := chunk.New(fts, 1, 1) err = cDecoder.DecodeToChunk(newRow, 0, kv.IntHandle(-1), chk) require.NoError(t, err) chkRow := chk.GetRow(0) cdt := chkRow.GetDatumRow(fts) require.True(t, cdt[0].IsNull()) require.Equal(t, int64(2), cdt[1].GetInt64()) } func TestDecodeDecimalFspNotMatch(t *testing.T) { var encoder rowcodec.Encoder colIDs := []int64{ 1, } dec := withFrac(4)(withLen(6)(types.NewDecimalDatum(types.NewDecFromStringForTest("11.9900")))) dts := []types.Datum{dec} ft := types.NewFieldType(mysql.TypeNewDecimal) ft.SetDecimal(4) fts := []*types.FieldType{ft} newRow, err := encoder.Encode(time.UTC, colIDs, dts, nil, nil) require.NoError(t, err) // decode to chunk. ft = types.NewFieldType(mysql.TypeNewDecimal) ft.SetDecimal(3) cols := make([]rowcodec.ColInfo, 0) cols = append(cols, rowcodec.ColInfo{ ID: 1, Ft: ft, }) cDecoder := rowcodec.NewChunkDecoder(cols, []int64{-1}, nil, time.UTC) chk := chunk.New(fts, 1, 1) err = cDecoder.DecodeToChunk(newRow, 0, kv.IntHandle(-1), chk) require.NoError(t, err) chkRow := chk.GetRow(0) cdt := chkRow.GetDatumRow(fts) dec = withFrac(3)(withLen(6)(types.NewDecimalDatum(types.NewDecFromStringForTest("11.990")))) require.Equal(t, dec.GetMysqlDecimal().String(), cdt[0].GetMysqlDecimal().String()) } func TestTypesNewRowCodec(t *testing.T) { getJSONDatum := func(value string) types.Datum { j, err := types.ParseBinaryJSONFromString(value) require.NoError(t, err) var d types.Datum d.SetMysqlJSON(j) return d } getSetDatum := func(name string, value uint64) types.Datum { var d types.Datum d.SetMysqlSet(types.Set{Name: name, Value: value}, mysql.DefaultCollationName) return d } getTime := func(value string) types.Time { d, err := types.ParseTime(types.DefaultStmtNoWarningContext, value, mysql.TypeTimestamp, 6) require.NoError(t, err) return d } blobTp := types.NewFieldType(mysql.TypeBlob) blobTp.SetCollate(mysql.DefaultCollationName) blobTp.SetFlen(types.UnspecifiedLength) strTp := types.NewFieldType(mysql.TypeString) strTp.SetCollate(mysql.DefaultCollationName) enumTp := types.NewFieldType(mysql.TypeEnum) enumTp.SetCollate(mysql.DefaultCollationName) enumTp.SetFlen(collate.DefaultLen) setTp := types.NewFieldType(mysql.TypeSet) setTp.SetCollate(mysql.DefaultCollationName) setTp.SetFlen(collate.DefaultLen) varStrTp := types.NewFieldType(mysql.TypeVarString) varStrTp.SetCollate(mysql.DefaultCollationName) smallTestDataList := []testData{ { 1, types.NewFieldType(mysql.TypeLonglong), types.NewIntDatum(1), types.NewIntDatum(1), nil, false, }, { 22, withUnsigned(types.NewFieldType(mysql.TypeShort)), types.NewUintDatum(1), types.NewUintDatum(1), nil, false, }, { 3, types.NewFieldType(mysql.TypeDouble), types.NewFloat64Datum(2), types.NewFloat64Datum(2), nil, false, }, { 24, blobTp, types.NewStringDatum("abc"), types.NewStringDatum("abc"), nil, false, }, { 25, strTp, types.NewStringDatum("ab"), types.NewBytesDatum([]byte("ab")), nil, false, }, { 5, withFsp(6)(types.NewFieldType(mysql.TypeTimestamp)), types.NewTimeDatum(getTime("2011-11-10 11:11:11.999999")), types.NewUintDatum(1840446893366133311), nil, false, }, { 16, withFsp(0)(types.NewFieldType(mysql.TypeDuration)), types.NewDurationDatum(getDuration("4:00:00")), types.NewIntDatum(14400000000000), nil, false, }, { 8, types.NewFieldType(mysql.TypeNewDecimal), withFrac(4)(withLen(6)(types.NewDecimalDatum(types.NewDecFromStringForTest("11.9900")))), withFrac(4)(withLen(6)(types.NewDecimalDatum(types.NewDecFromStringForTest("11.9900")))), nil, false, }, { 12, types.NewFieldType(mysql.TypeYear), types.NewIntDatum(1999), types.NewIntDatum(1999), nil, false, }, { 9, withEnumElems("y", "n")(enumTp), types.NewMysqlEnumDatum(types.Enum{Name: "n", Value: 2}), types.NewUintDatum(2), nil, false, }, { 14, types.NewFieldType(mysql.TypeJSON), getJSONDatum(`{"a":2}`), getJSONDatum(`{"a":2}`), nil, false, }, { 11, types.NewFieldType(mysql.TypeNull), types.NewDatum(nil), types.NewDatum(nil), nil, false, }, { 2, types.NewFieldType(mysql.TypeNull), types.NewDatum(nil), types.NewDatum(nil), nil, false, }, { 100, types.NewFieldType(mysql.TypeNull), types.NewDatum(nil), types.NewDatum(nil), nil, false, }, { 116, types.NewFieldType(mysql.TypeFloat), types.NewFloat32Datum(6), types.NewFloat64Datum(6), nil, false, }, { 117, withEnumElems("n1", "n2")(setTp), getSetDatum("n1", 1), types.NewUintDatum(1), nil, false, }, { 118, withFlen(24)(types.NewFieldType(mysql.TypeBit)), // 3 bit types.NewMysqlBitDatum(types.NewBinaryLiteralFromUint(3223600, 3)), types.NewUintDatum(3223600), nil, false, }, { 119, varStrTp, types.NewStringDatum(""), types.NewBytesDatum([]byte("")), nil, false, }, } largeColIDTestDataList := make([]testData, len(smallTestDataList)) copy(largeColIDTestDataList, smallTestDataList) largeColIDTestDataList[0].id = 300 largeTestDataList := make([]testData, len(smallTestDataList)) copy(largeTestDataList, smallTestDataList) largeTestDataList[3].input = types.NewStringDatum(strings.Repeat("a", math.MaxUint16+1)) largeTestDataList[3].output = types.NewStringDatum(strings.Repeat("a", math.MaxUint16+1)) var encoder rowcodec.Encoder tests := []struct { name string testData []testData }{ { "small", smallTestDataList, }, { "largeColID", largeColIDTestDataList, }, { "largeData", largeTestDataList, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { td := test.testData // transform test data into input. colIDs := make([]int64, 0, len(td)) dts := make([]types.Datum, 0, len(td)) fts := make([]*types.FieldType, 0, len(td)) cols := make([]rowcodec.ColInfo, 0, len(td)) for _, d := range td { colIDs = append(colIDs, d.id) dts = append(dts, d.input) fts = append(fts, d.ft) cols = append(cols, rowcodec.ColInfo{ ID: d.id, IsPKHandle: d.handle, Ft: d.ft, }) } // test encode input. newRow, err := encoder.Encode(time.UTC, colIDs, dts, nil, nil) require.NoError(t, err) // decode to datum map. mDecoder := rowcodec.NewDatumMapDecoder(cols, time.UTC) dm, err := mDecoder.DecodeToDatumMap(newRow, nil) require.NoError(t, err) for _, d := range td { dat, exists := dm[d.id] require.True(t, exists) require.Equal(t, d.input, dat) } // decode to chunk. cDecoder := rowcodec.NewChunkDecoder(cols, []int64{-1}, nil, time.UTC) chk := chunk.New(fts, 1, 1) err = cDecoder.DecodeToChunk(newRow, 0, kv.IntHandle(-1), chk) require.NoError(t, err) chkRow := chk.GetRow(0) cdt := chkRow.GetDatumRow(fts) for i, d := range td { dat := cdt[i] if dat.Kind() == types.KindMysqlDecimal { require.Equal(t, d.output.GetMysqlDecimal(), dat.GetMysqlDecimal()) } else { require.Equal(t, d.input, dat) } } // decode to old row bytes. colOffset := make(map[int64]int) for i, t := range td { colOffset[t.id] = i } bDecoder := rowcodec.NewByteDecoder(cols, []int64{-1}, nil, nil) oldRow, err := bDecoder.DecodeToBytes(colOffset, kv.IntHandle(-1), newRow, nil) require.NoError(t, err) for i, d := range td { remain, dat, err := codec.DecodeOne(oldRow[i]) require.NoError(t, err) require.Len(t, remain, 0) if dat.Kind() == types.KindMysqlDecimal { require.Equal(t, d.output.GetMysqlDecimal(), dat.GetMysqlDecimal()) } else if dat.Kind() == types.KindBytes { require.Equal(t, d.output.GetBytes(), dat.GetBytes()) } else { require.Equal(t, d.output, dat) } } }) } } func TestNilAndDefault(t *testing.T) { td := []testData{ { 1, types.NewFieldType(mysql.TypeLonglong), types.NewIntDatum(1), types.NewIntDatum(1), nil, false, }, { 2, withUnsigned(types.NewFieldType(mysql.TypeLonglong)), types.NewUintDatum(1), types.NewUintDatum(9), getDatumPoint(types.NewUintDatum(9)), false, }, } // transform test data into input. colIDs := make([]int64, 0, len(td)) dts := make([]types.Datum, 0, len(td)) cols := make([]rowcodec.ColInfo, 0, len(td)) fts := make([]*types.FieldType, 0, len(td)) for i := range td { d := td[i] if d.def == nil { colIDs = append(colIDs, d.id) dts = append(dts, d.input) } fts = append(fts, d.ft) cols = append(cols, rowcodec.ColInfo{ ID: d.id, IsPKHandle: d.handle, Ft: d.ft, }) } ddf := func(i int, chk *chunk.Chunk) error { d := td[i] if d.def == nil { chk.AppendNull(i) return nil } chk.AppendDatum(i, d.def) return nil } bdf := func(i int) ([]byte, error) { d := td[i] if d.def == nil { return nil, nil } return getOldDatumByte(*d.def), nil } // test encode input. var encoder rowcodec.Encoder newRow, err := encoder.Encode(time.UTC, colIDs, dts, nil, nil) require.NoError(t, err) // decode to datum map. mDecoder := rowcodec.NewDatumMapDecoder(cols, time.UTC) dm, err := mDecoder.DecodeToDatumMap(newRow, nil) require.NoError(t, err) for _, d := range td { dat, exists := dm[d.id] if d.def != nil { // for datum should not fill default value. require.False(t, exists) } else { require.True(t, exists) require.Equal(t, d.output, dat) } } // decode to chunk. chk := chunk.New(fts, 1, 1) cDecoder := rowcodec.NewChunkDecoder(cols, []int64{-1}, ddf, time.UTC) err = cDecoder.DecodeToChunk(newRow, 0, kv.IntHandle(-1), chk) require.NoError(t, err) chkRow := chk.GetRow(0) cdt := chkRow.GetDatumRow(fts) for i, d := range td { dat := cdt[i] if dat.Kind() == types.KindMysqlDecimal { require.Equal(t, d.output.GetMysqlDecimal(), dat.GetMysqlDecimal()) } else { require.Equal(t, d.output, dat) } } chk = chunk.New(fts, 1, 1) cDecoder = rowcodec.NewChunkDecoder(cols, []int64{-1}, nil, time.UTC) err = cDecoder.DecodeToChunk(newRow, 0, kv.IntHandle(-1), chk) require.NoError(t, err) chkRow = chk.GetRow(0) cdt = chkRow.GetDatumRow(fts) for i := range td { if i == 0 { continue } require.True(t, cdt[i].IsNull()) } // decode to old row bytes. colOffset := make(map[int64]int) for i, t := range td { colOffset[t.id] = i } bDecoder := rowcodec.NewByteDecoder(cols, []int64{-1}, bdf, time.UTC) oldRow, err := bDecoder.DecodeToBytes(colOffset, kv.IntHandle(-1), newRow, nil) require.NoError(t, err) for i, d := range td { remain, dat, err := codec.DecodeOne(oldRow[i]) require.NoError(t, err) require.Len(t, remain, 0) if dat.Kind() == types.KindMysqlDecimal { require.Equal(t, d.output.GetMysqlDecimal(), dat.GetMysqlDecimal()) } else { require.Equal(t, d.output, dat) } } } func TestVarintCompatibility(t *testing.T) { td := []testData{ { 1, types.NewFieldType(mysql.TypeLonglong), types.NewIntDatum(1), types.NewIntDatum(1), nil, false, }, { 2, withUnsigned(types.NewFieldType(mysql.TypeLonglong)), types.NewUintDatum(1), types.NewUintDatum(1), nil, false, }, } // transform test data into input. colIDs := make([]int64, 0, len(td)) dts := make([]types.Datum, 0, len(td)) cols := make([]rowcodec.ColInfo, 0, len(td)) for _, d := range td { colIDs = append(colIDs, d.id) dts = append(dts, d.input) cols = append(cols, rowcodec.ColInfo{ ID: d.id, IsPKHandle: d.handle, Ft: d.ft, }) } // test encode input. var encoder rowcodec.Encoder newRow, err := encoder.Encode(time.UTC, colIDs, dts, nil, nil) require.NoError(t, err) decoder := rowcodec.NewByteDecoder(cols, []int64{-1}, nil, time.UTC) // decode to old row bytes. colOffset := make(map[int64]int) for i, t := range td { colOffset[t.id] = i } oldRow, err := decoder.DecodeToBytes(colOffset, kv.IntHandle(1), newRow, nil) require.NoError(t, err) for i, d := range td { oldVarint, err := tablecodec.EncodeValue(nil, nil, d.output) // tablecodec will encode as varint/varuint require.NoError(t, err) require.Equal(t, oldRow[i], oldVarint) } } func TestCodecUtil(t *testing.T) { colIDs := []int64{1, 2, 3, 4} tps := make([]*types.FieldType, 4) for i := range 3 { tps[i] = types.NewFieldType(mysql.TypeLonglong) } tps[3] = types.NewFieldType(mysql.TypeNull) sc := stmtctx.NewStmtCtx() oldRow, err := tablecodec.EncodeOldRow(sc.TimeZone(), types.MakeDatums(1, 2, 3, nil), colIDs, nil, nil) require.NoError(t, err) var ( rb rowcodec.Encoder newRow []byte ) newRow, err = rowcodec.EncodeFromOldRow(&rb, nil, oldRow, nil) require.NoError(t, err) require.True(t, rowcodec.IsNewFormat(newRow)) require.False(t, rowcodec.IsNewFormat(oldRow)) // test stringer for decoder. var cols = make([]rowcodec.ColInfo, 0, len(tps)) for i, ft := range tps { cols = append(cols, rowcodec.ColInfo{ ID: colIDs[i], IsPKHandle: false, Ft: ft, }) } d := rowcodec.NewDecoder(cols, []int64{-1}, nil) // test ColumnIsNull isNull, err := d.ColumnIsNull(newRow, 4, nil) require.NoError(t, err) require.True(t, isNull) isNull, err = d.ColumnIsNull(newRow, 1, nil) require.NoError(t, err) require.False(t, isNull) isNull, err = d.ColumnIsNull(newRow, 5, nil) require.NoError(t, err) require.True(t, isNull) isNull, err = d.ColumnIsNull(newRow, 5, []byte{1}) require.NoError(t, err) require.False(t, isNull) // test isRowKey require.False(t, rowcodec.IsRowKey([]byte{'b', 't'})) require.False(t, rowcodec.IsRowKey([]byte{'t', 'r'})) } func TestOldRowCodec(t *testing.T) { colIDs := []int64{1, 2, 3, 4} tps := make([]*types.FieldType, 4) for i := range 3 { tps[i] = types.NewFieldType(mysql.TypeLonglong) } tps[3] = types.NewFieldType(mysql.TypeNull) sc := stmtctx.NewStmtCtx() oldRow, err := tablecodec.EncodeOldRow(sc.TimeZone(), types.MakeDatums(1, 2, 3, nil), colIDs, nil, nil) require.NoError(t, err) var ( rb rowcodec.Encoder newRow []byte ) newRow, err = rowcodec.EncodeFromOldRow(&rb, nil, oldRow, nil) require.NoError(t, err) cols := make([]rowcodec.ColInfo, len(tps)) for i, tp := range tps { cols[i] = rowcodec.ColInfo{ ID: colIDs[i], Ft: tp, } } rd := rowcodec.NewChunkDecoder(cols, []int64{-1}, nil, time.Local) chk := chunk.NewChunkWithCapacity(tps, 1) err = rd.DecodeToChunk(newRow, 0, kv.IntHandle(-1), chk) require.NoError(t, err) row := chk.GetRow(0) for i := range 3 { require.Equal(t, int64(i+1), row.GetInt64(i)) } } func Test65535Bug(t *testing.T) { colIds := []int64{1} tps := make([]*types.FieldType, 1) tps[0] = types.NewFieldType(mysql.TypeString) text65535 := strings.Repeat("a", 65535) encode := rowcodec.Encoder{} bd, err := encode.Encode(time.UTC, colIds, []types.Datum{types.NewStringDatum(text65535)}, nil, nil) require.NoError(t, err) cols := make([]rowcodec.ColInfo, 1) cols[0] = rowcodec.ColInfo{ ID: 1, Ft: tps[0], } dc := rowcodec.NewDatumMapDecoder(cols, nil) result, err := dc.DecodeToDatumMap(bd, nil) require.NoError(t, err) rs := result[1] require.Equal(t, text65535, rs.GetString()) } func TestColumnEncode(t *testing.T) { encodeUint64 := func(v uint64) []byte { return binary.LittleEndian.AppendUint64(nil, v) } encodeBytes := func(v []byte) []byte { return append(binary.LittleEndian.AppendUint32(nil, uint32(len(v))), v...) } convertTZ := func(ts types.Time) types.Time { require.NoError(t, ts.ConvertTimeZone(time.Local, time.UTC)) return ts } var ( buf = make([]byte, 0, 128) intZero = 0 intPos = 42 intNeg = -2 i8Min = math.MinInt8 i16Min = math.MinInt16 i32Min = math.MinInt32 i64Min = math.MinInt64 i24Min = -1 << 23 ct = types.FromDate(2023, 1, 2, 3, 4, 5, 678) dur = types.Duration{Duration: 123456*time.Microsecond + 7*time.Minute + 8*time.Hour, Fsp: 6} decZero = types.NewDecFromStringForTest("0.000") decPos = types.NewDecFromStringForTest("3.14") decNeg = types.NewDecFromStringForTest("-1.2") decMin = types.NewMaxOrMinDec(true, 12, 6) decMax = types.NewMaxOrMinDec(false, 12, 6) json1 = types.CreateBinaryJSON(nil) json2 = types.CreateBinaryJSON(int64(42)) json3 = types.CreateBinaryJSON(map[string]any{"foo": "bar", "a": int64(42)}) ) for _, tt := range []struct { name string typ *types.FieldType dat types.Datum raw []byte ok bool }{ {"unspecified", types.NewFieldType(mysql.TypeUnspecified), types.NewDatum(1), nil, false}, {"wrong", types.NewFieldType(42), types.NewDatum(1), nil, false}, {"mismatch/timestamp", types.NewFieldType(mysql.TypeTimestamp), types.NewDatum(1), nil, false}, {"mismatch/datetime", types.NewFieldType(mysql.TypeDatetime), types.NewDatum(1), nil, false}, {"mismatch/date", types.NewFieldType(mysql.TypeDate), types.NewDatum(1), nil, false}, {"mismatch/newdate", types.NewFieldType(mysql.TypeNewDate), types.NewDatum(1), nil, false}, {"mismatch/decimal", types.NewFieldType(mysql.TypeNewDecimal), types.NewDatum(1), nil, false}, {"null", types.NewFieldType(mysql.TypeNull), types.NewDatum(1), nil, true}, {"geometry", types.NewFieldType(mysql.TypeGeometry), types.NewDatum(1), nil, true}, {"tinyint/zero", types.NewFieldType(mysql.TypeTiny), types.NewDatum(intZero), encodeUint64(uint64(intZero)), true}, {"tinyint/pos", types.NewFieldType(mysql.TypeTiny), types.NewDatum(intPos), encodeUint64(uint64(intPos)), true}, {"tinyint/neg", types.NewFieldType(mysql.TypeTiny), types.NewDatum(intNeg), encodeUint64(uint64(intNeg)), true}, {"tinyint/min/signed", types.NewFieldType(mysql.TypeTiny), types.NewDatum(i8Min), encodeUint64(uint64(i8Min)), true}, {"tinyint/max/signed", types.NewFieldType(mysql.TypeTiny), types.NewDatum(math.MaxInt8), encodeUint64(math.MaxInt8), true}, {"tinyint/max/unsigned", types.NewFieldType(mysql.TypeTiny), types.NewDatum(math.MaxUint8), encodeUint64(math.MaxUint8), true}, {"smallint/zero", types.NewFieldType(mysql.TypeShort), types.NewDatum(intZero), encodeUint64(uint64(intZero)), true}, {"smallint/pos", types.NewFieldType(mysql.TypeShort), types.NewDatum(intPos), encodeUint64(uint64(intPos)), true}, {"smallint/neg", types.NewFieldType(mysql.TypeShort), types.NewDatum(intNeg), encodeUint64(uint64(intNeg)), true}, {"smallint/min/signed", types.NewFieldType(mysql.TypeShort), types.NewDatum(i16Min), encodeUint64(uint64(i16Min)), true}, {"smallint/max/signed", types.NewFieldType(mysql.TypeShort), types.NewDatum(math.MaxInt16), encodeUint64(math.MaxInt16), true}, {"smallint/max/unsigned", types.NewFieldType(mysql.TypeShort), types.NewDatum(math.MaxUint16), encodeUint64(math.MaxUint16), true}, {"int/zero", types.NewFieldType(mysql.TypeLong), types.NewDatum(intZero), encodeUint64(uint64(intZero)), true}, {"int/pos", types.NewFieldType(mysql.TypeLong), types.NewDatum(intPos), encodeUint64(uint64(intPos)), true}, {"int/neg", types.NewFieldType(mysql.TypeLong), types.NewDatum(intNeg), encodeUint64(uint64(intNeg)), true}, {"int/min/signed", types.NewFieldType(mysql.TypeLong), types.NewDatum(i32Min), encodeUint64(uint64(i32Min)), true}, {"int/max/signed", types.NewFieldType(mysql.TypeLong), types.NewDatum(math.MaxInt32), encodeUint64(math.MaxInt32), true}, {"int/max/unsigned", types.NewFieldType(mysql.TypeLong), types.NewDatum(math.MaxUint32), encodeUint64(math.MaxUint32), true}, {"bigint/zero", types.NewFieldType(mysql.TypeLonglong), types.NewDatum(intZero), encodeUint64(uint64(intZero)), true}, {"bigint/pos", types.NewFieldType(mysql.TypeLonglong), types.NewDatum(intPos), encodeUint64(uint64(intPos)), true}, {"bigint/neg", types.NewFieldType(mysql.TypeLonglong), types.NewDatum(intNeg), encodeUint64(uint64(intNeg)), true}, {"bigint/min/signed", types.NewFieldType(mysql.TypeLonglong), types.NewDatum(i64Min), encodeUint64(uint64(i64Min)), true}, {"bigint/max/signed", types.NewFieldType(mysql.TypeLonglong), types.NewDatum(math.MaxInt64), encodeUint64(math.MaxInt64), true}, {"bigint/max/unsigned", types.NewFieldType(mysql.TypeLonglong), types.NewDatum(uint64(math.MaxUint64)), encodeUint64(math.MaxUint64), true}, {"mediumint/zero", types.NewFieldType(mysql.TypeInt24), types.NewDatum(intZero), encodeUint64(uint64(intZero)), true}, {"mediumint/pos", types.NewFieldType(mysql.TypeInt24), types.NewDatum(intPos), encodeUint64(uint64(intPos)), true}, {"mediumint/neg", types.NewFieldType(mysql.TypeInt24), types.NewDatum(intNeg), encodeUint64(uint64(intNeg)), true}, {"mediumint/min/signed", types.NewFieldType(mysql.TypeInt24), types.NewDatum(i24Min), encodeUint64(uint64(i24Min)), true}, {"mediumint/max/signed", types.NewFieldType(mysql.TypeInt24), types.NewDatum(1<<23 - 1), encodeUint64(1<<23 - 1), true}, {"mediumint/max/unsigned", types.NewFieldType(mysql.TypeInt24), types.NewDatum(1<<24 - 1), encodeUint64(1<<24 - 1), true}, {"year", types.NewFieldType(mysql.TypeYear), types.NewDatum(2023), encodeUint64(2023), true}, {"varchar", types.NewFieldType(mysql.TypeVarchar), types.NewDatum("foo"), encodeBytes([]byte("foo")), true}, {"varchar/empty", types.NewFieldType(mysql.TypeVarchar), types.NewDatum(""), encodeBytes([]byte{}), true}, {"varbinary", types.NewFieldType(mysql.TypeVarString), types.NewDatum([]byte("foo")), encodeBytes([]byte("foo")), true}, {"varbinary/empty", types.NewFieldType(mysql.TypeVarString), types.NewDatum([]byte("")), encodeBytes([]byte{}), true}, {"char", types.NewFieldType(mysql.TypeString), types.NewDatum("foo"), encodeBytes([]byte("foo")), true}, {"char/empty", types.NewFieldType(mysql.TypeString), types.NewDatum(""), encodeBytes([]byte{}), true}, {"binary", types.NewFieldType(mysql.TypeString), types.NewDatum([]byte("foo")), encodeBytes([]byte("foo")), true}, {"binary/empty", types.NewFieldType(mysql.TypeString), types.NewDatum([]byte("")), encodeBytes([]byte{}), true}, {"text", types.NewFieldType(mysql.TypeBlob), types.NewDatum("foo"), encodeBytes([]byte("foo")), true}, {"text/empty", types.NewFieldType(mysql.TypeBlob), types.NewDatum(""), encodeBytes([]byte{}), true}, {"blob", types.NewFieldType(mysql.TypeBlob), types.NewDatum([]byte("foo")), encodeBytes([]byte("foo")), true}, {"blob/empty", types.NewFieldType(mysql.TypeBlob), types.NewDatum([]byte("")), encodeBytes([]byte{}), true}, {"longtext", types.NewFieldType(mysql.TypeLongBlob), types.NewDatum("foo"), encodeBytes([]byte("foo")), true}, {"longtext/empty", types.NewFieldType(mysql.TypeLongBlob), types.NewDatum(""), encodeBytes([]byte{}), true}, {"longblob", types.NewFieldType(mysql.TypeLongBlob), types.NewDatum([]byte("foo")), encodeBytes([]byte("foo")), true}, {"longblob/empty", types.NewFieldType(mysql.TypeLongBlob), types.NewDatum([]byte("")), encodeBytes([]byte{}), true}, {"mediumtext", types.NewFieldType(mysql.TypeMediumBlob), types.NewDatum("foo"), encodeBytes([]byte("foo")), true}, {"mediumtext/empty", types.NewFieldType(mysql.TypeMediumBlob), types.NewDatum(""), encodeBytes([]byte{}), true}, {"mediumblob", types.NewFieldType(mysql.TypeMediumBlob), types.NewDatum([]byte("foo")), encodeBytes([]byte("foo")), true}, {"mediumblob/empty", types.NewFieldType(mysql.TypeMediumBlob), types.NewDatum([]byte("")), encodeBytes([]byte{}), true}, {"tinytext", types.NewFieldType(mysql.TypeTinyBlob), types.NewDatum("foo"), encodeBytes([]byte("foo")), true}, {"tinytext/empty", types.NewFieldType(mysql.TypeTinyBlob), types.NewDatum(""), encodeBytes([]byte{}), true}, {"tinyblob", types.NewFieldType(mysql.TypeTinyBlob), types.NewDatum([]byte("foo")), encodeBytes([]byte("foo")), true}, {"tinyblob/empty", types.NewFieldType(mysql.TypeTinyBlob), types.NewDatum([]byte("")), encodeBytes([]byte{}), true}, {"float", types.NewFieldType(mysql.TypeFloat), types.NewDatum(float32(3.14)), encodeUint64(math.Float64bits(float64(float32(3.14)))), true}, {"float/nan", types.NewFieldType(mysql.TypeFloat), types.NewDatum(float32(math.NaN())), encodeUint64(math.Float64bits(0)), true}, {"float/+inf", types.NewFieldType(mysql.TypeFloat), types.NewDatum(float32(math.Inf(1))), encodeUint64(math.Float64bits(0)), true}, {"float/-inf", types.NewFieldType(mysql.TypeFloat), types.NewDatum(float32(math.Inf(-1))), encodeUint64(math.Float64bits(0)), true}, {"double", types.NewFieldType(mysql.TypeDouble), types.NewDatum(float64(3.14)), encodeUint64(math.Float64bits(3.14)), true}, {"double/nan", types.NewFieldType(mysql.TypeDouble), types.NewDatum(math.NaN()), encodeUint64(math.Float64bits(0)), true}, {"double/+inf", types.NewFieldType(mysql.TypeDouble), types.NewDatum(math.Inf(1)), encodeUint64(math.Float64bits(0)), true}, {"double/-inf", types.NewFieldType(mysql.TypeDouble), types.NewDatum(math.Inf(-1)), encodeUint64(math.Float64bits(0)), true}, {"enum", types.NewFieldType(mysql.TypeEnum), types.NewDatum(0b010), encodeUint64(0b010), true}, {"set", types.NewFieldType(mysql.TypeSet), types.NewDatum(0b101), encodeUint64(0b101), true}, {"bit", types.NewFieldType(mysql.TypeBit), types.NewBinaryLiteralDatum([]byte{0x12, 0x34}), encodeUint64(0x1234), true}, {"bit/truncate", types.NewFieldType(mysql.TypeBit), types.NewBinaryLiteralDatum([]byte{0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0xff}), encodeUint64(math.MaxUint64), true}, { "timestamp", types.NewFieldType(mysql.TypeTimestamp), types.NewTimeDatum(types.NewTime(ct, mysql.TypeTimestamp, 3)), encodeBytes([]byte(convertTZ(types.NewTime(ct, mysql.TypeTimestamp, 3)).String())), true, }, { "timestamp/zero", types.NewFieldType(mysql.TypeTimestamp), types.NewTimeDatum(types.ZeroTimestamp), encodeBytes([]byte(convertTZ(types.ZeroTimestamp).String())), true, }, { "timestamp/min", types.NewFieldType(mysql.TypeTimestamp), types.NewTimeDatum(types.MinTimestamp), encodeBytes([]byte(convertTZ(types.MinTimestamp).String())), true, }, { "timestamp/max", types.NewFieldType(mysql.TypeTimestamp), types.NewTimeDatum(types.MaxTimestamp), encodeBytes([]byte(convertTZ(types.MaxTimestamp).String())), true, }, { "datetime", types.NewFieldType(mysql.TypeDatetime), types.NewTimeDatum(types.NewTime(ct, mysql.TypeDatetime, 3)), encodeBytes([]byte(types.NewTime(ct, mysql.TypeDatetime, 3).String())), true, }, { "datetime/zero", types.NewFieldType(mysql.TypeDatetime), types.NewTimeDatum(types.ZeroDatetime), encodeBytes([]byte(types.ZeroTimestamp.String())), true, }, { "datetime/min", types.NewFieldType(mysql.TypeDatetime), types.NewTimeDatum(types.NewTime(types.MinDatetime, mysql.TypeDatetime, 6)), encodeBytes([]byte(types.NewTime(types.MinDatetime, mysql.TypeDatetime, 6).String())), true, }, { "datetime/max", types.NewFieldType(mysql.TypeDatetime), types.NewTimeDatum(types.NewTime(types.MaxDatetime, mysql.TypeDatetime, 6)), encodeBytes([]byte(types.NewTime(types.MaxDatetime, mysql.TypeDatetime, 6).String())), true, }, { "date", types.NewFieldType(mysql.TypeDate), types.NewTimeDatum(types.NewTime(ct, mysql.TypeDate, 3)), encodeBytes([]byte(types.NewTime(ct, mysql.TypeDate, 3).String())), true, }, { "date/zero", types.NewFieldType(mysql.TypeDate), types.NewTimeDatum(types.ZeroDate), encodeBytes([]byte(types.ZeroDate.String())), true, }, { "date/min", types.NewFieldType(mysql.TypeDate), types.NewTimeDatum(types.NewTime(types.MinDatetime, mysql.TypeDate, 6)), encodeBytes([]byte(types.NewTime(types.MinDatetime, mysql.TypeDate, 6).String())), true, }, { "date/max", types.NewFieldType(mysql.TypeDate), types.NewTimeDatum(types.NewTime(types.MaxDatetime, mysql.TypeDate, 6)), encodeBytes([]byte(types.NewTime(types.MaxDatetime, mysql.TypeDate, 6).String())), true, }, { "newdate", types.NewFieldType(mysql.TypeNewDate), types.NewTimeDatum(types.NewTime(ct, mysql.TypeNewDate, 3)), encodeBytes([]byte(types.NewTime(ct, mysql.TypeNewDate, 3).String())), true, }, { "newdate/zero", types.NewFieldType(mysql.TypeNewDate), types.NewTimeDatum(types.ZeroDate), encodeBytes([]byte(types.ZeroDate.String())), true, }, { "newdate/min", types.NewFieldType(mysql.TypeNewDate), types.NewTimeDatum(types.NewTime(types.MinDatetime, mysql.TypeNewDate, 6)), encodeBytes([]byte(types.NewTime(types.MinDatetime, mysql.TypeNewDate, 6).String())), true, }, { "newdate/max", types.NewFieldType(mysql.TypeNewDate), types.NewTimeDatum(types.NewTime(types.MaxDatetime, mysql.TypeNewDate, 6)), encodeBytes([]byte(types.NewTime(types.MaxDatetime, mysql.TypeNewDate, 6).String())), true, }, {"time", types.NewFieldType(mysql.TypeDuration), types.NewDurationDatum(dur), encodeBytes([]byte(dur.String())), true}, {"time/zero", types.NewFieldType(mysql.TypeDuration), types.NewDurationDatum(types.ZeroDuration), encodeBytes([]byte(types.ZeroDuration.String())), true}, {"time/max", types.NewFieldType(mysql.TypeDuration), types.NewDurationDatum(types.MaxMySQLDuration(3)), encodeBytes([]byte(types.MaxMySQLDuration(3).String())), true}, {"decimal/zero", types.NewFieldType(mysql.TypeNewDecimal), types.NewDecimalDatum(decZero), encodeBytes([]byte(decZero.String())), true}, {"decimal/pos", types.NewFieldType(mysql.TypeNewDecimal), types.NewDecimalDatum(decPos), encodeBytes([]byte(decPos.String())), true}, {"decimal/neg", types.NewFieldType(mysql.TypeNewDecimal), types.NewDecimalDatum(decNeg), encodeBytes([]byte(decNeg.String())), true}, {"decimal/min", types.NewFieldType(mysql.TypeNewDecimal), types.NewDecimalDatum(decMin), encodeBytes([]byte(decMin.String())), true}, {"decimal/max", types.NewFieldType(mysql.TypeNewDecimal), types.NewDecimalDatum(decMax), encodeBytes([]byte(decMax.String())), true}, {"json/1", types.NewFieldType(mysql.TypeJSON), types.NewJSONDatum(json1), encodeBytes([]byte(json1.String())), true}, {"json/2", types.NewFieldType(mysql.TypeJSON), types.NewJSONDatum(json2), encodeBytes([]byte(json2.String())), true}, {"json/3", types.NewFieldType(mysql.TypeJSON), types.NewJSONDatum(json3), encodeBytes([]byte(json3.String())), true}, } { t.Run(tt.name, func(t *testing.T) { col := rowcodec.ColData{&model.ColumnInfo{FieldType: *tt.typ}, &tt.dat} raw, err := col.Encode(time.Local, buf[:0]) if tt.ok { require.NoError(t, err) if len(tt.raw) == 0 { require.Len(t, raw, 0) } else { require.Equal(t, tt.raw, raw) } } else { require.Error(t, err) } }) } t.Run("nulldatum", func(t *testing.T) { for _, typ := range []byte{ mysql.TypeUnspecified, mysql.TypeTiny, mysql.TypeShort, mysql.TypeLong, mysql.TypeFloat, mysql.TypeDouble, mysql.TypeNull, mysql.TypeTimestamp, mysql.TypeLonglong, mysql.TypeInt24, mysql.TypeDate, mysql.TypeDuration, mysql.TypeDatetime, mysql.TypeYear, mysql.TypeNewDate, mysql.TypeVarchar, mysql.TypeBit, mysql.TypeJSON, mysql.TypeNewDecimal, mysql.TypeEnum, mysql.TypeSet, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob, mysql.TypeBlob, mysql.TypeVarString, mysql.TypeString, mysql.TypeGeometry, 42, // wrong type } { ft := types.NewFieldType(typ) dat := types.NewDatum(nil) col := rowcodec.ColData{&model.ColumnInfo{FieldType: *ft}, &dat} raw, err := col.Encode(time.Local, nil) require.NoError(t, err) require.Len(t, raw, 0) } }) } func TestRowChecksum(t *testing.T) { typ1 := types.NewFieldType(mysql.TypeNull) dat1 := types.NewDatum(nil) col1 := rowcodec.ColData{&model.ColumnInfo{ID: 1, FieldType: *typ1}, &dat1} typ2 := types.NewFieldType(mysql.TypeLong) dat2 := types.NewDatum(42) col2 := rowcodec.ColData{&model.ColumnInfo{ID: 2, FieldType: *typ2}, &dat2} typ3 := types.NewFieldType(mysql.TypeVarchar) dat3 := types.NewDatum("foobar") col3 := rowcodec.ColData{&model.ColumnInfo{ID: 3, FieldType: *typ3}, &dat3} typ4 := types.NewFieldType(mysql.TypeTimestamp) dat4 := types.NewTimeDatum(types.NewTime(types.FromGoTime(time.Now()), mysql.TypeTimestamp, 6)) col4 := rowcodec.ColData{&model.ColumnInfo{ID: 4, FieldType: *typ4}, &dat4} buf := make([]byte, 0, 64) for _, tt := range []struct { name string cols []rowcodec.ColData }{ {"nil", nil}, {"empty", []rowcodec.ColData{}}, {"nullonly", []rowcodec.ColData{col1}}, {"ordered", []rowcodec.ColData{col1, col2, col3, col4}}, {"unordered", []rowcodec.ColData{col3, col1, col4, col2}}, } { t.Run(tt.name, func(t *testing.T) { row := rowcodec.RowData{tt.cols, buf} if !sort.IsSorted(row) { sort.Sort(row) } checksum, err := row.Checksum(time.Local) require.NoError(t, err) raw, err := row.Encode(time.Local) require.NoError(t, err) require.Equal(t, crc32.ChecksumIEEE(raw), checksum) }) } } func TestEncodeDecodeRowWithChecksum(t *testing.T) { enc := rowcodec.Encoder{} raw, err := enc.Encode(time.UTC, nil, nil, nil, nil) require.NoError(t, err) dec := rowcodec.NewDatumMapDecoder([]rowcodec.ColInfo{}, time.UTC) _, err = dec.DecodeToDatumMap(raw, nil) require.NoError(t, err) checksum, ok := dec.GetChecksum() require.False(t, ok) require.Zero(t, checksum) rawChecksum := rowcodec.RawChecksum{ Handle: kv.IntHandle(1), } raw, err = enc.Encode(time.UTC, nil, nil, rawChecksum, nil) require.NoError(t, err) expected, ok := enc.GetChecksum() require.True(t, ok) require.NotZero(t, expected) _, err = dec.DecodeToDatumMap(raw, nil) require.NoError(t, err) checksum, ok = dec.GetChecksum() require.True(t, ok) require.Equal(t, expected, checksum) version := dec.ChecksumVersion() require.Equal(t, 2, version) } var ( withUnsigned = func(ft *types.FieldType) *types.FieldType { ft.AddFlag(mysql.UnsignedFlag) return ft } withEnumElems = func(elem ...string) func(ft *types.FieldType) *types.FieldType { return func(ft *types.FieldType) *types.FieldType { ft.SetElems(elem) return ft } } withFsp = func(fsp int) func(ft *types.FieldType) *types.FieldType { return func(ft *types.FieldType) *types.FieldType { ft.SetDecimal(fsp) return ft } } withFlen = func(flen int) func(ft *types.FieldType) *types.FieldType { return func(ft *types.FieldType) *types.FieldType { ft.SetFlen(flen) return ft } } getDuration = func(value string) types.Duration { dur, _, _ := types.ParseDuration(types.DefaultStmtNoWarningContext, value, 0) return dur } getOldDatumByte = func(d types.Datum) []byte { b, err := tablecodec.EncodeValue(nil, nil, d) if err != nil { panic(err) } return b } getDatumPoint = func(d types.Datum) *types.Datum { return &d } withFrac = func(f int) func(d types.Datum) types.Datum { return func(d types.Datum) types.Datum { d.SetFrac(f) return d } } withLen = func(l int) func(d types.Datum) types.Datum { return func(d types.Datum) types.Datum { d.SetLength(l) return d } } ) func TestDecodeWithCommitTS(t *testing.T) { cols := []rowcodec.ColInfo{ { ID: 1, Ft: types.NewFieldType(mysql.TypeString), }, { ID: model.ExtraCommitTSID, Ft: types.NewFieldType(mysql.TypeLonglong), }, { ID: 2, Ft: types.NewFieldType(mysql.TypeString), }, } cols[1].Ft.SetFlag(mysql.UnsignedFlag) var encoder rowcodec.Encoder newRow, err := encoder.Encode(time.UTC, []int64{1, 2}, []types.Datum{ types.NewStringDatum("test1"), types.NewStringDatum("test2"), }, nil, nil) require.NoError(t, err) decoder := rowcodec.NewChunkDecoder(cols, []int64{-1}, nil, time.UTC) chk := chunk.New([]*types.FieldType{cols[0].Ft, cols[1].Ft, cols[2].Ft}, 1, 1) err = decoder.DecodeToChunk(newRow, 123456, nil, chk) require.NoError(t, err) require.Equal(t, 1, chk.NumRows()) row := chk.GetRow(0) require.Equal(t, "test1", row.GetString(0)) require.Equal(t, uint64(123456), row.GetUint64(1)) require.Equal(t, "test2", row.GetString(2)) }