Files
dolthub--dolt/go/store/val/tuple_builder_test.go
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

660 lines
19 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2021 Dolthub, 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 val
import (
"bytes"
"context"
"encoding/json"
"math"
"math/rand"
"testing"
"github.com/dolthub/go-mysql-server/sql"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dolthub/dolt/go/store/hash"
)
func TestTupleBuilder(t *testing.T) {
t.Run("smoke test", func(t *testing.T) {
smokeTestTupleBuilder(t)
})
t.Run("round trip ints", func(t *testing.T) {
testRoundTripInts(t)
})
t.Run("build large tuple", func(t *testing.T) {
testBuildLargeTuple(t)
})
}
// assertJsonEqual unmarshals both JSON byte slices and compares the resulting structures.
func assertJsonEqual(t *testing.T, expected, actual []byte) {
t.Helper()
var expVal, actVal interface{}
require.NoError(t, json.Unmarshal(expected, &expVal))
require.NoError(t, json.Unmarshal(actual, &actVal))
require.Equal(t, expVal, actVal)
}
func smokeTestTupleBuilder(t *testing.T) {
ns := &TestValueStore{}
desc := NewTupleDescriptor(
Type{Enc: Int8Enc},
Type{Enc: Int16Enc},
Type{Enc: Int32Enc},
Type{Enc: Int64Enc},
Type{Enc: Uint8Enc},
Type{Enc: Uint16Enc},
Type{Enc: Uint32Enc},
Type{Enc: Uint64Enc},
Type{Enc: Float32Enc},
Type{Enc: Float64Enc},
Type{Enc: StringEnc},
Type{Enc: ByteStringEnc},
)
tb := NewTupleBuilder(desc, ns)
tb.PutInt8(0, math.MaxInt8)
tb.PutInt16(1, math.MaxInt16)
tb.PutInt32(2, math.MaxInt32)
tb.PutInt64(3, math.MaxInt64)
tb.PutUint8(4, math.MaxUint8)
tb.PutUint16(5, math.MaxUint16)
tb.PutUint32(6, math.MaxUint32)
tb.PutUint64(7, math.MaxUint64)
tb.PutFloat32(8, math.MaxFloat32)
tb.PutFloat64(9, math.MaxFloat64)
tb.PutString(10, "123")
tb.PutByteString(11, []byte("abc"))
tup, err := tb.Build(context.Background(), testPool)
assert.NoError(t, err)
i8, ok := desc.GetInt8(0, tup)
assert.True(t, ok)
assert.Equal(t, int8(math.MaxInt8), i8)
i16, ok := desc.GetInt16(1, tup)
assert.True(t, ok)
assert.Equal(t, int16(math.MaxInt16), i16)
i32, ok := desc.GetInt32(2, tup)
assert.True(t, ok)
assert.Equal(t, int32(math.MaxInt32), i32)
i64, ok := desc.GetInt64(3, tup)
assert.True(t, ok)
assert.Equal(t, int64(math.MaxInt64), i64)
u8, ok := desc.GetUint8(4, tup)
assert.True(t, ok)
assert.Equal(t, uint8(math.MaxUint8), u8)
u16, ok := desc.GetUint16(5, tup)
assert.True(t, ok)
assert.Equal(t, uint16(math.MaxUint16), u16)
u32, ok := desc.GetUint32(6, tup)
assert.True(t, ok)
assert.Equal(t, uint32(math.MaxUint32), u32)
u64, ok := desc.GetUint64(7, tup)
assert.True(t, ok)
assert.Equal(t, uint64(math.MaxUint64), u64)
f32, ok := desc.GetFloat32(8, tup)
assert.True(t, ok)
assert.Equal(t, float32(math.MaxFloat32), f32)
f64, ok := desc.GetFloat64(9, tup)
assert.True(t, ok)
assert.Equal(t, float64(math.MaxFloat64), f64)
str, ok := desc.GetString(10, tup)
assert.True(t, ok)
assert.Equal(t, "123", str)
byts, ok := desc.GetBytes(11, tup)
assert.True(t, ok)
assert.Equal(t, []byte("abc"), byts)
}
func testRoundTripInts(t *testing.T) {
ns := &TestValueStore{}
typ := Type{Enc: Int64Enc, Nullable: true}
tests := []struct {
data map[int]int64
desc *TupleDesc
}{
{
desc: NewTupleDescriptor(typ),
data: map[int]int64{
0: 0,
},
},
{
desc: NewTupleDescriptor(typ, typ, typ),
data: map[int]int64{
0: 0,
1: 1,
2: 2,
},
},
{
desc: NewTupleDescriptor(typ),
data: map[int]int64{
// 0: NULL,
},
},
{
desc: NewTupleDescriptor(typ, typ, typ),
data: map[int]int64{
// 0: NULL,
// 1: NULL,
2: 2,
},
},
}
for _, test := range tests {
// build
bld := NewTupleBuilder(test.desc, ns)
for idx, value := range test.data {
bld.PutInt64(idx, value)
}
tup, err := bld.Build(context.Background(), testPool)
assert.NoError(t, err)
// verify
n := test.desc.Count()
for idx := 0; idx < n; idx++ {
exp, ok := test.data[idx]
if !ok {
null := test.desc.IsNull(idx, tup)
assert.True(t, null)
} else {
act, ok := test.desc.GetInt64(idx, tup)
assert.True(t, ok)
assert.Equal(t, exp, act)
}
}
}
}
func testBuildLargeTuple(t *testing.T) {
desc := NewTupleDescriptor(
Type{Enc: Int8Enc},
Type{Enc: Int16Enc},
Type{Enc: Int32Enc},
Type{Enc: Int64Enc},
Type{Enc: Uint8Enc},
Type{Enc: Uint16Enc},
Type{Enc: Uint32Enc},
Type{Enc: Uint64Enc},
Type{Enc: Float32Enc},
Type{Enc: Float64Enc},
Type{Enc: StringEnc},
Type{Enc: ByteStringEnc},
)
s1 := make([]byte, 1024)
s2 := make([]byte, 1024)
rand.Read(s1)
rand.Read(s2)
tb := NewTupleBuilder(desc, nil)
tb.PutInt8(0, math.MaxInt8)
tb.PutInt16(1, math.MaxInt16)
tb.PutInt32(2, math.MaxInt32)
tb.PutInt64(3, math.MaxInt64)
tb.PutUint8(4, math.MaxUint8)
tb.PutUint16(5, math.MaxUint16)
tb.PutUint32(6, math.MaxUint32)
tb.PutUint64(7, math.MaxUint64)
tb.PutFloat32(8, math.MaxFloat32)
tb.PutFloat64(9, math.MaxFloat64)
tb.PutString(10, string(s1))
tb.PutByteString(11, []byte(s2))
}
type testCompare struct {
vs ValueStore
}
var _ TupleComparator = testCompare{}
func (tc testCompare) Compare(ctx context.Context, left, right Tuple, desc *TupleDesc) (cmp int, err error) {
for i, typ := range desc.Types {
cmp, err = compare(ctx, typ, left.GetField(i), right.GetField(i), tc.vs)
if err != nil {
return 0, err
}
if cmp != 0 {
break
}
}
return
}
func (tc testCompare) CompareValues(ctx context.Context, index int, left, right []byte, typ Type) (int, error) {
return compare(ctx, typ, left, right, tc.vs)
}
func (tc testCompare) Prefix(n int) TupleComparator {
return tc
}
func (tc testCompare) Suffix(n int) TupleComparator {
return tc
}
func (tc testCompare) Validated(types []Type) TupleComparator {
return tc
}
func (tc testCompare) WithValueStore(vs ValueStore) TupleComparator {
return testCompare{vs: vs}
}
type TestValueStore struct {
values [][]byte
}
func (t TestValueStore) ReadBytes(_ context.Context, h hash.Hash) ([]byte, error) {
idx := int(h[0]) - 1
return t.values[idx], nil
}
func (t TestValueStore) contains(val []byte) (int, bool) {
for i, v := range t.values {
if bytes.Equal(v, val) {
return i, true
}
}
return -1, false
}
func (t *TestValueStore) WriteBytes(_ context.Context, val []byte) (h hash.Hash, err error) {
idx, ok := t.contains(val)
if ok {
h[0] = byte(idx) + 1
return h, nil
}
t.values = append(t.values, val)
h[0] = byte(len(t.values))
return h, nil
}
func (t TestValueStore) CompareAdaptive(ctx context.Context, l AdaptiveValue, r AdaptiveValue, encoding Encoding) (int, error) {
panic("unsupported")
}
func (t TestValueStore) CompareAdaptiveCollatedStrings(ctx context.Context, l, r AdaptiveValue, collation sql.CollationID) (int, error) {
panic("unsupported")
}
var _ ValueStore = &TestValueStore{}
func TestTupleBuilderJsonAdaptiveEncoding(t *testing.T) {
ctx := sql.NewEmptyContext()
smallJson := []byte(`{"key":"value"}`)
largeJson := func() []byte {
// Build a JSON object large enough to exceed the inline target.
m := make(map[string]string)
for i := 0; i < 200; i++ {
m[string(rune('a'+i%26))+string(rune('A'+i%26))+string(rune('0'+i%10))] = "xxxxxxxxxx"
}
b, _ := json.Marshal(m)
return b
}()
t.Run("round trip inlined JSON value", func(t *testing.T) {
types := []Type{{Enc: JsonAdaptiveEnc}}
vs := &TestValueStore{}
td := NewTupleDescriptor(types...)
tb := NewTupleBuilder(td, vs)
err := tb.PutAdaptiveJsonFromInline(ctx, 0, smallJson)
require.NoError(t, err)
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
result, ok, err := td.GetJsonAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
require.True(t, ok)
require.NotNil(t, result)
bytes, ok := result.([]byte)
require.True(t, ok, "expected inlined JSON value to be []byte")
assertJsonEqual(t, smallJson, bytes)
})
t.Run("round trip out-of-band JSON value via outline", func(t *testing.T) {
types := []Type{{Enc: JsonAdaptiveEnc}}
vs := &TestValueStore{}
td := NewTupleDescriptor(types...)
tb := NewTupleBuilder(td, vs)
// Write the large JSON out-of-band and record the address.
h, err := vs.WriteBytes(ctx, largeJson)
require.NoError(t, err)
storage := NewJsonStorageOutOfBand(h, vs, int64(len(largeJson)))
tb.PutAdaptiveJsonFromOutline(0, storage)
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
result, ok, err := td.GetJsonAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
require.True(t, ok)
require.NotNil(t, result)
wrapper, ok := result.(*JsonAdaptiveStorage)
require.True(t, ok, "expected out-of-band JSON value to be a JsonAdaptiveStorage")
gotBytes, err := wrapper.GetBytes(ctx)
require.NoError(t, err)
assertJsonEqual(t, largeJson, gotBytes)
})
t.Run("large JSON promoted out-of-band by tuple builder", func(t *testing.T) {
// Two columns: one small (stays inline), one large (promoted out-of-band).
types := []Type{{Enc: JsonAdaptiveEnc}, {Enc: JsonAdaptiveEnc}}
vs := &TestValueStore{}
td := NewTupleDescriptor(types...)
tb := NewTupleBuilder(td, vs)
err := tb.PutAdaptiveJsonFromInline(ctx, 0, smallJson)
require.NoError(t, err)
err = tb.PutAdaptiveJsonFromInline(ctx, 1, largeJson)
require.NoError(t, err)
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
// Column 0 (small) should stay inline.
result0, ok, err := td.GetJsonAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
require.True(t, ok)
result0, ok = result0.([]byte)
require.True(t, ok, "small column should be stored inline")
assertJsonEqual(t, smallJson, result0.([]byte))
// Column 1 (large) should be out-of-band.
result1, ok, err := td.GetJsonAdaptiveValue(ctx, 1, vs, tup)
require.NoError(t, err)
require.True(t, ok)
result1, ok = result1.(*JsonAdaptiveStorage)
require.True(t, ok, "expected out-of-band JSON value to be a JsonAdaptiveStorage")
gotBytes1, err := result1.(*JsonAdaptiveStorage).GetBytes(ctx)
require.NoError(t, err)
assertJsonEqual(t, largeJson, gotBytes1)
})
t.Run("null JSON value", func(t *testing.T) {
types := []Type{{Enc: JsonAdaptiveEnc, Nullable: true}}
vs := &TestValueStore{}
td := NewTupleDescriptor(types...)
tb := NewTupleBuilder(td, vs)
// Don't write any value the field should be NULL.
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
result, ok, err := td.GetJsonAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
require.False(t, ok)
require.Nil(t, result)
})
t.Run("ToInterface deserializes correctly", func(t *testing.T) {
types := []Type{{Enc: JsonAdaptiveEnc}}
vs := &TestValueStore{}
td := NewTupleDescriptor(types...)
tb := NewTupleBuilder(td, vs)
err := tb.PutAdaptiveJsonFromInline(ctx, 0, largeJson)
require.NoError(t, err)
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
result, ok, err := td.GetJsonAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
require.True(t, ok)
jsonVal, ok := result.(*JsonAdaptiveStorage)
require.True(t, ok, "expected JSON value to be a JsonAdaptiveStorage")
iface, err := jsonVal.ToInterface(ctx)
require.NoError(t, err)
var expectedJson any
json.Unmarshal(largeJson, &expectedJson)
require.Equal(t, expectedJson, iface)
})
t.Run("out-of-band pass-through does not reload bytes", func(t *testing.T) {
// Write a large JSON value and get it back as a JsonStorage (out-of-band).
types := []Type{{Enc: JsonAdaptiveEnc}}
vs := &TestValueStore{}
td := NewTupleDescriptor(types...)
tb := NewTupleBuilder(td, vs)
err := tb.PutAdaptiveJsonFromInline(ctx, 0, largeJson)
require.NoError(t, err)
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
outOfBandResult, ok, err := td.GetJsonAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
require.True(t, ok)
_, ok = outOfBandResult.(*JsonAdaptiveStorage)
require.True(t, ok, "expected JSON value to be a JsonAdaptiveStorage")
// Put the out-of-band JsonStorage back into a new tuple (pass-through).
tb2 := NewTupleBuilder(td, vs)
tb2.PutAdaptiveJsonFromOutline(0, outOfBandResult.(*JsonAdaptiveStorage))
tup2, err := tb2.Build(context.Background(), testPool)
require.NoError(t, err)
// The value should still be readable after the pass-through.
result2, ok, err := td.GetJsonAdaptiveValue(ctx, 0, vs, tup2)
require.NoError(t, err)
require.True(t, ok)
retrieved, ok := result2.(*JsonAdaptiveStorage)
require.True(t, ok, "expected JSON value to be a JsonAdaptiveStorage")
gotBytes, err := retrieved.GetBytes(ctx)
require.NoError(t, err)
assertJsonEqual(t, largeJson, gotBytes)
})
}
func TestTupleBuilderAdaptiveEncodings(t *testing.T) {
ctx := sql.NewEmptyContext()
{
types := []Type{
{Enc: BytesAdaptiveEnc},
}
vs := &TestValueStore{}
td := NewTupleDescriptor(types...)
tb := NewTupleBuilder(td, vs)
t.Run("round trip inlined value", func(t *testing.T) {
shortByteArray := make([]byte, DefaultTupleLengthTarget/2)
err := tb.PutAdaptiveBytesFromInline(ctx, 0, shortByteArray)
require.NoError(t, err)
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
adaptiveEncodingBytes, _, err := td.GetBytesAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
require.Equal(t, shortByteArray, adaptiveEncodingBytes)
})
t.Run("round trip out-of-band value", func(t *testing.T) {
longByteArray := make([]byte, DefaultTupleLengthTarget*2)
h, err := vs.WriteBytes(ctx, longByteArray)
require.NoError(t, err)
byteArray := NewByteArray(h, vs).WithMaxByteLength(int64(len(longByteArray)))
tb.PutAdaptiveBytesFromOutline(0, byteArray)
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
adaptiveEncodingBytes, _, err := td.GetBytesAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
adaptiveEncodingByteArray := adaptiveEncodingBytes.(*ByteArray)
outBytes, err := adaptiveEncodingByteArray.ToBytes(ctx)
require.NoError(t, err)
require.Equal(t, longByteArray, outBytes)
})
}
{
types := []Type{
{Enc: BytesAdaptiveEnc},
{Enc: BytesAdaptiveEnc},
}
vs := &TestValueStore{}
td := NewTupleDescriptor(types...)
tb := NewTupleBuilder(td, vs)
t.Run("inline larger of two columns", func(t *testing.T) {
// In this test, only one of two equally sized columns needs to be stored out of band.
// Only the first column should be stored out-of-band.
columnSize := DefaultTupleLengthTarget / 2
mediumByteArray := make([]byte, columnSize)
err := tb.PutAdaptiveBytesFromInline(ctx, 0, mediumByteArray)
require.NoError(t, err)
err = tb.PutAdaptiveBytesFromInline(ctx, 1, mediumByteArray)
require.NoError(t, err)
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
{
// Check that first column is stored out-of-band
adaptiveEncodingBytes, _, err := td.GetBytesAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
adaptiveEncodingByteArray := adaptiveEncodingBytes.(*ByteArray)
outBytes, err := adaptiveEncodingByteArray.ToBytes(ctx)
require.NoError(t, err)
require.Equal(t, mediumByteArray, outBytes)
}
{
// Check that second column is stored inline
adaptiveEncodingBytes, _, err := td.GetBytesAdaptiveValue(ctx, 1, vs, tup)
require.NoError(t, err)
adaptiveEncodingByteArray := adaptiveEncodingBytes.([]byte)
require.Equal(t, mediumByteArray, adaptiveEncodingByteArray)
}
})
t.Run("largest value moved out-of-band first", func(t *testing.T) {
// Column 0: small value (1/4 of the target)
// Column 1: large value (the full target)
// Combined inline size exceeds the target, but only moving column 1
// out-of-band is sufficient to fit.
smallByteArray := make([]byte, DefaultTupleLengthTarget/4)
largeByteArray := make([]byte, DefaultTupleLengthTarget)
err := tb.PutAdaptiveBytesFromInline(ctx, 0, smallByteArray)
require.NoError(t, err)
err = tb.PutAdaptiveBytesFromInline(ctx, 1, largeByteArray)
require.NoError(t, err)
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
{
// Column 0 (small) should be stored inline
adaptiveEncodingBytes, _, err := td.GetBytesAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
adaptiveEncodingByteArray := adaptiveEncodingBytes.([]byte)
require.Equal(t, smallByteArray, adaptiveEncodingByteArray)
}
{
// Column 1 (large) should be stored out-of-band
adaptiveEncodingBytes, _, err := td.GetBytesAdaptiveValue(ctx, 1, vs, tup)
require.NoError(t, err)
adaptiveEncodingByteArray := adaptiveEncodingBytes.(*ByteArray)
outBytes, err := adaptiveEncodingByteArray.ToBytes(ctx)
require.NoError(t, err)
require.Equal(t, largeByteArray, outBytes)
}
})
}
{
// Test with three columns of varying sizes: the builder should move the
// largest values out-of-band first and keep the smallest inline.
types := []Type{
{Enc: BytesAdaptiveEnc},
{Enc: BytesAdaptiveEnc},
{Enc: BytesAdaptiveEnc},
}
vs := &TestValueStore{}
td := NewTupleDescriptor(types...)
tb := NewTupleBuilder(td, vs)
t.Run("three columns largest first ordering", func(t *testing.T) {
// Column 0: medium value (1/3 of target)
// Column 1: small value (1/4 of target)
// Column 2: large value (3/4 of target)
// Combined they exceed the target. Moving only column 2 (largest) out-of-band
// should be enough. Column 0 and 1 should remain inline.
smallByteArray := make([]byte, DefaultTupleLengthTarget/4)
mediumByteArray := make([]byte, DefaultTupleLengthTarget/3)
largeByteArray := make([]byte, DefaultTupleLengthTarget*3/4)
err := tb.PutAdaptiveBytesFromInline(ctx, 0, mediumByteArray)
require.NoError(t, err)
err = tb.PutAdaptiveBytesFromInline(ctx, 1, smallByteArray)
require.NoError(t, err)
err = tb.PutAdaptiveBytesFromInline(ctx, 2, largeByteArray)
require.NoError(t, err)
tup, err := tb.Build(context.Background(), testPool)
require.NoError(t, err)
{
// Column 0 (medium) should be inline
adaptiveEncodingBytes, _, err := td.GetBytesAdaptiveValue(ctx, 0, vs, tup)
require.NoError(t, err)
_, ok := adaptiveEncodingBytes.([]byte)
require.True(t, ok, "column 0 should be stored inline")
}
{
// Column 1 (small) should be inline
adaptiveEncodingBytes, _, err := td.GetBytesAdaptiveValue(ctx, 1, vs, tup)
require.NoError(t, err)
_, ok := adaptiveEncodingBytes.([]byte)
require.True(t, ok, "column 1 should be stored inline")
}
{
// Column 2 (large) should be out-of-band
adaptiveEncodingBytes, _, err := td.GetBytesAdaptiveValue(ctx, 2, vs, tup)
require.NoError(t, err)
adaptiveEncodingByteArray := adaptiveEncodingBytes.(*ByteArray)
outBytes, err := adaptiveEncodingByteArray.ToBytes(ctx)
require.NoError(t, err)
require.Equal(t, largeByteArray, outBytes)
}
})
}
}