diff --git a/quesma/go.mod b/quesma/go.mod index 601a83ec2..055da1e49 100644 --- a/quesma/go.mod +++ b/quesma/go.mod @@ -40,6 +40,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/H0llyW00dzZ/cidr v1.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect diff --git a/quesma/go.sum b/quesma/go.sum index f09e84c97..39f2d9293 100644 --- a/quesma/go.sum +++ b/quesma/go.sum @@ -7,6 +7,8 @@ github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/H0llyW00dzZ/cidr v1.2.1 h1:DfRHX+RqVVKZijQGO1aJSaWvN9Saan8sycK/4wrfY5g= +github.com/H0llyW00dzZ/cidr v1.2.1/go.mod h1:S+EgYkMandSAN27mGNG/CB3jeoXDAyalsvvVFpWdnXc= github.com/DataDog/go-sqllexer v0.0.18 h1:ErBvoO7/srJLdA2ebwd+HPqD4g1kN++BP64A8qvmh9U= github.com/DataDog/go-sqllexer v0.0.18/go.mod h1:KwkYhpFEVIq+BfobkTC1vfqm4gTi65skV/DpDBXtexc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= diff --git a/quesma/model/bucket_aggregations/ip_range.go b/quesma/model/bucket_aggregations/ip_range.go new file mode 100644 index 000000000..3d07bb84b --- /dev/null +++ b/quesma/model/bucket_aggregations/ip_range.go @@ -0,0 +1,185 @@ +// Copyright Quesma, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 +package bucket_aggregations + +import ( + "context" + "fmt" + "quesma/logger" + "quesma/model" + "reflect" +) + +// BiggestIpv4 is "255.255.255.255 + 1", so to say. Used in Elastic, because it always uses exclusive upper bounds. +// So instead of "<= 255.255.255.255", it uses "< ::1:0:0:0" +const BiggestIpv4 = "::1:0:0:0" + +// Current limitation: we expect Clickhouse field to be IPv4 (and not IPv6) + +// Clickhouse table to test SQLs: +// CREATE TABLE __quesma_table_name (clientip IPv4) ENGINE=Log +// INSERT INTO __quesma_table_name VALUES ('0.0.0.0'), ('5.5.5.5'), ('90.180.90.180'), ('128.200.0.8'), ('192.168.1.67'), ('222.168.22.67') + +// TODO make part of QueryType interface and implement for all aggregations +// TODO add bad requests to tests +// Doing so will ensure we see 100% of what we're interested in in our logs (now we see ~95%) +func CheckParamsIpRange(ctx context.Context, paramsRaw any) error { + requiredParams := map[string]string{ + "field": "string", + "ranges": "map_todo_improve_this_check", // TODO should add same type check to this 'ranges' field, will be fixed + } + optionalParams := map[string]string{ + "keyed": "bool", + } + + params, ok := paramsRaw.(model.JsonMap) + if !ok { + return fmt.Errorf("params is not a map, but %+v", paramsRaw) + } + + // check if required are present + for paramName, paramType := range requiredParams { + paramVal, exists := params[paramName] + if !exists { + return fmt.Errorf("required parameter %s not found in params", paramName) + } + if paramType == "map_todo_improve_this_check" { + continue // uncontinue after TODO is fixed + } + if reflect.TypeOf(paramVal).Name() != paramType { // TODO I'll make a small rewrite to not use reflect here + return fmt.Errorf("required parameter %s is not of type %s, but %T", paramName, paramType, paramVal) + } + } + + // check if only required/optional are present + for paramName := range params { + if _, isRequired := requiredParams[paramName]; !isRequired { + wantedType, isOptional := optionalParams[paramName] + if !isOptional { + return fmt.Errorf("unexpected parameter %s found in IP Range params %v", paramName, params) + } + if reflect.TypeOf(params[paramName]).Name() != wantedType { // TODO I'll make a small rewrite to not use reflect here + return fmt.Errorf("optional parameter %s is not of type %s, but %T", paramName, wantedType, params[paramName]) + } + } + } + + return nil +} + +type ( + IpRange struct { + ctx context.Context + field model.Expr + intervals []IpInterval + keyed bool + } + IpInterval struct { + begin string + end string + key *string // when nil, key is not present + } +) + +func NewIpRange(ctx context.Context, intervals []IpInterval, field model.Expr, keyed bool) *IpRange { + return &IpRange{ + ctx: ctx, + field: field, + intervals: intervals, + keyed: keyed, + } +} + +func NewIpInterval(begin, end string, key *string) IpInterval { + return IpInterval{begin: begin, end: end, key: key} +} + +func (interval IpInterval) ToWhereClause(field model.Expr) model.Expr { + isBegin := interval.begin != UnboundedInterval + isEnd := interval.end != UnboundedInterval && interval.end != BiggestIpv4 + + begin := model.NewInfixExpr(field, ">=", model.NewLiteralSingleQuoted(interval.begin)) + end := model.NewInfixExpr(field, "<", model.NewLiteralSingleQuoted(interval.end)) + + if isBegin && isEnd { + return model.NewInfixExpr(begin, "AND", end) + } else if isBegin { + return begin + } else if isEnd { + return end + } else { + return model.TrueExpr + } +} + +// String returns key part of the response, e.g. "1.0-2.0", or "*-6.55" +func (interval IpInterval) String() string { + if interval.key != nil { + return *interval.key + } + return fmt.Sprintf("%s-%s", interval.begin, interval.end) +} + +func (query *IpRange) AggregationType() model.AggregationType { + return model.BucketAggregation +} + +func (query *IpRange) TranslateSqlResponseToJson(rows []model.QueryResultRow) model.JsonMap { + return nil +} + +func (query *IpRange) String() string { + return "ip_range" +} + +func (query *IpRange) DoesNotHaveGroupBy() bool { + return true +} + +func (query *IpRange) CombinatorGroups() (result []CombinatorGroup) { + for intervalIdx, interval := range query.intervals { + prefix := fmt.Sprintf("range_%d__", intervalIdx) + if len(query.intervals) == 1 { + prefix = "" + } + result = append(result, CombinatorGroup{ + idx: intervalIdx, + Prefix: prefix, + Key: interval.String(), + WhereClause: interval.ToWhereClause(query.field), + }) + } + return +} + +// bad requests: both to/from and mask + +func (query *IpRange) CombinatorTranslateSqlResponseToJson(subGroup CombinatorGroup, rows []model.QueryResultRow) model.JsonMap { + if len(rows) == 0 || len(rows[0].Cols) == 0 { + logger.ErrorWithCtx(query.ctx).Msgf("need at least one row and column in ip_range aggregation response, rows: %d, cols: %d", len(rows), len(rows[0].Cols)) + return model.JsonMap{} + } + count := rows[0].Cols[len(rows[0].Cols)-1].Value + response := model.JsonMap{ + "key": subGroup.Key, + "doc_count": count, + } + + interval := query.intervals[subGroup.idx] + if interval.begin != UnboundedInterval { + response["from"] = interval.begin + } + if interval.end != UnboundedInterval { + response["to"] = interval.end + } + + return response +} + +func (query *IpRange) CombinatorSplit() []model.QueryType { + result := make([]model.QueryType, 0, len(query.intervals)) + for _, interval := range query.intervals { + result = append(result, NewIpRange(query.ctx, []IpInterval{interval}, query.field, query.keyed)) + } + return result +} diff --git a/quesma/model/expr.go b/quesma/model/expr.go index 28dbcc757..3cf0a20d0 100644 --- a/quesma/model/expr.go +++ b/quesma/model/expr.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Elastic-2.0 package model -import "strconv" +import ( + "fmt" + "strconv" +) // Expr is a generic representation of an expression which is a part of the SQL query. type Expr interface { @@ -126,6 +129,10 @@ func NewLiteral(value any) LiteralExpr { return LiteralExpr{Value: value} } +func NewLiteralSingleQuoted(value string) LiteralExpr { + return LiteralExpr{Value: fmt.Sprintf("'%s'", value)} +} + // DistinctExpr is a representation of DISTINCT keyword in SQL, e.g. `SELECT DISTINCT` ... or `SELECT COUNT(DISTINCT ...)` type DistinctExpr struct { Expr Expr diff --git a/quesma/queryparser/aggregation_parser.go b/quesma/queryparser/aggregation_parser.go index c50bf5b9e..75698c2c3 100644 --- a/quesma/queryparser/aggregation_parser.go +++ b/quesma/queryparser/aggregation_parser.go @@ -291,6 +291,16 @@ func (cw *ClickhouseQueryTranslator) parseStringField(queryMap QueryMap, fieldNa return defaultValue } +func (cw *ClickhouseQueryTranslator) parseStringFieldExistCheck(queryMap QueryMap, fieldName string) (value string, exists bool) { + if valueRaw, exists := queryMap[fieldName]; exists { + if asString, ok := valueRaw.(string); ok { + return asString, true + } + logger.WarnWithCtx(cw.Ctx).Msgf("%s is not a string, but %T, value: %v", fieldName, valueRaw, valueRaw) + } + return "", false +} + func (cw *ClickhouseQueryTranslator) parseArrayField(queryMap QueryMap, fieldName string) ([]any, error) { if valueRaw, exists := queryMap[fieldName]; exists { if asArray, ok := valueRaw.([]any); ok { diff --git a/quesma/queryparser/pancake_aggregation_parser_buckets.go b/quesma/queryparser/pancake_aggregation_parser_buckets.go index 33257166b..387e2662e 100644 --- a/quesma/queryparser/pancake_aggregation_parser_buckets.go +++ b/quesma/queryparser/pancake_aggregation_parser_buckets.go @@ -5,11 +5,15 @@ package queryparser import ( "fmt" + "github.com/H0llyW00dzZ/cidr" "github.com/pkg/errors" + "math" + "net" "quesma/clickhouse" "quesma/logger" "quesma/model" "quesma/model/bucket_aggregations" + "quesma/util" "sort" "strconv" "strings" @@ -37,6 +41,7 @@ func (cw *ClickhouseQueryTranslator) pancakeTryBucketAggregation(aggregation *pa }}, {"multi_terms", cw.parseMultiTerms}, {"composite", cw.parseComposite}, + {"ip_range", cw.parseIpRange}, {"ip_prefix", cw.parseIpPrefix}, } @@ -382,6 +387,48 @@ func (cw *ClickhouseQueryTranslator) parseComposite(aggregation *pancakeAggregat return nil } +func (cw *ClickhouseQueryTranslator) parseIpRange(aggregation *pancakeAggregationTreeNode, params QueryMap) error { + const defaultKeyed = false + + if err := bucket_aggregations.CheckParamsIpRange(cw.Ctx, params); err != nil { + return err + } + + rangesRaw := params["ranges"].([]any) + ranges := make([]bucket_aggregations.IpInterval, 0, len(rangesRaw)) + for _, rangeRaw := range rangesRaw { + var key *string + if keyIfPresent, exists := cw.parseStringFieldExistCheck(rangeRaw.(QueryMap), "key"); exists { + key = &keyIfPresent + } + var begin, end string + if maskIfExists, exists := cw.parseStringFieldExistCheck(rangeRaw.(QueryMap), "mask"); exists { + _, ipNet, err := net.ParseCIDR(maskIfExists) + if err != nil { + return err + } + beginAsInt, endAsInt := cidr.IPv4ToRange(ipNet) + begin = util.IntToIpv4(beginAsInt) + // endAsInt is inclusive, we do +1, because we need it exclusive + if endAsInt != math.MaxUint32 { + end = util.IntToIpv4(endAsInt + 1) + } else { + end = bucket_aggregations.BiggestIpv4 // "255.255.255.255 + 1", so to say (value in compliance with Elastic) + } + if key == nil { + key = &maskIfExists + } + } else { + begin = cw.parseStringField(rangeRaw.(QueryMap), "from", bucket_aggregations.UnboundedInterval) + end = cw.parseStringField(rangeRaw.(QueryMap), "to", bucket_aggregations.UnboundedInterval) + } + ranges = append(ranges, bucket_aggregations.NewIpInterval(begin, end, key)) + } + aggregation.isKeyed = cw.parseBoolField(params, "keyed", defaultKeyed) + aggregation.queryType = bucket_aggregations.NewIpRange(cw.Ctx, ranges, cw.parseFieldField(params, "ip_range"), aggregation.isKeyed) + return nil +} + func (cw *ClickhouseQueryTranslator) parseIpPrefix(aggregation *pancakeAggregationTreeNode, params QueryMap) error { const ( defaultIsIpv6 = false diff --git a/quesma/testdata/kibana-visualize/aggregation_requests.go b/quesma/testdata/kibana-visualize/aggregation_requests.go index c2056a5fa..7a0f3fd2d 100644 --- a/quesma/testdata/kibana-visualize/aggregation_requests.go +++ b/quesma/testdata/kibana-visualize/aggregation_requests.go @@ -3274,4 +3274,269 @@ var AggregationTests = []testdata.AggregationTestCase{ GROUP BY intDiv("clientip", 2147483648) AS "aggr__2__key_0" ORDER BY "aggr__2__key_0" ASC`, }, + { // [24] + TestName: "Simplest IP range. In Kibana: Add panel > Aggregation Based > Area. Buckets: X-asis: IP Range", + QueryRequestJson: ` + { + "_source": { + "excludes": [] + }, + "aggs": { + "2": { + "ip_range": { + "field": "clientip", + "ranges": [ + { + "from": "0.0.0.0", + "to": "127.255.255.255" + }, + { + "from": "128.0.0.0" + }, + { + "from": "128.129.130.131", + "key": "my-custom-key" + }, + { + "to": "10.0.0.5" + } + ] + } + } + }, + "size": 0, + "track_total_hits": true + }`, + ExpectedResponse: ` + { + "took": 14, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 14074, + "relation": "eq" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "2": { + "buckets": [ + { + "key": "0.0.0.0-127.255.255.255", + "from": "0.0.0.0", + "to": "127.255.255.255", + "doc_count": 7290 + }, + { + "key": "128.0.0.0-*", + "from": "128.0.0.0", + "doc_count": 6784 + }, + { + "key": "my-custom-key", + "from": "128.129.130.131", + "doc_count": 6752 + }, + { + "key": "*-10.0.0.5", + "to": "10.0.0.5", + "doc_count": 534 + } + ] + } + } + }`, + ExpectedPancakeResults: []model.QueryResultRow{ + {Cols: []model.QueryResultCol{ + model.NewQueryResultCol("range_0__aggr__2__count", int64(7290)), + model.NewQueryResultCol("range_1__aggr__2__count", int64(6784)), + model.NewQueryResultCol("range_2__aggr__2__count", int64(6752)), + model.NewQueryResultCol("range_3__aggr__2__count", int64(534)), + }}, + }, + ExpectedPancakeSQL: ` + SELECT countIf(("clientip">='0.0.0.0' AND "clientip"<'127.255.255.255')) AS + "range_0__aggr__2__count", + countIf("clientip">='128.0.0.0') AS "range_1__aggr__2__count", + countIf("clientip">='128.129.130.131') AS "range_2__aggr__2__count", + countIf("clientip"<'10.0.0.5') AS "range_3__aggr__2__count" + FROM __quesma_table_name`, + }, + { // [25] + TestName: "IP range, with ranges as CIDR masks. In Kibana: Add panel > Aggregation Based > Area. Buckets: X-asis: IP Range", + QueryRequestJson: ` + { + "_source": { + "excludes": [] + }, + "aggs": { + "2": { + "ip_range": { + "field": "clientip", + "ranges": [ + { + "mask": "255.255.255.253/30" + }, + { + "from": "128.129.130.131", + "key": "my-custom-key" + }, + { + "mask": "10.0.7.127/27", + "key": "custom-mask-key" + } + ] + } + } + }, + "size": 0, + "track_total_hits": true + }`, + ExpectedResponse: ` + { + "took": 14, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 14074, + "relation": "eq" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "2": { + "buckets": [ + { + "key": "255.255.255.253/30", + "from": "255.255.255.252", + "to": "::1:0:0:0", + "doc_count": 2 + }, + { + "key": "my-custom-key", + "from": "128.129.130.131", + "doc_count": 6752 + }, + { + "key": "custom-mask-key", + "from": "10.0.7.96", + "to": "10.0.7.128", + "doc_count": 3 + } + ] + } + } + }`, + ExpectedPancakeResults: []model.QueryResultRow{ + {Cols: []model.QueryResultCol{ + model.NewQueryResultCol("range_0__aggr__2__count", int64(2)), + model.NewQueryResultCol("range_1__aggr__2__count", int64(6752)), + model.NewQueryResultCol("range_2__aggr__2__count", int64(3)), + }}, + }, + ExpectedPancakeSQL: ` + SELECT countIf("clientip">='255.255.255.252') AS "range_0__aggr__2__count", + countIf("clientip">='128.129.130.131') AS "range_1__aggr__2__count", + countIf(("clientip">='10.0.7.96' AND "clientip"<'10.0.7.128')) AS + "range_2__aggr__2__count" + FROM __quesma_table_name`, + }, + { // [26] + TestName: "IP range, with ranges as CIDR masks, keyed=true. In Kibana: Add panel > Aggregation Based > Area. Buckets: X-asis: IP Range", + QueryRequestJson: ` + { + "_source": { + "excludes": [] + }, + "aggs": { + "2": { + "ip_range": { + "field": "clientip", + "keyed": true, + "ranges": [ + { + "mask": "255.255.255.255/31" + }, + { + "from": "128.129.130.131", + "key": "my-custom-key" + }, + { + "mask": "10.0.7.127/27", + "key": "custom-mask-key" + } + ] + } + } + }, + "size": 0, + "track_total_hits": true + }`, + ExpectedResponse: ` + { + "took": 14, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 14074, + "relation": "eq" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "2": { + "buckets": { + "custom-mask-key": { + "from": "10.0.7.96", + "to": "10.0.7.128", + "doc_count": 3 + }, + "my-custom-key": { + "from": "128.129.130.131", + "doc_count": 6752 + }, + "255.255.255.255/31": { + "from": "255.255.255.254", + "to": "::1:0:0:0", + "doc_count": 2 + } + } + } + } + }`, + ExpectedPancakeResults: []model.QueryResultRow{ + {Cols: []model.QueryResultCol{ + model.NewQueryResultCol("range_0__aggr__2__count", int64(2)), + model.NewQueryResultCol("range_1__aggr__2__count", int64(6752)), + model.NewQueryResultCol("range_2__aggr__2__count", int64(3)), + }}, + }, + ExpectedPancakeSQL: ` + SELECT countIf("clientip">='255.255.255.254') AS "range_0__aggr__2__count", + countIf("clientip">='128.129.130.131') AS "range_1__aggr__2__count", + countIf(("clientip">='10.0.7.96' AND "clientip"<'10.0.7.128')) AS + "range_2__aggr__2__count" + FROM __quesma_table_name`, + }, } diff --git a/quesma/testdata/unsupported_requests.go b/quesma/testdata/unsupported_requests.go index 971001474..b74f08289 100644 --- a/quesma/testdata/unsupported_requests.go +++ b/quesma/testdata/unsupported_requests.go @@ -25,7 +25,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [2] + { // [1] TestName: "bucket aggregation: categorize_text", QueryType: "categorize_text", QueryRequestJson: ` @@ -39,7 +39,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [3] + { // [2] TestName: "bucket aggregation: children", QueryType: "children", QueryRequestJson: ` @@ -69,7 +69,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [5] + { // [3] TestName: "bucket aggregation: diversified_sampler", QueryType: "diversified_sampler", QueryRequestJson: ` @@ -97,7 +97,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [6] + { // [4] TestName: "bucket aggregation: frequent_item_sets", QueryType: "frequent_item_sets", QueryRequestJson: ` @@ -122,7 +122,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [7] + { // [5] TestName: "bucket aggregation: geo_distance", QueryType: "geo_distance", QueryRequestJson: ` @@ -143,7 +143,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [8] + { // [6] TestName: "bucket aggregation: geohash_grid", QueryType: "geohash_grid", QueryRequestJson: ` @@ -162,7 +162,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [9] + { // [7] TestName: "bucket aggregation: geohex_grid", QueryType: "geohex_grid", QueryRequestJson: ` @@ -181,7 +181,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [11] + { // [8] TestName: "bucket aggregation: global", QueryType: "global", QueryRequestJson: ` @@ -200,26 +200,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [13] - TestName: "bucket aggregation: ip_range", - QueryType: "ip_range", - QueryRequestJson: ` - { - "size": 10, - "aggs": { - "ip_ranges": { - "ip_range": { - "field": "ip", - "ranges": [ - { "to": "10.0.0.5" }, - { "from": "10.0.0.5" } - ] - } - } - } - }`, - }, - { // [14] + { // [9] TestName: "bucket aggregation: missing", QueryType: "missing", QueryRequestJson: ` @@ -231,7 +212,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [16] + { // [10] TestName: "bucket aggregation: nested", QueryType: "nested", QueryRequestJson: ` @@ -257,7 +238,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [17] + { // [11] TestName: "bucket aggregation: parent", QueryType: "parent", QueryRequestJson: ` @@ -287,7 +268,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [18] + { // [12] TestName: "bucket aggregation: rare_terms", QueryType: "rare_terms", QueryRequestJson: ` @@ -301,7 +282,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [19] + { // [13] TestName: "bucket aggregation: reverse_nested", QueryType: "reverse_nested", QueryRequestJson: ` @@ -334,7 +315,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [20] + { // [14] TestName: "bucket aggregation: significant_text", QueryType: "significant_text", QueryRequestJson: ` @@ -356,7 +337,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [21] + { // [15] TestName: "bucket aggregation: time_series", QueryType: "time_series", QueryRequestJson: ` @@ -368,7 +349,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [22] + { // [16] TestName: "bucket aggregation: variable_width_histogram", QueryType: "variable_width_histogram", QueryRequestJson: ` @@ -384,7 +365,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ }`, }, // metrics: - { // [23] + { // [17] TestName: "metrics aggregation: boxplot", QueryType: "boxplot", QueryRequestJson: ` @@ -399,7 +380,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [25] + { // [18] TestName: "metrics aggregation: geo_bounds", QueryType: "geo_bounds", QueryRequestJson: ` @@ -413,7 +394,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [27] + { // [19] TestName: "metrics aggregation: geo_line", QueryType: "geo_line", QueryRequestJson: ` @@ -428,7 +409,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [28] + { // [20] TestName: "metrics aggregation: cartesian_bounds", QueryType: "cartesian_bounds", QueryRequestJson: ` @@ -445,7 +426,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [29] + { // [21] TestName: "metrics aggregation: cartesian_centroid", QueryType: "cartesian_centroid", QueryRequestJson: ` @@ -459,7 +440,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [30] + { // [22] TestName: "metrics aggregation: matrix_stats", QueryType: "matrix_stats", QueryRequestJson: ` @@ -473,7 +454,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [31] + { // [23] TestName: "metrics aggregation: median_absolute_deviation", QueryType: "median_absolute_deviation", QueryRequestJson: ` @@ -493,7 +474,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [32] + { // [24] TestName: "metrics aggregation: rate", QueryType: "rate", QueryRequestJson: ` @@ -516,7 +497,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [33] + { // [25] TestName: "metrics aggregation: scripted_metric", QueryType: "scripted_metric", QueryRequestJson: ` @@ -536,7 +517,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [34] + { // [26] TestName: "metrics aggregation: string_stats", QueryType: "string_stats", QueryRequestJson: ` @@ -563,7 +544,7 @@ var UnsupportedQueriesTests = []UnsupportedQueryTestCase{ } }`, }, - { // [36] + { // [27] TestName: "metrics aggregation: weighted_avg", QueryType: "weighted_avg", QueryRequestJson: `