Skip to content

Commit 455eff4

Browse files
committed
dynamically change discriminator precision
1 parent f994fff commit 455eff4

File tree

3 files changed

+85
-6
lines changed

3 files changed

+85
-6
lines changed

lib/column/dynamic.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package column
2020
import (
2121
"database/sql/driver"
2222
"fmt"
23+
"math"
2324
"reflect"
2425
"strings"
2526
"time"
@@ -265,14 +266,27 @@ func (c *Dynamic) encodeHeader(buffer *proto.Buffer) error {
265266
return nil
266267
}
267268

269+
func discriminatorWriter(totalTypes int, buffer *proto.Buffer) func(int) {
270+
switch {
271+
case totalTypes <= math.MaxUint8:
272+
return func(d int) { buffer.PutUInt8(uint8(d)) }
273+
case totalTypes <= math.MaxUint16:
274+
return func(d int) { buffer.PutUInt16(uint16(d)) }
275+
case totalTypes <= math.MaxUint32:
276+
return func(d int) { buffer.PutUInt32(uint32(d)) }
277+
default:
278+
return func(d int) { buffer.PutUInt64(uint64(d)) }
279+
}
280+
}
281+
268282
func (c *Dynamic) encodeData(buffer *proto.Buffer) {
269-
// TODO: dynamically switch types based on size requirements
283+
writeDiscriminator := discriminatorWriter(c.totalTypes, buffer)
270284
for _, typeIndex := range c.discriminators {
271285
if typeIndex == DynamicNullDiscriminator {
272286
typeIndex = c.totalTypes
273287
}
274288

275-
buffer.PutByte(uint8(typeIndex))
289+
writeDiscriminator(typeIndex)
276290
}
277291

278292
for _, col := range c.columns {
@@ -344,18 +358,42 @@ func (c *Dynamic) decodeHeader(reader *proto.Reader) error {
344358
return nil
345359
}
346360

361+
func discriminatorReader(totalTypes int, reader *proto.Reader) func() (int, error) {
362+
switch {
363+
case totalTypes <= math.MaxUint8:
364+
return func() (int, error) {
365+
v, err := reader.UInt8()
366+
return int(v), err
367+
}
368+
case totalTypes <= math.MaxUint16:
369+
return func() (int, error) {
370+
v, err := reader.UInt16()
371+
return int(v), err
372+
}
373+
case totalTypes <= math.MaxUint32:
374+
return func() (int, error) {
375+
v, err := reader.UInt32()
376+
return int(v), err
377+
}
378+
default:
379+
return func() (int, error) {
380+
v, err := reader.UInt64()
381+
return int(v), err
382+
}
383+
}
384+
}
385+
347386
func (c *Dynamic) decodeData(reader *proto.Reader, rows int) error {
348387
c.discriminators = make([]int, rows)
349388
c.offsets = make([]int, rows)
350389
rowCountByType := make([]int, len(c.columns))
351390

391+
readDiscriminator := discriminatorReader(c.totalTypes, reader)
352392
for i := 0; i < rows; i++ {
353-
// TODO: dynamically switch types based on size requirements
354-
discByte, err := reader.ReadByte()
393+
disc, err := readDiscriminator()
355394
if err != nil {
356395
return fmt.Errorf("failed to read discriminator at index %d: %w", i, err)
357396
}
358-
disc := int(discByte)
359397

360398
c.discriminators[i] = disc
361399
if disc != c.totalTypes {

tests/dynamic_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package tests
1919

2020
import (
2121
"context"
22+
"fmt"
2223
"testing"
2324
"time"
2425

@@ -202,6 +203,46 @@ func TestDynamicMaxTypes(t *testing.T) {
202203
})
203204
}
204205

206+
// Discriminator precision must grow dynamically depending on the number of types within the Dynamic.
207+
// This test confirms that we can go beyond UInt8/255 types.
208+
func TestDynamicExceededTypes(t *testing.T) {
209+
conn := setupDynamicTest(t, clickhouse.Native)
210+
ctx := context.Background()
211+
212+
const ddl = `
213+
CREATE TABLE IF NOT EXISTS test_dynamic_exceeded_types (
214+
c Dynamic
215+
) Engine = MergeTree() ORDER BY tuple()
216+
`
217+
require.NoError(t, conn.Exec(ctx, ddl))
218+
defer func() {
219+
require.NoError(t, conn.Exec(ctx, "DROP TABLE IF EXISTS test_dynamic_exceeded_types"))
220+
}()
221+
222+
testTypeCount := func(typeCount int) func(t *testing.T) {
223+
return func(t *testing.T) {
224+
batch, err := conn.PrepareBatch(ctx, "INSERT INTO test_dynamic_exceeded_types (c)")
225+
require.NoError(t, err)
226+
227+
for i := 0; i < typeCount; i++ {
228+
typeName := fmt.Sprintf("Tuple(\"%d\" Int64)", i)
229+
require.NoError(t, batch.Append(clickhouse.NewDynamicWithType([]int64{int64(i)}, typeName)))
230+
}
231+
require.NoError(t, batch.Send())
232+
233+
rows, err := conn.Query(ctx, "SELECT c FROM test_dynamic_exceeded_types")
234+
require.NoError(t, err)
235+
236+
require.NoError(t, rows.Close())
237+
require.NoError(t, rows.Err())
238+
}
239+
}
240+
241+
t.Run("less than UInt8", testTypeCount(16))
242+
t.Run("UInt8 bounds", testTypeCount(255))
243+
t.Run("UInt16 range", testTypeCount(300))
244+
}
245+
205246
func TestDynamicArray(t *testing.T) {
206247
TestProtocols(t, func(t *testing.T, protocol clickhouse.Protocol) {
207248
conn := setupDynamicTest(t, protocol)

tests/std/dynamic_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func setupDynamicTest(t *testing.T) *sql.DB {
4040
})
4141
require.NoError(t, err)
4242

43-
if !CheckMinServerVersion(conn, 24, 8, 0) {
43+
if !CheckMinServerVersion(conn, 25, 6, 0) {
4444
t.Skip(fmt.Errorf("unsupported clickhouse version for Dynamic type"))
4545
return nil
4646
}

0 commit comments

Comments
 (0)