mirror of
https://github.com/hanzo-ds/go.git
synced 2026-10-30 19:45:01 +00:00
Source: the unexported clickhouse struct becomes client, the ch receiver and chproto import alias follow, error message prefixes read datastore, and the JSON serializer interface drops the vendor infix. resources' metadata key and its accessor are renamed together, with a test proving the embedded YAML still unmarshals — a silent drift there would have made every connection take the unsupported-server-version branch. Codegen: all three templates still emitted the upstream module paths, so regenerating would have quietly undone the rename. Note the templates have drifted from the files they produce (regenerating today drops QBit and scanTypeDuration), so the generated files are left as tracked and only the templates are corrected. Dead links: an earlier mechanical pass invented URLs that do not resolve — github.com/ClickHouse/datastore-go, hanzo-ds/driver issue numbers that belong to upstream, and mangled names of independent projects (mailru, uptrace, nikepan, EpicStep). Third-party names are not ours to rewrite, so those directories are removed rather than repaired, and provenance comments now point at real files. Dev infra: compose runs our own server image at the tag production runs, since the vendor's alpine tag does not exist in our registry. Cluster hostnames, nginx upstreams and server configs are renamed as one set. CHANGELOG.md is upstream's release history for versions this module never released; rewriting it would fabricate a lineage, so it is dropped and the upstream base recorded in README instead. The two long-standing vet findings are fixed by keying the struct literals rather than suppressed. LICENSE and contributors/list keep the vendor name on purpose: one is the copyright notice Apache-2.0 requires, the other is real people's names. CI now enforces build, vet, test, gofmt and a grep gate.
480 lines
12 KiB
Go
480 lines
12 KiB
Go
package column
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hanzo-ds/go/lib/col"
|
|
"github.com/hanzo-ds/native/proto"
|
|
)
|
|
|
|
const DynamicSerializationVersion = 3
|
|
const DynamicDeprecatedSerializationVersion = 1
|
|
const DynamicNullDiscriminator = -1 // The Null index changes as data is being built, use -1 as placeholder for writes.
|
|
const DefaultMaxDynamicTypes = 32
|
|
|
|
func supportsFlatDynamicJSON(sc *ServerContext) bool {
|
|
// Any CH version more than 25.6
|
|
return sc.VersionMajor > 25 || (sc.VersionMajor == 25 && sc.VersionMinor >= 6)
|
|
}
|
|
|
|
type Dynamic struct {
|
|
chType Type
|
|
sc *ServerContext
|
|
name string
|
|
|
|
serializationVersion uint64
|
|
|
|
totalTypes int // Null is last type index + 1, so this doubles as the Null type index for reads.
|
|
discriminators []int
|
|
offsets []int
|
|
|
|
columns []Interface
|
|
columnIndexByType map[string]int
|
|
|
|
deprecated deprecatedDynamic
|
|
}
|
|
|
|
func (c *Dynamic) parse(t Type, sc *ServerContext) (_ *Dynamic, err error) {
|
|
c.chType = t
|
|
c.sc = sc
|
|
tStr := string(t)
|
|
|
|
c.columnIndexByType = make(map[string]int)
|
|
|
|
if !supportsFlatDynamicJSON(sc) {
|
|
// SharedVariant is special, and does not count against totalTypes
|
|
sv, _ := Type("SharedVariant").Column("", sc)
|
|
c.addColumn(sv)
|
|
|
|
c.deprecated.maxTypes = DefaultMaxDynamicTypes
|
|
c.totalTypes = 0 // Reset to 0 after adding SharedVariant
|
|
}
|
|
|
|
if tStr == "Dynamic" {
|
|
return c, nil
|
|
}
|
|
|
|
if !strings.HasPrefix(tStr, "Dynamic(") || !strings.HasSuffix(tStr, ")") {
|
|
return nil, &UnsupportedColumnTypeError{t: t}
|
|
}
|
|
|
|
if !supportsFlatDynamicJSON(sc) {
|
|
typeParamsStr := strings.TrimPrefix(tStr, "Dynamic(")
|
|
typeParamsStr = strings.TrimSuffix(typeParamsStr, ")")
|
|
|
|
if strings.HasPrefix(typeParamsStr, "max_types=") {
|
|
v := strings.TrimPrefix(typeParamsStr, "max_types=")
|
|
if maxTypes, err := strconv.Atoi(v); err == nil {
|
|
c.deprecated.maxTypes = maxTypes
|
|
}
|
|
}
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func (c *Dynamic) addColumn(col Interface) int {
|
|
typeName := string(col.Type())
|
|
c.deprecated.typeNames = append(c.deprecated.typeNames, typeName)
|
|
|
|
colIndex := len(c.deprecated.typeNames) - 1
|
|
c.columns = append(c.columns, col)
|
|
c.columnIndexByType[typeName] = colIndex
|
|
c.totalTypes++
|
|
|
|
return colIndex
|
|
}
|
|
|
|
func (c *Dynamic) Name() string {
|
|
return c.name
|
|
}
|
|
|
|
func (c *Dynamic) Type() Type {
|
|
return c.chType
|
|
}
|
|
|
|
func (c *Dynamic) Rows() int {
|
|
return len(c.discriminators)
|
|
}
|
|
|
|
func (c *Dynamic) Row(i int, ptr bool) any {
|
|
typeIndex := c.discriminators[i]
|
|
offsetIndex := c.offsets[i]
|
|
var value any
|
|
var chType string
|
|
if c.serializationVersion == DynamicDeprecatedSerializationVersion {
|
|
if typeIndex != DynamicNullDiscriminator {
|
|
value = c.columns[typeIndex].Row(offsetIndex, ptr)
|
|
chType = string(c.columns[typeIndex].Type())
|
|
}
|
|
} else {
|
|
if typeIndex != c.totalTypes {
|
|
value = c.columns[typeIndex].Row(offsetIndex, ptr)
|
|
chType = string(c.columns[typeIndex].Type())
|
|
}
|
|
}
|
|
|
|
dyn := col.NewDynamicWithType(value, chType)
|
|
if ptr {
|
|
return &dyn
|
|
}
|
|
|
|
return dyn
|
|
}
|
|
|
|
func (c *Dynamic) ScanRow(dest any, row int) error {
|
|
typeIndex := c.discriminators[row]
|
|
offsetIndex := c.offsets[row]
|
|
var value any
|
|
var chType string
|
|
if c.serializationVersion == DynamicDeprecatedSerializationVersion {
|
|
if typeIndex != DynamicNullDiscriminator {
|
|
value = c.columns[typeIndex].Row(offsetIndex, false)
|
|
chType = string(c.columns[typeIndex].Type())
|
|
}
|
|
} else {
|
|
if typeIndex != c.totalTypes {
|
|
value = c.columns[typeIndex].Row(offsetIndex, false)
|
|
chType = string(c.columns[typeIndex].Type())
|
|
}
|
|
}
|
|
|
|
switch v := dest.(type) {
|
|
case *col.Dynamic:
|
|
dyn := col.NewDynamicWithType(value, chType)
|
|
*v = dyn
|
|
case **col.Dynamic:
|
|
dyn := col.NewDynamicWithType(value, chType)
|
|
**v = dyn
|
|
default:
|
|
if c.serializationVersion == DynamicDeprecatedSerializationVersion {
|
|
if typeIndex == DynamicNullDiscriminator {
|
|
return nil
|
|
}
|
|
} else {
|
|
if typeIndex == c.totalTypes {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if err := c.columns[typeIndex].ScanRow(dest, offsetIndex); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Dynamic) appendDiscriminatorRow(d int) {
|
|
c.discriminators = append(c.discriminators, d)
|
|
}
|
|
|
|
func (c *Dynamic) appendNullRow() {
|
|
c.appendDiscriminatorRow(DynamicNullDiscriminator)
|
|
}
|
|
|
|
func (c *Dynamic) Append(v any) (nulls []uint8, err error) {
|
|
switch vv := v.(type) {
|
|
case []col.Dynamic:
|
|
for i, dyn := range vv {
|
|
err := c.AppendRow(dyn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to AppendRow at index %d: %w", i, err)
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
case []*col.Dynamic:
|
|
for i, dyn := range vv {
|
|
err := c.AppendRow(dyn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to AppendRow at index %d: %w", i, err)
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
default:
|
|
if valuer, ok := v.(driver.Valuer); ok {
|
|
val, err := valuer.Value()
|
|
if err != nil {
|
|
return nil, &ColumnConverterError{
|
|
Op: "Append",
|
|
To: string(c.chType),
|
|
From: fmt.Sprintf("%T", v),
|
|
Hint: "could not get driver.Valuer value",
|
|
}
|
|
}
|
|
|
|
return c.Append(val)
|
|
}
|
|
|
|
return nil, &ColumnConverterError{
|
|
Op: "Append",
|
|
To: string(c.chType),
|
|
From: fmt.Sprintf("%T", v),
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Dynamic) AppendRow(v any) error {
|
|
var requestedType string
|
|
switch vv := v.(type) {
|
|
case nil:
|
|
c.appendNullRow()
|
|
return nil
|
|
case col.Dynamic:
|
|
requestedType = vv.Type()
|
|
v = vv.Any()
|
|
if vv.Nil() {
|
|
c.appendNullRow()
|
|
return nil
|
|
}
|
|
case *col.Dynamic:
|
|
requestedType = vv.Type()
|
|
v = vv.Any()
|
|
if vv.Nil() {
|
|
c.appendNullRow()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if requestedType != "" {
|
|
var col Interface
|
|
colIndex, ok := c.columnIndexByType[requestedType]
|
|
if ok {
|
|
col = c.columns[colIndex]
|
|
} else {
|
|
newCol, err := Type(requestedType).Column("", c.sc)
|
|
if err != nil {
|
|
return fmt.Errorf("value \"%v\" cannot be stored in dynamic column %s with requested type %s: unable to append type: %w", v, c.chType, requestedType, err)
|
|
}
|
|
|
|
colIndex = c.addColumn(newCol)
|
|
col = newCol
|
|
}
|
|
|
|
if err := col.AppendRow(v); err != nil {
|
|
return fmt.Errorf("value \"%v\" cannot be stored in dynamic column %s with requested type %s: %w", v, c.chType, requestedType, err)
|
|
}
|
|
|
|
c.appendDiscriminatorRow(colIndex)
|
|
return nil
|
|
}
|
|
|
|
// If preferred type wasn't provided, try each column
|
|
for i, col := range c.columns {
|
|
if c.deprecated.typeNames[i] == "SharedVariant" {
|
|
// Do not try to fit into SharedVariant
|
|
continue
|
|
}
|
|
|
|
if err := col.AppendRow(v); err == nil {
|
|
c.appendDiscriminatorRow(i)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// If no existing columns match, try matching a Datastore type from common Go types
|
|
inferredTypeName := inferTypeFromGoType(v)
|
|
if inferredTypeName != "" {
|
|
return c.AppendRow(col.NewDynamicWithType(v, inferredTypeName))
|
|
}
|
|
|
|
return fmt.Errorf("value \"%v\" cannot be stored in dynamic column: no compatible types. hint: use datastore.DynamicWithType to wrap the value", v)
|
|
}
|
|
|
|
func (c *Dynamic) encodeHeader(buffer *proto.Buffer) error {
|
|
buffer.PutUInt64(DynamicSerializationVersion)
|
|
buffer.PutUVarInt(uint64(c.totalTypes))
|
|
|
|
for _, col := range c.columns {
|
|
buffer.PutString(string(col.Type()))
|
|
}
|
|
|
|
for _, col := range c.columns {
|
|
if serialize, ok := col.(CustomSerialization); ok {
|
|
if err := serialize.WriteStatePrefix(buffer); err != nil {
|
|
return fmt.Errorf("failed to write prefix for type %s in dynamic: %w", string(col.Type()), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func discriminatorWriter(totalTypes uint64, buffer *proto.Buffer) func(uint64) {
|
|
switch {
|
|
case totalTypes <= math.MaxUint8:
|
|
return func(d uint64) { buffer.PutUInt8(uint8(d)) }
|
|
case totalTypes <= math.MaxUint16:
|
|
return func(d uint64) { buffer.PutUInt16(uint16(d)) }
|
|
case totalTypes <= math.MaxUint32:
|
|
return func(d uint64) { buffer.PutUInt32(uint32(d)) }
|
|
default:
|
|
return func(d uint64) { buffer.PutUInt64(d) }
|
|
}
|
|
}
|
|
|
|
func (c *Dynamic) encodeData(buffer *proto.Buffer) {
|
|
writeDiscriminator := discriminatorWriter(uint64(c.totalTypes), buffer)
|
|
for _, typeIndex := range c.discriminators {
|
|
if typeIndex == DynamicNullDiscriminator {
|
|
typeIndex = c.totalTypes
|
|
}
|
|
|
|
writeDiscriminator(uint64(typeIndex))
|
|
}
|
|
|
|
for _, col := range c.columns {
|
|
col.Encode(buffer)
|
|
}
|
|
}
|
|
|
|
func (c *Dynamic) WriteStatePrefix(buffer *proto.Buffer) error {
|
|
if supportsFlatDynamicJSON(c.sc) {
|
|
return c.encodeHeader(buffer)
|
|
}
|
|
|
|
return c.encodeHeader_v1(buffer)
|
|
}
|
|
|
|
func (c *Dynamic) Encode(buffer *proto.Buffer) {
|
|
if supportsFlatDynamicJSON(c.sc) {
|
|
c.encodeData(buffer)
|
|
return
|
|
}
|
|
|
|
c.encodeData_v1(buffer)
|
|
}
|
|
|
|
func (c *Dynamic) ScanType() reflect.Type {
|
|
return scanTypeDynamic
|
|
}
|
|
|
|
func (c *Dynamic) Reset() {
|
|
c.discriminators = c.discriminators[:0]
|
|
|
|
for _, col := range c.columns {
|
|
col.Reset()
|
|
}
|
|
}
|
|
|
|
func (c *Dynamic) decodeHeader(reader *proto.Reader) error {
|
|
totalTypes, err := reader.UVarInt()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read total types for dynamic column: %w", err)
|
|
}
|
|
|
|
c.columns = make([]Interface, 0, totalTypes)
|
|
c.columnIndexByType = make(map[string]int, totalTypes)
|
|
for i := uint64(0); i < totalTypes; i++ {
|
|
typeName, err := reader.Str()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read type name at index %d for dynamic column: %w", i, err)
|
|
}
|
|
|
|
col, err := Type(typeName).Column("", c.sc)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add dynamic column with type %s: %w", typeName, err)
|
|
}
|
|
|
|
c.addColumn(col)
|
|
}
|
|
|
|
for _, col := range c.columns {
|
|
if serialize, ok := col.(CustomSerialization); ok {
|
|
if err := serialize.ReadStatePrefix(reader); err != nil {
|
|
return fmt.Errorf("failed to read prefix for type %s in dynamic: %w", col.Type(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func discriminatorReader(totalTypes uint64, reader *proto.Reader) func() (uint64, error) {
|
|
switch {
|
|
case totalTypes <= math.MaxUint8:
|
|
return func() (uint64, error) {
|
|
v, err := reader.UInt8()
|
|
return uint64(v), err
|
|
}
|
|
case totalTypes <= math.MaxUint16:
|
|
return func() (uint64, error) {
|
|
v, err := reader.UInt16()
|
|
return uint64(v), err
|
|
}
|
|
case totalTypes <= math.MaxUint32:
|
|
return func() (uint64, error) {
|
|
v, err := reader.UInt32()
|
|
return uint64(v), err
|
|
}
|
|
default:
|
|
return func() (uint64, error) {
|
|
return reader.UInt64()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Dynamic) decodeData(reader *proto.Reader, rows int) error {
|
|
c.discriminators = make([]int, rows)
|
|
c.offsets = make([]int, rows)
|
|
rowCountByType := make([]int, c.totalTypes)
|
|
|
|
readDiscriminator := discriminatorReader(uint64(c.totalTypes), reader)
|
|
for i := 0; i < rows; i++ {
|
|
disc, err := readDiscriminator()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read discriminator at index %d: %w", i, err)
|
|
}
|
|
|
|
c.discriminators[i] = int(disc)
|
|
if int(disc) != c.totalTypes {
|
|
c.offsets[i] = rowCountByType[disc]
|
|
rowCountByType[disc]++
|
|
}
|
|
}
|
|
|
|
for i, col := range c.columns {
|
|
cRows := rowCountByType[i]
|
|
if err := col.Decode(reader, cRows); err != nil {
|
|
return fmt.Errorf("failed to decode dynamic column with %s type: %w", col.Type(), err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Dynamic) ReadStatePrefix(reader *proto.Reader) error {
|
|
dynamicSerializationVersion, err := reader.UInt64()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read dynamic serialization version: %w", err)
|
|
}
|
|
c.serializationVersion = dynamicSerializationVersion
|
|
|
|
switch c.serializationVersion {
|
|
case DynamicSerializationVersion:
|
|
return c.decodeHeader(reader)
|
|
case DynamicDeprecatedSerializationVersion:
|
|
return c.decodeHeader_v1(reader)
|
|
default:
|
|
return fmt.Errorf("unsupported dynamic serialization version: %d", dynamicSerializationVersion)
|
|
}
|
|
}
|
|
|
|
func (c *Dynamic) Decode(reader *proto.Reader, rows int) error {
|
|
switch c.serializationVersion {
|
|
case DynamicSerializationVersion:
|
|
return c.decodeData(reader, rows)
|
|
case DynamicDeprecatedSerializationVersion:
|
|
return c.decodeData_v1(reader, rows)
|
|
default:
|
|
return fmt.Errorf("unsupported dynamic serialization version: %d", c.serializationVersion)
|
|
}
|
|
}
|