diff --git a/.golangci.yml b/.golangci.yml index b74dc0d5..7b103585 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 ] diff --git a/README.md b/README.md index 55b5b427..9a0c435c 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/proto/_golden/col_nullable_of_str.hex b/proto/_golden/col_nullable_of_str.hex new file mode 100644 index 00000000..aa2e3c27 --- /dev/null +++ b/proto/_golden/col_nullable_of_str.hex @@ -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| diff --git a/proto/_golden/col_nullable_of_str.raw b/proto/_golden/col_nullable_of_str.raw new file mode 100644 index 00000000..332439b3 Binary files /dev/null and b/proto/_golden/col_nullable_of_str.raw differ diff --git a/proto/col_nullable_of.go b/proto/col_nullable_of.go new file mode 100644 index 00000000..a7c2a392 --- /dev/null +++ b/proto/col_nullable_of.go @@ -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 +} diff --git a/proto/col_nullable_of_test.go b/proto/col_nullable_of_test.go new file mode 100644 index 00000000..14500647 --- /dev/null +++ b/proto/col_nullable_of_test.go @@ -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())) + }) +} diff --git a/query_test.go b/query_test.go index 2431ffc8..cc33651f 100644 --- a/query_test.go +++ b/query_test.go @@ -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) {