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

Add Variant Type #1453

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
37 changes: 37 additions & 0 deletions chcol.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Licensed to ClickHouse, Inc. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. ClickHouse, Inc. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package clickhouse

import "github.com/ClickHouse/clickhouse-go/v2/lib/chcol"

// Re-export chcol types/funcs to top level clickhouse package

type (
Variant = chcol.Variant
VariantWithType = chcol.VariantWithType
)

// NewVariant creates a new Variant with the given value
func NewVariant(v any) Variant {
return chcol.NewVariant(v)
}

// NewVariantWithType creates a new Variant with the given value and ClickHouse type
func NewVariantWithType(v any, chType string) VariantWithType {
return chcol.NewVariantWithType(v, chType)
}
4 changes: 4 additions & 0 deletions examples/clickhouse_api/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,7 @@ func TestSSL(t *testing.T) {
func TestSSLNoVerify(t *testing.T) {
require.NoError(t, SSLNoVerifyVersion())
}

func TestVariantExample(t *testing.T) {
require.NoError(t, VariantExample())
}
136 changes: 136 additions & 0 deletions examples/clickhouse_api/variant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Licensed to ClickHouse, Inc. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. ClickHouse, Inc. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package clickhouse_api

import (
"context"
"fmt"
"github.com/ClickHouse/clickhouse-go/v2"
)

func VariantExample() error {
ctx := context.Background()

conn, err := GetNativeConnection(clickhouse.Settings{
"allow_experimental_variant_type": true,
}, nil, nil)
if err != nil {
return err
}

err = conn.Exec(ctx, "DROP TABLE IF EXISTS go_variant_example")
if err != nil {
return err
}

err = conn.Exec(ctx, `
CREATE TABLE go_variant_example (
c Variant(Bool, Int64, String)
) ENGINE = Memory
`)
if err != nil {
return err
}

batch, err := conn.PrepareBatch(ctx, "INSERT INTO go_variant_example (c)")
if err != nil {
return err
}

if err = batch.Append(true); err != nil {
return err
}

if err = batch.Append(int64(42)); err != nil {
return err
}

if err = batch.Append("example"); err != nil {
return err
}

if err = batch.Append(clickhouse.NewVariant("example variant")); err != nil {
return err
}

if err = batch.Append(clickhouse.NewVariantWithType("example variant with specific type", "String")); err != nil {
return err
}

if err = batch.Append(nil); err != nil {
return err
}

if err = batch.Send(); err != nil {
return err
}

// Switch on Go Type

rows, err := conn.Query(ctx, "SELECT c FROM go_variant_example")
if err != nil {
return err
}

for i := 0; rows.Next(); i++ {
var row clickhouse.Variant
err := rows.Scan(&row)
if err != nil {
return fmt.Errorf("failed to scan row index %d: %w", i, err)
}

switch row.Any().(type) {
case bool:
fmt.Printf("row at index %d is Bool: %v\n", i, row.Any())
case int64:
fmt.Printf("row at index %d is Int64: %v\n", i, row.Any())
case string:
fmt.Printf("row at index %d is String: %v\n", i, row.Any())
case nil:
fmt.Printf("row at index %d is NULL\n", i)
}
}

// Switch on ClickHouse Type

rows, err = conn.Query(ctx, "SELECT c FROM go_variant_example")
if err != nil {
return err
}

for i := 0; rows.Next(); i++ {
var row clickhouse.VariantWithType
err := rows.Scan(&row)
if err != nil {
return fmt.Errorf("failed to scan row index %d: %w", i, err)
}

switch row.Type() {
case "Bool":
fmt.Printf("row at index %d is bool: %v\n", i, row.Any())
case "Int64":
fmt.Printf("row at index %d is int64: %v\n", i, row.Any())
case "String":
fmt.Printf("row at index %d is string: %v\n", i, row.Any())
case "":
fmt.Printf("row at index %d is nil\n", i)
}
}

return nil
}
126 changes: 126 additions & 0 deletions lib/chcol/variant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Licensed to ClickHouse, Inc. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. ClickHouse, Inc. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package chcol

import (
"database/sql/driver"
"encoding/json"
)

// Variant represents a ClickHouse Variant type that can hold multiple possible types
type Variant struct {
value any
}

// NewVariant creates a new Variant with the given value
func NewVariant(v any) Variant {
return Variant{value: v}
}

// Nil returns true if the underlying value is nil.
func (v Variant) Nil() bool {
return v.value == nil
}

// Any returns the underlying value as any. Same as Interface.
func (v Variant) Any() any {
return v.value
}

// Interface returns the underlying value as interface{}. Same as Any.
func (v Variant) Interface() interface{} {
return v.value
}
Comment on lines +45 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point? interface{} is an alias to any.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put both so users could choose whichever name they prefer within their application. Some apps prefer using any and others interface{}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only a name. Why have both functions?


// Int returns the value as an int if possible
func (v Variant) Int() (int, bool) {
if i, ok := v.value.(int); ok {
return i, true
}

return 0, false
}

// Int64 returns the value as an int64 if possible
func (v Variant) Int64() (int64, bool) {
if i, ok := v.value.(int64); ok {
return i, true
}

return 0, false
}

// String returns the value as a string if possible
func (v Variant) String() (string, bool) {
if s, ok := v.value.(string); ok {
return s, true
}

return "", false
}

// Bool returns the value as an bool if possible
func (v Variant) Bool() (bool, bool) {
if b, ok := v.value.(bool); ok {
return b, true
}

return false, false
}
Comment on lines +50 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wonder if we need these functions at all. See:

package main

import "fmt"

type Variant struct {
	value any
}

func (v *Variant) Value() any {
	return v.value
}

func main() {
	v := Variant{}
	i, ok := v.Value().(int)
	fmt.Println(i, ok)
}

We can just expose value and let the user freely type assert.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More: If we make value public, we don't need any value access functions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm considering removing Variant in favor of VariantWithType as the default. There are many cases where the ClickHouse type needs to be provided. If value were public then it doesn't make this type any different from a regular any.

For now I agree it may be best to simply make value public


// MarshalJSON implements the json.Marshaler interface
func (v *Variant) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}

// Scan implements the sql.Scanner interface
func (v *Variant) Scan(value interface{}) error {
v.value = value
return nil
}

// Value implements the driver.Valuer interface
func (v Variant) Value() (driver.Value, error) {
return v.value, nil
}

func (v Variant) WithType(chType string) VariantWithType {
return VariantWithType{
Variant: v,
chType: chType,
}
}

// VariantWithType is Variant with an extra value for specifying the preferred ClickHouse type for column encoding
type VariantWithType struct {
Variant
chType string
}

// NewVariantWithType creates a new Variant with the given value and ClickHouse type
func NewVariantWithType(v any, chType string) VariantWithType {
return VariantWithType{
Variant: Variant{value: v},
chType: chType,
}
}

// Type returns the ClickHouse type as a string.
func (v VariantWithType) Type() string {
return v.chType
}
72 changes: 72 additions & 0 deletions lib/chcol/variant_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Licensed to ClickHouse, Inc. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. ClickHouse, Inc. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package chcol

import (
"testing"
)

func TestVariant_Nil(t *testing.T) {
v := NewVariant(nil)

if !v.Nil() {
t.Fatalf("expected variant to be nil")
}
}

func TestVariant_Int64(t *testing.T) {
var in int64 = 42

v := NewVariant(in)

out, ok := v.Int64()
if !ok {
t.Fatalf("failed to get int64 from variant")
} else if out != in {
t.Fatalf("incorrect value from variant. expected: %d got: %d", in, out)
}
}

func TestVariant_String(t *testing.T) {
in := "test"

v := NewVariant(in)

out, ok := v.String()
if !ok {
t.Fatalf("failed to get string from variant")
} else if out != in {
t.Fatalf("incorrect value from variant. expected: %s got: %s", in, out)
}
}

func TestVariant_TypeSwitch(t *testing.T) {
var in any

v := NewVariant(in)

switch v.Any().(type) {
case int64:
t.Fatalf("unexpected int64 value from variant")
case string:
t.Fatalf("unexpected string value from variant")
case nil:
default:
t.Fatalf("expected nil value from variant")
}
}
Loading
Loading