648 lines
27 KiB
Go
648 lines
27 KiB
Go
// 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 join
|
|
|
|
import (
|
|
"strconv"
|
|
"testing"
|
|
"unsafe"
|
|
|
|
"github.com/pingcap/tidb/pkg/executor/internal/testutil"
|
|
"github.com/pingcap/tidb/pkg/expression"
|
|
"github.com/pingcap/tidb/pkg/parser/ast"
|
|
"github.com/pingcap/tidb/pkg/parser/mysql"
|
|
"github.com/pingcap/tidb/pkg/types"
|
|
"github.com/pingcap/tidb/pkg/util/chunk"
|
|
"github.com/pingcap/tidb/pkg/util/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func createSimpleFilter(t *testing.T) expression.CNFExprs {
|
|
tinyTp := types.NewFieldType(mysql.TypeTiny)
|
|
intTp := types.NewFieldType(mysql.TypeLonglong)
|
|
a := &expression.Column{Index: 0, RetType: intTp}
|
|
intConstant := int64(10000)
|
|
var d types.Datum
|
|
d.SetMinNotNull()
|
|
d.SetValueWithDefaultCollation(intConstant)
|
|
b := &expression.Constant{RetType: intTp, Value: d}
|
|
sf, err := expression.NewFunction(mock.NewContext(), ast.GT, tinyTp, a, b)
|
|
require.NoError(t, err, "error when create simple filter")
|
|
filter := make(expression.CNFExprs, 0)
|
|
filter = append(filter, sf)
|
|
return filter
|
|
}
|
|
|
|
func createImpossibleFilter(t *testing.T) expression.CNFExprs {
|
|
tinyTp := types.NewFieldType(mysql.TypeTiny)
|
|
intTp := types.NewFieldType(mysql.TypeLonglong)
|
|
a := &expression.Column{Index: 0, RetType: intTp}
|
|
intConstant := int64(10000)
|
|
var d types.Datum
|
|
d.SetMinNotNull()
|
|
d.SetValueWithDefaultCollation(intConstant)
|
|
b := &expression.Constant{RetType: intTp, Value: d}
|
|
sf, err := expression.NewFunction(mock.NewContext(), ast.GT, tinyTp, a, b)
|
|
require.NoError(t, err)
|
|
buildFilter := make(expression.CNFExprs, 0)
|
|
buildFilter = append(buildFilter, sf)
|
|
intConstant = int64(5000)
|
|
d.SetMinNotNull()
|
|
d.SetValueWithDefaultCollation(intConstant)
|
|
b = &expression.Constant{RetType: intTp, Value: d}
|
|
sf, err = expression.NewFunction(mock.NewContext(), ast.LT, tinyTp, a, b)
|
|
require.NoError(t, err)
|
|
buildFilter = append(buildFilter, sf)
|
|
return buildFilter
|
|
}
|
|
|
|
func checkRowLocationAlignment(t *testing.T, rowTables []*rowTable) {
|
|
for _, rt := range rowTables {
|
|
if rt == nil {
|
|
continue
|
|
}
|
|
for _, seg := range rt.segments {
|
|
for index := range seg.rowStartOffset {
|
|
rowLocation := seg.getRowPointer(index)
|
|
require.Equal(t, uint64(0), uint64(uintptr(rowLocation))%8, "row location must be 8 byte aligned")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkKeys(t *testing.T, withSelCol bool, buildFilter expression.CNFExprs, buildKeyIndex []int, buildTypes []*types.FieldType, buildKeyTypes []*types.FieldType, probeKeyTypes []*types.FieldType, keepFilteredRows bool) {
|
|
meta := newTableMeta(buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, nil, []int{1}, false)
|
|
buildSchema := &expression.Schema{}
|
|
for _, tp := range buildTypes {
|
|
buildSchema.Append(&expression.Column{
|
|
RetType: tp,
|
|
})
|
|
}
|
|
hasNullableKey := false
|
|
for _, buildKeyType := range buildKeyTypes {
|
|
if !mysql.HasNotNullFlag(buildKeyType.GetFlag()) {
|
|
hasNullableKey = true
|
|
break
|
|
}
|
|
}
|
|
chk := testutil.GenRandomChunks(buildTypes, 2049)
|
|
if withSelCol {
|
|
sel := make([]int, 0, 2049)
|
|
for i := 0; i < chk.NumRows(); i++ {
|
|
if i%3 == 0 {
|
|
continue
|
|
}
|
|
sel = append(sel, i)
|
|
}
|
|
chk.SetSel(sel)
|
|
}
|
|
hashJoinCtx := &HashJoinCtxV2{
|
|
hashTableMeta: meta,
|
|
BuildFilter: buildFilter,
|
|
}
|
|
hashJoinCtx.Concurrency = 1
|
|
hashJoinCtx.SetupPartitionInfo()
|
|
hashJoinCtx.initHashTableContext()
|
|
hashJoinCtx.SessCtx = mock.NewContext()
|
|
builder := createRowTableBuilder(buildKeyIndex, buildKeyTypes, hashJoinCtx.partitionNumber, hasNullableKey, buildFilter != nil, keepFilteredRows)
|
|
err := builder.processOneChunk(chk, hashJoinCtx.SessCtx.GetSessionVars().StmtCtx.TypeCtx(), hashJoinCtx, 0)
|
|
require.NoError(t, err, "processOneChunk returns error")
|
|
builder.appendRemainingRowLocations(0, hashJoinCtx.hashTableContext)
|
|
require.Equal(t, chk.NumRows(), len(builder.usedRows))
|
|
rowTables := hashJoinCtx.hashTableContext.rowTables[0]
|
|
checkRowLocationAlignment(t, rowTables)
|
|
// all the selected rows should be converted to row format, even for the rows that contains null key
|
|
if keepFilteredRows {
|
|
require.Equal(t, uint64(len(builder.usedRows)), rowTables[0].rowCount())
|
|
// check both validJoinKeyRow index and join key
|
|
validJoinKeyRowIndex := 0
|
|
for logicalIndex, physicalIndex := range builder.usedRows {
|
|
if (builder.filterVector != nil && !builder.filterVector[physicalIndex]) || (builder.nullKeyVector != nil && builder.nullKeyVector[physicalIndex]) {
|
|
continue
|
|
}
|
|
validKeyPos := rowTables[0].getValidJoinKeyPos(validJoinKeyRowIndex)
|
|
require.Equal(t, logicalIndex, validKeyPos, "valid key pos not match, logical index = "+strconv.Itoa(logicalIndex)+", physical index = "+strconv.Itoa(physicalIndex))
|
|
rowStart := rowTables[0].getRowPointer(validKeyPos)
|
|
require.NotEqual(t, unsafe.Pointer(nil), rowStart, "row start must not be nil, logical index = "+strconv.Itoa(logicalIndex)+", physical index = "+strconv.Itoa(physicalIndex))
|
|
require.Equal(t, builder.serializedKeyVectorBuffer[logicalIndex], meta.getKeyBytes(rowStart), "key not match, logical index = "+strconv.Itoa(logicalIndex)+", physical index = "+strconv.Itoa(physicalIndex))
|
|
validJoinKeyRowIndex++
|
|
}
|
|
validKeyPos := rowTables[0].getValidJoinKeyPos(validJoinKeyRowIndex)
|
|
require.Equal(t, -1, validKeyPos, "validKeyPos must be -1 at the end of test")
|
|
} else {
|
|
// check join key
|
|
rowIndex := 0
|
|
for logicalIndex, physicalIndex := range builder.usedRows {
|
|
if (builder.filterVector != nil && !builder.filterVector[physicalIndex]) || (builder.nullKeyVector != nil && builder.nullKeyVector[physicalIndex]) {
|
|
// filtered row, skip
|
|
continue
|
|
}
|
|
rowStart := rowTables[0].getRowPointer(rowIndex)
|
|
require.NotEqual(t, unsafe.Pointer(nil), rowStart, "row start must not be nil, logical index = "+strconv.Itoa(logicalIndex)+", physical index = "+strconv.Itoa(physicalIndex))
|
|
require.Equal(t, builder.serializedKeyVectorBuffer[logicalIndex], meta.getKeyBytes(rowStart), "key not match, logical index = "+strconv.Itoa(logicalIndex)+", physical index = "+strconv.Itoa(physicalIndex))
|
|
rowIndex++
|
|
}
|
|
rowStart := rowTables[0].getRowPointer(rowIndex)
|
|
require.Equal(t, unsafe.Pointer(nil), rowStart, "row start must be nil at the end of the test")
|
|
}
|
|
}
|
|
|
|
func TestLargeColumn(t *testing.T) {
|
|
intTp := types.NewFieldType(mysql.TypeLonglong)
|
|
stringTp := types.NewFieldType(mysql.TypeVarString)
|
|
buildKeyIndex := []int{0, 1}
|
|
buildKeyTypes := []*types.FieldType{intTp, stringTp}
|
|
buildTypes := []*types.FieldType{intTp, stringTp}
|
|
probeKeyTypes := []*types.FieldType{intTp, stringTp}
|
|
|
|
meta := newTableMeta(buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, nil, []int{1}, false)
|
|
buildSchema := &expression.Schema{}
|
|
for _, tp := range buildTypes {
|
|
buildSchema.Append(&expression.Column{
|
|
RetType: tp,
|
|
})
|
|
}
|
|
builder := createRowTableBuilder(buildKeyIndex, buildKeyTypes, 1, true, false, false)
|
|
rows := 2048
|
|
chk := chunk.NewEmptyChunk(buildTypes)
|
|
// each string value is 256k
|
|
stringValue := make([]byte, 1024*256)
|
|
for i := 0; i < rows; i++ {
|
|
// first column is int
|
|
chk.AppendInt64(0, int64(i))
|
|
chk.AppendBytes(1, stringValue)
|
|
}
|
|
|
|
hashJoinCtx := &HashJoinCtxV2{
|
|
hashTableMeta: meta,
|
|
}
|
|
hashJoinCtx.Concurrency = 1
|
|
hashJoinCtx.SetupPartitionInfo()
|
|
hashJoinCtx.initHashTableContext()
|
|
hashJoinCtx.SessCtx = mock.NewContext()
|
|
err := builder.processOneChunk(chk, hashJoinCtx.SessCtx.GetSessionVars().StmtCtx.TypeCtx(), hashJoinCtx, 0)
|
|
require.NoError(t, err, "processOneChunk returns error")
|
|
builder.appendRemainingRowLocations(0, hashJoinCtx.hashTableContext)
|
|
require.Equal(t, chk.NumRows(), len(builder.usedRows))
|
|
rowTables := hashJoinCtx.hashTableContext.rowTables[0]
|
|
checkRowLocationAlignment(t, rowTables)
|
|
for _, rowTable := range rowTables {
|
|
for _, seg := range rowTable.segments {
|
|
require.True(t, len(seg.rawData) < maxRowTableSegmentByteSize*2)
|
|
require.True(t, len(seg.hashValues) < maxRowTableSegmentSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKey(t *testing.T) {
|
|
intTp := types.NewFieldType(mysql.TypeLonglong)
|
|
uintTp := types.NewFieldType(mysql.TypeLonglong)
|
|
uintTp.AddFlag(mysql.UnsignedFlag)
|
|
stringTp := types.NewFieldType(mysql.TypeVarString)
|
|
binaryStringTp := types.NewFieldType(mysql.TypeBlob)
|
|
notNullIntTp := types.NewFieldType(mysql.TypeLonglong)
|
|
notNullIntTp.SetFlag(mysql.NotNullFlag)
|
|
|
|
// inlined fixed length keys
|
|
buildKeyIndex := []int{0}
|
|
buildTypes := []*types.FieldType{intTp, uintTp, uintTp}
|
|
buildKeyTypes := []*types.FieldType{intTp}
|
|
probeKeyTypes := []*types.FieldType{intTp}
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, false)
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, true)
|
|
buildKeyIndex = []int{0}
|
|
buildTypes = []*types.FieldType{notNullIntTp, uintTp, uintTp}
|
|
buildKeyTypes = []*types.FieldType{notNullIntTp}
|
|
probeKeyTypes = []*types.FieldType{notNullIntTp}
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, false)
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, true)
|
|
// inlined variable length keys
|
|
buildKeyIndex = []int{0, 1}
|
|
buildTypes = []*types.FieldType{binaryStringTp, intTp, uintTp}
|
|
buildKeyTypes = []*types.FieldType{binaryStringTp, intTp}
|
|
probeKeyTypes = []*types.FieldType{binaryStringTp, intTp}
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, false)
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, true)
|
|
|
|
// not inlined fixed length keys
|
|
buildKeyIndex = []int{0}
|
|
buildTypes = []*types.FieldType{intTp, uintTp, uintTp}
|
|
buildKeyTypes = []*types.FieldType{intTp}
|
|
probeKeyTypes = []*types.FieldType{uintTp}
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, false)
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, true)
|
|
|
|
// not inlined variable length keys
|
|
buildKeyIndex = []int{0, 1}
|
|
buildTypes = []*types.FieldType{stringTp, intTp, uintTp}
|
|
buildKeyTypes = []*types.FieldType{stringTp, intTp}
|
|
probeKeyTypes = []*types.FieldType{stringTp, intTp}
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, false)
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, true)
|
|
buildKeyIndex = []int{2, 0, 1}
|
|
buildTypes = []*types.FieldType{stringTp, intTp, binaryStringTp, uintTp}
|
|
buildKeyTypes = []*types.FieldType{binaryStringTp, stringTp, intTp}
|
|
probeKeyTypes = []*types.FieldType{binaryStringTp, stringTp, intTp}
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, false)
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, true)
|
|
|
|
// keepRows = true
|
|
buildKeyIndex = []int{0}
|
|
buildTypes = []*types.FieldType{intTp, uintTp, uintTp}
|
|
buildKeyTypes = []*types.FieldType{intTp}
|
|
probeKeyTypes = []*types.FieldType{intTp}
|
|
checkKeys(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, true)
|
|
// with sel = true
|
|
checkKeys(t, true, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, true)
|
|
checkKeys(t, true, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, false)
|
|
// buildFilter != nil
|
|
buildFilter := createSimpleFilter(t)
|
|
checkKeys(t, true, buildFilter, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, true)
|
|
checkKeys(t, true, buildFilter, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, false)
|
|
buildKeyIndex = []int{0}
|
|
buildTypes = []*types.FieldType{notNullIntTp, uintTp, uintTp}
|
|
buildKeyTypes = []*types.FieldType{notNullIntTp}
|
|
probeKeyTypes = []*types.FieldType{notNullIntTp}
|
|
checkKeys(t, true, buildFilter, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, true)
|
|
checkKeys(t, true, buildFilter, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, false)
|
|
}
|
|
|
|
func checkColumnResult(t *testing.T, builder *rowTableBuilder, keepFilteredRows bool, result *chunk.Chunk, expected *chunk.Chunk, ctx *HashJoinCtxV2, forOtherCondition bool) {
|
|
if keepFilteredRows {
|
|
require.Equal(t, expected.NumRows(), result.NumRows())
|
|
}
|
|
meta := ctx.hashTableMeta
|
|
if forOtherCondition {
|
|
for i := 0; i < meta.columnCountNeededForOtherCondition; i++ {
|
|
colIndex := meta.rowColumnsOrder[i]
|
|
resultCol := result.Column(colIndex)
|
|
require.Equal(t, result.NumRows(), resultCol.Rows())
|
|
resultIndex := 0
|
|
for logicalIndex, physicalIndex := range builder.usedRows {
|
|
if !keepFilteredRows {
|
|
if (builder.filterVector != nil && !builder.filterVector[physicalIndex]) || (builder.nullKeyVector != nil && builder.nullKeyVector[physicalIndex]) {
|
|
// filtered row, skip
|
|
continue
|
|
}
|
|
}
|
|
isNull := expected.GetRow(logicalIndex).IsNull(colIndex)
|
|
require.Equal(t, isNull, resultCol.IsNull(resultIndex), "data not match, col index = "+strconv.Itoa(colIndex)+" row index = "+strconv.Itoa(logicalIndex))
|
|
if !isNull {
|
|
require.Equal(t, expected.GetRow(logicalIndex).GetRaw(colIndex), resultCol.GetRaw(resultIndex), "data not match, col index = "+strconv.Itoa(colIndex)+" row index = "+strconv.Itoa(logicalIndex))
|
|
}
|
|
resultIndex++
|
|
}
|
|
require.Equal(t, resultIndex, result.NumRows())
|
|
}
|
|
} else {
|
|
for index, orgIndex := range ctx.LUsed {
|
|
resultCol := result.Column(index)
|
|
require.Equal(t, result.NumRows(), resultCol.Rows())
|
|
resultIndex := 0
|
|
for logicalIndex, physicalIndex := range builder.usedRows {
|
|
if !keepFilteredRows {
|
|
if (builder.filterVector != nil && !builder.filterVector[physicalIndex]) || (builder.nullKeyVector != nil && builder.nullKeyVector[physicalIndex]) {
|
|
// filtered row, skip
|
|
continue
|
|
}
|
|
}
|
|
isNull := expected.GetRow(logicalIndex).IsNull(orgIndex)
|
|
require.Equal(t, isNull, resultCol.IsNull(resultIndex), "data not match, col index = "+strconv.Itoa(orgIndex)+" row index = "+strconv.Itoa(logicalIndex))
|
|
if !isNull {
|
|
require.Equal(t, expected.GetRow(logicalIndex).GetRaw(orgIndex), resultCol.GetRaw(resultIndex), "data not match, col index = "+strconv.Itoa(orgIndex)+" row index = "+strconv.Itoa(logicalIndex))
|
|
}
|
|
resultIndex++
|
|
}
|
|
require.Equal(t, resultIndex, result.NumRows())
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkColumns(t *testing.T, withSelCol bool, buildFilter expression.CNFExprs, buildKeyIndex []int, buildTypes []*types.FieldType,
|
|
buildKeyTypes []*types.FieldType, probeKeyTypes []*types.FieldType, keepFilteredRows bool, columnsUsedByOtherCondition []int,
|
|
outputColumns []int, needUsedFlag bool) {
|
|
meta := newTableMeta(buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, columnsUsedByOtherCondition, outputColumns, needUsedFlag)
|
|
resultTypes := make([]*types.FieldType, 0, len(outputColumns))
|
|
for _, index := range outputColumns {
|
|
resultTypes = append(resultTypes, buildTypes[index])
|
|
}
|
|
buildSchema := &expression.Schema{}
|
|
for _, tp := range buildTypes {
|
|
buildSchema.Append(&expression.Column{
|
|
RetType: tp,
|
|
})
|
|
}
|
|
hasNullableKey := false
|
|
for _, buildKeyType := range buildKeyTypes {
|
|
if !mysql.HasNotNullFlag(buildKeyType.GetFlag()) {
|
|
hasNullableKey = true
|
|
break
|
|
}
|
|
}
|
|
builder := createRowTableBuilder(buildKeyIndex, buildKeyTypes, 1, hasNullableKey, buildFilter != nil, keepFilteredRows)
|
|
chk := testutil.GenRandomChunks(buildTypes, 2049)
|
|
if withSelCol {
|
|
sel := make([]int, 0, 2049)
|
|
for i := 0; i < chk.NumRows(); i++ {
|
|
if i%3 == 0 {
|
|
continue
|
|
}
|
|
sel = append(sel, i)
|
|
}
|
|
chk.SetSel(sel)
|
|
}
|
|
hashJoinCtx := &HashJoinCtxV2{
|
|
hashTableMeta: meta,
|
|
BuildFilter: buildFilter,
|
|
LUsedInOtherCondition: columnsUsedByOtherCondition,
|
|
LUsed: outputColumns,
|
|
}
|
|
hashJoinCtx.Concurrency = 1
|
|
hashJoinCtx.SetupPartitionInfo()
|
|
hashJoinCtx.initHashTableContext()
|
|
hashJoinCtx.SessCtx = mock.NewContext()
|
|
err := builder.processOneChunk(chk, hashJoinCtx.SessCtx.GetSessionVars().StmtCtx.TypeCtx(), hashJoinCtx, 0)
|
|
builder.appendRemainingRowLocations(0, hashJoinCtx.hashTableContext)
|
|
require.NoError(t, err, "processOneChunk returns error")
|
|
require.Equal(t, chk.NumRows(), len(builder.usedRows))
|
|
rowTables := hashJoinCtx.hashTableContext.rowTables[0]
|
|
checkRowLocationAlignment(t, rowTables)
|
|
mockJoinProber := newMockJoinProbe(hashJoinCtx)
|
|
resultChunk := chunk.NewEmptyChunk(resultTypes)
|
|
resultChunk.SetInCompleteChunk(true)
|
|
tmpChunk := chunk.NewEmptyChunk(buildTypes)
|
|
tmpChunk.SetInCompleteChunk(true)
|
|
hasOtherConditionColumns := len(columnsUsedByOtherCondition) > 0
|
|
// all the selected rows should be converted to row format, even for the rows that contains null key
|
|
if keepFilteredRows {
|
|
require.Equal(t, uint64(len(builder.usedRows)), rowTables[0].rowCount())
|
|
for logicalIndex, physicalIndex := range builder.usedRows {
|
|
rowStart := rowTables[0].getRowPointer(logicalIndex)
|
|
require.NotEqual(t, unsafe.Pointer(nil), rowStart, "row start must not be nil, logical index = "+strconv.Itoa(logicalIndex)+", physical index = "+strconv.Itoa(physicalIndex))
|
|
if hasOtherConditionColumns {
|
|
mockJoinProber.appendBuildRowToCachedBuildRowsAndConstructBuildRowsIfNeeded(createMatchRowInfo(0, rowStart), tmpChunk, 0, hasOtherConditionColumns)
|
|
} else {
|
|
mockJoinProber.appendBuildRowToCachedBuildRowsAndConstructBuildRowsIfNeeded(createMatchRowInfo(0, rowStart), resultChunk, 0, hasOtherConditionColumns)
|
|
}
|
|
}
|
|
if len(mockJoinProber.cachedBuildRows) > 0 {
|
|
if hasOtherConditionColumns {
|
|
mockJoinProber.batchConstructBuildRows(tmpChunk, 0, hasOtherConditionColumns)
|
|
} else {
|
|
mockJoinProber.batchConstructBuildRows(resultChunk, 0, hasOtherConditionColumns)
|
|
}
|
|
}
|
|
if hasOtherConditionColumns {
|
|
checkColumnResult(t, builder, keepFilteredRows, tmpChunk, chk, hashJoinCtx, hasOtherConditionColumns)
|
|
// assume all the column is selected
|
|
mockJoinProber.selected = make([]bool, 0, tmpChunk.NumRows())
|
|
for i := 0; i < tmpChunk.NumRows(); i++ {
|
|
mockJoinProber.selected = append(mockJoinProber.selected, true)
|
|
}
|
|
// need to append the rest columns
|
|
err1 := mockJoinProber.buildResultAfterOtherCondition(resultChunk, tmpChunk)
|
|
require.NoError(t, err1)
|
|
require.Equal(t, rowTables[0].rowCount(), uint64(resultChunk.NumRows()))
|
|
checkColumnResult(t, builder, keepFilteredRows, resultChunk, chk, hashJoinCtx, false)
|
|
} else {
|
|
checkColumnResult(t, builder, keepFilteredRows, resultChunk, chk, hashJoinCtx, hasOtherConditionColumns)
|
|
}
|
|
} else {
|
|
// check join key
|
|
rowIndex := 0
|
|
for logicalIndex, physicalIndex := range builder.usedRows {
|
|
if (builder.filterVector != nil && !builder.filterVector[physicalIndex]) || (builder.nullKeyVector != nil && builder.nullKeyVector[physicalIndex]) {
|
|
// filtered row, skip
|
|
continue
|
|
}
|
|
rowStart := rowTables[0].getRowPointer(rowIndex)
|
|
require.NotEqual(t, unsafe.Pointer(nil), rowStart, "row start must not be nil, logical index = "+strconv.Itoa(logicalIndex)+", physical index = "+strconv.Itoa(physicalIndex))
|
|
if hasOtherConditionColumns {
|
|
mockJoinProber.appendBuildRowToCachedBuildRowsAndConstructBuildRowsIfNeeded(createMatchRowInfo(0, rowStart), tmpChunk, 0, hasOtherConditionColumns)
|
|
} else {
|
|
mockJoinProber.appendBuildRowToCachedBuildRowsAndConstructBuildRowsIfNeeded(createMatchRowInfo(0, rowStart), resultChunk, 0, hasOtherConditionColumns)
|
|
}
|
|
rowIndex++
|
|
}
|
|
if len(mockJoinProber.cachedBuildRows) > 0 {
|
|
if hasOtherConditionColumns {
|
|
mockJoinProber.batchConstructBuildRows(tmpChunk, 0, hasOtherConditionColumns)
|
|
} else {
|
|
mockJoinProber.batchConstructBuildRows(resultChunk, 0, hasOtherConditionColumns)
|
|
}
|
|
}
|
|
rowStart := rowTables[0].getRowPointer(rowIndex)
|
|
require.Equal(t, unsafe.Pointer(nil), rowStart, "row start must be nil at the end of the test")
|
|
if hasOtherConditionColumns {
|
|
checkColumnResult(t, builder, keepFilteredRows, tmpChunk, chk, hashJoinCtx, hasOtherConditionColumns)
|
|
mockJoinProber.selected = make([]bool, 0, tmpChunk.NumRows())
|
|
for i := 0; i < tmpChunk.NumRows(); i++ {
|
|
mockJoinProber.selected = append(mockJoinProber.selected, true)
|
|
}
|
|
err1 := mockJoinProber.buildResultAfterOtherCondition(resultChunk, tmpChunk)
|
|
require.NoError(t, err1)
|
|
require.Equal(t, rowTables[0].rowCount(), uint64(resultChunk.NumRows()))
|
|
checkColumnResult(t, builder, keepFilteredRows, resultChunk, chk, hashJoinCtx, false)
|
|
} else {
|
|
checkColumnResult(t, builder, keepFilteredRows, resultChunk, chk, hashJoinCtx, hasOtherConditionColumns)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestColumnsBasic(t *testing.T) {
|
|
notNullIntTp := types.NewFieldType(mysql.TypeLonglong)
|
|
notNullIntTp.AddFlag(mysql.NotNullFlag)
|
|
notNullUintTp := types.NewFieldType(mysql.TypeLonglong)
|
|
notNullUintTp.AddFlag(mysql.UnsignedFlag)
|
|
notNullUintTp.AddFlag(mysql.NotNullFlag)
|
|
notNullString := types.NewFieldType(mysql.TypeVarString)
|
|
notNullString.AddFlag(mysql.NotNullFlag)
|
|
notNullBinaryStringTp := types.NewFieldType(mysql.TypeBlob)
|
|
notNullBinaryStringTp.AddFlag(mysql.NotNullFlag)
|
|
intTp := types.NewFieldType(mysql.TypeLonglong)
|
|
uintTp := types.NewFieldType(mysql.TypeLonglong)
|
|
uintTp.AddFlag(mysql.UnsignedFlag)
|
|
stringTp := types.NewFieldType(mysql.TypeVarString)
|
|
binaryStringTp := types.NewFieldType(mysql.TypeBlob)
|
|
buildKeyIndex := []int{0}
|
|
buildTypes := []*types.FieldType{notNullIntTp, notNullIntTp, notNullString, notNullUintTp, notNullBinaryStringTp, notNullIntTp}
|
|
buildKeyTypes := []*types.FieldType{notNullIntTp}
|
|
probeKeyTypes := []*types.FieldType{notNullIntTp}
|
|
buildTypesNullable := []*types.FieldType{intTp, intTp, stringTp, uintTp, binaryStringTp, intTp}
|
|
buildKeyTypesNullable := []*types.FieldType{intTp}
|
|
probeKeyTypesNullable := []*types.FieldType{intTp}
|
|
columnUsedByOtherConditions := [][]int{{2, 3}, {0, 2}, nil}
|
|
outputColumns := [][]int{{0, 1, 2, 3, 4, 5}, {2, 3, 4, 5, 1, 0}}
|
|
keepFilteredRows := []bool{true, false}
|
|
needUsedFlag := []bool{true, false}
|
|
filter := createSimpleFilter(t)
|
|
filters := []expression.CNFExprs{filter, nil}
|
|
withSelCol := []bool{true, false}
|
|
for _, otherCondition := range columnUsedByOtherConditions {
|
|
for _, allColumns := range outputColumns {
|
|
for _, keep := range keepFilteredRows {
|
|
for _, usedFlag := range needUsedFlag {
|
|
for _, buildFilter := range filters {
|
|
for _, withSel := range withSelCol {
|
|
checkColumns(t, withSel, buildFilter, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, keep, otherCondition, allColumns, usedFlag)
|
|
checkColumns(t, withSel, buildFilter, buildKeyIndex, buildTypesNullable, buildKeyTypesNullable, probeKeyTypesNullable, keep, otherCondition, allColumns, usedFlag)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestColumnsAllDataTypes(t *testing.T) {
|
|
tinyTp := types.NewFieldType(mysql.TypeTiny)
|
|
tinyTp.AddFlag(mysql.NotNullFlag)
|
|
intTp := types.NewFieldType(mysql.TypeLonglong)
|
|
intTp.AddFlag(mysql.NotNullFlag)
|
|
uintTp := types.NewFieldType(mysql.TypeLonglong)
|
|
uintTp.AddFlag(mysql.UnsignedFlag)
|
|
uintTp.AddFlag(mysql.NotNullFlag)
|
|
yearTp := types.NewFieldType(mysql.TypeYear)
|
|
yearTp.AddFlag(mysql.NotNullFlag)
|
|
durationTp := types.NewFieldType(mysql.TypeDuration)
|
|
durationTp.AddFlag(mysql.NotNullFlag)
|
|
enumTp := types.NewFieldType(mysql.TypeEnum)
|
|
enumTp.AddFlag(mysql.NotNullFlag)
|
|
enumWithIntFlag := types.NewFieldType(mysql.TypeEnum)
|
|
enumWithIntFlag.AddFlag(mysql.EnumSetAsIntFlag)
|
|
enumWithIntFlag.AddFlag(mysql.NotNullFlag)
|
|
setTp := types.NewFieldType(mysql.TypeSet)
|
|
setTp.AddFlag(mysql.NotNullFlag)
|
|
bitTp := types.NewFieldType(mysql.TypeBit)
|
|
bitTp.AddFlag(mysql.NotNullFlag)
|
|
jsonTp := types.NewFieldType(mysql.TypeJSON)
|
|
jsonTp.AddFlag(mysql.NotNullFlag)
|
|
floatTp := types.NewFieldType(mysql.TypeFloat)
|
|
floatTp.AddFlag(mysql.NotNullFlag)
|
|
doubleTp := types.NewFieldType(mysql.TypeDouble)
|
|
doubleTp.AddFlag(mysql.NotNullFlag)
|
|
stringTp := types.NewFieldType(mysql.TypeVarString)
|
|
stringTp.AddFlag(mysql.NotNullFlag)
|
|
datetimeTp := types.NewFieldType(mysql.TypeDatetime)
|
|
datetimeTp.AddFlag(mysql.NotNullFlag)
|
|
decimalTp := types.NewFieldType(mysql.TypeNewDecimal)
|
|
decimalTp.AddFlag(mysql.NotNullFlag)
|
|
timestampTp := types.NewFieldType(mysql.TypeTimestamp)
|
|
timestampTp.AddFlag(mysql.NotNullFlag)
|
|
dateTp := types.NewFieldType(mysql.TypeDate)
|
|
dateTp.AddFlag(mysql.NotNullFlag)
|
|
buildKeyIndex := []int{0}
|
|
buildTypes := []*types.FieldType{tinyTp, intTp, uintTp, yearTp, durationTp, enumTp, enumWithIntFlag, setTp, bitTp, jsonTp, floatTp, doubleTp, stringTp, datetimeTp, decimalTp, timestampTp, dateTp}
|
|
buildKeyTypes := []*types.FieldType{tinyTp}
|
|
probeKeyTypes := []*types.FieldType{tinyTp}
|
|
nullableBuildTypes := toNullableTypes(buildTypes)
|
|
nullableBuildKeyTypes := toNullableTypes(buildKeyTypes)
|
|
nullableProbeKeyTypes := toNullableTypes(probeKeyTypes)
|
|
outputColumns := make([]int, 0, len(buildTypes))
|
|
for index := range buildTypes {
|
|
outputColumns = append(outputColumns, index)
|
|
}
|
|
keepFilteredRows := []bool{true, false}
|
|
needUsedFlag := []bool{true, false}
|
|
for _, keep := range keepFilteredRows {
|
|
for _, usedFlag := range needUsedFlag {
|
|
checkColumns(t, false, nil, buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, keep, nil, outputColumns, usedFlag)
|
|
checkColumns(t, false, nil, buildKeyIndex, nullableBuildTypes, nullableBuildKeyTypes, nullableProbeKeyTypes, keep, nil, outputColumns, usedFlag)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBalanceOfFilteredRows(t *testing.T) {
|
|
intTp := types.NewFieldType(mysql.TypeLonglong)
|
|
stringTp := types.NewFieldType(mysql.TypeVarString)
|
|
binaryStringTp := types.NewFieldType(mysql.TypeBlob)
|
|
buildKeyIndex := []int{0}
|
|
buildTypes := []*types.FieldType{intTp, stringTp, binaryStringTp}
|
|
buildKeyTypes := []*types.FieldType{intTp}
|
|
probeKeyTypes := []*types.FieldType{intTp}
|
|
buildSchema := &expression.Schema{}
|
|
for _, tp := range buildTypes {
|
|
buildSchema.Append(&expression.Column{
|
|
RetType: tp,
|
|
})
|
|
}
|
|
|
|
meta := newTableMeta(buildKeyIndex, buildTypes, buildKeyTypes, probeKeyTypes, nil, []int{}, false)
|
|
hasNullableKey := false
|
|
for _, buildKeyType := range buildKeyTypes {
|
|
if !mysql.HasNotNullFlag(buildKeyType.GetFlag()) {
|
|
hasNullableKey = true
|
|
break
|
|
}
|
|
}
|
|
|
|
buildFilter := createImpossibleFilter(t)
|
|
chk := testutil.GenRandomChunks(buildTypes, 3000)
|
|
hashJoinCtx := &HashJoinCtxV2{
|
|
hashTableMeta: meta,
|
|
BuildFilter: buildFilter,
|
|
}
|
|
hashJoinCtx.Concurrency = 4
|
|
hashJoinCtx.SetupPartitionInfo()
|
|
hashJoinCtx.initHashTableContext()
|
|
hashJoinCtx.SessCtx = mock.NewContext()
|
|
builder := createRowTableBuilder(buildKeyIndex, buildKeyTypes, hashJoinCtx.partitionNumber, hasNullableKey, true, true)
|
|
err := builder.processOneChunk(chk, hashJoinCtx.SessCtx.GetSessionVars().StmtCtx.TypeCtx(), hashJoinCtx, 0)
|
|
require.NoError(t, err)
|
|
builder.appendRemainingRowLocations(0, hashJoinCtx.hashTableContext)
|
|
rowTables := hashJoinCtx.hashTableContext.rowTables[0]
|
|
for i := 0; i < int(hashJoinCtx.partitionNumber); i++ {
|
|
require.Equal(t, int(3000/hashJoinCtx.partitionNumber), int(rowTables[i].rowCount()))
|
|
}
|
|
}
|
|
|
|
func TestSetupPartitionInfo(t *testing.T) {
|
|
type testCase struct {
|
|
concurrency uint
|
|
partitionNumber uint
|
|
partitionMaskOffset int
|
|
}
|
|
|
|
testCases := []testCase{
|
|
{1, 1, 64},
|
|
{2, 2, 63},
|
|
{3, 4, 62},
|
|
{4, 4, 62},
|
|
{5, 8, 61},
|
|
{6, 8, 61},
|
|
{7, 8, 61},
|
|
{8, 8, 61},
|
|
{9, 16, 60},
|
|
{10, 16, 60},
|
|
{11, 16, 60},
|
|
{12, 16, 60},
|
|
{13, 16, 60},
|
|
{14, 16, 60},
|
|
{15, 16, 60},
|
|
{16, 16, 60},
|
|
{17, 16, 60},
|
|
{18, 16, 60},
|
|
{100, 16, 60},
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
hashJoinCtx := &HashJoinCtxV2{}
|
|
hashJoinCtx.Concurrency = test.concurrency
|
|
hashJoinCtx.SetupPartitionInfo()
|
|
require.Equal(t, test.partitionNumber, hashJoinCtx.partitionNumber)
|
|
require.Equal(t, test.partitionMaskOffset, hashJoinCtx.partitionMaskOffset)
|
|
}
|
|
}
|