Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 82 additions & 2 deletions src/query/expression/src/filter/like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,28 @@ impl LikePattern<'_> {
has_end_percent: bool,
segments: &[Vec<u8>],
) -> bool {
let segments_len = segments.len();
match segments_len {
// Repeated '%' can collapse a simple pattern to zero concrete segments.
0 => return true,
// Repeated '%' can also collapse to a single concrete segment, which is
// equivalent to one of the simpler LIKE variants.
1 => {
let segment = &segments[0];
return match (has_start_percent, has_end_percent) {
(false, false) => haystack == segment,
(true, false) => haystack.ends_with(segment),
(false, true) => haystack.starts_with(segment),
(true, true) => find(haystack, segment).is_some(),
Comment thread
sundy-li marked this conversation as resolved.
};
}
_ => {}
}

let haystack_len = haystack.len();
if haystack_len == 0 {
return false;
}
let segments_len = segments.len();
debug_assert!(haystack_len > 0);
debug_assert!(segments_len > 1);
let mut haystack_start_idx = 0;
Expand Down Expand Up @@ -272,14 +289,43 @@ pub fn generate_like_pattern<'a, B: Into<Cow<'a, [u8]>>>(
if first_non_percent < len {
segments.push(pattern[first_non_percent..len].to_vec());
}
LikePattern::SimplePattern((has_start_percent, has_end_percent, segments))
normalize_simple_pattern(
has_start_percent,
has_end_percent,
segments,
haystack_size_hint,
)
} else {
LikePattern::ComplexPattern(pattern)
}
}
}
}

fn normalize_simple_pattern<'a>(
has_start_percent: bool,
has_end_percent: bool,
mut segments: Vec<Vec<u8>>,
haystack_size_hint: usize,
) -> LikePattern<'a> {
match segments.len() {
0 => LikePattern::Constant(true),
1 => {
let segment = segments.pop().unwrap();
match (has_start_percent, has_end_percent) {
(false, false) => LikePattern::OrdinalStr(Cow::Owned(segment)),
(true, false) => LikePattern::StartOfPercent(Cow::Owned(segment)),
(false, true) => LikePattern::EndOfPercent(Cow::Owned(segment)),
(true, true) => LikePattern::SurroundByPercent(VolnitskyBase::new_cow(
Cow::Owned(segment),
haystack_size_hint,
)),
}
}
_ => LikePattern::SimplePattern((has_start_percent, has_end_percent, segments)),
}
}

#[inline]
fn decode_one(data: &[u8]) -> Option<(u8, usize)> {
if data.is_empty() {
Expand Down Expand Up @@ -379,6 +425,19 @@ fn test_generate_like_pattern() {
"%databend%cloud%data%warehouse%",
LikePattern::SimplePattern((true, true, segments)),
),
("%%%%%", LikePattern::Constant(true)),
(
"%%%%databend",
LikePattern::StartOfPercent("databend".as_bytes().into()),
),
(
"databend%%%%",
LikePattern::EndOfPercent("databend".as_bytes().into()),
),
(
"%%%%databend%%%%",
LikePattern::SurroundByPercent(VolnitskyBase::new("databend".as_bytes(), 1)),
),
(
"databend_cloud%data%warehouse",
LikePattern::ComplexPattern("databend_cloud%data%warehouse".as_bytes().into()),
Expand All @@ -396,3 +455,24 @@ fn test_generate_like_pattern() {
assert_eq!(pattern_type, generate_like_pattern(pattern.as_bytes(), 1));
}
}

#[test]
fn test_like_pattern_with_repeated_percent() {
let test_cases = vec![
("ababac", "abab%%%%%", true),
("aba", "abab%%%%%", false),
("zzabab", "%%%%abab", true),
("zzababzz", "%%%%abab%%%%", true),
("zzabazz", "%%%%abab%%%%", false),
("", "%%%%%", true),
("anything", "%%%%%", true),
];

for (haystack, pattern, expected) in test_cases {
assert_eq!(
generate_like_pattern(pattern.as_bytes(), haystack.len()).compare(haystack.as_bytes()),
expected,
"{haystack:?} LIKE {pattern:?}"
);
}
}
87 changes: 83 additions & 4 deletions src/query/functions/src/scalars/comparison.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1013,10 +1013,8 @@ fn calc_like_domain(lhs: &StringDomain, pattern: String) -> Option<FunctionDomai
min: pattern.clone(),
max: Some(pattern),
})),
LikePattern::EndOfPercent(_) => {
let mut pat_str = pattern;
// remove the last char '%'
pat_str.pop();
LikePattern::EndOfPercent(v) => {
let pat_str = std::str::from_utf8(v.as_ref()).ok()?.to_string();
let pat_len = pat_str.chars().count();
let other = StringDomain {
min: pat_str.clone(),
Expand Down Expand Up @@ -1362,3 +1360,84 @@ fn compare_bitmap_bytes(lhs: &[u8], rhs: &[u8], ctx: &mut EvalContext, row: usiz

left.iter().eq(right.iter())
}

#[cfg(test)]
mod tests {
use databend_common_expression::FunctionContext;
use databend_common_expression::types::string::StringDomain;
use jsonb::OwnedJsonb;

use super::*;

#[test]
fn test_calc_like_domain_repeated_trailing_percent_matches_normalized_prefix() {
let matching = StringDomain {
min: "ababac".to_string(),
max: Some("ababac".to_string()),
};
let non_matching = StringDomain {
min: "aba".to_string(),
max: Some("aba".to_string()),
};

assert_eq!(
calc_like_domain(&matching, "abab%".to_string()),
calc_like_domain(&matching, "abab%%%%%".to_string()),
"repeated trailing % should fold like a single trailing %"
);
assert_eq!(
calc_like_domain(&non_matching, "abab%%%%%".to_string()),
calc_like_domain(&non_matching, "abab%".to_string()),
"non-matching prefixes should also stay consistent"
);
}

#[test]
fn test_variant_like_repeated_percent_preserves_simple_scalar_semantics() {
let like = variant_vectorize_like_jsonb();
let value = Value::<VariantType>::Scalar(
r#"{"name":"abab"}"#.parse::<OwnedJsonb>().unwrap().to_vec(),
);
let escape = Value::<StringType>::Scalar("".to_string());
let func_ctx = FunctionContext::default();
let mut ctx = EvalContext {
generics: &[],
num_rows: 1,
func_ctx: &func_ctx,
validity: None,
errors: None,
suppress_error: false,
strict_eval: false,
};

let leading = like(
value.clone(),
Value::<StringType>::Scalar("%abab".to_string()),
escape.clone(),
&mut ctx,
);
let repeated_leading = like(
value.clone(),
Value::<StringType>::Scalar("%%%%abab".to_string()),
escape.clone(),
&mut ctx,
);
let trailing = like(
value.clone(),
Value::<StringType>::Scalar("abab%".to_string()),
escape.clone(),
&mut ctx,
);
let repeated_trailing = like(
value,
Value::<StringType>::Scalar("abab%%%%".to_string()),
escape,
&mut ctx,
);

assert!(matches!(leading, Value::Scalar(false)));
assert_eq!(leading, repeated_leading);
assert!(matches!(trailing, Value::Scalar(false)));
assert_eq!(trailing, repeated_trailing);
}
}
11 changes: 11 additions & 0 deletions tests/sqllogictests/suites/query/filter.test
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,17 @@ select * from t where b not like 'databend%cloud%data%warehouse';
3 query databend
4 query databend, query

# Repeated '%' should preserve VARIANT LIKE semantics.
query BB
select parse_json('{"name":"abab"}') like '%abab', parse_json('{"name":"abab"}') like '%%%%abab';
----
0 0

query BB
select parse_json('{"name":"abab"}') like 'abab%', parse_json('{"name":"abab"}') like 'abab%%%%';
----
0 0

statement ok
create or replace table t(a int) change_tracking = true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,21 @@ select 'a%b_c\d' like 'a!%b!_c!\\d' escape '!'
----
1

query B
select 'ababac' like 'abab%%%%%'
Comment thread
sundy-li marked this conversation as resolved.
----
1

query B
select 'zzabab' like '%%%%abab'
----
1

query B
select '' like '%%%%%'
----
1

query TBBBBBB
select ('PROMO' || number::string) a, a like 'PROMO%',
a < 'PROMO1', a > 'PROMO1',
Expand Down
Loading