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

Commit 3b9c22d

Browse files
authored
Better handling DateTime(N), where N > 3 (#1302)
Followup to this issue #1199 and this PR #1214 During testing I noticed we can't really filter for e.g. `DateTime(9)`, because we rely there on `unixTimestampMilli`, which handles only `DateTime(3)` precision. Fixed that here by using `toDateTime64()` which can handle any precision.
1 parent b15fa13 commit 3b9c22d

File tree

2 files changed

+56
-37
lines changed

2 files changed

+56
-37
lines changed

platform/parsers/elastic_query_dsl/dates.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@ var acceptableDateTimeFormats = []string{"2006", "2006-01", "2006-01-02", "2006-
2626

2727
// parseStrictDateOptionalTimeOrEpochMillis parses date, which is in [strict_date_optional_time || epoch_millis] format
2828
// (https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html)
29-
func (dm DateManager) parseStrictDateOptionalTimeOrEpochMillis(date any) (unixTimestamp int64, parsingSucceeded bool) {
29+
func (dm DateManager) parseStrictDateOptionalTimeOrEpochMillis(date any) (utcTimestamp time.Time, parsingSucceeded bool) {
3030
if asInt, success := util.ExtractInt64Maybe(date); success {
31-
return asInt, true
31+
return time.UnixMilli(asInt), true
3232
}
3333

3434
if asFloat, success := util.ExtractFloat64Maybe(date); success {
35-
return int64(asFloat), true
35+
return time.UnixMilli(int64(asFloat)), true
3636
}
3737

3838
asString, success := date.(string)
3939
if !success {
40-
return -1, false
40+
return time.Time{}, false
4141
}
4242

4343
// * When missing is a single number >= 10000, it's already a unix timestamp (e.g. "10000" -> 10000th second after 01.01.1970)
@@ -55,25 +55,38 @@ func (dm DateManager) parseStrictDateOptionalTimeOrEpochMillis(date any) (unixTi
5555
// It could be replaced with iso8601.ParseString() after the fixes to 1.4.0:
5656
// https://github.com/relvacode/iso8601/pull/26
5757
for _, format := range acceptableDateTimeFormats {
58-
if date, err := time.Parse(format, asString); err == nil {
59-
return date.UnixMilli(), true
58+
if t, err := time.Parse(format, asString); err == nil {
59+
return t, true
6060
}
6161
}
6262

63-
return -1, false
63+
return time.Time{}, false
6464
}
6565

6666
// ParseDateUsualFormat parses date expression, which is in [strict_date_optional_time || epoch_millis] format
6767
// (https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html)
6868
// It's most usual format for date in Kibana, used e.g. in Query DSL's range, or date_histogram.
6969
func (dm DateManager) ParseDateUsualFormat(exprFromRequest any, datetimeType clickhouse.DateTimeType) (
7070
resultExpr model.Expr, parsingSucceeded bool) {
71-
if unixTs, success := dm.parseStrictDateOptionalTimeOrEpochMillis(exprFromRequest); success {
71+
if utcTs, success := dm.parseStrictDateOptionalTimeOrEpochMillis(exprFromRequest); success {
7272
switch datetimeType {
7373
case clickhouse.DateTime64:
74-
return model.NewFunction("fromUnixTimestamp64Milli", model.NewLiteral(unixTs)), true
74+
threeDigitsOfPrecisionSuffice := utcTs.UnixNano()%1_000_000 == 0
75+
if threeDigitsOfPrecisionSuffice {
76+
return model.NewFunction("fromUnixTimestamp64Milli", model.NewLiteral(utcTs.UnixMilli())), true
77+
} else {
78+
return model.NewFunction(
79+
"toDateTime64",
80+
model.NewInfixExpr(
81+
model.NewLiteral(utcTs.UnixNano()),
82+
"/",
83+
model.NewLiteral(1_000_000_000),
84+
),
85+
model.NewLiteral(9),
86+
), true
87+
}
7588
case clickhouse.DateTime:
76-
return model.NewFunction("fromUnixTimestamp", model.NewLiteral(unixTs/1000)), true
89+
return model.NewFunction("fromUnixTimestamp", model.NewLiteral(utcTs.Unix())), true
7790
default:
7891
logger.WarnWithCtx(dm.ctx).Msgf("Unknown datetimeType: %v", datetimeType)
7992
}

platform/parsers/elastic_query_dsl/dates_test.go

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,47 @@ import (
88
"fmt"
99
"github.com/stretchr/testify/assert"
1010
"testing"
11+
"time"
1112
)
1213

1314
func TestDateManager_parseStrictDateOptionalTimeOrEpochMillis(t *testing.T) {
15+
empty := time.Time{}
1416
tests := []struct {
15-
missing any
16-
wantUnixTimestamp int64
17-
wantParsingSucceeded bool
17+
input any
18+
wantedTimestamp time.Time
19+
wantedParsingSucceeded bool
1820
}{
19-
{nil, -1, false},
20-
{"2024", 1704067200000, true},
21-
{int64(123), 123, true},
22-
{"4234324223", 4234324223, true},
23-
{"4234", 71444937600000, true},
24-
{"42340", 42340, true},
25-
{"42340.234", 42340, true},
26-
{"2024/02", -1, false},
27-
{"2024-02", 1706745600000, true},
28-
{"2024-2", -1, false},
29-
{"2024-02-02", 1706832000000, true},
30-
{"2024-02-3", -1, false},
31-
{"2024-02-30", -1, false},
32-
{"2024-02-25T1", 1708822800000, true}, // this fails in Kibana, so we're better
33-
{"2024-02-25T13:00:00", 1708866000000, true},
34-
{"2024-02-25 13:00:00", -1, false},
35-
{"2024-02-25T13:11", 1708866660000, true},
36-
{"2024-02-25T25:00:00", -1, false},
37-
{"2024-02-25T13:00:00+05", 1708848000000, true},
38-
{"2024-02-25T13:00:00+05:00", 1708848000000, true},
21+
{nil, empty, false},
22+
{"2024", time.UnixMilli(1704067200000), true},
23+
{int64(123), time.UnixMilli(123), true},
24+
{"4234324223", time.UnixMilli(4234324223), true},
25+
{"4234", time.UnixMilli(71444937600000), true},
26+
{"42340", time.UnixMilli(42340), true},
27+
{"42340.234", time.UnixMilli(42340), true},
28+
{"2024/02", empty, false},
29+
{"2024-02", time.UnixMilli(1706745600000), true},
30+
{"2024-2", empty, false},
31+
{"2024-02-02", time.UnixMilli(1706832000000), true},
32+
{"2024-02-3", empty, false},
33+
{"2024-02-30", empty, false},
34+
{"2024-02-25T1", time.UnixMilli(1708822800000), true}, // this fails in Kibana, so we're better
35+
{"2024-02-25T13:00:00", time.UnixMilli(1708866000000), true},
36+
{"2024-02-25 13:00:00", empty, false},
37+
{"2024-02-25T13:11", time.UnixMilli(1708866660000), true},
38+
{"2024-02-25T25:00:00", empty, false},
39+
{"2024-02-25T13:00:00+05", time.UnixMilli(1708848000000), true},
40+
{"2024-02-25T13:00:00+05:00", time.UnixMilli(1708848000000), true},
41+
{"2024-02-25T13:00:00.123", time.UnixMilli(1708866000123), true},
42+
{"2024-02-25T13:00:00.123Z", time.UnixMilli(1708866000123), true},
43+
{"2024-02-25T13:00:00.123456789", time.Unix(1708866000, 123456789), true},
44+
{"2024-02-25T13:00:00.123456789Z", time.Unix(1708866000, 123456789), true},
3945
}
4046
for _, tt := range tests {
41-
t.Run(fmt.Sprintf("%v", tt.missing), func(t *testing.T) {
47+
t.Run(fmt.Sprintf("%v", tt.input), func(t *testing.T) {
4248
dm := NewDateManager(context.Background())
43-
gotUnixTs, gotParsingSucceeded := dm.parseStrictDateOptionalTimeOrEpochMillis(tt.missing)
44-
assert.Equalf(t, tt.wantUnixTimestamp, gotUnixTs, "MissingInDateHistogramToUnixTimestamp(%v)", tt.missing)
45-
assert.Equalf(t, tt.wantParsingSucceeded, gotParsingSucceeded, "MissingInDateHistogramToUnixTimestamp(%v)", tt.missing)
49+
gotUnixTs, gotParsingSucceeded := dm.parseStrictDateOptionalTimeOrEpochMillis(tt.input)
50+
assert.Truef(t, tt.wantedTimestamp.Equal(gotUnixTs), "MissingInDateHistogramToUnixTimestamp(%v)", tt.input)
51+
assert.Equalf(t, tt.wantedParsingSucceeded, gotParsingSucceeded, "MissingInDateHistogramToUnixTimestamp(%v)", tt.input)
4652
})
4753
}
4854
}

0 commit comments

Comments
 (0)