Skip to content

Commit 99e5511

Browse files
Merge branch 'main' into feat/issues/1579
2 parents c89034d + a349f9c commit 99e5511

19 files changed

+530
-1538
lines changed

chcol.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ func NewJSON() *JSON {
6565

6666
// ExtractJSONPathAs is a convenience function for asserting a path to a specific type.
6767
// The underlying value is also extracted from its Dynamic wrapper if present.
68+
// T cannot be a Dynamic, if you want a Dynamic simply use ExtractJSONPathAsDynamic.
6869
func ExtractJSONPathAs[T any](o *JSON, path string) (valueAs T, ok bool) {
6970
return chcol.ExtractJSONPathAs[T](o, path)
7071
}
72+
73+
// ExtractJSONPathAsDynamic is a convenience function for asserting a path to a Dynamic.
74+
// If the value is not a Dynamic, the value is wrapped in an untyped Dynamic with false returned.
75+
func ExtractJSONPathAsDynamic(o *JSON, path string) (Dynamic, bool) {
76+
return chcol.ExtractJSONPathAsDynamic(o, path)
77+
}

examples/clickhouse_api/json_fast_structs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (p *FastProduct) SerializeClickHouseJSON() (*clickhouse.JSON, error) {
5757

5858
// DeserializeClickHouseJSON implements clickhouse.JSONDeserializer for faster struct scanning
5959
func (p *FastProduct) DeserializeClickHouseJSON(obj *clickhouse.JSON) error {
60-
p.ID, _ = clickhouse.ExtractJSONPathAs[clickhouse.Dynamic](obj, "id")
60+
p.ID, _ = clickhouse.ExtractJSONPathAsDynamic(obj, "id")
6161
p.Name, _ = clickhouse.ExtractJSONPathAs[string](obj, "name")
6262
p.Tags, _ = clickhouse.ExtractJSONPathAs[[]string](obj, "tags")
6363
p.Pricing.Price, _ = clickhouse.ExtractJSONPathAs[int64](obj, "pricing.price")

lib/chcol/json.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type JSONDeserializer interface {
4040

4141
// ExtractJSONPathAs is a convenience function for asserting a path to a specific type.
4242
// The underlying value is also extracted from its Dynamic wrapper if present.
43+
// T cannot be a Dynamic, if you want a Dynamic simply use ExtractJSONPathAsDynamic.
4344
func ExtractJSONPathAs[T any](o *JSON, path string) (T, bool) {
4445
value, ok := o.valuesByPath[path]
4546
if !ok || value == nil {
@@ -57,6 +58,21 @@ func ExtractJSONPathAs[T any](o *JSON, path string) (T, bool) {
5758
return valueAs, ok
5859
}
5960

61+
// ExtractJSONPathAsDynamic is a convenience function for asserting a path to a Dynamic.
62+
// If the value is not a Dynamic, the value is wrapped in an untyped Dynamic with false returned.
63+
func ExtractJSONPathAsDynamic(o *JSON, path string) (Dynamic, bool) {
64+
value, ok := o.valuesByPath[path]
65+
if !ok || value == nil {
66+
return Dynamic{}, false
67+
}
68+
69+
if dynValue, ok := value.(Dynamic); ok {
70+
return dynValue, true
71+
}
72+
73+
return Dynamic{value: value}, false
74+
}
75+
6076
// JSON represents a ClickHouse JSON type that can hold multiple possible types
6177
type JSON struct {
6278
valuesByPath map[string]any

lib/column/codegen/column.tpl

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,9 @@ func (t Type) Column(name string, sc *ServerContext) (Interface, error) {
136136
case strings.HasPrefix(string(t), "Variant("):
137137
return (&Variant{name: name}).parse(t, sc)
138138
case strings.HasPrefix(string(t), "Dynamic"):
139-
if sc.VersionMajor >= 25 && sc.VersionMinor >= 6 {
140-
return (&Dynamic{name: name}).parse(t, sc)
141-
} else {
142-
return (&Dynamic_v1{name: name}).parse(t, sc)
143-
}
139+
return (&Dynamic{name: name}).parse(t, sc)
144140
case strings.HasPrefix(string(t), "JSON"):
145-
if sc.VersionMajor >= 25 && sc.VersionMinor >= 6 {
146-
return (&JSON{name: name}).parse(t, sc)
147-
} else {
148-
return (&JSON_v1{name: name}).parse(t, sc)
149-
}
141+
return (&JSON{name: name}).parse(t, sc)
150142
case strings.HasPrefix(string(t), "Decimal("):
151143
return (&Decimal{name: name}).parse(t)
152144
case strings.HasPrefix(strType, "Nested("):

lib/column/column_gen.go

Lines changed: 2 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/column/dynamic.go

Lines changed: 109 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,56 @@ package column
2020
import (
2121
"database/sql/driver"
2222
"fmt"
23-
"github.com/ClickHouse/ch-go/proto"
24-
"github.com/ClickHouse/clickhouse-go/v2/lib/chcol"
2523
"math"
2624
"reflect"
25+
"strconv"
2726
"strings"
27+
28+
"github.com/ClickHouse/ch-go/proto"
29+
"github.com/ClickHouse/clickhouse-go/v2/lib/chcol"
2830
)
2931

30-
const SupportedDynamicSerializationVersion = 3
31-
const DefaultMaxDynamicTypes = 32
32+
const DynamicSerializationVersion = 3
33+
const DynamicDeprecatedSerializationVersion = 1
3234
const DynamicNullDiscriminator = -1 // The Null index changes as data is being built, use -1 as placeholder for writes.
35+
const DefaultMaxDynamicTypes = 32
36+
37+
func supportsFlatDynamicJSON(sc *ServerContext) bool {
38+
return sc.VersionMajor >= 25 && sc.VersionMinor >= 6
39+
}
3340

3441
type Dynamic struct {
3542
chType Type
3643
sc *ServerContext
3744
name string
3845

46+
serializationVersion uint64
47+
3948
totalTypes int // Null is last type index + 1, so this doubles as the Null type index for reads.
4049
discriminators []int
4150
offsets []int
4251

4352
columns []Interface
44-
columnIndexByName map[string]int
53+
columnIndexByType map[string]int
54+
55+
deprecated deprecatedDynamic
4556
}
4657

4758
func (c *Dynamic) parse(t Type, sc *ServerContext) (_ *Dynamic, err error) {
4859
c.chType = t
4960
c.sc = sc
5061
tStr := string(t)
5162

52-
c.columnIndexByName = make(map[string]int)
63+
c.columnIndexByType = make(map[string]int)
64+
65+
if !supportsFlatDynamicJSON(sc) {
66+
// SharedVariant is special, and does not count against totalTypes
67+
sv, _ := Type("SharedVariant").Column("", sc)
68+
c.addColumn(sv)
69+
70+
c.deprecated.maxTypes = DefaultMaxDynamicTypes
71+
c.totalTypes = 0 // Reset to 0 after adding SharedVariant
72+
}
5373

5474
if tStr == "Dynamic" {
5575
return c, nil
@@ -59,13 +79,28 @@ func (c *Dynamic) parse(t Type, sc *ServerContext) (_ *Dynamic, err error) {
5979
return nil, &UnsupportedColumnTypeError{t: t}
6080
}
6181

82+
if !supportsFlatDynamicJSON(sc) {
83+
typeParamsStr := strings.TrimPrefix(tStr, "Dynamic(")
84+
typeParamsStr = strings.TrimSuffix(typeParamsStr, ")")
85+
86+
if strings.HasPrefix(typeParamsStr, "max_types=") {
87+
v := strings.TrimPrefix(typeParamsStr, "max_types=")
88+
if maxTypes, err := strconv.Atoi(v); err == nil {
89+
c.deprecated.maxTypes = maxTypes
90+
}
91+
}
92+
}
93+
6294
return c, nil
6395
}
6496

6597
func (c *Dynamic) addColumn(col Interface) int {
66-
colIndex := len(c.columns)
98+
typeName := string(col.Type())
99+
c.deprecated.typeNames = append(c.deprecated.typeNames, typeName)
100+
101+
colIndex := len(c.deprecated.typeNames) - 1
67102
c.columns = append(c.columns, col)
68-
c.columnIndexByName[string(col.Type())] = colIndex
103+
c.columnIndexByType[typeName] = colIndex
69104
c.totalTypes++
70105

71106
return colIndex
@@ -88,9 +123,16 @@ func (c *Dynamic) Row(i int, ptr bool) any {
88123
offsetIndex := c.offsets[i]
89124
var value any
90125
var chType string
91-
if typeIndex != c.totalTypes {
92-
value = c.columns[typeIndex].Row(offsetIndex, ptr)
93-
chType = string(c.columns[typeIndex].Type())
126+
if c.serializationVersion == DynamicDeprecatedSerializationVersion {
127+
if typeIndex != DynamicNullDiscriminator {
128+
value = c.columns[typeIndex].Row(offsetIndex, ptr)
129+
chType = string(c.columns[typeIndex].Type())
130+
}
131+
} else {
132+
if typeIndex != c.totalTypes {
133+
value = c.columns[typeIndex].Row(offsetIndex, ptr)
134+
chType = string(c.columns[typeIndex].Type())
135+
}
94136
}
95137

96138
dyn := chcol.NewDynamicWithType(value, chType)
@@ -106,9 +148,16 @@ func (c *Dynamic) ScanRow(dest any, row int) error {
106148
offsetIndex := c.offsets[row]
107149
var value any
108150
var chType string
109-
if typeIndex != c.totalTypes {
110-
value = c.columns[typeIndex].Row(offsetIndex, false)
111-
chType = string(c.columns[typeIndex].Type())
151+
if c.serializationVersion == DynamicDeprecatedSerializationVersion {
152+
if typeIndex != DynamicNullDiscriminator {
153+
value = c.columns[typeIndex].Row(offsetIndex, false)
154+
chType = string(c.columns[typeIndex].Type())
155+
}
156+
} else {
157+
if typeIndex != c.totalTypes {
158+
value = c.columns[typeIndex].Row(offsetIndex, false)
159+
chType = string(c.columns[typeIndex].Type())
160+
}
112161
}
113162

114163
switch v := dest.(type) {
@@ -119,8 +168,14 @@ func (c *Dynamic) ScanRow(dest any, row int) error {
119168
dyn := chcol.NewDynamicWithType(value, chType)
120169
**v = dyn
121170
default:
122-
if typeIndex == c.totalTypes {
123-
return nil
171+
if c.serializationVersion == DynamicDeprecatedSerializationVersion {
172+
if typeIndex == DynamicNullDiscriminator {
173+
return nil
174+
}
175+
} else {
176+
if typeIndex == c.totalTypes {
177+
return nil
178+
}
124179
}
125180

126181
if err := c.columns[typeIndex].ScanRow(dest, offsetIndex); err != nil {
@@ -206,7 +261,7 @@ func (c *Dynamic) AppendRow(v any) error {
206261

207262
if requestedType != "" {
208263
var col Interface
209-
colIndex, ok := c.columnIndexByName[requestedType]
264+
colIndex, ok := c.columnIndexByType[requestedType]
210265
if ok {
211266
col = c.columns[colIndex]
212267
} else {
@@ -229,6 +284,11 @@ func (c *Dynamic) AppendRow(v any) error {
229284

230285
// If preferred type wasn't provided, try each column
231286
for i, col := range c.columns {
287+
if c.deprecated.typeNames[i] == "SharedVariant" {
288+
// Do not try to fit into SharedVariant
289+
continue
290+
}
291+
232292
if err := col.AppendRow(v); err == nil {
233293
c.appendDiscriminatorRow(i)
234294
return nil
@@ -245,7 +305,7 @@ func (c *Dynamic) AppendRow(v any) error {
245305
}
246306

247307
func (c *Dynamic) encodeHeader(buffer *proto.Buffer) error {
248-
buffer.PutUInt64(SupportedDynamicSerializationVersion)
308+
buffer.PutUInt64(DynamicSerializationVersion)
249309
buffer.PutUVarInt(uint64(c.totalTypes))
250310

251311
for _, col := range c.columns {
@@ -292,11 +352,20 @@ func (c *Dynamic) encodeData(buffer *proto.Buffer) {
292352
}
293353

294354
func (c *Dynamic) WriteStatePrefix(buffer *proto.Buffer) error {
295-
return c.encodeHeader(buffer)
355+
if supportsFlatDynamicJSON(c.sc) {
356+
return c.encodeHeader(buffer)
357+
}
358+
359+
return c.encodeHeader_v1(buffer)
296360
}
297361

298362
func (c *Dynamic) Encode(buffer *proto.Buffer) {
299-
c.encodeData(buffer)
363+
if supportsFlatDynamicJSON(c.sc) {
364+
c.encodeData(buffer)
365+
return
366+
}
367+
368+
c.encodeData_v1(buffer)
300369
}
301370

302371
func (c *Dynamic) ScanType() reflect.Type {
@@ -312,24 +381,13 @@ func (c *Dynamic) Reset() {
312381
}
313382

314383
func (c *Dynamic) decodeHeader(reader *proto.Reader) error {
315-
dynamicSerializationVersion, err := reader.UInt64()
316-
if err != nil {
317-
return fmt.Errorf("failed to read dynamic serialization version: %w", err)
318-
}
319-
320-
if dynamicSerializationVersion == DeprecatedDynamicSerializationVersion {
321-
return fmt.Errorf("deprecated dynamic serialization version: %d, enable \"output_format_native_use_flattened_dynamic_and_json_serialization\" in your settings", dynamicSerializationVersion)
322-
} else if dynamicSerializationVersion != SupportedDynamicSerializationVersion {
323-
return fmt.Errorf("unsupported dynamic serialization version: %d", dynamicSerializationVersion)
324-
}
325-
326384
totalTypes, err := reader.UVarInt()
327385
if err != nil {
328386
return fmt.Errorf("failed to read total types for dynamic column: %w", err)
329387
}
330388

331389
c.columns = make([]Interface, 0, totalTypes)
332-
c.columnIndexByName = make(map[string]int, totalTypes)
390+
c.columnIndexByType = make(map[string]int, totalTypes)
333391
for i := uint64(0); i < totalTypes; i++ {
334392
typeName, err := reader.Str()
335393
if err != nil {
@@ -383,7 +441,7 @@ func discriminatorReader(totalTypes int, reader *proto.Reader) func() (int, erro
383441
func (c *Dynamic) decodeData(reader *proto.Reader, rows int) error {
384442
c.discriminators = make([]int, rows)
385443
c.offsets = make([]int, rows)
386-
rowCountByType := make([]int, len(c.columns))
444+
rowCountByType := make([]int, c.totalTypes)
387445

388446
readDiscriminator := discriminatorReader(c.totalTypes, reader)
389447
for i := 0; i < rows; i++ {
@@ -410,19 +468,29 @@ func (c *Dynamic) decodeData(reader *proto.Reader, rows int) error {
410468
}
411469

412470
func (c *Dynamic) ReadStatePrefix(reader *proto.Reader) error {
413-
err := c.decodeHeader(reader)
471+
dynamicSerializationVersion, err := reader.UInt64()
414472
if err != nil {
415-
return fmt.Errorf("failed to decode dynamic header: %w", err)
473+
return fmt.Errorf("failed to read dynamic serialization version: %w", err)
416474
}
475+
c.serializationVersion = dynamicSerializationVersion
417476

418-
return nil
477+
switch c.serializationVersion {
478+
case DynamicSerializationVersion:
479+
return c.decodeHeader(reader)
480+
case DynamicDeprecatedSerializationVersion:
481+
return c.decodeHeader_v1(reader)
482+
default:
483+
return fmt.Errorf("unsupported dynamic serialization version: %d", dynamicSerializationVersion)
484+
}
419485
}
420486

421487
func (c *Dynamic) Decode(reader *proto.Reader, rows int) error {
422-
err := c.decodeData(reader, rows)
423-
if err != nil {
424-
return fmt.Errorf("failed to decode dynamic data: %w", err)
488+
switch c.serializationVersion {
489+
case DynamicSerializationVersion:
490+
return c.decodeData(reader, rows)
491+
case DynamicDeprecatedSerializationVersion:
492+
return c.decodeData_v1(reader, rows)
493+
default:
494+
return fmt.Errorf("unsupported dynamic serialization version: %d", c.serializationVersion)
425495
}
426-
427-
return nil
428496
}

0 commit comments

Comments
 (0)