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

Commit 1da1dbe

Browse files
trzysiekpdelewski
andauthored
Support timestamp in Kibana being int (not DateTime) column in ClickHouse (#1381)
Basically support for #1380 After: we can add a data view with a timestamp and all seems to be looking good while `ts` being an `UInt64` 😉 ![Screenshot 2025-03-26 at 00 29 30](https://github.com/user-attachments/assets/f8292b79-4b82-47f5-a021-e385904c0cc2) --------- Signed-off-by: Przemyslaw Delewski <[email protected]> Co-authored-by: Przemyslaw Delewski <[email protected]>
1 parent 6165e9d commit 1da1dbe

File tree

15 files changed

+574
-88
lines changed

15 files changed

+574
-88
lines changed

platform/clickhouse/table.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,8 @@ func (t *Table) GetFieldInfo(ctx context.Context, fieldName string) FieldInfo {
191191
}
192192
return ExistsAndIsBaseType
193193
}
194+
195+
func (t *Table) IsInt(fieldName string) bool {
196+
col, ok := t.Cols[fieldName]
197+
return ok && col.Type != nil && strings.Contains(col.Type.String(), "Int")
198+
}

platform/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ type QuesmaConfiguration struct {
5959
MapFieldsDiscoveringEnabled bool
6060
}
6161

62+
func NewQuesmaConfigurationIndexConfigOnly(indexConfig map[string]IndexConfiguration) QuesmaConfiguration {
63+
return QuesmaConfiguration{IndexConfig: indexConfig}
64+
}
65+
6266
func (c *QuesmaConfiguration) AliasFields(indexName string) map[string]string {
6367
aliases := make(map[string]string)
6468
if indexConfig, found := c.IndexConfig[indexName]; found {

platform/frontend_connectors/schema_transformer.go

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
package frontend_connectors
44

55
import (
6+
"context"
67
"fmt"
78
"github.com/QuesmaOrg/quesma/platform/clickhouse"
89
"github.com/QuesmaOrg/quesma/platform/common_table"
910
"github.com/QuesmaOrg/quesma/platform/config"
1011
"github.com/QuesmaOrg/quesma/platform/logger"
1112
"github.com/QuesmaOrg/quesma/platform/model"
1213
"github.com/QuesmaOrg/quesma/platform/model/typical_queries"
14+
"github.com/QuesmaOrg/quesma/platform/parsers/elastic_query_dsl"
1315
"github.com/QuesmaOrg/quesma/platform/schema"
1416
"github.com/QuesmaOrg/quesma/platform/transformations"
1517
"sort"
@@ -58,7 +60,8 @@ func (s *SchemaCheckPass) applyBooleanLiteralLowering(index schema.Schema, query
5860
if strings.Contains(boolLiteral, "true") || strings.Contains(boolLiteral, "false") {
5961
boolLiteral = strings.TrimLeft(boolLiteral, "'")
6062
boolLiteral = strings.TrimRight(boolLiteral, "'")
61-
return model.NewLiteralWithEscapeType(boolLiteral, e.EscapeType)
63+
var asAny any = boolLiteral
64+
return e.CloneAndOverride(&asAny, nil, nil)
6265
}
6366
}
6467
return e.Clone()
@@ -235,7 +238,7 @@ func (s *SchemaCheckPass) applyGeoTransformations(schemaInstance schema.Schema,
235238
}
236239
}
237240

238-
return model.NewFunction(e.Name, b.VisitChildren(e.Args)...)
241+
return visitFunction(b, e)
239242
}
240243

241244
visitor.OverrideVisitSelectCommand = func(v *model.BaseExprVisitor, query model.SelectCommand) interface{} {
@@ -534,7 +537,7 @@ func (s *SchemaCheckPass) applyWildcardExpansion(indexSchema schema.Schema, quer
534537

535538
for _, selectColumn := range query.SelectCommand.Columns {
536539

537-
if selectColumn == model.NewWildcardExpr {
540+
if model.IsWildcard(selectColumn) {
538541
hasWildcard = true
539542
} else {
540543
newColumns = append(newColumns, selectColumn)
@@ -615,9 +618,9 @@ func (s *SchemaCheckPass) applyFullTextField(indexSchema schema.Schema, query *m
615618

616619
if len(fullTextFields) == 0 {
617620
if (strings.ToUpper(e.Op) == "LIKE" || strings.ToUpper(e.Op) == "ILIKE") && model.AsString(e.Right) == "'%'" {
618-
return model.NewLiteral(true)
621+
return model.TrueExpr
619622
}
620-
return model.NewLiteral(false)
623+
return model.FalseExpr
621624
}
622625

623626
var expressions []model.Expr
@@ -632,7 +635,7 @@ func (s *SchemaCheckPass) applyFullTextField(indexSchema schema.Schema, query *m
632635
}
633636
}
634637

635-
return model.NewInfixExpr(e.Left.Accept(b).(model.Expr), e.Op, e.Right.Accept(b).(model.Expr))
638+
return visitInfix(b, e)
636639
}
637640

638641
expr := query.SelectCommand.Accept(visitor)
@@ -750,7 +753,7 @@ func (s *SchemaCheckPass) applyFieldEncoding(indexSchema schema.Schema, query *m
750753
if hasAttributesValuesColumn {
751754
return model.NewArrayAccess(model.NewColumnRef(clickhouse.AttributesValuesColumn), model.NewLiteral(fmt.Sprintf("'%s'", e.ColumnName)))
752755
} else {
753-
return model.NewLiteral("NULL")
756+
return model.NullExpr
754757
}
755758
}
756759
}
@@ -870,7 +873,7 @@ func (s *SchemaCheckPass) convertQueryDateTimeFunctionToClickhouse(indexSchema s
870873
// add more
871874

872875
default:
873-
return model.NewFunction(e.Name, b.VisitChildren(e.Args)...)
876+
return visitFunction(b, e)
874877
}
875878
}
876879

@@ -904,7 +907,7 @@ func (s *SchemaCheckPass) checkAggOverUnsupportedType(indexSchema schema.Schema,
904907
if strings.HasPrefix(col.InternalPropertyType, dbTypePrefix) {
905908
logger.Warn().Msgf("Aggregation '%s' over unsupported type '%s' in column '%s'", e.Name, dbTypePrefix, col.InternalPropertyName.AsString())
906909
args := b.VisitChildren(e.Args)
907-
args[0] = model.NewLiteral("NULL")
910+
args[0] = model.NullExpr
908911
return model.NewFunction(e.Name, args...)
909912
}
910913
}
@@ -915,15 +918,15 @@ func (s *SchemaCheckPass) checkAggOverUnsupportedType(indexSchema schema.Schema,
915918
if access.ColumnRef.ColumnName == clickhouse.AttributesValuesColumn {
916919
logger.Warn().Msgf("Unsupported case. Aggregation '%s' over attribute named: '%s'", e.Name, access.Index)
917920
args := b.VisitChildren(e.Args)
918-
args[0] = model.NewLiteral("NULL")
921+
args[0] = model.NullExpr
919922
return model.NewFunction(e.Name, args...)
920923
}
921924
}
922925
}
923926
}
924927
}
925928

926-
return model.NewFunction(e.Name, b.VisitChildren(e.Args)...)
929+
return visitFunction(b, e)
927930
}
928931

929932
expr := query.SelectCommand.Accept(visitor)
@@ -974,6 +977,76 @@ func (s *SchemaCheckPass) applyAliasColumns(indexSchema schema.Schema, query *mo
974977
return query, nil
975978
}
976979

980+
func visitFunction(b *model.BaseExprVisitor, f model.FunctionExpr) interface{} {
981+
return model.NewFunction(f.Name, b.VisitChildren(f.Args)...)
982+
}
983+
984+
func visitInfix(b *model.BaseExprVisitor, e model.InfixExpr) interface{} {
985+
return model.NewInfixExpr(e.Left.Accept(b).(model.Expr), e.Op, e.Right.Accept(b).(model.Expr))
986+
}
987+
988+
func (s *SchemaCheckPass) acceptIntsAsTimestamps(indexSchema schema.Schema, query *model.Query) (*model.Query, error) {
989+
table, exists := s.tableDiscovery.TableDefinitions().Load(query.TableName)
990+
if !exists {
991+
return nil, fmt.Errorf("table %s not found", query.TableName)
992+
}
993+
994+
dateManager := elastic_query_dsl.NewDateManager(context.Background())
995+
visitor := model.NewBaseVisitor()
996+
997+
visitor.OverrideVisitInfix = func(b *model.BaseExprVisitor, e model.InfixExpr) interface{} {
998+
col, okLeft := model.ExtractColRef(e.Left)
999+
lit, _ := model.ToLiteral(e.Right)
1000+
ts, okRight := model.ToLiteralsValue(e.Right)
1001+
if okLeft && okRight && table.IsInt(col.ColumnName) {
1002+
format := ""
1003+
if f, ok := lit.Format(); ok {
1004+
format = f
1005+
}
1006+
expr, ok := dateManager.ParseDateUsualFormat(ts, clickhouse.DateTime64, format)
1007+
if !ok {
1008+
// FIXME hacky but seems working
1009+
if tsStr, ok_ := ts.(string); ok_ && len(tsStr) > 2 {
1010+
expr, ok = dateManager.ParseDateUsualFormat(tsStr[1:len(tsStr)-1], clickhouse.DateTime64, format)
1011+
}
1012+
}
1013+
if ok {
1014+
if f, okF := model.ToFunction(expr); okF && f.Name == "fromUnixTimestamp64Milli" && len(f.Args) == 1 {
1015+
if l, okL := model.ToLiteral(f.Args[0]); okL {
1016+
if _, exists := l.Format(); exists { // heuristics: it's a date <=> it has a format
1017+
return model.NewInfixExpr(col, e.Op, f.Args[0])
1018+
}
1019+
}
1020+
}
1021+
}
1022+
}
1023+
return visitInfix(b, e)
1024+
}
1025+
1026+
visitor.OverrideVisitFunction = func(b *model.BaseExprVisitor, f model.FunctionExpr) interface{} {
1027+
if f.Name == "toUnixTimestamp64Milli" && len(f.Args) == 1 {
1028+
if col, ok := model.ExtractColRef(f.Args[0]); ok && table.IsInt(col.ColumnName) {
1029+
// erases toUnixTimestamp64Milli
1030+
return f.Args[0]
1031+
}
1032+
}
1033+
if f.Name == "toTimezone" && len(f.Args) == 2 {
1034+
if col, ok := model.ExtractColRef(f.Args[0]); ok && table.IsInt(col.ColumnName) {
1035+
// adds fromUnixTimestamp64Milli
1036+
return model.NewFunction("toTimezone", model.NewFunction("fromUnixTimestamp64Milli", f.Args[0]), f.Args[1])
1037+
}
1038+
}
1039+
return visitFunction(b, f)
1040+
}
1041+
1042+
expr := query.SelectCommand.Accept(visitor)
1043+
if _, ok := expr.(*model.SelectCommand); ok {
1044+
query.SelectCommand = *expr.(*model.SelectCommand)
1045+
}
1046+
1047+
return query, nil
1048+
}
1049+
9771050
func (s *SchemaCheckPass) Transform(plan *model.ExecutionPlan) (*model.ExecutionPlan, error) {
9781051

9791052
transformationChain := []struct {
@@ -988,6 +1061,7 @@ func (s *SchemaCheckPass) Transform(plan *model.ExecutionPlan) (*model.Execution
9881061
return transformations.ApplyAllNecessaryCommonTransformations(query, schema, s.cfg.MapFieldsDiscoveringEnabled)
9891062
}},
9901063
{TransformationName: "AliasColumnsTransformation", Transformation: s.applyAliasColumns},
1064+
{TransformationName: "AcceptIntsAsTimestamps", Transformation: s.acceptIntsAsTimestamps},
9911065

9921066
// Section 2: generic schema based transformations
9931067
//
@@ -1082,7 +1156,7 @@ func (s *SchemaCheckPass) applyMatchOperator(indexSchema schema.Schema, query *m
10821156
okLeft = true
10831157
lhsCol = lhsT.ColumnRef
10841158
default:
1085-
return model.NewInfixExpr(e.Left.Accept(b).(model.Expr), e.Op, e.Right.Accept(b).(model.Expr))
1159+
return visitInfix(b, e)
10861160
}
10871161

10881162
rhsValue, ok := rhs.Value.(string)
@@ -1091,7 +1165,7 @@ func (s *SchemaCheckPass) applyMatchOperator(indexSchema schema.Schema, query *m
10911165
// only strings can be ILIKEd, everything else is a simple =
10921166
return model.NewInfixExpr(e.Left.Accept(b).(model.Expr), "=", e.Right.Accept(b).(model.Expr))
10931167
} else {
1094-
return model.NewInfixExpr(e.Left.Accept(b).(model.Expr), e.Op, e.Right.Accept(b).(model.Expr))
1168+
return visitInfix(b, e)
10951169
}
10961170
}
10971171

@@ -1131,7 +1205,7 @@ func (s *SchemaCheckPass) applyMatchOperator(indexSchema schema.Schema, query *m
11311205
switch field.Type.String() {
11321206
case schema.QuesmaTypeInteger.Name, schema.QuesmaTypeLong.Name, schema.QuesmaTypeUnsignedLong.Name, schema.QuesmaTypeFloat.Name, schema.QuesmaTypeBoolean.Name:
11331207
rhs.Value = strings.Trim(rhsValue, "%")
1134-
rhs.EscapeType = model.NormalNotEscaped
1208+
rhs.Attrs[model.EscapeKey] = model.NormalNotEscaped
11351209
return equal()
11361210
case schema.QuesmaTypeKeyword.Name:
11371211
return equal()
@@ -1168,14 +1242,17 @@ func (s *SchemaCheckPass) applyMatchOperator(indexSchema schema.Schema, query *m
11681242

11691243
// sanity check for map type with two elements
11701244
if len(kvTypes) == 2 {
1171-
rhsValue := rhs.Value.(string)
1245+
rhsValue = rhs.Value.(string)
11721246
rhsValue = strings.TrimPrefix(rhsValue, "'")
11731247
rhsValue = strings.TrimSuffix(rhsValue, "'")
11741248

11751249
// here we check if the value of the map is string or not
11761250

11771251
if strings.Contains(kvTypes[1], "String") {
1178-
return model.NewInfixExpr(arrayElementFn.Accept(b).(model.Expr), "iLIKE", model.NewLiteralWithEscapeType(rhsValue, model.NotEscapedLikeFull))
1252+
newRhs := rhs.Clone()
1253+
newRhs.Value = rhsValue
1254+
newRhs.Attrs[model.EscapeKey] = model.NotEscapedLikeFull
1255+
return model.NewInfixExpr(arrayElementFn.Accept(b).(model.Expr), "iLIKE", newRhs)
11791256
} else {
11801257
return model.NewInfixExpr(arrayElementFn.Accept(b).(model.Expr), "=", e.Right.Accept(b).(model.Expr))
11811258
}
@@ -1190,7 +1267,7 @@ func (s *SchemaCheckPass) applyMatchOperator(indexSchema schema.Schema, query *m
11901267
if e.Op == model.MatchOperator {
11911268
logger.Error().Msgf("Match operator is not supported for column %v (expr: %v)", lhsCol, e)
11921269
}
1193-
return model.NewInfixExpr(e.Left.Accept(b).(model.Expr), e.Op, e.Right.Accept(b).(model.Expr))
1270+
return visitInfix(b, e)
11941271
}
11951272

11961273
expr := query.SelectCommand.Accept(visitor)

0 commit comments

Comments
 (0)