Skip to content

Commit

Permalink
Merge pull request #126 from ClickHouse/feat/enum8-auto-column-of
Browse files Browse the repository at this point in the history
feat(proto): automatic inference for both Enum16 and Enum8
  • Loading branch information
ernado authored Jun 14, 2022
2 parents de35aa5 + 82d0f92 commit f2c3d2b
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 125 deletions.
2 changes: 1 addition & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func ExampleQuery_multipleInputColumns() {
body proto.ColStr
timestamp proto.ColDateTime64
name proto.ColStr
sevText proto.ColEnum8Auto
sevText proto.ColEnum
sevNumber proto.ColUInt8

arr = new(proto.ColStr).Array() // Array(String)
Expand Down
2 changes: 1 addition & 1 deletion otel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func NewMapPairs() MapPairs {
type OTEL struct {
Body proto.ColStr
Timestamp proto.ColDateTime64
SevText proto.ColEnum8Auto
SevText proto.ColEnum
SevNumber proto.ColUInt8

TraceID proto.ColFixedStr
Expand Down
4 changes: 2 additions & 2 deletions proto/col_auto.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ func (c *ColAuto) Infer(t ColumnType) error {
c.Data = new(ColDateTime)
c.DataType = t
return nil
case ColumnTypeEnum8:
v := new(ColEnum8Auto)
case ColumnTypeEnum8, ColumnTypeEnum16:
v := new(ColEnum)
if err := v.Infer(t); err != nil {
return errors.Wrap(err, "enum")
}
Expand Down
1 change: 1 addition & 0 deletions proto/col_auto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestColAuto_Infer(t *testing.T) {
ColumnTypeDateTime64.Sub("9"),
"Map(String,String)",
"Enum8('hello'=1,'world'=2)",
"Enum16('hello'=-1,'world'=10)",
} {
r := AutoResult("foo")
require.NoError(t, r.Data.(Inferable).Infer(columnType))
Expand Down
169 changes: 169 additions & 0 deletions proto/col_enum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package proto

import (
"strconv"
"strings"

"github.com/go-faster/errors"
)

var (
_ Column = (*ColEnum)(nil)
_ ColumnOf[string] = (*ColEnum)(nil)
_ Inferable = (*ColEnum)(nil)
_ Preparable = (*ColEnum)(nil)
)

// ColEnum is inference helper for enums.
//
// You can set Values and actual enum mapping will be inferred during query
// execution.
type ColEnum struct {
t ColumnType
base ColumnType

rawToStr map[int]string
strToRaw map[string]int
raw8 ColEnum8
raw16 ColEnum16

// Values of ColEnum.
Values []string
}

func (e *ColEnum) raw() Column {
if e.t.Base() == ColumnTypeEnum8 {
return &e.raw8
}
return &e.raw16
}

func (e ColEnum) Row(i int) string {
return e.Values[i]
}

// Append value to Enum8 column.
func (e *ColEnum) Append(v string) {
e.Values = append(e.Values, v)
}

func (e *ColEnum) parse(t ColumnType) error {
if e.rawToStr == nil {
e.rawToStr = map[int]string{}
}
if e.strToRaw == nil {
e.strToRaw = map[string]int{}
}

elements := t.Elem().String()
for _, elem := range strings.Split(elements, ",") {
def := strings.TrimSpace(elem)
// 'hello' = 1
parts := strings.SplitN(def, "=", 2)
if len(parts) != 2 {
return errors.Errorf("bad enum definition %q", def)
}
var (
left = strings.TrimSpace(parts[0]) // 'hello'
right = strings.TrimSpace(parts[1]) // 1
)
idx, err := strconv.Atoi(right)
if err != nil {
return errors.Errorf("bad right side of definition %q", right)
}
left = strings.TrimFunc(left, func(c rune) bool {
return c == '\''
})
e.strToRaw[left] = idx
e.rawToStr[idx] = left
}
return nil
}

func (e *ColEnum) Infer(t ColumnType) error {
if !strings.HasPrefix(t.Base().String(), "Enum") {
return errors.Errorf("invalid base %q to infer enum", t.Base())
}
if err := e.parse(t); err != nil {
return errors.Wrap(err, "parse type")
}
base := t.Base()
switch base {
case ColumnTypeEnum8, ColumnTypeEnum16:
e.base = base
default:
return errors.Errorf("invalid base %q", base)
}
e.t = t
return nil
}

func (e *ColEnum) Rows() int {
return len(e.Values)
}

func appendEnum[E Enum8 | Enum16](c []E, mapping map[int]string, values []string) ([]string, error) {
for _, v := range c {
s, ok := mapping[int(v)]
if !ok {
return nil, errors.Errorf("unknown enum value %d", v)
}
values = append(values, s)
}
return values, nil
}

func (e *ColEnum) DecodeColumn(r *Reader, rows int) error {
if err := e.raw().DecodeColumn(r, rows); err != nil {
return errors.Wrap(err, "raw")
}
var (
err error
v []string
)
switch e.base {
case ColumnTypeEnum8:
v, err = appendEnum[Enum8](e.raw8, e.rawToStr, e.Values[:0])
case ColumnTypeEnum16:
v, err = appendEnum[Enum16](e.raw16, e.rawToStr, e.Values[:0])
default:
return errors.Errorf("invalid enum base %q", e.base)
}
if err != nil {
return errors.Wrap(err, "map values")
}
e.Values = v
return nil
}

func (e *ColEnum) Reset() {
e.raw().Reset()
e.Values = e.Values[:0]
}

func (e *ColEnum) Prepare() error {
if e.raw().Rows() != 0 {
return errors.New("already prepared")
}
for _, v := range e.Values {
raw, ok := e.strToRaw[v]
if !ok {
return errors.Errorf("unknown enum value %q", v)
}
switch e.base {
case ColumnTypeEnum8:
e.raw8.Append(Enum8(raw))
case ColumnTypeEnum16:
e.raw16.Append(Enum16(raw))
default:
return errors.Errorf("invalid base %q", e.base)
}
}
return nil
}

func (e *ColEnum) EncodeColumn(b *Buffer) {
e.raw().EncodeColumn(b)
}

func (e *ColEnum) Type() ColumnType { return e.t }
119 changes: 0 additions & 119 deletions proto/col_enum8.go

This file was deleted.

35 changes: 33 additions & 2 deletions query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestClient_Query(t *testing.T) {
}
require.NoError(t, conn.Do(ctx, createTable), "create table")

data := proto.ColEnum8Auto{
data := proto.ColEnum{
Values: []string{"foo", "bar"},
}
insertQuery := Query{
Expand All @@ -108,7 +108,38 @@ func TestClient_Query(t *testing.T) {
}
require.NoError(t, conn.Do(ctx, insertQuery), "insert")

var gotData proto.ColEnum8Auto
var gotData proto.ColEnum
selectData := Query{
Body: "SELECT * FROM test_table",
Result: proto.Results{
{Name: "v", Data: &gotData},
},
}
require.NoError(t, conn.Do(ctx, selectData), "select")
require.Equal(t, data.Values, gotData.Values)
t.Log(gotData.Values)
})
t.Run("InsertEnum16", func(t *testing.T) {
t.Parallel()
conn := Conn(t)

createTable := Query{
Body: "CREATE TABLE test_table (v Enum16('foo' = 1, 'bar' = 2)) ENGINE = TinyLog",
}
require.NoError(t, conn.Do(ctx, createTable), "create table")

data := proto.ColEnum{
Values: []string{"foo", "bar"},
}
insertQuery := Query{
Body: "INSERT INTO test_table VALUES",
Input: []proto.InputColumn{
{Name: "v", Data: &data},
},
}
require.NoError(t, conn.Do(ctx, insertQuery), "insert")

var gotData proto.ColEnum
selectData := Query{
Body: "SELECT * FROM test_table",
Result: proto.Results{
Expand Down

0 comments on commit f2c3d2b

Please sign in to comment.