Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(proto): add ColNullableOf #107

Merged
merged 4 commits into from
May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ issues:
- linters: [ govet ]
text: 'declaration of "(err|ctx|log|lg|c)"'

# Probably some broken linter for generics?
- linters: [ revive ]
text: 'receiver-naming: receiver name \S+ should be consistent with previous receiver name \S+ for invalid-type'

# Ignore linters in main packages.
- path: main\.go
linters: [ goconst, funlen, gocognit, gocyclo ]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ q := ch.Query{
## Features
* OpenTelemetry support
* No reflection or `interface{}`
* Generics (go1.18) for `ArrayOf[T]`, `LowCardinaliyOf[T]`, `MapOf[K, V]`
* Generics (go1.18) for `ArrayOf[T]`, `LowCardinaliyOf[T]`, `MapOf[K, V]`, `NullableOf[T]`
* **Column**-oriented design that operates with **blocks**
* [Dramatically more efficient](https://github.com/go-faster/ch-bench)
* Up to 100x faster than row-first design around `sql`
Expand Down
2 changes: 2 additions & 0 deletions proto/_golden/col_nullable_of_str.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
00000000 00 01 00 00 03 66 6f 6f 00 03 62 61 72 03 62 61 |.....foo..bar.ba|
00000010 7a |z|
Binary file added proto/_golden/col_nullable_of_str.raw
Binary file not shown.
120 changes: 120 additions & 0 deletions proto/col_nullable_of.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package proto

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

// Compile-time assertions for ColNullableOf.
var (
_ ColInput = (*ColNullableOf[string])(nil)
_ ColResult = (*ColNullableOf[string])(nil)
_ Column = (*ColNullableOf[string])(nil)
_ ColumnOf[Nullable[string]] = (*ColNullableOf[string])(nil)
_ StateEncoder = (*ColNullableOf[string])(nil)
_ StateDecoder = (*ColNullableOf[string])(nil)

_ = ColNullableOf[string]{
Values: new(ColStr),
}
)

// Nullable is T value that can be null.
type Nullable[T any] struct {
Set bool
Value T
}

// NewNullable returns set value of Nullable[T] to v.
func NewNullable[T any](v T) Nullable[T] {
return Nullable[T]{Set: true, Value: v}
}

// Null returns null value for Nullable[T].
func Null[T any]() Nullable[T] {
return Nullable[T]{}
}

func (n Nullable[T]) IsSet() bool { return n.Set }

func (n Nullable[T]) Or(v T) T {
if n.Set {
return v
}
return n.Value
}

// ColNullableOf is Nullable(T) column.
type ColNullableOf[T any] struct {
Nulls ColUInt8
Values ColumnOf[T]
}

func (c *ColNullableOf[T]) DecodeState(r *Reader) error {
if s, ok := c.Values.(StateDecoder); ok {
if err := s.DecodeState(r); err != nil {
return errors.Wrap(err, "values state")
}
}
return nil
}

func (c ColNullableOf[T]) EncodeState(b *Buffer) {
if s, ok := c.Values.(StateEncoder); ok {
s.EncodeState(b)
}
}

func (c ColNullableOf[T]) Type() ColumnType {
return ColumnTypeNullable.Sub(c.Values.Type())
}

func (c *ColNullableOf[T]) DecodeColumn(r *Reader, rows int) error {
if err := c.Nulls.DecodeColumn(r, rows); err != nil {
return errors.Wrap(err, "nulls")
}
if err := c.Values.DecodeColumn(r, rows); err != nil {
return errors.Wrap(err, "values")
}
return nil
}

func (c ColNullableOf[T]) Rows() int {
return c.Nulls.Rows()
}

func (c *ColNullableOf[T]) Append(v Nullable[T]) {
null := boolTrue
if v.Set {
null = boolFalse
}
c.Nulls.Append(null)
c.Values.Append(v.Value)
}

func (c *ColNullableOf[T]) AppendArr(v []Nullable[T]) {
for _, vv := range v {
c.Append(vv)
}
}

func (c ColNullableOf[T]) Row(i int) Nullable[T] {
return Nullable[T]{
Value: c.Values.Row(i),
Set: c.Nulls.Row(i) == boolFalse,
}
}

func (c *ColNullableOf[T]) Reset() {
c.Nulls.Reset()
c.Values.Reset()
}

func (c ColNullableOf[T]) EncodeColumn(b *Buffer) {
c.Nulls.EncodeColumn(b)
c.Values.EncodeColumn(b)
}

func (c ColNullableOf[T]) IsElemNull(i int) bool {
if i < c.Rows() {
return c.Nulls[i] == boolTrue
}
return false
}
52 changes: 52 additions & 0 deletions proto/col_nullable_of_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package proto

import (
"bytes"
"io"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/go-faster/ch/internal/gold"
)

func TestColNullableOf(t *testing.T) {
col := &ColNullableOf[string]{
Values: new(ColStr),
}
v := []Nullable[string]{
NewNullable("foo"),
Null[string](),
NewNullable("bar"),
NewNullable("baz"),
}
col.AppendArr(v)

var buf Buffer
col.EncodeColumn(&buf)
t.Run("Golden", func(t *testing.T) {
gold.Bytes(t, buf.Buf, "col_nullable_of_str")
})
t.Run("Ok", func(t *testing.T) {
br := bytes.NewReader(buf.Buf)
r := NewReader(br)
dec := &ColNullableOf[string]{Values: new(ColStr)}

require.NoError(t, dec.DecodeColumn(r, col.Rows()))
require.Equal(t, col.Rows(), dec.Rows())
for i, s := range v {
assert.Equal(t, s, col.Row(i))
}
assert.Equal(t, ColumnType("Nullable(String)"), dec.Type())
})
t.Run("ErrUnexpectedEOF", func(t *testing.T) {
r := NewReader(bytes.NewReader(nil))
dec := &ColNullableOf[string]{Values: new(ColStr)}
require.ErrorIs(t, dec.DecodeColumn(r, col.Rows()), io.ErrUnexpectedEOF)
})
t.Run("NoShortRead", func(t *testing.T) {
dec := &ColNullableOf[string]{Values: new(ColStr)}
requireNoShortRead(t, buf.Buf, colAware(dec, col.Rows()))
})
}
43 changes: 43 additions & 0 deletions query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,49 @@ func TestClient_Query(t *testing.T) {
require.Equal(t, data.Row(i), gotData.Row(i))
}
})
t.Run("InsertNullableString", func(t *testing.T) {
t.Parallel()
conn := Conn(t)
createTable := Query{
Body: "CREATE TABLE test_table (v Nullable(String)) ENGINE = Memory",
}
require.NoError(t, conn.Do(ctx, createTable), "create table")

data := &proto.ColNullableOf[string]{
Values: new(proto.ColStr),
}
data.AppendArr([]proto.Nullable[string]{
proto.Null[string](),
proto.NewNullable("hello"),
proto.NewNullable("world"),
proto.Null[string](),
proto.Null[string](),
proto.NewNullable("end"),
})

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

gotData := &proto.ColNullableOf[string]{
Values: new(proto.ColStr),
}
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.Rows(), gotData.Rows())
for i := 0; i < data.Rows(); i++ {
require.Equal(t, data.Row(i), gotData.Row(i))
}
})
}

func TestClientCompression(t *testing.T) {
Expand Down