Skip to content
This repository was archived by the owner on Nov 7, 2025. It is now read-only.

Commit 15bcad3

Browse files
authored
Report errors in queries better #2 (#1012)
Similar to #1006 , but for pipeline aggregations. I want to simplify and unify interfaces of our parsers, so all of them simply return something like `(aggregation, error)`. Code should be cleaner, and proper returning errors to Kibana much easier.
1 parent 00f0922 commit 15bcad3

File tree

3 files changed

+92
-158
lines changed

3 files changed

+92
-158
lines changed

quesma/queryparser/aggregation_parser.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type metricsAggregation struct {
2929
sigma float64 // only for standard deviation
3030
}
3131

32+
type aggregationParser = func(queryMap QueryMap) (model.QueryType, error)
33+
3234
const metricsAggregationDefaultFieldType = clickhouse.Invalid
3335

3436
// Tries to parse metrics aggregation from queryMap. If it's not a metrics aggregation, returns false.

quesma/queryparser/pancake_aggregation_parser.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func (cw *ClickhouseQueryTranslator) pancakeParseAggregation(aggregationName str
114114
return nil, nil
115115
}
116116

117-
// check if metadata's present
117+
// check if metadata is present
118118
var metadata model.JsonMap
119119
if metaRaw, exists := queryMap["meta"]; exists {
120120
metadata = metaRaw.(model.JsonMap)
@@ -143,7 +143,10 @@ func (cw *ClickhouseQueryTranslator) pancakeParseAggregation(aggregationName str
143143
}
144144

145145
// 2. Pipeline aggregation => always leaf (for now)
146-
if pipelineAggr, isPipeline := cw.parsePipelineAggregations(queryMap); isPipeline {
146+
if pipelineAggr, err := cw.parsePipelineAggregations(queryMap); err != nil || pipelineAggr != nil {
147+
if err != nil {
148+
return nil, err
149+
}
147150
aggregation.queryType = pipelineAggr
148151
return aggregation, nil
149152
}

quesma/queryparser/pipeline_aggregations.go

Lines changed: 85 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
package queryparser
44

55
import (
6-
"quesma/logger"
6+
"errors"
7+
"fmt"
78
"quesma/model"
89
"quesma/model/pipeline_aggregations"
910
"quesma/util"
@@ -12,217 +13,147 @@ import (
1213

1314
// CAUTION: maybe "return" everywhere isn't corrent, as maybe there can be multiple pipeline aggregations at one level.
1415
// But I've tested some complex queries and it seems to not be the case. So let's keep it this way for now.
15-
func (cw *ClickhouseQueryTranslator) parsePipelineAggregations(queryMap QueryMap) (aggregationType model.QueryType, success bool) {
16-
if aggregationType, success = cw.parseBucketScriptBasic(queryMap); success {
17-
delete(queryMap, "bucket_script")
18-
return
19-
}
20-
if aggregationType, success = cw.parseCumulativeSum(queryMap); success {
21-
delete(queryMap, "cumulative_sum")
22-
return
23-
}
24-
if aggregationType, success = cw.parseDerivative(queryMap); success {
25-
delete(queryMap, "derivative")
26-
return
27-
}
28-
if aggregationType, success = cw.parseSerialDiff(queryMap); success {
29-
delete(queryMap, "derivative")
30-
return
31-
}
32-
if aggregationType, success = cw.parseAverageBucket(queryMap); success {
33-
delete(queryMap, "avg_bucket")
34-
return
35-
}
36-
if aggregationType, success = cw.parseMinBucket(queryMap); success {
37-
delete(queryMap, "min_bucket")
38-
return
39-
}
40-
if aggregationType, success = cw.parseMaxBucket(queryMap); success {
41-
delete(queryMap, "max_bucket")
42-
return
43-
}
44-
if aggregationType, success = cw.parseSumBucket(queryMap); success {
45-
delete(queryMap, "sum_bucket")
46-
return
16+
func (cw *ClickhouseQueryTranslator) parsePipelineAggregations(queryMap QueryMap) (aggregationType model.QueryType, err error) {
17+
parsers := map[string]aggregationParser{
18+
"bucket_script": cw.parseBucketScriptBasic,
19+
"cumulative_sum": cw.parseCumulativeSum,
20+
"derivative": cw.parseDerivative,
21+
"serial_diff": cw.parseSerialDiff,
22+
"avg_bucket": cw.parseAverageBucket,
23+
"min_bucket": cw.parseMinBucket,
24+
"max_bucket": cw.parseMaxBucket,
25+
"sum_bucket": cw.parseSumBucket,
26+
}
27+
28+
for aggrName, aggrParser := range parsers {
29+
if paramsRaw, exists := queryMap[aggrName]; exists {
30+
if params, ok := paramsRaw.(QueryMap); ok {
31+
delete(queryMap, aggrName)
32+
return aggrParser(params)
33+
}
34+
return nil, fmt.Errorf("%s is not a map, but %T, value: %v", aggrName, paramsRaw, paramsRaw)
35+
}
4736
}
48-
return
49-
}
5037

51-
func (cw *ClickhouseQueryTranslator) parseCumulativeSum(queryMap QueryMap) (aggregationType model.QueryType, success bool) {
52-
cumulativeSumRaw, exists := queryMap["cumulative_sum"]
53-
if !exists {
54-
return
55-
}
56-
bucketsPath, ok := cw.parseBucketsPath(cumulativeSumRaw, "cumulative_sum")
57-
if !ok {
58-
return
59-
}
60-
return pipeline_aggregations.NewCumulativeSum(cw.Ctx, bucketsPath), true
38+
return nil, nil
6139
}
6240

63-
func (cw *ClickhouseQueryTranslator) parseDerivative(queryMap QueryMap) (aggregationType model.QueryType, success bool) {
64-
derivativeRaw, exists := queryMap["derivative"]
65-
if !exists {
66-
return
67-
}
68-
bucketsPath, ok := cw.parseBucketsPath(derivativeRaw, "derivative")
69-
if !ok {
70-
return
41+
func (cw *ClickhouseQueryTranslator) parseCumulativeSum(params QueryMap) (model.QueryType, error) {
42+
bucketsPath, err := cw.parseBucketsPath(params, "cumulative_sum")
43+
if err != nil {
44+
return nil, err
7145
}
72-
return pipeline_aggregations.NewDerivative(cw.Ctx, bucketsPath), true
46+
return pipeline_aggregations.NewCumulativeSum(cw.Ctx, bucketsPath), nil
7347
}
7448

75-
func (cw *ClickhouseQueryTranslator) parseAverageBucket(queryMap QueryMap) (aggregationType model.QueryType, success bool) {
76-
avgBucketRaw, exists := queryMap["avg_bucket"]
77-
if !exists {
78-
return
79-
}
80-
bucketsPath, ok := cw.parseBucketsPath(avgBucketRaw, "avg_bucket")
81-
if !ok {
82-
return
49+
func (cw *ClickhouseQueryTranslator) parseDerivative(params QueryMap) (model.QueryType, error) {
50+
bucketsPath, err := cw.parseBucketsPath(params, "derivative")
51+
if err != nil {
52+
return nil, err
8353
}
84-
return pipeline_aggregations.NewAverageBucket(cw.Ctx, bucketsPath), true
54+
return pipeline_aggregations.NewDerivative(cw.Ctx, bucketsPath), nil
8555
}
8656

87-
func (cw *ClickhouseQueryTranslator) parseMinBucket(queryMap QueryMap) (aggregationType model.QueryType, success bool) {
88-
minBucketRaw, exists := queryMap["min_bucket"]
89-
if !exists {
90-
return
57+
func (cw *ClickhouseQueryTranslator) parseAverageBucket(params QueryMap) (model.QueryType, error) {
58+
bucketsPath, err := cw.parseBucketsPath(params, "avg_bucket")
59+
if err != nil {
60+
return nil, err
9161
}
92-
bucketsPath, ok := cw.parseBucketsPath(minBucketRaw, "min_bucket")
93-
if !ok {
94-
return
95-
}
96-
return pipeline_aggregations.NewMinBucket(cw.Ctx, bucketsPath), true
62+
return pipeline_aggregations.NewAverageBucket(cw.Ctx, bucketsPath), nil
9763
}
9864

99-
func (cw *ClickhouseQueryTranslator) parseMaxBucket(queryMap QueryMap) (aggregationType model.QueryType, success bool) {
100-
maxBucketRaw, exists := queryMap["max_bucket"]
101-
if !exists {
102-
return
103-
}
104-
bucketsPath, ok := cw.parseBucketsPath(maxBucketRaw, "max_bucket")
105-
if !ok {
106-
return
65+
func (cw *ClickhouseQueryTranslator) parseMinBucket(params QueryMap) (model.QueryType, error) {
66+
bucketsPath, err := cw.parseBucketsPath(params, "min_bucket")
67+
if err != nil {
68+
return nil, err
10769
}
108-
return pipeline_aggregations.NewMaxBucket(cw.Ctx, bucketsPath), true
70+
return pipeline_aggregations.NewMinBucket(cw.Ctx, bucketsPath), nil
10971
}
11072

111-
func (cw *ClickhouseQueryTranslator) parseSumBucket(queryMap QueryMap) (aggregationType model.QueryType, success bool) {
112-
sumBucketRaw, exists := queryMap["sum_bucket"]
113-
if !exists {
114-
return
73+
func (cw *ClickhouseQueryTranslator) parseMaxBucket(params QueryMap) (model.QueryType, error) {
74+
bucketsPath, err := cw.parseBucketsPath(params, "max_bucket")
75+
if err != nil {
76+
return nil, err
11577
}
116-
bucketsPath, ok := cw.parseBucketsPath(sumBucketRaw, "sum_bucket")
117-
if !ok {
118-
return
119-
}
120-
return pipeline_aggregations.NewSumBucket(cw.Ctx, bucketsPath), true
78+
return pipeline_aggregations.NewMaxBucket(cw.Ctx, bucketsPath), nil
12179
}
12280

123-
func (cw *ClickhouseQueryTranslator) parseSerialDiff(queryMap QueryMap) (aggregationType model.QueryType, success bool) {
124-
serialDiffRaw, exists := queryMap["serial_diff"]
125-
if !exists {
126-
return
81+
func (cw *ClickhouseQueryTranslator) parseSumBucket(params QueryMap) (model.QueryType, error) {
82+
bucketsPath, err := cw.parseBucketsPath(params, "sum_bucket")
83+
if err != nil {
84+
return nil, err
12785
}
86+
return pipeline_aggregations.NewSumBucket(cw.Ctx, bucketsPath), nil
87+
}
12888

89+
func (cw *ClickhouseQueryTranslator) parseSerialDiff(params QueryMap) (model.QueryType, error) {
12990
// buckets_path
130-
bucketsPath, ok := cw.parseBucketsPath(serialDiffRaw, "serial_diff")
131-
if !ok {
132-
return
91+
bucketsPath, err := cw.parseBucketsPath(params, "serial_diff")
92+
if err != nil {
93+
return nil, err
13394
}
13495

13596
// lag
13697
const defaultLag = 1
137-
serialDiff, ok := serialDiffRaw.(QueryMap)
138-
if !ok {
139-
logger.WarnWithCtx(cw.Ctx).Msgf("serial_diff is not a map, but %T, value: %v", serialDiffRaw, serialDiffRaw)
140-
return
141-
}
142-
lagRaw, exists := serialDiff["lag"]
98+
lagRaw, exists := params["lag"]
14399
if !exists {
144-
return pipeline_aggregations.NewSerialDiff(cw.Ctx, bucketsPath, defaultLag), true
100+
return pipeline_aggregations.NewSerialDiff(cw.Ctx, bucketsPath, defaultLag), nil
145101
}
146102
if lag, ok := lagRaw.(float64); ok {
147-
return pipeline_aggregations.NewSerialDiff(cw.Ctx, bucketsPath, int(lag)), true
103+
return pipeline_aggregations.NewSerialDiff(cw.Ctx, bucketsPath, int(lag)), nil
148104
}
149105

150-
logger.WarnWithCtx(cw.Ctx).Msgf("lag is not a float64, but %T, value: %v", lagRaw, lagRaw)
151-
return
106+
return nil, fmt.Errorf("lag is not a float64, but %T, value: %v", lagRaw, lagRaw)
152107
}
153108

154-
func (cw *ClickhouseQueryTranslator) parseBucketScriptBasic(queryMap QueryMap) (aggregationType model.QueryType, success bool) {
155-
bucketScriptRaw, exists := queryMap["bucket_script"]
156-
if !exists {
157-
return
158-
}
159-
160-
// so far we only handle "count" here :D
161-
delete(queryMap, "bucket_script")
162-
bucketScript, ok := bucketScriptRaw.(QueryMap)
163-
if !ok {
164-
logger.WarnWithCtx(cw.Ctx).Msgf("bucket_script is not a map, but %T, value: %v. Skipping this aggregation", bucketScriptRaw, bucketScriptRaw)
165-
return
166-
}
167-
168-
// if ["buckets_path"] != "_count", skip the aggregation
169-
bucketsPath, ok := cw.parseBucketsPath(bucketScript, "bucket_script")
170-
if !ok {
171-
return
109+
func (cw *ClickhouseQueryTranslator) parseBucketScriptBasic(params QueryMap) (model.QueryType, error) {
110+
bucketsPath, err := cw.parseBucketsPath(params, "bucket_script")
111+
if err != nil {
112+
return nil, err
172113
}
173114
if !strings.HasSuffix(bucketsPath, pipeline_aggregations.BucketsPathCount) {
174-
logger.WarnWithCtx(cw.Ctx).Msgf("buckets_path is not '_count', but %s. Skipping this aggregation", bucketsPath)
175-
return
115+
//lint:ignore ST1005 I want Quesma capitalized
116+
return nil, fmt.Errorf("Quesma limitation, contact us if you need it fixed: buckets_path is not '_count', but %s", bucketsPath)
176117
}
177118

178-
scriptRaw, exists := bucketScript["script"]
119+
scriptRaw, exists := params["script"]
179120
if !exists {
180-
logger.WarnWithCtx(cw.Ctx).Msg("no script in bucket_script. Skipping this aggregation")
181-
return
121+
return nil, errors.New("no script in bucket_script")
182122
}
183123
if script, ok := scriptRaw.(string); ok {
184-
return pipeline_aggregations.NewBucketScript(cw.Ctx, bucketsPath, script), true
124+
return pipeline_aggregations.NewBucketScript(cw.Ctx, bucketsPath, script), nil
185125
}
186126

187127
script, ok := scriptRaw.(QueryMap)
188128
if !ok {
189-
logger.WarnWithCtx(cw.Ctx).Msgf("script is not a map, but %T, value: %v. Skipping this aggregation", scriptRaw, scriptRaw)
190-
return
129+
return nil, fmt.Errorf("script is not a map, but %T, value: %v", scriptRaw, scriptRaw)
191130
}
192131
if sourceRaw, exists := script["source"]; exists {
193132
if source, ok := sourceRaw.(string); ok {
194133
if source != "_value" && source != "count * 1" {
195-
logger.WarnWithCtx(cw.Ctx).Msgf("source is not '_value'/'count * 1', but %s. Skipping this aggregation", source)
196-
return
134+
//lint:ignore ST1005 I want Quesma capitalized
135+
return nil, fmt.Errorf("Quesma limitation, contact us if you need it fixed: source is not '_value'/'count * 1', but %s", source)
197136
}
198137
} else {
199-
logger.WarnWithCtx(cw.Ctx).Msgf("source is not a string, but %T, value: %v. Skipping this aggregation", sourceRaw, sourceRaw)
200-
return
138+
return nil, fmt.Errorf("source is not a string, but %T, value: %v", sourceRaw, sourceRaw)
201139
}
202140
} else {
203-
logger.WarnWithCtx(cw.Ctx).Msg("no source in script. Skipping this aggregation")
204-
return
141+
return nil, errors.New("no source in script")
205142
}
206143

207144
// okay, we've checked everything, it's indeed a simple count
208-
return pipeline_aggregations.NewBucketScript(cw.Ctx, bucketsPath, ""), true
145+
return pipeline_aggregations.NewBucketScript(cw.Ctx, bucketsPath, ""), nil
209146
}
210147

211-
func (cw *ClickhouseQueryTranslator) parseBucketsPath(shouldBeQueryMap any, aggregationName string) (bucketsPathStr string, success bool) {
212-
queryMap, ok := shouldBeQueryMap.(QueryMap)
213-
if !ok {
214-
logger.WarnWithCtx(cw.Ctx).Msgf("%s is not a map, but %T, value: %v", aggregationName, shouldBeQueryMap, shouldBeQueryMap)
215-
return
216-
}
217-
bucketsPathRaw, exists := queryMap["buckets_path"]
148+
func (cw *ClickhouseQueryTranslator) parseBucketsPath(params QueryMap, aggregationName string) (bucketsPathStr string, err error) {
149+
bucketsPathRaw, exists := params["buckets_path"]
218150
if !exists {
219-
logger.WarnWithCtx(cw.Ctx).Msg("no buckets_path in avg_bucket")
220-
return
151+
return "", fmt.Errorf("no buckets_path in %s", aggregationName)
221152
}
222153

223154
switch bucketsPath := bucketsPathRaw.(type) {
224155
case string:
225-
return bucketsPath, true
156+
return bucketsPath, nil
226157
case QueryMap:
227158
// TODO: handle arbitrary nr of keys (and arbitrary scripts, because we also handle only one special case)
228159
if len(bucketsPath) == 1 || len(bucketsPath) == 2 {
@@ -231,17 +162,15 @@ func (cw *ClickhouseQueryTranslator) parseBucketsPath(shouldBeQueryMap any, aggr
231162
// After fixing the TODO above, it should also get fixed.
232163
for _, key := range util.MapKeysSorted(bucketsPath) {
233164
if path, ok := bucketsPath[key].(string); ok {
234-
return path, true
165+
return path, nil
235166
} else {
236-
logger.WarnWithCtx(cw.Ctx).Msgf("buckets_path is not a map with string values, but %T. Skipping this aggregation", path)
237-
return
167+
return "", fmt.Errorf("buckets_path is not a map with string values, but %T %v", bucketsPath[key], bucketsPath[key])
238168
}
239169
}
240170
} else {
241-
logger.WarnWithCtx(cw.Ctx).Msgf("buckets_path is not a map with one or two keys, but %d. Skipping this aggregation", len(bucketsPath))
171+
return "", fmt.Errorf("buckets_path is not a map with one or two keys, but it is: %v", bucketsPath)
242172
}
243173
}
244174

245-
logger.WarnWithCtx(cw.Ctx).Msgf("buckets_path in wrong format, type: %T, value: %v", bucketsPathRaw, bucketsPathRaw)
246-
return
175+
return "", fmt.Errorf("buckets_path in wrong format, type: %T, value: %v", bucketsPathRaw, bucketsPathRaw)
247176
}

0 commit comments

Comments
 (0)