├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── duck ├── data │ ├── converters.go │ ├── parquet.go │ └── parquet_test.go ├── duckdb.go ├── duckdb_test.go └── frame-data.go ├── go.mod └── go.sum /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: "1.22" 22 | 23 | - name: Install DuckDB CLI 24 | run: | 25 | export DUCKDB_CLI_URL=https://github.com/duckdb/duckdb/releases/download/v1.0.0/ 26 | curl -sSL -o /tmp/duckdb.zip ${DUCKDB_CLI_URL}/duckdb_cli-linux-amd64.zip 27 | unzip /tmp/duckdb.zip -d /usr/local/bin/ 28 | duckdb --version 29 | 30 | - name: add duckdb image 31 | run: docker pull datacatering/duckdb:v1.0.0 32 | 33 | - name: Build 34 | run: go build -v ./... 35 | 36 | - name: Test 37 | run: go test -v ./... 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Scott Lepper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://github.com/scottlepp/go-duck/assets/6108819/78f3fe95-227a-4b7f-99d5-81d979e5fc2e) 2 | 3 | 4 | # Go wrapper for [DuckDB CLI](https://duckdb.org/docs/api/cli/overview) 5 | * Doesn't require CGO. 6 | * Requires duckdb cli to be in the path. 7 | 8 | ## In Memory Database 9 | ``` 10 | db := NewInMemoryDB() 11 | 12 | commands := []string{ 13 | "CREATE TABLE t1 (i INTEGER, j INTEGER);", 14 | "INSERT INTO t1 VALUES (1, 5);", 15 | "SELECT * from t1;", 16 | } 17 | res, err := db.RunCommands(commands) 18 | ``` 19 | ## File based Database 20 | ``` 21 | db := NewDuckDB("foo") 22 | 23 | commands := []string{ 24 | "CREATE TABLE t1 (i INTEGER, j INTEGER);", 25 | "INSERT INTO t1 VALUES (1, 5);", 26 | } 27 | _, err := db.RunCommands(commands) 28 | res, err := db.Query("SELECT * from t1;") 29 | ``` 30 | 31 | ## Dataframes 32 | * Allows querying dataframes using SQL, with all the aggregate/window/analytics functions from DuckDB. 33 | 34 | ## Query Dataframes 35 | ``` 36 | db := NewInMemoryDB() 37 | 38 | var values = []string{"test"} 39 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 40 | frame.RefID = "foo" 41 | frames := []*data.Frame{frame} 42 | 43 | res, err := db.QueryFrames("foo", "select * from foo", frames) 44 | ``` 45 | -------------------------------------------------------------------------------- /duck/data/converters.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/araddon/dateparse" 7 | "github.com/grafana/grafana-plugin-sdk-go/data" 8 | "github.com/grafana/grafana-plugin-sdk-go/data/framestruct" 9 | ) 10 | 11 | func Converters(frames []*data.Frame) []framestruct.FramestructOption { 12 | var convs = []framestruct.FramestructOption{} 13 | var fields = map[string]*data.Field{} 14 | for _, f := range frames { 15 | for _, fld := range f.Fields { 16 | fields[fld.Name] = fld 17 | } 18 | } 19 | for _, fld := range fields { 20 | conv := converterMap[fld.Type()] 21 | if conv != nil { 22 | converter := framestruct.WithConverterFor(fld.Name, conv) 23 | convs = append(convs, converter) 24 | } 25 | } 26 | return convs 27 | } 28 | 29 | var converterMap = map[data.FieldType]func(i interface{}) (interface{}, error){ 30 | data.FieldTypeTime: timeConverter, 31 | data.FieldTypeNullableTime: timeConverter, 32 | } 33 | 34 | var timeConverter = func(i interface{}) (interface{}, error) { 35 | if s, ok := i.(string); ok { 36 | return parseDate(s) 37 | } 38 | if s, ok := i.(*string); ok { 39 | return parseDate(*s) 40 | } 41 | return i, nil 42 | } 43 | 44 | const layout = "2006-01-02 15:04:05-07" 45 | 46 | func parseDate(s string) (time.Time, error) { 47 | t, err := time.Parse(layout, s) 48 | if err != nil { 49 | logger.Error("failed to parse time", "error", err) 50 | return t, err 51 | } 52 | return t.UTC(), nil 53 | } 54 | 55 | // TODO: just define converters for the date fields we find 56 | // then we can avoid looping through all the results and fields here 57 | func ConvertDateFields(results []map[string]any) { 58 | dateFields := findDateFields(results) 59 | for row, result := range results { 60 | for key, value := range result { 61 | isDateField := dateFields[key] 62 | if isDateField != nil && *isDateField { 63 | if s, ok := value.(string); ok { 64 | dateValue := isDate(s) 65 | results[row][key] = dateValue 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | func findDateFields(results []map[string]any) map[string]*bool { 73 | dateFields := make(map[string]*bool) 74 | for _, result := range results { 75 | if len(dateFields) == len(result) { 76 | break 77 | } 78 | for key, value := range result { 79 | if value == nil { 80 | continue 81 | } 82 | 83 | isDateField := dateFields[key] 84 | if isDateField != nil { 85 | continue 86 | } 87 | 88 | if s, ok := value.(string); ok { 89 | if s == "" { 90 | continue 91 | } 92 | 93 | dateValue := isDate(s) 94 | flag := true 95 | flagPtr := &flag 96 | if dateValue != nil { 97 | dateFields[key] = flagPtr 98 | } else { 99 | *flagPtr = false 100 | } 101 | } 102 | } 103 | } 104 | return dateFields 105 | } 106 | 107 | func isDate(s string) *time.Time { 108 | val, err := dateparse.ParseStrict(s) 109 | if err != nil { 110 | return nil 111 | } 112 | return &val 113 | } 114 | -------------------------------------------------------------------------------- /duck/data/parquet.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "time" 8 | 9 | "github.com/apache/arrow/go/v15/parquet" 10 | "github.com/apache/arrow/go/v15/parquet/pqarrow" 11 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 12 | "github.com/grafana/grafana-plugin-sdk-go/data" 13 | ) 14 | 15 | var logger = log.DefaultLogger 16 | 17 | func ToParquet(frames []*data.Frame, chunk int) (map[string]string, error) { 18 | dirs := map[string]string{} 19 | frameIndex := framesByRef(frames) 20 | 21 | // TODO - appending lables to fields for now 22 | // need to return multiple frames instead 23 | // for _, f := range frames { 24 | // for _, fld := range f.Fields { 25 | // if len(fld.Labels) > 0 { 26 | // lbls := fld.Labels.String() 27 | // fld.Name = fmt.Sprintf("%s %s", fld.Name, lbls) 28 | // } 29 | // } 30 | // } 31 | writerProps := parquet.NewWriterProperties() 32 | SIZELEN := int64(1024 * 1024) 33 | 34 | for _, frameList := range frameIndex { 35 | 36 | labelsToFields(frameList) 37 | 38 | dir, err := os.MkdirTemp("", "duck") 39 | if err != nil { 40 | logger.Error("failed to create temp dir", "error", err) 41 | return nil, err 42 | } 43 | 44 | mergeFrames(frameList) 45 | for i, frame := range frameList { 46 | dirs[frame.RefID] = dir 47 | 48 | // Mutate the name to match display name 49 | // OK, because framesByRef already clones the field? 50 | for _, f := range frame.Fields { 51 | if f.Config != nil && f.Config.DisplayName != "" { 52 | f.Name = f.Config.DisplayName 53 | f.Config.DisplayName = "" 54 | } 55 | } 56 | 57 | table, err := data.FrameToArrowTable(frame) 58 | if err != nil { 59 | logger.Error("failed to create arrow table", "error", err) 60 | return nil, err 61 | } 62 | defer table.Release() 63 | 64 | name := fmt.Sprintf("%s%d", frame.RefID, i) 65 | filename := path.Join(dir, name+".parquet") 66 | output, err := os.Create(filename) 67 | if err != nil { 68 | logger.Error("failed to create parquet file", "file", filename, "error", err) 69 | return nil, err 70 | } 71 | defer output.Close() 72 | 73 | err = pqarrow.WriteTable(table, output, SIZELEN, writerProps, pqarrow.DefaultWriterProps()) 74 | if err != nil { 75 | logger.Error("error writing parquet", "error", err) 76 | return nil, err 77 | } 78 | } 79 | } 80 | return dirs, nil 81 | } 82 | 83 | func framesByRef(frames []*data.Frame) map[string][]*data.Frame { 84 | byRef := map[string][]*data.Frame{} 85 | for _, f := range frames { 86 | fr := byRef[f.RefID] 87 | if fr == nil { 88 | refFrames := []*data.Frame{} 89 | byRef[f.RefID] = refFrames 90 | } 91 | byRef[f.RefID] = append(byRef[f.RefID], clone(f)) 92 | } 93 | return byRef 94 | } 95 | 96 | func clone(f *data.Frame) *data.Frame { 97 | copy := data.NewFrame(f.Name, f.Fields...) 98 | copy.RefID = f.RefID 99 | copy.Meta = f.Meta 100 | return copy 101 | } 102 | 103 | func mergeFrames(frames []*data.Frame) { 104 | fields := map[string]*data.Field{} 105 | for _, f := range frames { 106 | for _, fld := range f.Fields { 107 | fields[fld.Name] = fld 108 | } 109 | } 110 | for _, fld := range fields { 111 | for _, f := range frames { 112 | found := false 113 | for _, fld2 := range f.Fields { 114 | if fld2.Name == fld.Name { 115 | found = true 116 | break 117 | } 118 | } 119 | if !found { 120 | makeArray := maker[fld.Type()] 121 | arr := makeArray(f.Rows()) 122 | nullField := data.NewField(fld.Name, fld.Labels, arr) 123 | f.Fields = append(f.Fields, nullField) 124 | } 125 | } 126 | } 127 | } 128 | 129 | var maker = map[data.FieldType]func(length int) any{ 130 | data.FieldTypeFloat64: func(length int) any { return makeArray[float64](length) }, 131 | data.FieldTypeFloat32: func(length int) any { return makeArray[float32](length) }, 132 | data.FieldTypeInt16: func(length int) any { return makeArray[int16](length) }, 133 | data.FieldTypeInt64: func(length int) any { return makeArray[int64](length) }, 134 | data.FieldTypeInt8: func(length int) any { return makeArray[int8](length) }, 135 | data.FieldTypeUint8: func(length int) any { return makeArray[uint8](length) }, 136 | data.FieldTypeUint16: func(length int) any { return makeArray[uint16](length) }, 137 | data.FieldTypeUint32: func(length int) any { return makeArray[uint32](length) }, 138 | data.FieldTypeUint64: func(length int) any { return makeArray[uint64](length) }, 139 | data.FieldTypeNullableFloat64: func(length int) any { return makeArray[*float64](length) }, 140 | data.FieldTypeNullableFloat32: func(length int) any { return makeArray[*float32](length) }, 141 | data.FieldTypeNullableInt16: func(length int) any { return makeArray[*int16](length) }, 142 | data.FieldTypeNullableInt64: func(length int) any { return makeArray[*int64](length) }, 143 | data.FieldTypeNullableInt8: func(length int) any { return makeArray[*int8](length) }, 144 | data.FieldTypeNullableUint8: func(length int) any { return makeArray[*uint8](length) }, 145 | data.FieldTypeNullableUint16: func(length int) any { return makeArray[*uint16](length) }, 146 | data.FieldTypeNullableUint32: func(length int) any { return makeArray[*uint32](length) }, 147 | data.FieldTypeNullableUint64: func(length int) any { return makeArray[*uint64](length) }, 148 | data.FieldTypeString: func(length int) any { return makeArray[string](length) }, 149 | data.FieldTypeNullableString: func(length int) any { return makeArray[*string](length) }, 150 | data.FieldTypeTime: func(length int) any { return makeArray[time.Time](length) }, 151 | data.FieldTypeNullableTime: func(length int) any { return makeArray[*time.Time](length) }, 152 | } 153 | 154 | func makeArray[T any](length int) []T { 155 | return make([]T, length) 156 | } 157 | 158 | func labelsToFields(frames []*data.Frame) { 159 | for _, f := range frames { 160 | fields := []*data.Field{} 161 | for _, fld := range f.Fields { 162 | if fld.Labels != nil { 163 | for lbl, val := range fld.Labels { 164 | newFld := newField(lbl, val, f.Rows()) 165 | fields = append(fields, newFld) 166 | } 167 | } 168 | } 169 | f.Fields = append(f.Fields, fields...) 170 | } 171 | } 172 | 173 | func newField(name string, val string, size int) *data.Field { 174 | values := make([]string, size) 175 | newField := data.NewField(name, nil, values) 176 | for i := 0; i < size; i++ { 177 | newField.Set(i, val) 178 | } 179 | return newField 180 | } 181 | -------------------------------------------------------------------------------- /duck/data/parquet_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | "testing" 8 | 9 | "github.com/grafana/grafana-plugin-sdk-go/data" 10 | ) 11 | 12 | func TestWrite(t *testing.T) { 13 | var values = []string{"test"} 14 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 15 | frame.RefID = "foo" 16 | frames := []*data.Frame{frame} 17 | 18 | dir, err := ToParquet(frames, 0) 19 | if err != nil { 20 | fmt.Println(err.Error()) 21 | t.Fail() 22 | } 23 | fmt.Println(dir) 24 | } 25 | 26 | func TestRead(t *testing.T) { 27 | t.Skip() // need parquet file to test 28 | fmt.Println("test") 29 | var b bytes.Buffer 30 | b.Write([]byte(".mode json \n")) 31 | b.Write([]byte("SELECT * from foo.parquet; \n")) 32 | 33 | var stdout bytes.Buffer 34 | var stderr bytes.Buffer 35 | 36 | cmd := exec.Command("duckdb", "") 37 | cmd.Stdin = &b 38 | cmd.Stdout = &stdout 39 | cmd.Stderr = &stderr 40 | err := cmd.Run() // add error checking 41 | if err != nil { 42 | t.Fail() 43 | } 44 | 45 | fmt.Println(stdout.String()) 46 | fmt.Println(stderr.String()) 47 | } 48 | -------------------------------------------------------------------------------- /duck/duckdb.go: -------------------------------------------------------------------------------- 1 | package duck 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | 12 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 13 | sdk "github.com/grafana/grafana-plugin-sdk-go/data" 14 | "github.com/grafana/grafana-plugin-sdk-go/data/framestruct" 15 | "github.com/hairyhenderson/go-which" 16 | "github.com/iancoleman/orderedmap" 17 | "github.com/jeremywohl/flatten" 18 | "github.com/scottlepp/go-duck/duck/data" 19 | ) 20 | 21 | var logger = log.DefaultLogger 22 | 23 | type Dirs map[string]string 24 | 25 | type DuckDB struct { 26 | Name string 27 | mode string 28 | format string 29 | exe string 30 | chunk int 31 | cacheDuration int 32 | cache cache 33 | docker bool 34 | image string 35 | } 36 | 37 | type Opts struct { 38 | Mode string 39 | Format string 40 | Chunk int 41 | Exe string 42 | CacheDuration int 43 | Docker bool 44 | Image string 45 | } 46 | 47 | const newline = "\n" 48 | const duckdbImage = "datacatering/duckdb:v1.0.0" 49 | 50 | var tempDir = getTempDir() 51 | 52 | // NewInMemoryDB creates a new in-memory DuckDB 53 | func NewInMemoryDB(opts ...Opts) *DuckDB { 54 | return NewDuckDB("", opts...) 55 | } 56 | 57 | // NewDuckDB creates a new DuckDB 58 | func NewDuckDB(name string, opts ...Opts) *DuckDB { 59 | db := DuckDB{ 60 | Name: name, 61 | mode: "json", 62 | format: "parquet", 63 | } 64 | for _, opt := range opts { 65 | if opt.Mode != "" { 66 | db.mode = opt.Mode 67 | } 68 | if opt.Format != "" { 69 | db.format = opt.Format 70 | } 71 | if opt.Exe != "" { 72 | db.exe = opt.Exe 73 | } 74 | if opt.Chunk > 0 { 75 | db.chunk = opt.Chunk 76 | } 77 | if opt.CacheDuration > 0 { 78 | db.cacheDuration = opt.CacheDuration 79 | } 80 | db.image = duckdbImage 81 | if opt.Image != "" { 82 | db.image = opt.Image 83 | } 84 | db.docker = opt.Docker 85 | } 86 | 87 | // Find the executable if it is not configured 88 | if db.exe == "" && !db.docker { 89 | db.exe = which.Which("duckdb") 90 | if db.exe == "" { 91 | db.exe = "/usr/local/bin/duckdb" 92 | } 93 | } 94 | db.cache = cache{} 95 | return &db 96 | } 97 | 98 | // RunCommands runs a series of of sql commands against duckdb 99 | func (d *DuckDB) RunCommands(commands []string) (string, error) { 100 | return d.runCommands(commands) 101 | } 102 | 103 | // Query runs a query against the database. For Databases that are NOT in-memory. 104 | func (d *DuckDB) Query(query string) (string, error) { 105 | return d.RunCommands([]string{query}) 106 | } 107 | 108 | // QueryFrame will load a dataframe into a view named RefID, and run the query against that view 109 | func (d *DuckDB) QueryFrames(name string, query string, frames []*sdk.Frame) (string, bool, error) { 110 | err := d.validate(query) 111 | if err != nil { 112 | return "", false, err 113 | } 114 | data := FrameData{ 115 | cacheDuration: d.cacheDuration, 116 | cache: &d.cache, 117 | db: d, 118 | } 119 | 120 | return data.Query(name, query, frames) 121 | } 122 | 123 | func wipe(dirs map[string]string) { 124 | for _, dir := range dirs { 125 | err := os.RemoveAll(dir) 126 | if err != nil { 127 | logger.Error("failed to remove parquet files", "error", err) 128 | } 129 | } 130 | } 131 | 132 | func (d *DuckDB) QueryFramesToFrames(name string, query string, frames []*sdk.Frame) (*sdk.Frame, error) { 133 | err := d.validate(query) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | f := &sdk.Frame{} 139 | res, cached, err := d.QueryFrames(name, query, frames) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | err = resultsToFrame(name, res, f, frames) 145 | if err != nil { 146 | return nil, err 147 | } 148 | if cached { 149 | for _, frame := range frames { 150 | if frame.Meta == nil { 151 | frame.Meta = &sdk.FrameMeta{} 152 | } 153 | notice := sdk.Notice{ 154 | Severity: sdk.NoticeSeverityInfo, 155 | Text: "Data retrieved from cache", 156 | } 157 | frame.Meta.Notices = append(frame.Meta.Notices, notice) 158 | } 159 | } 160 | return f, nil 161 | } 162 | 163 | // Destroy will remove database files created by duckdb 164 | func (d *DuckDB) Destroy() error { 165 | if d.Name != "" { 166 | return os.Remove(d.Name) 167 | } 168 | return nil 169 | } 170 | 171 | func resultsToFrame(name string, res string, f *sdk.Frame, frames []*sdk.Frame) error { 172 | if res == "" { 173 | return nil 174 | } 175 | var results []map[string]any 176 | err := json.Unmarshal([]byte(res), &results) 177 | if err != nil { 178 | logger.Error("error unmarshalling results", "error", err) 179 | return err 180 | } 181 | 182 | data.ConvertDateFields(results) 183 | 184 | converters := data.Converters(frames) 185 | resultsFrame, err := framestruct.ToDataFrame(name, results, converters...) 186 | 187 | if err != nil { 188 | logger.Error("error converting results to frame", "error", err) 189 | return err 190 | } 191 | 192 | // Order the fields in the same order as the source frame: 193 | // Build a slice of ordered keys 194 | 195 | var orderedKeys []string 196 | var temp []orderedmap.OrderedMap 197 | err = json.Unmarshal([]byte(res), &temp) 198 | if err == nil { 199 | orderedKeys = temp[0].Keys() 200 | } 201 | 202 | // Create a map of column names to indexes 203 | columnIndex := make(map[string]int) 204 | for i, field := range resultsFrame.Fields { 205 | columnIndex[field.Name] = i 206 | } 207 | // Add columns to the DataFrame 208 | for _, key := range orderedKeys { 209 | i := columnIndex[key] 210 | f.Fields = append(f.Fields, resultsFrame.Fields[i]) 211 | } 212 | 213 | f.Name = resultsFrame.Name 214 | f.Meta = resultsFrame.Meta 215 | f.RefID = resultsFrame.RefID 216 | 217 | kind := f.TimeSeriesSchema().Type 218 | if kind == sdk.TimeSeriesTypeLong { 219 | fillMode := &sdk.FillMissing{Mode: sdk.FillModeNull} 220 | frame, err := sdk.LongToWide(f, fillMode) 221 | if err != nil { 222 | logger.Warn("could not convert frame long to wide", "error", err) 223 | return nil 224 | } 225 | f.Fields = frame.Fields 226 | f.Meta = frame.Meta 227 | return nil 228 | } 229 | 230 | if kind == sdk.TimeSeriesTypeWide { 231 | if f.Meta == nil { 232 | f.Meta = &sdk.FrameMeta{} 233 | } 234 | f.Meta.Type = sdk.FrameTypeTimeSeriesWide 235 | } 236 | 237 | // TODO - appending to field names for now 238 | // applyLabels(*resultsFrame, frames) 239 | 240 | return nil 241 | } 242 | 243 | func (d *DuckDB) runCommands(commands []string) (string, error) { 244 | var stdout bytes.Buffer 245 | var stderr bytes.Buffer 246 | 247 | var b bytes.Buffer 248 | b.Write([]byte(fmt.Sprintf(".mode %s %s", d.mode, newline))) 249 | for _, c := range commands { 250 | cmd := fmt.Sprintf("%s %s", c, newline) 251 | b.Write([]byte(cmd)) 252 | } 253 | 254 | var cmd *exec.Cmd 255 | if d.docker { 256 | volume := fmt.Sprintf("%s:%s", tempDir, tempDir) 257 | logger.Debug("running command in docker", "volume", volume, "image", duckdbImage) 258 | cmd = exec.Command("docker", "run", "-i", "-v", volume, duckdbImage) 259 | } else { 260 | cmd = exec.Command(d.exe, d.Name) 261 | } 262 | cmd.Stdin = &b 263 | cmd.Stdout = &stdout 264 | cmd.Stderr = &stderr 265 | 266 | err := cmd.Run() 267 | if err != nil { 268 | message := err.Error() + stderr.String() 269 | logger.Error("error running command", "cmd", b.String(), "message", message, "error", err) 270 | return "", errors.New(message) 271 | } 272 | if stderr.String() != "" { 273 | logger.Error("error running command", "cmd", b.String(), "error", stderr.String()) 274 | return "", errors.New(stderr.String()) 275 | } 276 | return stdout.String(), nil 277 | } 278 | 279 | // TODO 280 | 281 | // func applyLabels(resultsFrame sdk.Frame, sourceFrames []*sdk.Frame) { 282 | // for _, fld := range resultsFrame.Fields { 283 | // for _, f := range sourceFrames { 284 | // srcField := find(f, fld) 285 | // if srcField != nil { 286 | // fld.Labels = srcField.Labels 287 | // break 288 | // } 289 | // } 290 | // } 291 | // } 292 | 293 | // func find(f *sdk.Frame, fld *sdk.Field) *sdk.Field { 294 | // for _, sfld := range f.Fields { 295 | // if sfld.Name == fld.Name { 296 | // return sfld 297 | // } 298 | // } 299 | // return nil 300 | // } 301 | 302 | func getTempDir() string { 303 | temp := os.Getenv("TMPDIR") 304 | if temp == "" { 305 | temp = "/tmp" 306 | } 307 | return temp 308 | } 309 | 310 | const ( 311 | TABLE_NAME = "table_name" 312 | ERROR = ".error" 313 | ERROR_MESSAGE = ".error_message" 314 | ) 315 | 316 | func (d *DuckDB) validate(rawSQL string) error { 317 | rawSQL = strings.Replace(rawSQL, "'", "''", -1) 318 | cmd := fmt.Sprintf("SELECT json_serialize_sql('%s')", rawSQL) 319 | ret, err := d.RunCommands([]string{cmd}) 320 | if err != nil { 321 | logger.Error("error validating sql", "error", err.Error(), "sql", rawSQL, "cmd", cmd) 322 | return fmt.Errorf("error validating sql: %s", err.Error()) 323 | } 324 | 325 | result := []map[string]any{} 326 | err = json.Unmarshal([]byte(ret), &result) 327 | if err != nil { 328 | logger.Error("error converting json sql to ast", "error", err.Error(), "ret", ret) 329 | return fmt.Errorf("error converting json to ast: %s", err.Error()) 330 | } 331 | 332 | if len(result) == 0 { 333 | logger.Error("no ast returned", "ret", ret) 334 | } 335 | 336 | var ast map[string]any 337 | for _, v := range result[0] { 338 | validAst, ok := v.(map[string]any) 339 | if !ok { 340 | logger.Error("invalid sql", "sql", ret) 341 | return fmt.Errorf("invalid sql: %s", ret) 342 | } 343 | ast = validAst 344 | break 345 | } 346 | 347 | errMsg := ast["error"] 348 | if errMsg != nil { 349 | errMsgBool, ok := errMsg.(bool) 350 | if !ok { 351 | logger.Error("error in ast", "error", ret) 352 | return fmt.Errorf("error in ast: %v", ret) 353 | } 354 | if errMsgBool { 355 | logger.Error("error in ast", "error", ret) 356 | return fmt.Errorf("error in ast: %v", ret) 357 | } 358 | } 359 | 360 | statements := ast["statements"] 361 | if statements == nil { 362 | logger.Error("no statements in ast", "ast", ast) 363 | return fmt.Errorf("no statements in ast: %v", ast) 364 | } 365 | 366 | flat, err := flatten.Flatten(ast, "", flatten.DotStyle) 367 | if err != nil { 368 | logger.Error("error flattening ast", "error", err.Error(), "ast", ast) 369 | return fmt.Errorf("error flattening ast: %s", err.Error()) 370 | } 371 | 372 | for k, v := range flat { 373 | if strings.HasSuffix(k, ERROR) { 374 | v, ok := v.(bool) 375 | if ok && v { 376 | logger.Error("error in sql", "error", k) 377 | return fmt.Errorf("error flattening ast: %s", k) 378 | } 379 | } 380 | if strings.Contains(k, "from_table.function.function_name") { 381 | logger.Error("function not allowed", "function", v) 382 | return fmt.Errorf("function not allowed: %s", v) 383 | } 384 | if strings.HasSuffix(k, "from_table.table_name") { 385 | v, ok := v.(string) 386 | if ok && strings.Contains(v, ".") { 387 | logger.Error("table names with . not allowed", "table", v) 388 | return fmt.Errorf("table names with . not allowed: %s", v) 389 | } 390 | } 391 | } 392 | 393 | return nil 394 | } 395 | -------------------------------------------------------------------------------- /duck/duckdb_test.go: -------------------------------------------------------------------------------- 1 | package duck 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/araddon/dateparse" 9 | "github.com/grafana/grafana-plugin-sdk-go/data" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestCommands(t *testing.T) { 14 | db := NewInMemoryDB() 15 | 16 | commands := []string{ 17 | "CREATE TABLE t1 (i INTEGER, j INTEGER);", 18 | "INSERT INTO t1 VALUES (1, 5);", 19 | "SELECT * from t1;", 20 | } 21 | res, err := db.RunCommands(commands) 22 | if err != nil { 23 | t.Fail() 24 | return 25 | } 26 | assert.Contains(t, res, `[{"i":1,"j":5}]`) 27 | } 28 | 29 | func TestDotCommands(t *testing.T) { 30 | db := NewInMemoryDB() 31 | 32 | commands := []string{ 33 | ".databases", 34 | } 35 | res, err := db.RunCommands(commands) 36 | if err != nil { 37 | t.Fail() 38 | return 39 | } 40 | assert.Contains(t, res, `memory`) 41 | } 42 | 43 | func TestCommandsDocker(t *testing.T) { 44 | db := NewInMemoryDB(Opts{Docker: true}) 45 | 46 | commands := []string{ 47 | "CREATE TABLE t1 (i INTEGER, j INTEGER);", 48 | "INSERT INTO t1 VALUES (1, 5);", 49 | "SELECT * from t1;", 50 | } 51 | res, err := db.RunCommands(commands) 52 | if err != nil { 53 | t.Fail() 54 | return 55 | } 56 | assert.Contains(t, res, `[{"i":1,"j":5}]`) 57 | } 58 | 59 | func TestQuery(t *testing.T) { 60 | db := NewDuckDB("foo") 61 | 62 | commands := []string{ 63 | "CREATE TABLE t1 (i INTEGER, j INTEGER);", 64 | "INSERT INTO t1 VALUES (1, 5);", 65 | } 66 | _, err := db.RunCommands(commands) 67 | assert.Nil(t, err) 68 | 69 | res, err := db.Query("SELECT * from t1;") 70 | assert.Nil(t, err) 71 | assert.Contains(t, res, `[{"i":1,"j":5}]`) 72 | 73 | err = db.Destroy() 74 | assert.Nil(t, err) 75 | } 76 | 77 | func TestQueryFrame(t *testing.T) { 78 | db := NewInMemoryDB() 79 | 80 | var values = []string{"test"} 81 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 82 | frame.RefID = "foo" 83 | frames := []*data.Frame{frame} 84 | 85 | res, _, err := db.QueryFrames("foo", "select * from foo", frames) 86 | assert.Nil(t, err) 87 | 88 | assert.Contains(t, res, `[{"value":"test"}]`) 89 | } 90 | 91 | func TestQueryAgg(t *testing.T) { 92 | db := NewInMemoryDB() 93 | 94 | var values = []string{"test"} 95 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 96 | frame.RefID = "foo" 97 | frames := []*data.Frame{frame} 98 | 99 | res, _, err := db.QueryFrames("foo", "select min(value) as value from foo", frames) 100 | assert.Nil(t, err) 101 | 102 | assert.Contains(t, res, `[{"value":"test"}]`) 103 | } 104 | 105 | func TestQueryJson(t *testing.T) { 106 | db := NewInMemoryDB() 107 | 108 | var values = []string{"test"} 109 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 110 | frame.RefID = "foo" 111 | frames := []*data.Frame{frame} 112 | 113 | _, _, err := db.QueryFrames("foo", "SELECT * FROM read_json('todos.json')", frames) 114 | assert.NotNil(t, err) 115 | } 116 | 117 | func TestValid(t *testing.T) { 118 | db := NewInMemoryDB() 119 | 120 | var values = []string{"test"} 121 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 122 | frame.RefID = "foo" 123 | frames := []*data.Frame{frame} 124 | 125 | query := fmt.Sprintf(".databases %s", newline) 126 | _, _, err := db.QueryFrames("foo", query, frames) 127 | assert.NotNil(t, err) 128 | } 129 | 130 | func TestQueryFrameNoFileRead(t *testing.T) { 131 | db := NewInMemoryDB() 132 | 133 | var values = []string{"test"} 134 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 135 | frame.RefID = "foo" 136 | frames := []*data.Frame{frame} 137 | 138 | _, _, err := db.QueryFrames("foo", "SELECT * FROM read_csv('flights.csv')", frames) 139 | assert.NotNil(t, err) 140 | 141 | _, _, err = db.QueryFrames("foo", "SELECT * FROM read_json('flights.json')", frames) 142 | assert.NotNil(t, err) 143 | 144 | _, _, err = db.QueryFrames("foo", "SELECT * FROM 'test.parquet'", frames) 145 | assert.NotNil(t, err) 146 | 147 | _, _, err = db.QueryFrames("foo", "COPY test FROM 'test.parquet'", frames) 148 | assert.NotNil(t, err) 149 | } 150 | 151 | func TestQueryFrameCache(t *testing.T) { 152 | opts := Opts{ 153 | CacheDuration: 5, 154 | } 155 | db := NewInMemoryDB(opts) 156 | 157 | var values = []string{"test"} 158 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 159 | frame.RefID = "foo" 160 | frames := []*data.Frame{frame} 161 | 162 | res, cached, err := db.QueryFrames("foo", "select * from foo", frames) 163 | assert.Nil(t, err) 164 | assert.False(t, cached) 165 | assert.Contains(t, res, `[{"value":"test"}]`) 166 | 167 | res, cached, err = db.QueryFrames("foo", "select * from foo", frames) 168 | assert.Nil(t, err) 169 | assert.True(t, cached) 170 | assert.Contains(t, res, `[{"value":"test"}]`) 171 | 172 | // wait for cache to expire 173 | time.Sleep(6 * time.Second) 174 | 175 | res, cached, err = db.QueryFrames("foo", "select * from foo", frames) 176 | assert.Nil(t, err) 177 | assert.False(t, cached) 178 | assert.Contains(t, res, `[{"value":"test"}]`) 179 | } 180 | 181 | func TestQueryFrameWithDisplayName(t *testing.T) { 182 | 183 | db := NewInMemoryDB() 184 | 185 | var values = []string{"test"} 186 | field := data.NewField("value", nil, values) 187 | field.Config = &data.FieldConfig{ 188 | DisplayName: "some value", 189 | } 190 | frame := data.NewFrame("foo", field) 191 | frame.RefID = "foo" 192 | frames := []*data.Frame{frame} 193 | 194 | res, _, err := db.QueryFrames("foo", "select * from foo", frames) 195 | assert.Nil(t, err) 196 | 197 | assert.Contains(t, res, `[{"some value":"test"}]`) 198 | } 199 | 200 | func TestQueryFrameChunks(t *testing.T) { 201 | opts := Opts{ 202 | Chunk: 3, 203 | } 204 | db := NewInMemoryDB(opts) 205 | 206 | var values = []string{"test", "test", "test", "test", "test", "test2"} 207 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 208 | frame.RefID = "foo" 209 | frames := []*data.Frame{frame} 210 | 211 | res, _, err := db.QueryFrames("foo", "select * from foo", frames) 212 | assert.Nil(t, err) 213 | 214 | assert.Contains(t, res, `test2`) 215 | } 216 | 217 | func TestQueryFrameIntoFrame(t *testing.T) { 218 | db := NewInMemoryDB() 219 | 220 | var values = []string{"2024-02-23 09:01:54"} 221 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 222 | frame.RefID = "foo" 223 | 224 | var values2 = []string{"2024-02-23 09:02:54"} 225 | frame2 := data.NewFrame("foo", data.NewField("value", nil, values2)) 226 | frame2.RefID = "foo" 227 | 228 | frames := []*data.Frame{frame, frame2} 229 | 230 | model, err := db.QueryFramesToFrames("foo", "select * from foo order by value desc", frames) 231 | assert.Nil(t, err) 232 | 233 | assert.Equal(t, 2, model.Rows()) 234 | 235 | txt, err := model.StringTable(-1, -1) 236 | assert.Nil(t, err) 237 | 238 | fmt.Printf("GOT: %s", txt) 239 | } 240 | 241 | func TestQueryFrameIntoFrameDocker(t *testing.T) { 242 | db := NewInMemoryDB(Opts{Docker: true}) 243 | 244 | var values = []string{"2024-02-23 09:01:54"} 245 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 246 | frame.RefID = "foo" 247 | 248 | var values2 = []string{"2024-02-23 09:02:54"} 249 | frame2 := data.NewFrame("foo", data.NewField("value", nil, values2)) 250 | frame2.RefID = "foo" 251 | 252 | frames := []*data.Frame{frame, frame2} 253 | 254 | model, err := db.QueryFramesToFrames("foo", "select * from foo order by value desc", frames) 255 | assert.Nil(t, err) 256 | 257 | assert.Equal(t, 2, model.Rows()) 258 | 259 | txt, err := model.StringTable(-1, -1) 260 | assert.Nil(t, err) 261 | 262 | fmt.Printf("GOT: %s", txt) 263 | } 264 | 265 | func TestQueryFrameIntoFrameMultipleColumns(t *testing.T) { 266 | db := NewInMemoryDB() 267 | 268 | frame := data.NewFrame( 269 | "A", 270 | data.NewField("Z State", nil, []string{"Alaska"}), 271 | data.NewField("Y Lat", nil, []string{"61"}), 272 | data.NewField("X Lng", nil, []string{"32"}), 273 | ) 274 | frame.RefID = "A" 275 | 276 | frames := []*data.Frame{frame} 277 | 278 | model, err := db.QueryFramesToFrames("B", "select * from A", frames) 279 | assert.Nil(t, err) 280 | 281 | assert.Equal(t, "Z State", model.Fields[0].Name) 282 | assert.Equal(t, "Y Lat", model.Fields[1].Name) 283 | assert.Equal(t, "X Lng", model.Fields[2].Name) 284 | 285 | txt, err := model.StringTable(-1, -1) 286 | assert.Nil(t, err) 287 | 288 | fmt.Printf("GOT: %s", txt) 289 | } 290 | 291 | func TestMultiFrame(t *testing.T) { 292 | db := NewInMemoryDB() 293 | 294 | var values = []string{"test"} 295 | frame := data.NewFrame("foo", data.NewField("value1", nil, values)) 296 | frame.RefID = "foo" 297 | 298 | var values2 = []string{"foo"} 299 | frame2 := data.NewFrame("foo", data.NewField("value2", nil, values2)) 300 | frame2.RefID = "foo" 301 | 302 | frames := []*data.Frame{frame, frame2} 303 | 304 | model, err := db.QueryFramesToFrames("foo", "select * from foo", frames) 305 | assert.Nil(t, err) 306 | 307 | assert.Equal(t, 2, model.Rows()) 308 | txt, err := model.StringTable(-1, -1) 309 | assert.Nil(t, err) 310 | 311 | fmt.Printf("GOT: %s", txt) 312 | } 313 | 314 | func TestMultiFrame2(t *testing.T) { 315 | db := NewInMemoryDB() 316 | 317 | f := new(float64) 318 | *f = 12345 319 | 320 | var values = []*float64{f} 321 | frame := data.NewFrame("foo", data.NewField("value1", nil, values)) 322 | frame.RefID = "foo" 323 | 324 | var values2 = []*float64{f} 325 | frame2 := data.NewFrame("foo", data.NewField("value2", nil, values2)) 326 | frame2.RefID = "foo" 327 | 328 | frames := []*data.Frame{frame, frame2} 329 | 330 | model, err := db.QueryFramesToFrames("foo", "select * from foo", frames) 331 | assert.Nil(t, err) 332 | 333 | assert.Equal(t, 2, model.Rows()) 334 | txt, err := model.StringTable(-1, -1) 335 | assert.Nil(t, err) 336 | 337 | fmt.Printf("GOT: %s", txt) 338 | } 339 | 340 | func TestTimestamps(t *testing.T) { 341 | db := NewInMemoryDB() 342 | 343 | tt := "2024-02-23 09:01:54" 344 | dd, err := dateparse.ParseAny(tt) 345 | assert.Nil(t, err) 346 | 347 | var values = []time.Time{dd} 348 | frame := data.NewFrame("foo", data.NewField("value", nil, values)) 349 | frame.RefID = "foo" 350 | 351 | frames := []*data.Frame{frame} 352 | 353 | model, err := db.QueryFramesToFrames("foo", "select * from foo", frames) 354 | assert.Nil(t, err) 355 | 356 | assert.Equal(t, 1, model.Rows()) 357 | txt, err := model.StringTable(-1, -1) 358 | assert.Nil(t, err) 359 | 360 | fmt.Printf("GOT: %s", txt) 361 | assert.Contains(t, txt, "Type: []*time.Time") 362 | } 363 | 364 | func TestTimeSeries(t *testing.T) { 365 | db := NewInMemoryDB() 366 | 367 | tt := "2024-02-23 09:01:54" 368 | dd, err := dateparse.ParseAny(tt) 369 | assert.Nil(t, err) 370 | 371 | var values = []time.Time{dd} 372 | timeField := data.NewField("time", nil, values) 373 | 374 | groupField := data.NewField("group", nil, []string{"A"}) 375 | valueField := data.NewField("value", nil, []*float64{new(float64)}) 376 | 377 | frame := data.NewFrame("foo", timeField, groupField, valueField) 378 | frame.RefID = "foo" 379 | 380 | frames := []*data.Frame{frame} 381 | 382 | model, err := db.QueryFramesToFrames("foo", "select * from foo", frames) 383 | assert.Nil(t, err) 384 | 385 | assert.Equal(t, data.FrameTypeTimeSeriesWide, model.Meta.Type) 386 | 387 | assert.Equal(t, 1, model.Rows()) 388 | txt, err := model.StringTable(-1, -1) 389 | assert.Nil(t, err) 390 | 391 | fmt.Printf("GOT: %s", txt) 392 | assert.Contains(t, txt, "Type: []time.Time") 393 | } 394 | 395 | func TestTimeSeriesWide(t *testing.T) { 396 | db := NewInMemoryDB() 397 | 398 | tt := "2024-02-23 09:01:54" 399 | dd, err := dateparse.ParseAny(tt) 400 | assert.Nil(t, err) 401 | 402 | var values = []time.Time{dd} 403 | timeField := data.NewField("time", nil, values) 404 | valueField := data.NewField("value", nil, []*float64{new(float64)}) 405 | 406 | frame := data.NewFrame("foo", timeField, valueField) 407 | frame.RefID = "foo" 408 | 409 | frames := []*data.Frame{frame} 410 | 411 | model, err := db.QueryFramesToFrames("foo", "select * from foo", frames) 412 | assert.Nil(t, err) 413 | 414 | assert.Equal(t, data.FrameTypeTimeSeriesWide, model.Meta.Type) 415 | 416 | assert.Equal(t, 1, model.Rows()) 417 | txt, err := model.StringTable(-1, -1) 418 | assert.Nil(t, err) 419 | 420 | fmt.Printf("GOT: %s", txt) 421 | assert.Contains(t, txt, "Type: []*time.Time") 422 | } 423 | 424 | func TestLabels(t *testing.T) { 425 | db := NewInMemoryDB() 426 | 427 | f := new(float64) 428 | *f = 12345 429 | 430 | var values = []*float64{f} 431 | labels := map[string]string{ 432 | "server": "A", 433 | } 434 | frame := data.NewFrame("foo", data.NewField("value1", labels, values)) 435 | frame.RefID = "foo" 436 | 437 | var values2 = []*float64{f} 438 | labels2 := map[string]string{ 439 | "server": "B", 440 | } 441 | frame2 := data.NewFrame("foo", data.NewField("value2", labels2, values2)) 442 | frame2.RefID = "foo" 443 | 444 | frames := []*data.Frame{frame, frame2} 445 | 446 | model, err := db.QueryFramesToFrames("foo", "select * from foo", frames) 447 | assert.Nil(t, err) 448 | 449 | assert.Equal(t, 2, model.Rows()) 450 | txt, err := model.StringTable(-1, -1) 451 | assert.Nil(t, err) 452 | 453 | fmt.Printf("GOT: %s", txt) 454 | 455 | assert.Contains(t, txt, "server") 456 | assert.Contains(t, txt, "A") 457 | assert.Contains(t, txt, "B") 458 | } 459 | 460 | // TODO - neeed to return 2 frames here 461 | // or just append the labels to the fields??? 462 | func TestLabelsMultiFrame(t *testing.T) { 463 | db := NewInMemoryDB() 464 | 465 | tt := "2024-02-23 09:01:54" 466 | dd, err := dateparse.ParseAny(tt) 467 | assert.Nil(t, err) 468 | 469 | ttt := "2024-02-23 09:02:54" 470 | ddd, err := dateparse.ParseAny(ttt) 471 | assert.Nil(t, err) 472 | 473 | var timeValues = []time.Time{dd, ddd} 474 | 475 | f := new(float64) 476 | *f = 12345 477 | 478 | var values = []*float64{f, f} 479 | labels := map[string]string{ 480 | "server": "A", 481 | } 482 | frame := data.NewFrame("foo", data.NewField("timestamp", nil, timeValues), data.NewField("value", labels, values)) 483 | frame.RefID = "foo" 484 | 485 | var values2 = []*float64{f, f} 486 | labels2 := map[string]string{ 487 | "server": "B", 488 | } 489 | frame2 := data.NewFrame("foo", data.NewField("timestamp", nil, timeValues), data.NewField("value", labels2, values2)) 490 | frame2.RefID = "foo" 491 | 492 | frames := []*data.Frame{frame, frame2} 493 | 494 | // TODO - ordering is broken! 495 | model, err := db.QueryFramesToFrames("foo", "select * from foo order by timestamp desc", frames) 496 | assert.Nil(t, err) 497 | 498 | assert.Equal(t, 4, model.Rows()) 499 | txt, err := model.StringTable(-1, -1) 500 | assert.Nil(t, err) 501 | 502 | fmt.Printf("GOT: %s", txt) 503 | 504 | assert.Contains(t, txt, "server") 505 | assert.Contains(t, txt, "A") 506 | assert.Contains(t, txt, "B") 507 | } 508 | 509 | func TestTimeSeriesAggregate(t *testing.T) { 510 | db := NewInMemoryDB() 511 | 512 | tt := "2024-02-23 09:01:54" 513 | dd, err := dateparse.ParseAny(tt) 514 | assert.Nil(t, err) 515 | 516 | var values = []time.Time{dd, dd, dd} 517 | timeField := data.NewField("time", nil, values) 518 | valueField := data.NewField("value", nil, []*float64{new(float64), new(float64), new(float64)}) 519 | categoryField := data.NewField("category", nil, []string{"a", "a", "b"}) 520 | 521 | frame := data.NewFrame("foo", timeField, valueField, categoryField) 522 | frame.RefID = "foo" 523 | 524 | frames := []*data.Frame{frame} 525 | 526 | model, err := db.QueryFramesToFrames("foo", "select min(time) as t, 1 as j from foo group by category", frames) 527 | assert.Nil(t, err) 528 | 529 | assert.Equal(t, data.FrameTypeTimeSeriesWide, model.Meta.Type) 530 | 531 | assert.Equal(t, 2, model.Rows()) 532 | txt, err := model.StringTable(-1, -1) 533 | assert.Nil(t, err) 534 | 535 | fmt.Printf("GOT: %s", txt) 536 | assert.Contains(t, txt, "Type: []*time.Time") 537 | } 538 | 539 | // TODO - don't think this is valid to have a frame with duplicate fields 540 | // func TestWideFrameWithDuplicateFields(t *testing.T) { 541 | // db := NewInMemoryDB() 542 | 543 | // tt := "2024-02-23 09:01:54" 544 | // dd, err := dateparse.ParseAny(tt) 545 | // assert.Nil(t, err) 546 | 547 | // ttt := "2024-02-23 09:02:54" 548 | // ddd, err := dateparse.ParseAny(ttt) 549 | // assert.Nil(t, err) 550 | 551 | // var timeValues = []time.Time{dd, ddd} 552 | 553 | // f := new(float64) 554 | // *f = 12345 555 | 556 | // var values = []*float64{f, f} 557 | // labels := map[string]string{ 558 | // "server": "A", 559 | // } 560 | 561 | // var values2 = []*float64{f, f} 562 | // labels2 := map[string]string{ 563 | // "server": "B", 564 | // } 565 | // frame := data.NewFrame("foo", 566 | // data.NewField("timestamp", nil, timeValues), 567 | // data.NewField("value", labels, values), 568 | // data.NewField("value", labels2, values2), 569 | // ) 570 | 571 | // frame.RefID = "foo" 572 | 573 | // frames := []*data.Frame{frame} 574 | 575 | // // TODO - ordering is broken! 576 | // model := &data.Frame{} 577 | // err = db.QueryFramesInto("foo", "select * from foo order by timestamp desc", frames, model) 578 | // assert.Nil(t, err) 579 | 580 | // assert.Equal(t, 2, model.Rows()) 581 | // txt, err := model.StringTable(-1, -1) 582 | // assert.Nil(t, err) 583 | 584 | // fmt.Printf("GOT: %s", txt) 585 | 586 | // assert.Contains(t, txt, "server") 587 | // assert.Contains(t, txt, "A") 588 | // assert.Contains(t, txt, "B") 589 | // } 590 | -------------------------------------------------------------------------------- /duck/frame-data.go: -------------------------------------------------------------------------------- 1 | package duck 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | sdk "github.com/grafana/grafana-plugin-sdk-go/data" 9 | "github.com/scottlepp/go-duck/duck/data" 10 | ) 11 | 12 | type FrameData struct { 13 | cacheDuration int 14 | cache *cache 15 | db *DuckDB 16 | } 17 | 18 | func (f *FrameData) Query(name string, query string, frames []*sdk.Frame) (string, bool, error) { 19 | dirs, cached, err := f.data(name, query, frames) 20 | if err != nil { 21 | logger.Error("error converting to parquet", "error", err) 22 | return "", cached, err 23 | } 24 | 25 | defer f.postProcess(name, query, dirs, cached) 26 | 27 | // create a wait group to wait for the query to finish 28 | // if the cache duration is exceeded, wait before deleting the cache ( parquet files ) 29 | var wg sync.WaitGroup 30 | wg.Add(1) 31 | f.cache.setWait(fmt.Sprintf("%s:%s", name, query), &wg) 32 | 33 | var res string 34 | var qerr error 35 | 36 | go func() { 37 | res, qerr = f.runQuery(query, dirs, frames) 38 | wg.Done() 39 | }() 40 | 41 | wg.Wait() 42 | f.cache.deleteWait(fmt.Sprintf("%s:%s", name, query)) 43 | 44 | if qerr != nil { 45 | logger.Error("error running commands", "error", err) 46 | return "", cached, err 47 | } 48 | 49 | key := fmt.Sprintf("%s:%s", name, query) 50 | if f.cacheDuration > 0 && !cached { 51 | f.cache.set(key, dirs) 52 | } 53 | 54 | return res, cached, nil 55 | } 56 | 57 | func (f *FrameData) runQuery(query string, dirs Dirs, frames []*sdk.Frame) (string, error) { 58 | commands := createViews(frames, dirs) 59 | commands = append(commands, query) 60 | return f.db.RunCommands(commands) 61 | } 62 | 63 | func createViews(frames []*sdk.Frame, dirs Dirs) []string { 64 | commands := []string{} 65 | created := map[string]bool{} 66 | logger.Debug("starting to create views from frames", "frames", len(frames)) 67 | for _, frame := range frames { 68 | if created[frame.RefID] { 69 | continue 70 | } 71 | cmd := fmt.Sprintf("CREATE VIEW %s AS (SELECT * from '%s/*.parquet');", frame.RefID, dirs[frame.RefID]) 72 | logger.Debug("creating view", "cmd", cmd) 73 | commands = append(commands, cmd) 74 | created[frame.RefID] = true 75 | } 76 | return commands 77 | } 78 | 79 | func (f *FrameData) data(name string, query string, frames []*sdk.Frame) (Dirs, bool, error) { 80 | if f.cacheDuration > 0 { 81 | // check the cache 82 | key := fmt.Sprintf("%s:%s", name, query) 83 | if d, ok := f.cache.get(key); ok { 84 | return d, true, nil 85 | } 86 | } 87 | 88 | dirs, err := data.ToParquet(frames, f.db.chunk) 89 | return dirs, false, err 90 | } 91 | 92 | func (f *FrameData) postProcess(name string, query string, dirs Dirs, cached bool) { 93 | go func() { 94 | if f.cacheDuration == 0 { 95 | wipe(dirs) 96 | return 97 | } 98 | // if result is not cached, a new cache entry will be created 99 | // delete the new cache entry after cacheDuration 100 | if !cached { 101 | time.Sleep(time.Duration(f.cacheDuration) * time.Second) 102 | key := fmt.Sprintf("%s:%s", name, query) 103 | f.cache.delete(key) 104 | // if the query is running wait for the query to finish before deleting the parquet files 105 | wg, wait := f.cache.getWait(key) 106 | if wait { 107 | wg.Wait() 108 | wipe(dirs) 109 | return 110 | } 111 | wipe(dirs) 112 | } 113 | }() 114 | } 115 | 116 | type cache struct { 117 | store sync.Map 118 | wait sync.Map 119 | } 120 | 121 | func (c *cache) set(key string, value Dirs) { 122 | c.store.Store(key, value) 123 | } 124 | 125 | func (c *cache) get(key string) (Dirs, bool) { 126 | val, ok := c.store.Load(key) 127 | if !ok { 128 | return nil, false 129 | } 130 | return val.(Dirs), true 131 | } 132 | 133 | func (c *cache) delete(key string) { 134 | c.store.Delete(key) 135 | } 136 | 137 | func (c *cache) setWait(key string, value *sync.WaitGroup) { 138 | c.wait.Store(key, value) 139 | } 140 | 141 | func (c *cache) getWait(key string) (*sync.WaitGroup, bool) { 142 | val, ok := c.wait.Load(key) 143 | if !ok { 144 | return nil, false 145 | } 146 | return val.(*sync.WaitGroup), true 147 | } 148 | 149 | func (c *cache) deleteWait(key string) { 150 | c.wait.Delete(key) 151 | } 152 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scottlepp/go-duck 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/apache/arrow/go/v15 v15.0.2 7 | github.com/grafana/grafana-plugin-sdk-go v0.242.0 8 | github.com/hairyhenderson/go-which v0.2.0 9 | github.com/iancoleman/orderedmap v0.3.0 10 | github.com/stretchr/testify v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect 15 | github.com/andybalholm/brotli v1.1.0 // indirect 16 | github.com/apache/thrift v0.20.0 // indirect 17 | github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de 18 | github.com/cheekybits/genny v1.0.0 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/fatih/color v1.17.0 // indirect 21 | github.com/goccy/go-json v0.10.3 // indirect 22 | github.com/golang/snappy v0.0.4 // indirect 23 | github.com/google/flatbuffers v24.3.25+incompatible // indirect 24 | github.com/google/go-cmp v0.6.0 // indirect 25 | github.com/hashicorp/go-hclog v1.6.3 // indirect 26 | github.com/jeremywohl/flatten v1.0.1 27 | github.com/json-iterator/go v1.1.12 // indirect 28 | github.com/klauspost/asmfmt v1.3.2 // indirect 29 | github.com/klauspost/compress v1.17.9 // indirect 30 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 31 | github.com/mattetti/filebuffer v1.0.1 // indirect 32 | github.com/mattn/go-colorable v0.1.13 // indirect 33 | github.com/mattn/go-isatty v0.0.20 // indirect 34 | github.com/mattn/go-runewidth v0.0.16 // indirect 35 | github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect 36 | github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect 37 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 38 | github.com/modern-go/reflect2 v1.0.2 // indirect 39 | github.com/olekukonko/tablewriter v0.0.5 // indirect 40 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 41 | github.com/pmezard/go-difflib v1.0.0 // indirect 42 | github.com/rivo/uniseg v0.4.7 // indirect 43 | github.com/spf13/afero v1.11.0 // indirect 44 | github.com/zeebo/xxh3 v1.0.2 // indirect 45 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 46 | golang.org/x/mod v0.20.0 // indirect 47 | golang.org/x/net v0.27.0 // indirect 48 | golang.org/x/sync v0.8.0 // indirect 49 | golang.org/x/sys v0.23.0 // indirect 50 | golang.org/x/text v0.16.0 // indirect 51 | golang.org/x/tools v0.23.0 // indirect 52 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect 53 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4 // indirect 54 | google.golang.org/grpc v1.65.0 // indirect 55 | google.golang.org/protobuf v1.34.2 // indirect 56 | gopkg.in/yaml.v3 v3.0.1 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= 4 | github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= 5 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 6 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 7 | github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= 8 | github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= 9 | github.com/apache/thrift v0.20.0 h1:631+KvYbsBZxmuJjYwhezVsrfc/TbqtZV4QcxOX1fOI= 10 | github.com/apache/thrift v0.20.0/go.mod h1:hOk1BQqcp2OLzGsyVXdfMk7YFlMxK3aoEVhjD06QhB8= 11 | github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= 12 | github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= 13 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 14 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 16 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 17 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 18 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= 20 | github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= 21 | github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 h1:XCdvHbz3LhewBHN7+mQPx0sg/Hxil/1USnBmxkjHcmY= 22 | github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 24 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 h1:1L0aalTpPz7YlMxETKpmQoWMBkeiuorElZIXoNmgiPE= 29 | github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 30 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 31 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 32 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 33 | github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= 34 | github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= 35 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 36 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 37 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 38 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 39 | github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= 40 | github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= 41 | github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= 42 | github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= 43 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 44 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 45 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 46 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 47 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 48 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 49 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 50 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 51 | github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= 52 | github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 53 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 54 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 55 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 56 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 57 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 58 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 59 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 60 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 61 | github.com/grafana/grafana-plugin-sdk-go v0.242.0 h1:MUI/6aPLofRHInrdU0+XgVhB7SunJ5oIuOU9Bsz6KJc= 62 | github.com/grafana/grafana-plugin-sdk-go v0.242.0/go.mod h1:2HjNwzGCfaFAyR2HGoECTwAmq8vSIn2L1/1yOt4XRS4= 63 | github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= 64 | github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= 65 | github.com/grafana/pyroscope-go/godeltaprof v0.1.7 h1:C11j63y7gymiW8VugJ9ZW0pWfxTZugdSJyC48olk5KY= 66 | github.com/grafana/pyroscope-go/godeltaprof v0.1.7/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= 67 | github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= 68 | github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= 69 | github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= 70 | github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= 71 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= 72 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 73 | github.com/hairyhenderson/go-which v0.2.0 h1:vxoCKdgYc6+MTBzkJYhWegksHjjxuXPNiqo5G2oBM+4= 74 | github.com/hairyhenderson/go-which v0.2.0/go.mod h1:U1BQQRCjxYHfOkXDyCgst7OZVknbqI7KuGKhGnmyIik= 75 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 76 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 77 | github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= 78 | github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= 79 | github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= 80 | github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 81 | github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= 82 | github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= 83 | github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= 84 | github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= 85 | github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs= 86 | github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ= 87 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 88 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 89 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 90 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 91 | github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= 92 | github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= 93 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 94 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 95 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 96 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 97 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 98 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 99 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 100 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 101 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 102 | github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= 103 | github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 104 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 105 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 106 | github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= 107 | github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= 108 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 109 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 110 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 111 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 112 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 113 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 114 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 115 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 116 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 117 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 118 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 119 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 120 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 121 | github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= 122 | github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= 123 | github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= 124 | github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= 125 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 126 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 127 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 128 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 129 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 130 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 131 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 132 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 133 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 134 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 135 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 136 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 137 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 138 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 139 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 140 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 141 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 142 | github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= 143 | github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 144 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 145 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 146 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 147 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 148 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 149 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 150 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 151 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 152 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 153 | github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= 154 | github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= 155 | github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= 156 | github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ= 157 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 158 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 159 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 160 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 161 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 162 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 163 | github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= 164 | github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= 165 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 166 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 167 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 168 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 169 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 170 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 171 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 172 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 173 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 174 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 175 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 176 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 177 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 178 | github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI= 179 | github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8/go.mod h1:fVle4kNr08ydeohzYafr20oZzbAkhQT39gKK/pFQ5M4= 180 | github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= 181 | github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= 182 | github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg= 183 | github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= 184 | github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= 185 | github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= 186 | github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8= 187 | github.com/wk8/go-ordered-map v1.0.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk= 188 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= 189 | github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 190 | github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= 191 | github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= 192 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= 193 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= 194 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 h1:IVtyPth4Rs5P8wIf0mP2KVKFNTJ4paX9qQ4Hkh5gFdc= 195 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0/go.mod h1:ImRBLMJv177/pwiLZ7tU7HDGNdBv7rS0HQ99eN/zBl8= 196 | go.opentelemetry.io/contrib/propagators/jaeger v1.28.0 h1:xQ3ktSVS128JWIaN1DiPGIjcH+GsvkibIAVRWFjS9eM= 197 | go.opentelemetry.io/contrib/propagators/jaeger v1.28.0/go.mod h1:O9HIyI2kVBrFoEwQZ0IN6PHXykGoit4mZV2aEjkTRH4= 198 | go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0 h1:ja+d7Aea/9PgGxB63+E0jtRFpma717wubS0KFkZpmYw= 199 | go.opentelemetry.io/contrib/samplers/jaegerremote v0.20.0/go.mod h1:Yc1eg51SJy7xZdOTyg1xyFcwE+ghcWh3/0hKeLo6Wlo= 200 | go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= 201 | go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= 202 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= 203 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= 204 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= 205 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= 206 | go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= 207 | go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= 208 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= 209 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= 210 | go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= 211 | go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= 212 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 213 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 214 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 215 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 216 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 217 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 218 | golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= 219 | golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 220 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 221 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 222 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 223 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 224 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 226 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 227 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 228 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 229 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 230 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 231 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 232 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 233 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 234 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 235 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 236 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 237 | golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 238 | golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 239 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 240 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 241 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 242 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 243 | golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= 244 | golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 245 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= 246 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 247 | gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= 248 | gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= 249 | google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= 250 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= 251 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= 252 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4 h1:OsSGQeIIsyOEOimVxLEIL4rwGcnrjOydQaiA2bOnZUM= 253 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 254 | google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= 255 | google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 256 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 257 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 258 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 259 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 260 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 261 | gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= 262 | gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= 263 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 264 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 265 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 266 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 267 | gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= 268 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 269 | --------------------------------------------------------------------------------