Skip to content

Commit e6798cc

Browse files
committed
feat(cubesql): Support Thoughtspot starts/ends LIKE exprs
1 parent b303aa6 commit e6798cc

File tree

3 files changed

+287
-6
lines changed

3 files changed

+287
-6
lines changed

rust/cubesql/cubesql/src/compile/mod.rs

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14385,7 +14385,7 @@ ORDER BY \"COUNT(count)\" DESC"
1438514385
}
1438614386

1438714387
#[tokio::test]
14388-
async fn test_thoughtspot_search_in_filter() {
14388+
async fn test_thoughtspot_like_with_escape() {
1438914389
init_logger();
1439014390

1439114391
let logical_plan = convert_select_to_query_plan(
@@ -14436,6 +14436,106 @@ ORDER BY \"COUNT(count)\" DESC"
1443614436
and: None
1443714437
}])
1443814438
}
14439+
);
14440+
14441+
let logical_plan = convert_select_to_query_plan(
14442+
r#"
14443+
SELECT "ta_1"."customer_gender" "ca_1"
14444+
FROM "db"."public"."KibanaSampleDataEcommerce" "ta_1"
14445+
WHERE NOT(LOWER("ta_1"."customer_gender") LIKE (replace(
14446+
replace(
14447+
replace(
14448+
'test',
14449+
'!',
14450+
'!!'
14451+
),
14452+
'%',
14453+
'!%'
14454+
),
14455+
'_',
14456+
'!_'
14457+
) || '%') ESCAPE '!')
14458+
GROUP BY "ca_1"
14459+
ORDER BY "ca_1" ASC
14460+
LIMIT 1000
14461+
"#
14462+
.to_string(),
14463+
DatabaseProtocol::PostgreSQL,
14464+
)
14465+
.await
14466+
.as_logical_plan();
14467+
14468+
assert_eq!(
14469+
logical_plan.find_cube_scan().request,
14470+
V1LoadRequestQuery {
14471+
measures: Some(vec![]),
14472+
dimensions: Some(vec!["KibanaSampleDataEcommerce.customer_gender".to_string()]),
14473+
segments: Some(vec![]),
14474+
time_dimensions: None,
14475+
order: Some(vec![vec![
14476+
"KibanaSampleDataEcommerce.customer_gender".to_string(),
14477+
"asc".to_string(),
14478+
]]),
14479+
limit: Some(1000),
14480+
offset: None,
14481+
filters: Some(vec![V1LoadRequestQueryFilterItem {
14482+
member: Some("KibanaSampleDataEcommerce.customer_gender".to_string()),
14483+
operator: Some("notStartsWith".to_string()),
14484+
values: Some(vec!["test".to_string()]),
14485+
or: None,
14486+
and: None
14487+
}])
14488+
}
14489+
);
14490+
14491+
let logical_plan = convert_select_to_query_plan(
14492+
r#"
14493+
SELECT "ta_1"."customer_gender" "ca_1"
14494+
FROM "db"."public"."KibanaSampleDataEcommerce" "ta_1"
14495+
WHERE NOT(LOWER("ta_1"."customer_gender") LIKE ('%' || replace(
14496+
replace(
14497+
replace(
14498+
'known',
14499+
'!',
14500+
'!!'
14501+
),
14502+
'%',
14503+
'!%'
14504+
),
14505+
'_',
14506+
'!_'
14507+
)) ESCAPE '!')
14508+
GROUP BY "ca_1"
14509+
ORDER BY "ca_1" ASC
14510+
LIMIT 1000
14511+
"#
14512+
.to_string(),
14513+
DatabaseProtocol::PostgreSQL,
14514+
)
14515+
.await
14516+
.as_logical_plan();
14517+
14518+
assert_eq!(
14519+
logical_plan.find_cube_scan().request,
14520+
V1LoadRequestQuery {
14521+
measures: Some(vec![]),
14522+
dimensions: Some(vec!["KibanaSampleDataEcommerce.customer_gender".to_string()]),
14523+
segments: Some(vec![]),
14524+
time_dimensions: None,
14525+
order: Some(vec![vec![
14526+
"KibanaSampleDataEcommerce.customer_gender".to_string(),
14527+
"asc".to_string(),
14528+
]]),
14529+
limit: Some(1000),
14530+
offset: None,
14531+
filters: Some(vec![V1LoadRequestQueryFilterItem {
14532+
member: Some("KibanaSampleDataEcommerce.customer_gender".to_string()),
14533+
operator: Some("notEndsWith".to_string()),
14534+
values: Some(vec!["known".to_string()]),
14535+
or: None,
14536+
and: None
14537+
}])
14538+
}
1443914539
)
1444014540
}
1444114541

rust/cubesql/cubesql/src/compile/rewrite/rules/filters.rs

Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,7 +1392,7 @@ impl RewriteRules for FilterRules {
13921392
),
13931393
),
13941394
transforming_rewrite(
1395-
"filter-thoughtspot-search-includes-excludes",
1395+
"filter-thoughtspot-like-escape-contains",
13961396
filter_replacer(
13971397
like_expr(
13981398
"LikeExprLikeType:Like",
@@ -1436,6 +1436,115 @@ impl RewriteRules for FilterRules {
14361436
),
14371437
filter_member("?filter_member", "?filter_op", "?filter_values"),
14381438
self.transform_like_escape(
1439+
"contains",
1440+
"?negated",
1441+
"?column",
1442+
"?literal",
1443+
"?escape_char",
1444+
"?alias_to_cube",
1445+
"?members",
1446+
"?filter_aliases",
1447+
"?filter_member",
1448+
"?filter_op",
1449+
"?filter_values",
1450+
),
1451+
),
1452+
transforming_rewrite(
1453+
"filter-thoughtspot-like-escape-starts-with",
1454+
filter_replacer(
1455+
like_expr(
1456+
"LikeExprLikeType:Like",
1457+
"?negated",
1458+
fun_expr("Lower", vec![column_expr("?column")]),
1459+
binary_expr(
1460+
fun_expr(
1461+
"Replace",
1462+
vec![
1463+
fun_expr(
1464+
"Replace",
1465+
vec![
1466+
fun_expr(
1467+
"Replace",
1468+
vec![
1469+
literal_expr("?literal"),
1470+
literal_string("!"),
1471+
literal_string("!!"),
1472+
],
1473+
),
1474+
literal_string("%"),
1475+
literal_string("!%"),
1476+
],
1477+
),
1478+
literal_string("_"),
1479+
literal_string("!_"),
1480+
],
1481+
),
1482+
"||",
1483+
literal_string("%"),
1484+
),
1485+
"?escape_char",
1486+
),
1487+
"?alias_to_cube",
1488+
"?members",
1489+
"?filter_aliases",
1490+
),
1491+
filter_member("?filter_member", "?filter_op", "?filter_values"),
1492+
self.transform_like_escape(
1493+
"startsWith",
1494+
"?negated",
1495+
"?column",
1496+
"?literal",
1497+
"?escape_char",
1498+
"?alias_to_cube",
1499+
"?members",
1500+
"?filter_aliases",
1501+
"?filter_member",
1502+
"?filter_op",
1503+
"?filter_values",
1504+
),
1505+
),
1506+
transforming_rewrite(
1507+
"filter-thoughtspot-like-escape-ends-with",
1508+
filter_replacer(
1509+
like_expr(
1510+
"LikeExprLikeType:Like",
1511+
"?negated",
1512+
fun_expr("Lower", vec![column_expr("?column")]),
1513+
binary_expr(
1514+
literal_string("%"),
1515+
"||",
1516+
fun_expr(
1517+
"Replace",
1518+
vec![
1519+
fun_expr(
1520+
"Replace",
1521+
vec![
1522+
fun_expr(
1523+
"Replace",
1524+
vec![
1525+
literal_expr("?literal"),
1526+
literal_string("!"),
1527+
literal_string("!!"),
1528+
],
1529+
),
1530+
literal_string("%"),
1531+
literal_string("!%"),
1532+
],
1533+
),
1534+
literal_string("_"),
1535+
literal_string("!_"),
1536+
],
1537+
),
1538+
),
1539+
"?escape_char",
1540+
),
1541+
"?alias_to_cube",
1542+
"?members",
1543+
"?filter_aliases",
1544+
),
1545+
filter_member("?filter_member", "?filter_op", "?filter_values"),
1546+
self.transform_like_escape(
1547+
"endsWith",
14391548
"?negated",
14401549
"?column",
14411550
"?literal",
@@ -1693,15 +1802,33 @@ impl RewriteRules for FilterRules {
16931802
self.unwrap_datetrunc("?granularity", "second"),
16941803
),
16951804
rewrite(
1696-
"not-expt-ilike-to-expr-not-ilike",
1805+
"not-expr-ilike-to-expr-not-ilike",
16971806
not_expr(binary_expr("?left", "ILIKE", "?right")),
16981807
binary_expr("?left", "NOT_ILIKE", "?right"),
16991808
),
17001809
rewrite(
1701-
"not-expt-like-to-expr-not-like",
1810+
"not-expr-like-to-expr-not-like",
17021811
not_expr(binary_expr("?left", "LIKE", "?right")),
17031812
binary_expr("?left", "NOT_LIKE", "?right"),
17041813
),
1814+
transforming_rewrite(
1815+
"not-like-expr-to-like-negated-expr",
1816+
not_expr(like_expr(
1817+
"?like_type",
1818+
"?negated",
1819+
"?expr",
1820+
"?pattern",
1821+
"?escape_char",
1822+
)),
1823+
like_expr(
1824+
"?like_type",
1825+
"?new_negated",
1826+
"?expr",
1827+
"?pattern",
1828+
"?escape_char",
1829+
),
1830+
self.transform_negate_like_expr("?negated", "?new_negated"),
1831+
),
17051832
rewrite(
17061833
"plus-value-minus-value",
17071834
binary_expr(
@@ -3028,6 +3155,29 @@ impl FilterRules {
30283155
}
30293156
}
30303157

3158+
fn transform_negate_like_expr(
3159+
&self,
3160+
negated_var: &'static str,
3161+
new_negated_var: &'static str,
3162+
) -> impl Fn(&mut EGraph<LogicalPlanLanguage, LogicalPlanAnalysis>, &mut Subst) -> bool {
3163+
let negated_var = var!(negated_var);
3164+
let new_negated_var = var!(new_negated_var);
3165+
move |egraph, subst| {
3166+
for negated in var_iter!(egraph[subst[negated_var]], LikeExprNegated).cloned() {
3167+
subst.insert(
3168+
new_negated_var,
3169+
egraph.add(LogicalPlanLanguage::LikeExprNegated(LikeExprNegated(
3170+
!negated,
3171+
))),
3172+
);
3173+
3174+
return true;
3175+
}
3176+
3177+
false
3178+
}
3179+
}
3180+
30313181
fn filter_member_name(
30323182
egraph: &EGraph<LogicalPlanLanguage, LogicalPlanAnalysis>,
30333183
subst: &Subst,
@@ -3455,6 +3605,7 @@ impl FilterRules {
34553605

34563606
fn transform_like_escape(
34573607
&self,
3608+
filter_op: &'static str,
34583609
negated_var: &'static str,
34593610
column_var: &'static str,
34603611
literal_var: &'static str,
@@ -3508,8 +3659,11 @@ impl FilterRules {
35083659
var_iter!(egraph[subst[negated_var]], LikeExprNegated)
35093660
{
35103661
let filter_member_op = match &negated {
3511-
true => "notContains",
3512-
false => "contains",
3662+
true => match utils::negated_cube_filter_op(filter_op) {
3663+
Some(op) => op,
3664+
None => continue,
3665+
},
3666+
false => filter_op,
35133667
};
35143668

35153669
subst.insert(

rust/cubesql/cubesql/src/compile/rewrite/rules/utils.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,30 @@ fn granularity_int_order_to_str(granularity: i32, week_as_day: Option<bool>) ->
106106
}
107107
.map(|g| g.to_string())
108108
}
109+
110+
pub fn negated_cube_filter_op(op: &str) -> Option<&'static str> {
111+
macro_rules! define_op_eq {
112+
($($EXPR:expr => $NEG:expr,)*) => {
113+
match op {
114+
$(
115+
$EXPR => $NEG,
116+
$NEG => $EXPR,
117+
)*
118+
_ => return None,
119+
}
120+
}
121+
}
122+
123+
let negated = define_op_eq![
124+
"equals" => "notEquals",
125+
"contains" => "notContains",
126+
"startsWith" => "notStartsWith",
127+
"endsWith" => "notEndsWith",
128+
"gt" => "lte",
129+
"lt" => "gte",
130+
"set" => "notSet",
131+
"inDateRange" => "notInDateRange",
132+
];
133+
134+
Some(negated)
135+
}

0 commit comments

Comments
 (0)