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

Commit e75fda2

Browse files
trzysiektrzysiek
andauthored
Resolve MatchOperator for maps (#1369)
Fixes #1361 You can see there's no `__quesma_match` in the SQL, it's properly replaced by `ILIKE` <img width="645" alt="Screenshot 2025-03-14 at 12 09 29" src="https://github.com/user-attachments/assets/5a7c7529-cd94-4615-8bf0-5fd48e512ee5" /> --------- Co-authored-by: trzysiek <[email protected]>
1 parent 75b2a46 commit e75fda2

File tree

4 files changed

+183
-18
lines changed

4 files changed

+183
-18
lines changed

platform/clickhouse/schema.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ const (
2121
DeprecatedAttributesValueColumn = "attributes_string_value"
2222
DeprecatedAttributesValueType = "attributes_string_type"
2323

24-
attributesColumnType = "Map(String, String)" // ClickHouse type of AttributesValuesColumn, AttributesMetadataColumn
24+
// ClickHouse type of AttributesValuesColumn, AttributesMetadataColumn
25+
// Important: If we ever introduce attributes with values of no-String type,
26+
// consider updating SchemaCheckPass.applyMatchOperator as well.
27+
attributesColumnType = "Map(String, String)"
2528
AttributesValuesColumn = "attributes_values"
2629
AttributesMetadataColumn = "attributes_metadata"
2730

@@ -427,3 +430,7 @@ func NewDefaultBoolAttribute() Attribute {
427430
func (dt DateTimeType) String() string {
428431
return []string{"DateTime64", "DateTime", "Invalid"}[dt]
429432
}
433+
434+
func IsColumnAttributes(colName string) bool {
435+
return colName == AttributesValuesColumn || colName == AttributesMetadataColumn
436+
}

platform/frontend_connectors/schema_transformer.go

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,28 +1068,67 @@ func (s *SchemaCheckPass) applyMatchOperator(indexSchema schema.Schema, query *m
10681068

10691069
visitor := model.NewBaseVisitor()
10701070

1071-
var err error
1072-
10731071
visitor.OverrideVisitInfix = func(b *model.BaseExprVisitor, e model.InfixExpr) interface{} {
1074-
lhs, ok := e.Left.(model.ColumnRef)
1075-
rhs, ok2 := e.Right.(model.LiteralExpr)
1072+
var (
1073+
lhs = e.Left
1074+
rhs, okRight = e.Right.(model.LiteralExpr)
1075+
col, okLeft = e.Left.(model.ColumnRef)
1076+
lhsIsArrayAccess bool
1077+
)
1078+
1079+
if !okLeft {
1080+
if arrayAccess, ok := lhs.(model.ArrayAccess); ok {
1081+
lhsIsArrayAccess = true
1082+
okLeft = true
1083+
col = arrayAccess.ColumnRef
1084+
}
1085+
}
1086+
1087+
if okLeft && okRight && e.Op == model.MatchOperator {
1088+
if _, ok := rhs.Value.(string); !ok {
1089+
// only strings can be ILIKEd, everything else is a simple =
1090+
return model.NewInfixExpr(lhs, "=", rhs.Clone())
1091+
}
10761092

1077-
if ok && ok2 && e.Op == model.MatchOperator {
1078-
field, found := indexSchema.ResolveFieldByInternalName(lhs.ColumnName)
1093+
var colIsAttributes bool
1094+
field, found := indexSchema.ResolveFieldByInternalName(col.ColumnName)
10791095
if !found {
1080-
logger.Error().Msgf("Field %s not found in schema for table %s, should never happen here", lhs.ColumnName, query.TableName)
1096+
// indexSchema won't find attributes columns, that's why this check
1097+
if clickhouse.IsColumnAttributes(col.ColumnName) {
1098+
colIsAttributes = true
1099+
} else {
1100+
logger.Error().Msgf("Field %s not found in schema for table %s, should never happen here", col.ColumnName, query.TableName)
1101+
}
10811102
}
10821103

1083-
rhsValue := rhs.Value.(string)
1104+
rhsValue := rhs.Value.(string) // checked above
10841105
rhsValue = strings.TrimPrefix(rhsValue, "'")
10851106
rhsValue = strings.TrimSuffix(rhsValue, "'")
10861107

1087-
switch field.Type.String() {
1088-
case schema.QuesmaTypeInteger.Name, schema.QuesmaTypeLong.Name, schema.QuesmaTypeUnsignedLong.Name, schema.QuesmaTypeFloat.Name, schema.QuesmaTypeBoolean.Name:
1108+
ilike := func() model.Expr {
1109+
return model.NewInfixExpr(lhs, "ILIKE", model.NewLiteralWithEscapeType(rhsValue, rhs.EscapeType))
1110+
}
1111+
equal := func() model.Expr {
10891112
rhsValue = strings.Trim(rhsValue, "%")
10901113
return model.NewInfixExpr(lhs, "=", model.NewLiteral(rhsValue))
1114+
}
1115+
1116+
// handling case when e.Left is an array access
1117+
if lhsIsArrayAccess {
1118+
if colIsAttributes || field.IsMapWithStringValues() { // attributes always have string values, so ilike
1119+
return ilike()
1120+
} else {
1121+
return equal()
1122+
}
1123+
}
1124+
1125+
// handling case when e.Left is a simple column ref
1126+
// TODO: improve? we seem to be `ilike'ing` too much
1127+
switch field.Type.String() {
1128+
case schema.QuesmaTypeInteger.Name, schema.QuesmaTypeLong.Name, schema.QuesmaTypeUnsignedLong.Name, schema.QuesmaTypeFloat.Name, schema.QuesmaTypeBoolean.Name:
1129+
return equal()
10911130
default:
1092-
return model.NewInfixExpr(lhs, "ILIKE", model.NewLiteralWithEscapeType(rhsValue, rhs.EscapeType))
1131+
return ilike()
10931132
}
10941133
}
10951134

@@ -1133,15 +1172,14 @@ func (s *SchemaCheckPass) applyMatchOperator(indexSchema schema.Schema, query *m
11331172
}
11341173
}
11351174

1175+
if e.Op == model.MatchOperator {
1176+
logger.Error().Msgf("Match operator is not supported for column %v", col)
1177+
}
11361178
return model.NewInfixExpr(e.Left.Accept(b).(model.Expr), e.Op, e.Right.Accept(b).(model.Expr))
11371179
}
11381180

11391181
expr := query.SelectCommand.Accept(visitor)
11401182

1141-
if err != nil {
1142-
return nil, err
1143-
}
1144-
11451183
if _, ok := expr.(*model.SelectCommand); ok {
11461184
query.SelectCommand = *expr.(*model.SelectCommand)
11471185
}

platform/frontend_connectors/schema_transformer_test.go

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,8 +1141,10 @@ func TestFullTextFields(t *testing.T) {
11411141
func Test_applyMatchOperator(t *testing.T) {
11421142
schemaTable := schema.Table{
11431143
Columns: map[string]schema.Column{
1144-
"message": {Name: "message", Type: "String"},
1145-
"count": {Name: "count", Type: "Int64"},
1144+
"message": {Name: "message", Type: "String"},
1145+
"map_str_str": {Name: "map_str_str", Type: "Map(String, String)"},
1146+
"map_str_int": {Name: "map_str_int", Type: "Map(String, Int)"},
1147+
"count": {Name: "count", Type: "Int64"},
11461148
},
11471149
}
11481150

@@ -1205,6 +1207,114 @@ func Test_applyMatchOperator(t *testing.T) {
12051207
},
12061208
},
12071209
},
1210+
{
1211+
name: "match operator transformation for map(string, string) (ILIKE)",
1212+
query: &model.Query{
1213+
TableName: "test",
1214+
SelectCommand: model.SelectCommand{
1215+
FromClause: model.NewTableRef("test"),
1216+
Columns: []model.Expr{model.NewColumnRef("message")},
1217+
WhereClause: model.NewInfixExpr(
1218+
model.NewArrayAccess(model.NewColumnRef("map_str_str"), model.NewLiteral("'warsaw'")),
1219+
model.MatchOperator,
1220+
model.NewLiteralWithEscapeType("'needle'", model.NotEscapedLikeFull),
1221+
),
1222+
},
1223+
},
1224+
expected: &model.Query{
1225+
TableName: "test",
1226+
SelectCommand: model.SelectCommand{
1227+
FromClause: model.NewTableRef("test"),
1228+
Columns: []model.Expr{model.NewColumnRef("message")},
1229+
WhereClause: model.NewInfixExpr(
1230+
model.NewArrayAccess(model.NewColumnRef("map_str_str"), model.NewLiteral("'warsaw'")),
1231+
"ILIKE",
1232+
model.NewLiteralWithEscapeType("needle", model.NotEscapedLikeFull),
1233+
),
1234+
},
1235+
},
1236+
},
1237+
{
1238+
name: "match operator transformation for map(string, int) (=)",
1239+
query: &model.Query{
1240+
TableName: "test",
1241+
SelectCommand: model.SelectCommand{
1242+
FromClause: model.NewTableRef("test"),
1243+
Columns: []model.Expr{model.NewColumnRef("message")},
1244+
WhereClause: model.NewInfixExpr(
1245+
model.NewArrayAccess(model.NewColumnRef("map_str_int"), model.NewLiteral("'warsaw'")),
1246+
model.MatchOperator,
1247+
model.NewLiteral(50),
1248+
),
1249+
},
1250+
},
1251+
expected: &model.Query{
1252+
TableName: "test",
1253+
SelectCommand: model.SelectCommand{
1254+
FromClause: model.NewTableRef("test"),
1255+
Columns: []model.Expr{model.NewColumnRef("message")},
1256+
WhereClause: model.NewInfixExpr(
1257+
model.NewArrayAccess(model.NewColumnRef("map_str_int"), model.NewLiteral("'warsaw'")),
1258+
"=",
1259+
model.NewLiteral(50),
1260+
),
1261+
},
1262+
},
1263+
},
1264+
{
1265+
name: "match operator transformation for Attributes map (1/2)",
1266+
query: &model.Query{
1267+
TableName: "test",
1268+
SelectCommand: model.SelectCommand{
1269+
FromClause: model.NewTableRef("test"),
1270+
Columns: []model.Expr{model.NewColumnRef("message")},
1271+
WhereClause: model.NewInfixExpr(
1272+
model.NewArrayAccess(model.NewColumnRef(clickhouse.AttributesValuesColumn), model.NewLiteral("'warsaw'")),
1273+
model.MatchOperator,
1274+
model.NewLiteralWithEscapeType("needle", model.NotEscapedLikeFull),
1275+
),
1276+
},
1277+
},
1278+
expected: &model.Query{
1279+
TableName: "test",
1280+
SelectCommand: model.SelectCommand{
1281+
FromClause: model.NewTableRef("test"),
1282+
Columns: []model.Expr{model.NewColumnRef("message")},
1283+
WhereClause: model.NewInfixExpr(
1284+
model.NewArrayAccess(model.NewColumnRef(clickhouse.AttributesValuesColumn), model.NewLiteral("'warsaw'")),
1285+
"ILIKE",
1286+
model.NewLiteralWithEscapeType("needle", model.NotEscapedLikeFull),
1287+
),
1288+
},
1289+
},
1290+
},
1291+
{
1292+
name: "match operator transformation for Attributes map (2/2)",
1293+
query: &model.Query{
1294+
TableName: "test",
1295+
SelectCommand: model.SelectCommand{
1296+
FromClause: model.NewTableRef("test"),
1297+
Columns: []model.Expr{model.NewColumnRef("message")},
1298+
WhereClause: model.NewInfixExpr(
1299+
model.NewArrayAccess(model.NewColumnRef(clickhouse.AttributesMetadataColumn), model.NewLiteral("'warsaw'")),
1300+
model.MatchOperator,
1301+
model.NewLiteralWithEscapeType("needle", model.NotEscapedLikeFull),
1302+
),
1303+
},
1304+
},
1305+
expected: &model.Query{
1306+
TableName: "test",
1307+
SelectCommand: model.SelectCommand{
1308+
FromClause: model.NewTableRef("test"),
1309+
Columns: []model.Expr{model.NewColumnRef("message")},
1310+
WhereClause: model.NewInfixExpr(
1311+
model.NewArrayAccess(model.NewColumnRef(clickhouse.AttributesMetadataColumn), model.NewLiteral("'warsaw'")),
1312+
"ILIKE",
1313+
model.NewLiteralWithEscapeType("needle", model.NotEscapedLikeFull),
1314+
),
1315+
},
1316+
},
1317+
},
12081318
}
12091319

12101320
for _, tt := range tests {

platform/schema/schema.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ func NewSchema(fields map[FieldName]Field, existsInDataSource bool, databaseName
5151
return NewSchemaWithAliases(fields, map[FieldName]FieldName{}, existsInDataSource, databaseName)
5252
}
5353

54+
// IsMapWithStringValues returns true if the field is a map with string values,
55+
// e.g. Map(T, String), or Map(T, Nullable(String))
56+
func (f Field) IsMapWithStringValues() bool {
57+
typename := f.InternalPropertyType
58+
return typename == "Map(String, String)" ||
59+
typename == "Map(String,String)" ||
60+
typename == "Map(String, Nullable(String))" ||
61+
typename == "Map(String,Nullable(String))"
62+
}
63+
5464
func (f FieldName) AsString() string {
5565
return string(f)
5666
}

0 commit comments

Comments
 (0)