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

Commit 939452c

Browse files
trzysiekjakozaur
andauthored
Fix Hydrolix dates 1 (#835)
This eliminates an error we encountered with Hydrolix. ``` Q3006: Unspecified database error. clickhouse: query failed. err: code: 169, message: Key expression contains comparison between inconvertible types: DateTime64(3) and Float64 inside reqTimeSec >= 1727858503270 ``` It's possible to compare dates and floats in Clickhouse from 2022 year, but in 2021 it wasn't possible, and Hydrolix must use some earlier version, so I created a solution which works in both scenarios I move date parsing from Clickhouse to Quesma, removing usage of Clickhouse's `parseDateTimeBestEffort()`. Now we parse date ourselves, and use `toDateTime64()` to compare this date with date field (checked, it works fine also for fields of basic `DateTime` type, not `DateTime64`, even in Clickhouse 4 years ago). Elastic has like 50 different date formats available, so I doubt all of them are available in Clickhouse or other databases, so it's a move in the right direction, I guess. After: <img width="1728" alt="Screenshot 2024-10-06 at 17 15 22" src="https://github.com/user-attachments/assets/952c6a9e-2a3e-43ce-85f5-fd19c1373018"> Timestamps are fine: there's `"gte": 1727858503270` in the request, which is `Wed Oct 02 2024 08:41:43 GMT+0000`, so histogram starts fine from `8:41:30`: <img width="505" alt="Screenshot 2024-10-06 at 17 15 49" src="https://github.com/user-attachments/assets/3d9acfb9-90c6-4341-b149-48c214e3d0d2"> --------- Co-authored-by: Jacek Migdal <[email protected]>
1 parent e97ce49 commit 939452c

17 files changed

+360
-458
lines changed

quesma/go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ require (
1919
github.com/knadh/koanf/providers/file v1.1.2
2020
github.com/knadh/koanf/v2 v2.1.1
2121
github.com/markbates/goth v1.80.0
22-
github.com/relvacode/iso8601 v1.4.0
2322
github.com/rs/zerolog v1.33.0
2423
github.com/shirou/gopsutil/v3 v3.24.5
2524
github.com/stretchr/testify v1.9.0

quesma/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
113113
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
114114
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
115115
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
116-
github.com/relvacode/iso8601 v1.4.0 h1:GsInVSEJfkYuirYFxa80nMLbH2aydgZpIf52gYZXUJs=
117-
github.com/relvacode/iso8601 v1.4.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I=
118116
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
119117
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
120118
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=

quesma/kibana/dates.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package kibana
44

55
import (
6+
"quesma/model"
67
"quesma/util"
78
"strconv"
89
"time"
@@ -17,19 +18,18 @@ func NewDateManager() DateManager {
1718
var acceptableDateTimeFormats = []string{"2006", "2006-01", "2006-01-02", "2006-01-02", "2006-01-02T15",
1819
"2006-01-02T15:04", "2006-01-02T15:04:05", "2006-01-02T15:04:05Z07", "2006-01-02T15:04:05Z07:00"}
1920

20-
// MissingInDateHistogramToUnixTimestamp parses date_histogram's missing field.
21-
// If missing is present, it's in [strict_date_optional_time || epoch_millis] format
21+
// parseStrictDateOptionalTimeOrEpochMillis parses date, which is in [strict_date_optional_time || epoch_millis] format
2222
// (https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html)
23-
func (dm DateManager) MissingInDateHistogramToUnixTimestamp(missing any) (unixTimestamp int64, parsingSucceeded bool) {
24-
if asInt, success := util.ExtractInt64Maybe(missing); success {
23+
func (dm DateManager) parseStrictDateOptionalTimeOrEpochMillis(date any) (unixTimestamp int64, parsingSucceeded bool) {
24+
if asInt, success := util.ExtractInt64Maybe(date); success {
2525
return asInt, true
2626
}
2727

28-
if asFloat, success := util.ExtractFloat64Maybe(missing); success {
28+
if asFloat, success := util.ExtractFloat64Maybe(date); success {
2929
return int64(asFloat), true
3030
}
3131

32-
asString, success := missing.(string)
32+
asString, success := date.(string)
3333
if !success {
3434
return -1, false
3535
}
@@ -41,9 +41,9 @@ func (dm DateManager) MissingInDateHistogramToUnixTimestamp(missing any) (unixTi
4141
const yearOrTsDelimiter = 10000
4242

4343
if asInt, err := strconv.ParseInt(asString, 10, 64); err == nil && asInt >= yearOrTsDelimiter {
44-
return dm.MissingInDateHistogramToUnixTimestamp(asInt)
44+
return dm.parseStrictDateOptionalTimeOrEpochMillis(asInt)
4545
} else if asFloat, err := strconv.ParseFloat(asString, 64); err == nil && asFloat >= yearOrTsDelimiter {
46-
return dm.MissingInDateHistogramToUnixTimestamp(asFloat)
46+
return dm.parseStrictDateOptionalTimeOrEpochMillis(asFloat)
4747
}
4848

4949
// It could be replaced with iso8601.ParseString() after the fixes to 1.4.0:
@@ -56,3 +56,20 @@ func (dm DateManager) MissingInDateHistogramToUnixTimestamp(missing any) (unixTi
5656

5757
return -1, false
5858
}
59+
60+
// ParseMissingInDateHistogram parses date_histogram's missing field.
61+
// If missing is present, it's in [strict_date_optional_time || epoch_millis] format
62+
// (https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html)
63+
func (dm DateManager) ParseMissingInDateHistogram(missing any) (unixTimestamp int64, parsingSucceeded bool) {
64+
return dm.parseStrictDateOptionalTimeOrEpochMillis(missing)
65+
}
66+
67+
// ParseRange parses range filter.
68+
// We assume it's in [strict_date_optional_time || epoch_millis] format (TODO: other formats)
69+
// (https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html)
70+
func (dm DateManager) ParseRange(Range any) (timestampExpr model.Expr, parsingSucceeded bool) {
71+
if timestamp, success := dm.parseStrictDateOptionalTimeOrEpochMillis(Range); success {
72+
return model.NewFunction("fromUnixTimestamp64Milli", model.NewLiteral(timestamp)), true
73+
}
74+
return nil, false
75+
}

quesma/kibana/dates_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"testing"
99
)
1010

11-
func TestDateManager_MissingInDateHistogramToUnixTimestamp(t *testing.T) {
11+
func TestDateManager_parseStrictDateOptionalTimeOrEpochMillis(t *testing.T) {
1212
tests := []struct {
1313
missing any
1414
wantUnixTimestamp int64
@@ -38,7 +38,7 @@ func TestDateManager_MissingInDateHistogramToUnixTimestamp(t *testing.T) {
3838
for _, tt := range tests {
3939
t.Run(fmt.Sprintf("%v", tt.missing), func(t *testing.T) {
4040
dm := NewDateManager()
41-
gotUnixTs, gotParsingSucceeded := dm.MissingInDateHistogramToUnixTimestamp(tt.missing)
41+
gotUnixTs, gotParsingSucceeded := dm.parseStrictDateOptionalTimeOrEpochMillis(tt.missing)
4242
assert.Equalf(t, tt.wantUnixTimestamp, gotUnixTs, "MissingInDateHistogramToUnixTimestamp(%v)", tt.missing)
4343
assert.Equalf(t, tt.wantParsingSucceeded, gotParsingSucceeded, "MissingInDateHistogramToUnixTimestamp(%v)", tt.missing)
4444
})

quesma/queryparser/pancake_aggregation_parser_buckets.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,14 @@ func (cw *ClickhouseQueryTranslator) pancakeTryBucketAggregation(aggregation *pa
7878
}
7979
field := cw.parseFieldField(dateHistogram, "date_histogram")
8080

81-
didWeAddMissing := false
81+
weAddedMissing := false
8282
if missingRaw, exists := dateHistogram["missing"]; exists {
8383
if missing, ok := missingRaw.(string); ok {
8484
dateManager := kibana.NewDateManager()
85-
timestamp, parsingTimestampOk := dateManager.MissingInDateHistogramToUnixTimestamp(missing)
86-
if parsingTimestampOk {
85+
if unixTimestamp, parsingOk := dateManager.ParseMissingInDateHistogram(missing); parsingOk {
8786
field = model.NewFunction("COALESCE", field,
88-
model.NewFunction("toDateTime", model.NewLiteral(timestamp)))
89-
didWeAddMissing = true
87+
model.NewFunction("fromUnixTimestamp64Milli", model.NewLiteral(unixTimestamp)))
88+
weAddedMissing = true
9089
} else {
9190
logger.ErrorWithCtx(cw.Ctx).Msgf("unknown format of missing in date_histogram: %v. Skipping it.", missing)
9291
}
@@ -95,7 +94,8 @@ func (cw *ClickhouseQueryTranslator) pancakeTryBucketAggregation(aggregation *pa
9594
}
9695
}
9796

98-
if !didWeAddMissing {
97+
if !weAddedMissing {
98+
// if we don't add missing, we need to filter out nulls later
9999
aggregation.filterOutEmptyKeyBucket = true
100100
}
101101

quesma/queryparser/query_parser.go

Lines changed: 55 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"encoding/hex"
88
"encoding/json"
99
"fmt"
10+
"github.com/k0kubun/pp"
1011
"quesma/clickhouse"
12+
"quesma/kibana"
1113
"quesma/logger"
1214
"quesma/model"
1315
"quesma/model/bucket_aggregations"
@@ -19,9 +21,6 @@ import (
1921
"strconv"
2022
"strings"
2123
"unicode"
22-
23-
"github.com/k0kubun/pp"
24-
"github.com/relvacode/iso8601"
2524
)
2625

2726
type QueryMap = map[string]interface{}
@@ -764,7 +763,6 @@ func (cw *ClickhouseQueryTranslator) parseDateMathExpression(expr string) (strin
764763

765764
exp, err := ParseDateMathExpression(expr)
766765
if err != nil {
767-
logger.Warn().Msgf("error parsing date math expression: %s", expr)
768766
return "", err
769767
}
770768

@@ -775,7 +773,6 @@ func (cw *ClickhouseQueryTranslator) parseDateMathExpression(expr string) (strin
775773

776774
sql, err := builder.RenderSQL(exp)
777775
if err != nil {
778-
logger.Warn().Msgf("error rendering date math expression: %s", expr)
779776
return "", err
780777
}
781778

@@ -792,84 +789,81 @@ func (cw *ClickhouseQueryTranslator) parseRange(queryMap QueryMap) model.SimpleQ
792789
return model.NewSimpleQuery(nil, false)
793790
}
794791

795-
for field, v := range queryMap {
796-
field = cw.ResolveField(cw.Ctx, field)
792+
for fieldName, v := range queryMap {
793+
fieldName = cw.ResolveField(cw.Ctx, fieldName)
794+
fieldType := cw.Table.GetDateTimeType(cw.Ctx, cw.ResolveField(cw.Ctx, fieldName))
797795
stmts := make([]model.Expr, 0)
798796
if _, ok := v.(QueryMap); !ok {
799797
logger.WarnWithCtx(cw.Ctx).Msgf("invalid range type: %T, value: %v", v, v)
800798
continue
801799
}
802-
isDatetimeInDefaultFormat := true // in 99% requests, format is "strict_date_optional_time", which we can parse with time.Parse(time.RFC3339Nano, ..)
803-
if format, ok := v.(QueryMap)["format"]; ok && format == "epoch_millis" {
804-
isDatetimeInDefaultFormat = false
805-
}
806800

807801
keysSorted := util.MapKeysSorted(v.(QueryMap))
808802
for _, op := range keysSorted {
809-
v := v.(QueryMap)[op]
810-
var timeFormatFuncName string
811-
var finalLHS, valueToCompare model.Expr
812-
fieldType := cw.Table.GetDateTimeType(cw.Ctx, cw.ResolveField(cw.Ctx, field))
813-
vToPrint := sprint(v)
814-
valueToCompare = model.NewLiteral(vToPrint)
815-
finalLHS = model.NewColumnRef(field)
816-
if !isDatetimeInDefaultFormat {
817-
timeFormatFuncName = "toUnixTimestamp64Milli"
818-
finalLHS = model.NewFunction(timeFormatFuncName, model.NewColumnRef(field))
819-
} else {
820-
switch fieldType {
821-
case clickhouse.DateTime64, clickhouse.DateTime:
822-
if dateTime, ok := v.(string); ok {
823-
// if it's a date, we need to parse it to Clickhouse's DateTime format
824-
// how to check if it does not contain date math expression?
825-
if _, err := iso8601.ParseString(dateTime); err == nil {
826-
_, timeFormatFuncName = cw.parseDateTimeString(cw.Table, field, dateTime)
827-
// TODO Investigate the quotation below
828-
valueToCompare = model.NewFunction(timeFormatFuncName, model.NewLiteral(fmt.Sprintf("'%s'", dateTime)))
829-
} else if op == "gte" || op == "lte" || op == "gt" || op == "lt" {
830-
vToPrint, err = cw.parseDateMathExpression(vToPrint)
831-
valueToCompare = model.NewLiteral(vToPrint)
832-
if err != nil {
833-
logger.WarnWithCtx(cw.Ctx).Msgf("error parsing date math expression: %s", vToPrint)
834-
return model.NewSimpleQuery(nil, false)
835-
}
836-
}
837-
} else if v == nil {
838-
vToPrint = "NULL"
839-
valueToCompare = model.NewLiteral("NULL")
803+
valueRaw := v.(QueryMap)[op]
804+
value := sprint(valueRaw)
805+
defaultValue := model.NewLiteral(value)
806+
dateManager := kibana.NewDateManager()
807+
808+
// Three stages:
809+
// 1. dateManager.ParseRange
810+
// 2. cw.parseDateMathExpression
811+
// 3. just a number
812+
// Dates use 1-3 and finish as soon as any succeeds
813+
// Numbers use just 3rd
814+
815+
var finalValue model.Expr
816+
doneParsing, isQuoted := false, len(value) > 2 && value[0] == '\'' && value[len(value)-1] == '\''
817+
switch fieldType {
818+
case clickhouse.DateTime, clickhouse.DateTime64:
819+
// TODO add support for "time_zone" parameter in ParseRange
820+
finalValue, doneParsing = dateManager.ParseRange(value) // stage 1
821+
822+
if !doneParsing && (op == "gte" || op == "lte" || op == "gt" || op == "lt") { // stage 2
823+
parsed, err := cw.parseDateMathExpression(value)
824+
if err == nil {
825+
doneParsing = true
826+
finalValue = model.NewLiteral(parsed)
840827
}
841-
case clickhouse.Invalid: // assumes it is number that does not need formatting
842-
if len(vToPrint) > 2 && vToPrint[0] == '\'' && vToPrint[len(vToPrint)-1] == '\'' {
843-
isNumber := true
844-
for _, c := range vToPrint[1 : len(vToPrint)-1] {
845-
if !unicode.IsDigit(c) && c != '.' {
846-
isNumber = false
847-
}
848-
}
849-
if isNumber {
850-
vToPrint = vToPrint[1 : len(vToPrint)-1]
851-
} else {
852-
logger.WarnWithCtx(cw.Ctx).Msgf("we use range with unknown literal %s, field %s", vToPrint, field)
828+
}
829+
830+
if !doneParsing && isQuoted { // stage 3
831+
finalValue, doneParsing = dateManager.ParseRange(value[1 : len(value)-1])
832+
}
833+
case clickhouse.Invalid:
834+
if isQuoted {
835+
isNumber, unquoted := true, value[1:len(value)-1]
836+
for _, c := range unquoted {
837+
if !unicode.IsDigit(c) && c != '.' {
838+
isNumber = false
853839
}
854-
valueToCompare = model.NewLiteral(vToPrint)
855840
}
856-
default:
857-
logger.WarnWithCtx(cw.Ctx).Msgf("invalid DateTime type for field: %s, parsed dateTime value: %s", field, vToPrint)
841+
if isNumber {
842+
finalValue = model.NewLiteral(unquoted)
843+
doneParsing = true
844+
}
858845
}
846+
default:
847+
logger.ErrorWithCtx(cw.Ctx).Msgf("invalid DateTime type for field: %s, parsed dateTime value: %s", fieldName, value)
848+
}
849+
850+
if !doneParsing {
851+
finalValue = defaultValue
859852
}
860853

854+
field := model.NewColumnRef(fieldName)
861855
switch op {
862856
case "gte":
863-
stmt := model.NewInfixExpr(finalLHS, ">=", valueToCompare)
857+
stmt := model.NewInfixExpr(field, ">=", finalValue)
864858
stmts = append(stmts, stmt)
865859
case "lte":
866-
stmt := model.NewInfixExpr(finalLHS, "<=", valueToCompare)
860+
stmt := model.NewInfixExpr(field, "<=", finalValue)
867861
stmts = append(stmts, stmt)
868862
case "gt":
869-
stmt := model.NewInfixExpr(finalLHS, ">", valueToCompare)
863+
stmt := model.NewInfixExpr(field, ">", finalValue)
870864
stmts = append(stmts, stmt)
871865
case "lt":
872-
stmt := model.NewInfixExpr(finalLHS, "<", valueToCompare)
866+
stmt := model.NewInfixExpr(field, "<", finalValue)
873867
stmts = append(stmts, stmt)
874868
case "format":
875869
// ignored
@@ -885,21 +879,6 @@ func (cw *ClickhouseQueryTranslator) parseRange(queryMap QueryMap) model.SimpleQ
885879
return model.NewSimpleQuery(nil, false)
886880
}
887881

888-
// parseDateTimeString returns string used to parse DateTime in Clickhouse (depends on column type)
889-
890-
func (cw *ClickhouseQueryTranslator) parseDateTimeString(table *clickhouse.Table, field, dateTime string) (string, string) {
891-
typ := table.GetDateTimeType(cw.Ctx, cw.ResolveField(cw.Ctx, field))
892-
switch typ {
893-
case clickhouse.DateTime64:
894-
return "parseDateTime64BestEffort('" + dateTime + "')", "parseDateTime64BestEffort"
895-
case clickhouse.DateTime:
896-
return "parseDateTimeBestEffort('" + dateTime + "')", "parseDateTimeBestEffort"
897-
default:
898-
logger.Error().Msgf("invalid DateTime type: %T for field: %s, parsed dateTime value: %s", typ, field, dateTime)
899-
return "", ""
900-
}
901-
}
902-
903882
// TODO: not supported:
904883
// - The field has "index" : false and "doc_values" : false set in the mapping
905884
// - The length of the field value exceeded an ignore_above setting in the mapping

quesma/queryparser/query_parser_range_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ var parseRangeTests = []parseRangeTest{
3232
`CREATE TABLE ` + tableName + `
3333
( "message" String, "timestamp" DateTime64(3, 'UTC') )
3434
ENGINE = Memory`,
35-
`("timestamp">=parseDateTime64BestEffort('2024-02-02T13:47:16.029Z') AND "timestamp"<=parseDateTime64BestEffort('2024-02-09T13:47:16.029Z'))`,
35+
`("timestamp">=fromUnixTimestamp64Milli(1706881636029) AND "timestamp"<=fromUnixTimestamp64Milli(1707486436029))`,
3636
},
3737
{
3838
"parseDateTimeBestEffort",
@@ -46,7 +46,7 @@ var parseRangeTests = []parseRangeTest{
4646
`CREATE TABLE ` + tableName + `
4747
( "message" String, "timestamp" DateTime )
4848
ENGINE = Memory`,
49-
`("timestamp">=parseDateTimeBestEffort('2024-02-02T13:47:16.029Z') AND "timestamp"<=parseDateTimeBestEffort('2024-02-09T13:47:16.029Z'))`,
49+
`("timestamp">=fromUnixTimestamp64Milli(1706881636029) AND "timestamp"<=fromUnixTimestamp64Milli(1707486436029))`,
5050
},
5151
{
5252
"numeric range",
@@ -72,7 +72,7 @@ var parseRangeTests = []parseRangeTest{
7272
`CREATE TABLE ` + tableName + `
7373
( "message" String, "timestamp" DateTime64(3, 'UTC') )
7474
ENGINE = Memory`,
75-
`("timestamp">=parseDateTime64BestEffort('2024-02-02T13:47:16') AND "timestamp"<=parseDateTime64BestEffort('2024-02-09T13:47:16'))`,
75+
`("timestamp">=fromUnixTimestamp64Milli(1706881636000) AND "timestamp"<=fromUnixTimestamp64Milli(1707486436000))`,
7676
},
7777
}
7878

quesma/quesma/functionality/terms_enum/terms_enum_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ func testHandleTermsEnumRequest(t *testing.T, requestBody []byte) {
109109
}
110110
qt := &queryparser.ClickhouseQueryTranslator{ClickhouseLM: lm, Table: table, Ctx: context.Background(), Schema: s.Tables[schema.TableName(testTableName)]}
111111
// Here we additionally verify that terms for `_tier` are **NOT** included in the SQL query
112-
expectedQuery1 := `SELECT DISTINCT "client_name" FROM ` + testTableName + ` WHERE ("epoch_time">=parseDateTimeBestEffort('2024-02-27T12:25:00.000Z') AND "epoch_time"<=parseDateTimeBestEffort('2024-02-27T12:40:59.999Z')) LIMIT 13`
113-
expectedQuery2 := `SELECT DISTINCT "client_name" FROM ` + testTableName + ` WHERE ("epoch_time"<=parseDateTimeBestEffort('2024-02-27T12:40:59.999Z') AND "epoch_time">=parseDateTimeBestEffort('2024-02-27T12:25:00.000Z')) LIMIT 13`
112+
expectedQuery1 := `SELECT DISTINCT "client_name" FROM ` + testTableName + ` WHERE ("epoch_time">=fromUnixTimestamp64Milli(1709036700000) AND "epoch_time"<=fromUnixTimestamp64Milli(1709037659999)) LIMIT 13`
113+
expectedQuery2 := `SELECT DISTINCT "client_name" FROM ` + testTableName + ` WHERE ("epoch_time">=fromUnixTimestamp64Milli(1709036700000) AND "epoch_time"<=fromUnixTimestamp64Milli(1709037659999)) LIMIT 13`
114114

115115
// Once in a while `AND` conditions could be swapped, so we match both cases
116116
mock.ExpectQuery(fmt.Sprintf("%s|%s", regexp.QuoteMeta(expectedQuery1), regexp.QuoteMeta(expectedQuery2))).

0 commit comments

Comments
 (0)