660 lines
19 KiB
Go
660 lines
19 KiB
Go
// 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)
|
||
}
|
||
})
|
||
}
|
||
}
|