diff --git a/client_test.go b/client_test.go index 578e001..af1be21 100644 --- a/client_test.go +++ b/client_test.go @@ -378,25 +378,49 @@ func TestDataTypes(t *testing.T) { metric.SetTimestampAlias("time_v") series := Series{} - series.AddTag("int64_v", data.int64V) - series.AddTag("int32_v", data.int32V) - series.AddTag("int16_v", data.int16V) - series.AddTag("int8_v", data.int8V) - series.AddTag("int_v", data.intV) - - series.AddTag("uint64_v", data.uint64V) - series.AddField("uint32_v", data.uint32V) - series.AddField("uint16_v", data.uint16V) - series.AddField("uint8_v", data.uint8V) - series.AddField("uint_v", data.uintV) - - series.AddField("float64_v", data.float64V) - series.AddField("float32_v", data.float32V) - - series.AddField("string_v", data.stringV) - series.AddField("byte_v", data.byteV) - series.AddField("bool_v", data.boolV) - series.SetTimestamp(data.timeV) + // int + assert.Nil(t, series.AddIntTag("int64_v_tag", data.int64V)) + assert.Nil(t, series.AddTag("int32_v_tag", data.int32V)) + assert.Nil(t, series.AddTag("int16_v_tag", data.int16V)) + assert.Nil(t, series.AddTag("int8_v_tag", data.int8V)) + assert.Nil(t, series.AddTag("int_v_tag", data.intV)) + assert.Nil(t, series.AddIntField("int64_v_field", data.int64V)) + assert.Nil(t, series.AddField("int32_v_field", data.int32V)) + assert.Nil(t, series.AddField("int16_v_field", data.int16V)) + assert.Nil(t, series.AddField("int8_v_field", data.int8V)) + assert.Nil(t, series.AddField("int_v_field", data.intV)) + + // uint + assert.Nil(t, series.AddUintTag("uint64_v_tag", data.uint64V)) + assert.Nil(t, series.AddTag("uint32_v_tag", data.uint32V)) + assert.Nil(t, series.AddTag("uint16_v_tag", data.uint16V)) + assert.Nil(t, series.AddTag("uint8_v_tag", data.uint8V)) + assert.Nil(t, series.AddTag("uint_v_tag", data.uintV)) + assert.Nil(t, series.AddUintField("uint64_v_field", data.uint64V)) + assert.Nil(t, series.AddField("uint32_v_field", data.uint32V)) + assert.Nil(t, series.AddField("uint16_v_field", data.uint16V)) + assert.Nil(t, series.AddField("uint8_v_field", data.uint8V)) + assert.Nil(t, series.AddField("uint_v_field", data.uintV)) + + // float + assert.Nil(t, series.AddFloatTag("float64_v_tag", data.float64V)) + assert.Nil(t, series.AddTag("float32_v_tag", data.float32V)) + assert.Nil(t, series.AddFloatField("float64_v_field", data.float64V)) + assert.Nil(t, series.AddField("float32_v_field", data.float32V)) + + // string + assert.Nil(t, series.AddStringTag("string_v_tag", data.stringV)) + assert.Nil(t, series.AddStringField("string_v_field", data.stringV)) + + // bytes + assert.Nil(t, series.AddBytesTag("byte_v_tag", data.byteV)) + assert.Nil(t, series.AddBytesField("byte_v_field", data.byteV)) + + // bool + assert.Nil(t, series.AddBoolTag("bool_v_tag", data.boolV)) + assert.Nil(t, series.AddBoolField("bool_v_field", data.boolV)) + + assert.Nil(t, series.SetTimestamp(data.timeV)) metric.AddSeries(series) req := InsertRequest{} @@ -415,36 +439,84 @@ func TestDataTypes(t *testing.T) { assert.Equal(t, 1, len(resMetric.GetSeries())) series = resMetric.GetSeries()[0] - int64V, ok := series.GetInt("int64_v") + // int + int64V, ok := series.GetInt("int64_v_tag") assert.True(t, ok) - int32V, ok := series.GetInt("int32_v") + int32V, ok := series.GetInt("int32_v_tag") assert.True(t, ok) - int16V, ok := series.GetInt("int16_v") + int16V, ok := series.GetInt("int16_v_tag") assert.True(t, ok) - int8V, ok := series.GetInt("int8_v") + int8V, ok := series.GetInt("int8_v_tag") assert.True(t, ok) - intV, ok := series.GetInt("int_v") + intV, ok := series.GetInt("int_v_tag") assert.True(t, ok) - uint64V, ok := series.GetUint("uint64_v") + + _, ok = series.GetInt("int64_v_field") + assert.True(t, ok) + _, ok = series.GetInt("int32_v_field") + assert.True(t, ok) + _, ok = series.GetInt("int16_v_field") + assert.True(t, ok) + _, ok = series.GetInt("int8_v_field") + assert.True(t, ok) + _, ok = series.GetInt("int_v_field") + assert.True(t, ok) + + // uint + uint64V, ok := series.GetUint("uint64_v_tag") assert.True(t, ok) - uint32V, ok := series.GetUint("uint32_v") + uint32V, ok := series.GetUint("uint32_v_tag") assert.True(t, ok) - uint16V, ok := series.GetUint("uint16_v") + uint16V, ok := series.GetUint("uint16_v_tag") assert.True(t, ok) - uint8V, ok := series.GetUint("uint8_v") + uint8V, ok := series.GetUint("uint8_v_tag") assert.True(t, ok) - uintV, ok := series.GetUint("uint_v") + uintV, ok := series.GetUint("uint_v_tag") assert.True(t, ok) - float64V, ok := series.GetFloat("float64_v") + + _, ok = series.GetUint("uint64_v_field") + assert.True(t, ok) + _, ok = series.GetUint("uint32_v_field") assert.True(t, ok) - float32V, ok := series.GetFloat("float32_v") + _, ok = series.GetUint("uint16_v_field") assert.True(t, ok) - stringV, ok := series.GetString("string_v") + _, ok = series.GetUint("uint8_v_field") assert.True(t, ok) - byteV, ok := series.GetBytes("byte_v") + _, ok = series.GetUint("uint_v_field") + assert.True(t, ok) + + // float + float64V, ok := series.GetFloat("float64_v_tag") + assert.True(t, ok) + float32V, ok := series.GetFloat("float32_v_tag") + assert.True(t, ok) + + _, ok = series.GetFloat("float64_v_field") assert.True(t, ok) - boolV, ok := series.GetBool("bool_v") + _, ok = series.GetFloat("float32_v_field") assert.True(t, ok) + + // string + stringV, ok := series.GetString("string_v_tag") + assert.True(t, ok) + + _, ok = series.GetString("string_v_field") + assert.True(t, ok) + + // bytes + byteV, ok := series.GetBytes("byte_v_tag") + assert.True(t, ok) + + _, ok = series.GetBytes("byte_v_field") + assert.True(t, ok) + + // bool + boolV, ok := series.GetBool("bool_v_tag") + assert.True(t, ok) + + _, ok = series.GetBool("bool_v_field") + assert.True(t, ok) + timeV := series.GetTimestamp() querydata := datatype{ diff --git a/doc.go b/doc.go index 48342b3..a693afc 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,34 @@ // Package greptime provides API for using GreptimeDB client in Go. // +// # Basic Insert and Query +// // call [NewClient] with [Config] to init a concurrent safe [Client], and // prepare rows of data by [Metric] and [Series], call [Client.Insert] to insert -// metric into greptimedb, and call [Client.Query] to retrieve data from greptimedb. +// [InsertRequest] into greptimedb, and call [Client.Query] to retrieve data from +// greptimedb via [QueryRequest]. +// +// # Series +// +// You don't need to create the table, it will be created automatically via [Series] fields. +// What you have to know about [Series] in advance: +// +// - Tag is like index, it helps you to retrive data more efficiently +// - Field is like value, it can be used to analyze, calculate, aggregate, etc,. +// - Timestamp is required for timeseries data +// +// Once the schema is created automatically, it can not be changed by [Client], it +// will fail if the column type does not match +// +// # Metric +// +// [Metric] is like multiple [Series], it will check if all of the [Series] are valid: +// +// - the same column name in different series: data type MUST BE the same +// - Tag and Field MUST NOT contain the same column name +// - timestamp MUST NOT BE empty +// +// Also, [Metric] can set: +// +// - [Metric.SetTimePrecision] +// - [Metric.SetTimestampAlias] package greptime diff --git a/doc_test.go b/doc_test.go index d0ff7f5..25c3fda 100644 --- a/doc_test.go +++ b/doc_test.go @@ -9,17 +9,6 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -type Monitor struct { - host string - cpu float64 - memory int64 - ts time.Time -} - -func (m Monitor) String() string { - return fmt.Sprintf("{%s,%.2f,%d}", m.host, m.cpu, m.memory) -} - func Example() { // leave `addr`, `database`, `username`, `password` untouched in local machine, // but in GreptimeCloud you need to create a service in advance @@ -48,11 +37,11 @@ func Example() { } // inserting - series := Series{} // Create one row of data - series.AddTag("host", "localhost") // add index column, for query efficiency - series.AddField("cpu", 0.90) // add value column - series.AddField("memory", 1024) // add value column - series.SetTimestamp(time.Now()) // requird + series := Series{} // Create one row of data + series.AddStringTag("host", "localhost") // add index column, for query efficiency + series.AddFloatField("cpu", 0.90) // add value column + series.AddIntField("memory", 1024) // add value column + series.SetTimestamp(time.Now()) // requird metric := Metric{} // Create a Metric and add the Series metric.AddSeries(series) @@ -83,6 +72,13 @@ func Example() { return } + type Monitor struct { + host string + cpu float64 + memory int64 + ts time.Time + } + monitors := []Monitor{} for _, series := range resMetric.GetSeries() { one := &Monitor{} @@ -90,11 +86,10 @@ func Example() { if exist { one.host = host.(string) } - one.cpu, _ = series.GetFloat("cpu") // you can directly GetFloat - one.memory, _ = series.GetInt("memory") // you can directly GetInt + one.cpu, _ = series.GetFloat("cpu") // also, you can directly GetFloat + one.memory, _ = series.GetInt("memory") // also, you can directly GetInt one.ts = series.GetTimestamp() // GetTimestamp monitors = append(monitors, *one) } - fmt.Println(len(monitors) == 1) - fmt.Println(monitors[0]) + fmt.Println(monitors) } diff --git a/metric.go b/metric.go index 5a30f30..14c5a19 100644 --- a/metric.go +++ b/metric.go @@ -13,194 +13,6 @@ import ( "github.com/apache/arrow/go/arrow/flight" ) -type column struct { - typ greptimepb.ColumnDataType - semantic greptimepb.Column_SemanticType -} - -func checkColumnEquality(key string, col1, col2 column) error { - if col1.typ != col2.typ { - return fmt.Errorf("the type of '%s' does not match: '%T' and '%T'", key, col1.typ, col2.typ) - } - if col1.semantic != col2.semantic { - return fmt.Errorf("tag and field MUST NOT contain same name") - } - - return nil -} - -// Series represents one row of data you want to insert into GreptimeDB. -// - Tag fields are the index columns, which helps you to query data efficiently -// - Field fields are the value columns, which are used for value -// - Timestamp field is the timestamp column, which is required -// -// you do not need to create schema in advance, it will be created based on Series. -// But once the schema is created, [Client] has no ability to alert it. -type Series struct { - orders []string - columns map[string]column - vals map[string]any - - timestamp time.Time // required -} - -// GetTagsAndFields get all column names from metric, except timestamp column -func (s *Series) GetTagsAndFields() []string { - dst := make([]string, len(s.orders)) - copy(dst, s.orders) - return dst -} - -// Get helps to get value of specifid column. The second return value -// indicates if the key was present in Series -func (s *Series) Get(key string) (any, bool) { - val, exist := s.vals[key] - return val, exist -} - -func (s *Series) GetUint(key string) (uint64, bool) { - val, exist := s.Get(key) - if !exist { - return 0, exist - } - - switch val.(type) { - case uint64: - return val.(uint64), true - case uint32: - return uint64(val.(uint32)), true - case uint16: - return uint64(val.(uint16)), true - case uint8: - return uint64(val.(uint8)), true - default: - return 0, false - } -} - -func (s *Series) GetInt(key string) (int64, bool) { - val, exist := s.Get(key) - if !exist { - return 0, exist - } - - switch val.(type) { - case int64: - return val.(int64), true - case int32: - return int64(val.(int32)), true - case int16: - return int64(val.(int16)), true - case int8: - return int64(val.(int8)), true - default: - return 0, false - } -} - -func (s *Series) GetFloat(key string) (float64, bool) { - val, exist := s.Get(key) - if !exist { - return 0, exist - } - - switch val.(type) { - case float64: - return val.(float64), true - case float32: - return float64(val.(float32)), true - default: - return 0, false - } -} - -func (s *Series) GetBool(key string) (bool, bool) { - val, exist := s.Get(key) - if !exist { - return false, exist - } - - v, ok := val.(bool) - return v, ok -} - -func (s *Series) GetString(key string) (string, bool) { - val, exist := s.Get(key) - if !exist { - return "", exist - } - - v, ok := val.(string) - return v, ok -} - -func (s *Series) GetBytes(key string) ([]byte, bool) { - val, exist := s.GetString(key) - if !exist { - return nil, exist - } - - return []byte(val), true -} - -// GetTimestamp get timestamp field -func (s *Series) GetTimestamp() time.Time { - return s.timestamp -} - -func (s *Series) add(name string, val any, semantic greptimepb.Column_SemanticType) error { - key, err := toColumnName(name) - if err != nil { - return err - } - - if s.columns == nil { - s.columns = map[string]column{} - } - - v, err := convert(val) - if err != nil { - return fmt.Errorf("add tag err: %w", err) - } - - newCol := column{ - typ: v.typ, - semantic: semantic, - } - if col, seen := s.columns[key]; seen { - if err := checkColumnEquality(key, col, newCol); err != nil { - return err - } - } - s.columns[key] = newCol - s.orders = append(s.orders, key) - - if s.vals == nil { - s.vals = map[string]any{} - } - s.vals[key] = v.val - - return nil -} - -// AddTag prepare tag column, and old value will be replaced if same tag is set. -// the length of key CAN NOT be longer than 100 -func (s *Series) AddTag(key string, val any) error { - return s.add(key, val, greptimepb.Column_TAG) -} - -// AddField prepare field column, and old value will be replaced if same field is set. -// the length of key CAN NOT be longer than 100 -func (s *Series) AddField(key string, val any) error { - return s.add(key, val, greptimepb.Column_FIELD) -} - -// SetTimestamp is required -func (s *Series) SetTimestamp(t time.Time) error { - s.timestamp = t - return nil -} - // Metric represents multiple rows of data, and also Metric can specify // the timestamp column name and precision type Metric struct { diff --git a/metric_test.go b/metric_test.go index b86e8c9..a6c1a86 100644 --- a/metric_test.go +++ b/metric_test.go @@ -8,82 +8,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSeries(t *testing.T) { - s := Series{} - s.AddTag("Tag1", "tag val") - s.AddTag("tag2 ", true) - s.AddTag(" tag3", int32(32)) - s.AddTag("tag4", float64(32.0)) - timestamp := time.Now() - s.SetTimestamp(timestamp) - s.AddField("field1", []byte("field val")) - s.AddField("field2", float32(32.0)) - s.AddField("field3", uint8(8)) - s.AddField("field4", uint64(64)) - - // check columns - assert.Equal(t, 8, len(s.columns)) - assert.Equal(t, greptimepb.ColumnDataType_STRING, s.columns["tag1"].typ) - assert.Equal(t, greptimepb.Column_TAG, s.columns["tag1"].semantic) - assert.Equal(t, greptimepb.ColumnDataType_BOOLEAN, s.columns["tag2"].typ) - assert.Equal(t, greptimepb.Column_TAG, s.columns["tag2"].semantic) - assert.Equal(t, greptimepb.ColumnDataType_INT32, s.columns["tag3"].typ) - assert.Equal(t, greptimepb.Column_TAG, s.columns["tag3"].semantic) - assert.Equal(t, greptimepb.ColumnDataType_FLOAT64, s.columns["tag4"].typ) - assert.Equal(t, greptimepb.Column_TAG, s.columns["tag4"].semantic) - assert.Equal(t, greptimepb.ColumnDataType_STRING, s.columns["field1"].typ) - assert.Equal(t, greptimepb.Column_FIELD, s.columns["field1"].semantic) - assert.Equal(t, greptimepb.ColumnDataType_FLOAT64, s.columns["field2"].typ) - assert.Equal(t, greptimepb.Column_FIELD, s.columns["field2"].semantic) - assert.Equal(t, greptimepb.ColumnDataType_UINT32, s.columns["field3"].typ) - assert.Equal(t, greptimepb.Column_FIELD, s.columns["field3"].semantic) - assert.Equal(t, greptimepb.ColumnDataType_UINT64, s.columns["field4"].typ) - assert.Equal(t, greptimepb.Column_FIELD, s.columns["field4"].semantic) - - // check values - assert.Equal(t, 8, len(s.vals)) - assert.Equal(t, "tag val", s.vals["tag1"]) - assert.Equal(t, true, s.vals["tag2"]) - assert.Equal(t, int32(32), s.vals["tag3"]) - assert.Equal(t, float64(32.0), s.vals["tag4"]) - assert.Equal(t, "field val", s.vals["field1"]) - assert.Equal(t, float64(32.0), s.vals["field2"]) - assert.Equal(t, uint32(8), s.vals["field3"]) - assert.Equal(t, uint64(64), s.vals["field4"]) - - // check timestamp - assert.Equal(t, timestamp, s.timestamp) -} - -func TestValueReplaced(t *testing.T) { - s := Series{} - val := "tag val" - err := s.AddTag("tag1", val) - assert.Nil(t, err) - assert.Equal(t, val, s.vals["tag1"]) - - newVal := "tag val again" - err = s.AddTag("tag1", newVal) - assert.Nil(t, err) - assert.Equal(t, newVal, s.vals["tag1"]) -} - -func TestSeriesError(t *testing.T) { - s := Series{} - - // type not match - err := s.AddTag("tag1", "tag val") - assert.Nil(t, err) - err = s.AddTag("tag1", true) - assert.NotNil(t, err) - - // tag and field contain same column - err = s.AddTag("name", "tag val") - assert.Nil(t, err) - err = s.AddField("name", "field val") - assert.NotNil(t, err) -} - func TestMetric(t *testing.T) { s := Series{} s.AddTag("tag1", "tag val") diff --git a/series.go b/series.go new file mode 100644 index 0000000..996b4eb --- /dev/null +++ b/series.go @@ -0,0 +1,321 @@ +package greptime + +import ( + "fmt" + "time" + + greptimepb "github.com/GreptimeTeam/greptime-proto/go/greptime/v1" +) + +type column struct { + typ greptimepb.ColumnDataType + semantic greptimepb.Column_SemanticType +} + +func checkColumnEquality(key string, col1, col2 column) error { + if col1.typ != col2.typ { + return fmt.Errorf("the type of '%s' does not match: '%v' and '%v'", key, col1.typ, col2.typ) + } + if col1.semantic != col2.semantic { + return fmt.Errorf("Tag and Field MUST NOT contain same key: '%s'", key) + } + + return nil +} + +// Series represents one row of data you want to insert into GreptimeDB. +// - Tag fields are the index columns, which helps you to query data efficiently +// - Field fields are the value columns, which are used for value +// - Timestamp field is the timestamp column, which is required +// +// you do not need to create schema in advance, it will be created based on Series. +// But once the schema is created, [Client] has no ability to alert it. +type Series struct { + orders []string + columns map[string]column + vals map[string]any + + timestamp time.Time // required +} + +// GetTagsAndFields get all column names from metric, except timestamp column +func (s *Series) GetTagsAndFields() []string { + dst := make([]string, len(s.orders)) + copy(dst, s.orders) + return dst +} + +// Get helps to get value of specifid column. The second return value +// indicates if the key was present in Series +func (s *Series) Get(key string) (any, bool) { + val, exist := s.vals[key] + return val, exist +} + +// GetUint helps to get uint64 type of the specified key. It can retrieve the following type: +// - uint64 +// - uint32 +// - uint16 +// - uint8 +// - uint +// +// if you want uint32 instead of uint64, you can do it like: +// +// if v, ok := s.GetUint(key); ok { +// val := uint32(v) +// } +func (s *Series) GetUint(key string) (uint64, bool) { + val, exist := s.Get(key) + if !exist { + return 0, exist + } + + switch val.(type) { + case uint64: + return val.(uint64), true + case uint32: + return uint64(val.(uint32)), true + case uint16: + return uint64(val.(uint16)), true + case uint8: + return uint64(val.(uint8)), true + case uint: + return uint64(val.(uint)), true + default: + return 0, false + } +} + +// GetInt helps to get int64 type of the specified key. It can retrieve the following type: +// - int64 +// - int32 +// - int16 +// - int8 +// - int +// +// if you want int32 instead of int64, you can do it like: +// +// if v, ok := s.GetInt(key); ok { +// val := int32(v) +// } +func (s *Series) GetInt(key string) (int64, bool) { + val, exist := s.Get(key) + if !exist { + return 0, exist + } + + switch val.(type) { + case int: + return int64(val.(int)), true + case int64: + return val.(int64), true + case int32: + return int64(val.(int32)), true + case int16: + return int64(val.(int16)), true + case int8: + return int64(val.(int8)), true + default: + return 0, false + } +} + +// GetFloat helps to get float64 type of the specified key. It can retrieve the following type: +// - float64 +// - float32 +// +// if you want float32 instead of float64, you can do it like: +// +// if v, ok := s.GetFloat(key); ok { +// val := float32(v) +// } +func (s *Series) GetFloat(key string) (float64, bool) { + val, exist := s.Get(key) + if !exist { + return 0, exist + } + + switch val.(type) { + case float64: + return val.(float64), true + case float32: + return float64(val.(float32)), true + default: + return 0, false + } +} + +func (s *Series) GetBool(key string) (bool, bool) { + val, exist := s.Get(key) + if !exist { + return false, exist + } + + v, ok := val.(bool) + return v, ok +} + +func (s *Series) GetString(key string) (string, bool) { + val, exist := s.Get(key) + if !exist { + return "", exist + } + + v, ok := val.(string) + return v, ok +} + +func (s *Series) GetBytes(key string) ([]byte, bool) { + val, exist := s.GetString(key) + if !exist { + return nil, exist + } + + return []byte(val), true +} + +// GetTimestamp get timestamp field +func (s *Series) GetTimestamp() time.Time { + return s.timestamp +} + +func (s *Series) add(name string, val any, semantic greptimepb.Column_SemanticType) error { + key, err := toColumnName(name) + if err != nil { + return err + } + + if s.columns == nil { + s.columns = map[string]column{} + } + + v, err := convert(val) + if err != nil { + return fmt.Errorf("add tag err: %w", err) + } + + newCol := column{ + typ: v.typ, + semantic: semantic, + } + if col, seen := s.columns[key]; seen { + if err := checkColumnEquality(key, col, newCol); err != nil { + return err + } + } + s.columns[key] = newCol + s.orders = append(s.orders, key) + + if s.vals == nil { + s.vals = map[string]any{} + } + s.vals[key] = v.val + + return nil +} + +// AddTag prepare tag column, and old value will be replaced if same tag is set. +// the length of key CAN NOT be longer than 100. +// If you want to constain the column type, you can directly use like: +// - [Series.AddFloatTag] +// - [Series.AddIntTag] +// - ... +func (s *Series) AddTag(key string, val any) error { + return s.add(key, val, greptimepb.Column_TAG) +} + +// AddFloatTag helps to constrain the key to be float64 type, if you want to +// add float32 tag instead of float64, you can do it like: +// +// var i float32 = 1.0 +// return s.AddFloatTag("memory", float64(i)) +func (s *Series) AddFloatTag(key string, val float64) error { + return s.AddTag(key, val) +} + +// AddIntTag helps to constrain the key to be int64 type, if you want to +// add int32 tag instead of int64, you can do it like: +// +// var i int32 = 1 +// return s.AddIntTag("account", int64(i)) +func (s *Series) AddIntTag(key string, val int64) error { + return s.AddTag(key, val) +} + +// AddUintTag helps to constrain the key to be uint64 type, if you want to +// add uint32 tag instead of uint64, you can do it like: +// +// var i uint32 = 1 +// return s.AddUintTag("account", uint64(i)) +func (s *Series) AddUintTag(key string, val uint64) error { + return s.AddTag(key, val) +} + +// AddBoolTag helps to constrain the key to be bool type +func (s *Series) AddBoolTag(key string, val bool) error { + return s.AddTag(key, val) +} + +// AddStringTag helps to constrain the key to be string type +func (s *Series) AddStringTag(key string, val string) error { + return s.AddTag(key, val) +} + +// AddBytesTag helps to constrain the key to be []byte type +func (s *Series) AddBytesTag(key string, val []byte) error { + return s.AddTag(key, val) +} + +// AddField prepare field column, and old value will be replaced if same field is set. +// the length of key CAN NOT be longer than 100 +func (s *Series) AddField(key string, val any) error { + return s.add(key, val, greptimepb.Column_FIELD) +} + +// AddFloatField helps to constrain the key to be float64 type, if you want to +// add float32 tag instead of float64, you can do it like: +// +// var i float32 = 1.0 +// return s.AddFloatField("memory", float64(i)) +func (s *Series) AddFloatField(key string, val float64) error { + return s.AddField(key, val) +} + +// AddIntField helps to constrain the key to be int64 type, if you want to +// add int32 tag instead of int64, you can do it like: +// +// var i int32 = 1 +// return s.AddIntField("account", int64(i)) +func (s *Series) AddIntField(key string, val int64) error { + return s.AddField(key, val) +} + +// AddUintField helps to constrain the key to be uint64 type, if you want to +// add uint32 tag instead of uint64, you can do it like: +// +// var i uint32 = 1 +// return s.AddUintField("account", uint64(i)) +func (s *Series) AddUintField(key string, val uint64) error { + return s.AddField(key, val) +} + +// AddBoolField helps to constrain the key to be bool type +func (s *Series) AddBoolField(key string, val bool) error { + return s.AddField(key, val) +} + +// AddStringField helps to constrain the key to be string type +func (s *Series) AddStringField(key string, val string) error { + return s.AddField(key, val) +} + +// AddBytesField helps to constrain the key to be []byte type +func (s *Series) AddBytesField(key string, val []byte) error { + return s.AddField(key, val) +} + +// SetTimestamp is required +func (s *Series) SetTimestamp(t time.Time) error { + s.timestamp = t + return nil +} diff --git a/series_test.go b/series_test.go new file mode 100644 index 0000000..83bb6f8 --- /dev/null +++ b/series_test.go @@ -0,0 +1,158 @@ +package greptime + +import ( + "fmt" + "testing" + "time" + + greptimepb "github.com/GreptimeTeam/greptime-proto/go/greptime/v1" + "github.com/stretchr/testify/assert" +) + +func TestSeries(t *testing.T) { + s := Series{} + s.AddTag("Tag1", "tag val") + s.AddTag("tag2 ", true) + s.AddTag(" tag3", int32(32)) + s.AddTag("tag4", float64(32.0)) + timestamp := time.Now() + s.SetTimestamp(timestamp) + s.AddField("field1", []byte("field val")) + s.AddField("field2", float32(32.0)) + s.AddField("field3", uint8(8)) + s.AddField("field4", uint64(64)) + + // check columns + assert.Equal(t, 8, len(s.columns)) + assert.Equal(t, greptimepb.ColumnDataType_STRING, s.columns["tag1"].typ) + assert.Equal(t, greptimepb.Column_TAG, s.columns["tag1"].semantic) + assert.Equal(t, greptimepb.ColumnDataType_BOOLEAN, s.columns["tag2"].typ) + assert.Equal(t, greptimepb.Column_TAG, s.columns["tag2"].semantic) + assert.Equal(t, greptimepb.ColumnDataType_INT32, s.columns["tag3"].typ) + assert.Equal(t, greptimepb.Column_TAG, s.columns["tag3"].semantic) + assert.Equal(t, greptimepb.ColumnDataType_FLOAT64, s.columns["tag4"].typ) + assert.Equal(t, greptimepb.Column_TAG, s.columns["tag4"].semantic) + assert.Equal(t, greptimepb.ColumnDataType_STRING, s.columns["field1"].typ) + assert.Equal(t, greptimepb.Column_FIELD, s.columns["field1"].semantic) + assert.Equal(t, greptimepb.ColumnDataType_FLOAT64, s.columns["field2"].typ) + assert.Equal(t, greptimepb.Column_FIELD, s.columns["field2"].semantic) + assert.Equal(t, greptimepb.ColumnDataType_UINT32, s.columns["field3"].typ) + assert.Equal(t, greptimepb.Column_FIELD, s.columns["field3"].semantic) + assert.Equal(t, greptimepb.ColumnDataType_UINT64, s.columns["field4"].typ) + assert.Equal(t, greptimepb.Column_FIELD, s.columns["field4"].semantic) + + // check values + assert.Equal(t, 8, len(s.vals)) + assert.Equal(t, "tag val", s.vals["tag1"]) + assert.Equal(t, true, s.vals["tag2"]) + assert.Equal(t, int32(32), s.vals["tag3"]) + assert.Equal(t, float64(32.0), s.vals["tag4"]) + assert.Equal(t, "field val", s.vals["field1"]) + assert.Equal(t, float64(32.0), s.vals["field2"]) + assert.Equal(t, uint32(8), s.vals["field3"]) + assert.Equal(t, uint64(64), s.vals["field4"]) + + // check timestamp + assert.Equal(t, timestamp, s.timestamp) +} + +func TestValueReplaced(t *testing.T) { + s := Series{} + val := "tag val" + err := s.AddTag("tag1", val) + assert.Nil(t, err) + assert.Equal(t, val, s.vals["tag1"]) + + newVal := "tag val again" + err = s.AddTag("tag1", newVal) + assert.Nil(t, err) + assert.Equal(t, newVal, s.vals["tag1"]) +} + +func TestSeriesError(t *testing.T) { + s := Series{} + + // type not match + err := s.AddTag("tag1", "tag val") + assert.Nil(t, err) + err = s.AddTag("tag1", true) + assert.NotNil(t, err) + + // tag and field contain same column + err = s.AddTag("name", "tag val") + assert.Nil(t, err) + err = s.AddField("name", "field val") + assert.NotNil(t, err) +} + +func TestSeriesTypeNotMatch(t *testing.T) { + s := &Series{} + + key := "int_tag" + err := s.AddIntTag(key, 1) + assert.Nil(t, err) + + err = s.AddFloatTag(key, 1) + assert.NotNil(t, err) + assert.Equal(t, fmt.Sprintf("the type of '%s' does not match: 'INT64' and 'FLOAT64'", key), err.Error()) +} + +func TestSeriesTagAndFieldCanNotContainSameKey(t *testing.T) { + s := &Series{} + + key := "tag_column" + err := s.AddIntTag(key, 1) + assert.Nil(t, err) + + err = s.AddIntField(key, 1) + assert.NotNil(t, err) + assert.Equal(t, fmt.Sprintf("Tag and Field MUST NOT contain same key: '%s'", key), err.Error()) + + // type checks before tag/field + err = s.AddFloatField(key, 1) + assert.NotNil(t, err) + assert.Equal(t, fmt.Sprintf("the type of '%s' does not match: 'INT64' and 'FLOAT64'", key), err.Error()) +} + +func TestSeriesAddTypedTagAndField(t *testing.T) { + s := &Series{} + + // tags + err := s.AddIntTag("int_tag", 1) + assert.Nil(t, err) + + err = s.AddFloatTag("float_tag", 1) + assert.Nil(t, err) + + err = s.AddUintTag("uint_tag", 1) + assert.Nil(t, err) + + err = s.AddBoolTag("bool_tag", true) + assert.Nil(t, err) + + err = s.AddStringTag("string_tag", "1") + assert.Nil(t, err) + + err = s.AddBytesTag("bytes_tag", []byte("1")) + assert.Nil(t, err) + + // fields + err = s.AddIntField("int_field", 1) + assert.Nil(t, err) + + err = s.AddFloatField("float_field", 1) + assert.Nil(t, err) + + err = s.AddUintField("uint_field", 1) + assert.Nil(t, err) + + err = s.AddBoolField("bool_field", true) + assert.Nil(t, err) + + err = s.AddStringField("string_field", "1") + assert.Nil(t, err) + + err = s.AddBytesField("bytes_field", []byte("1")) + assert.Nil(t, err) + +}