diff --git a/vortex-duckdb/src/convert/expr.rs b/vortex-duckdb/src/convert/expr.rs index 4ed3cc66060..b487ab4fdc4 100644 --- a/vortex-duckdb/src/convert/expr.rs +++ b/vortex-duckdb/src/convert/expr.rs @@ -106,6 +106,18 @@ fn string_matcher_pattern(children: &[&ExpressionRef]) -> Option { } } +/// Escape each of `chars` in `pattern` with `\`, the escape character of Vortex LIKE. +fn escape_like(pattern: &str, chars: &[char]) -> String { + let mut escaped = String::with_capacity(pattern.len()); + for c in pattern.chars() { + if chars.contains(&c) { + escaped.push('\\'); + } + escaped.push(c); + } + escaped +} + /// Whether the expression's return type is a `LIST` or fixed-size `ARRAY`. fn returns_a_list(expr: &ExpressionRef) -> bool { matches!( @@ -312,17 +324,17 @@ fn try_from_bound_function( like @ ("~~" | "!~~") => { let children: Vec<_> = func.children().collect(); vortex_ensure_eq!(children.len(), 2); - let Some(string) = try_from_expression_inner(children[0], ctx)? else { + let Some(pattern) = from_bound_str(children[1]) else { return Ok(None); }; - let Some(target) = try_from_expression_inner(children[1], ctx)? else { + let Some(string) = try_from_expression_inner(children[0], ctx)? else { return Ok(None); }; let opts = LikeOptions { negated: like == "!~~", case_insensitive: false, }; - Like.new_expr(opts, [string, target]) + Like.new_expr(opts, [string, lit(escape_like(&pattern, &['\\']))]) } matchers @ ("contains" | "prefix" | "suffix") => { let children: Vec<_> = func.children().collect(); @@ -332,6 +344,7 @@ fn try_from_bound_function( let Some(value) = try_from_expression_inner(children[0], ctx)? else { return Ok(None); }; + let pattern = escape_like(&pattern, &['%', '_', '\\']); let pattern = match matchers { "contains" => format!("%{pattern}%"), "prefix" => format!("{pattern}%"), @@ -459,7 +472,8 @@ pub fn can_push_expression(value: &ExpressionRef) -> bool { ExpressionClass::BoundFunction(func) => { let children: Vec<_> = func.children().collect(); let supported = match func.scalar_function.name() { - "~~" | "!~~" | "strlen" => true, + "~~" | "!~~" => children.len() == 2 && from_bound_str(children[1]).is_some(), + "strlen" => true, "struct_extract" => children.len() == 2 && from_bound_str(children[1]).is_some(), "contains" | "prefix" | "suffix" => string_matcher_pattern(&children).is_some(), "array_length" => children.len() == 1, diff --git a/vortex-sqllogictest/slt/duckdb/like_pushdown.slt b/vortex-sqllogictest/slt/duckdb/like_pushdown.slt new file mode 100644 index 00000000000..3b37d90a99d --- /dev/null +++ b/vortex-sqllogictest/slt/duckdb/like_pushdown.slt @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors + +include ../setup.slt.no + +statement ok +COPY (SELECT * FROM (VALUES ('a_c', 'a_c'), ('abc', 'b'), ('100%', '1%'), ('1000', '1%'), + ('page_view', 'p%'), ('pageXview', 'p%'), ('C:' || chr(92) || 'x', 'C:' || chr(92) || '_'), ('C:%', 'C:%'), + (NULL, NULL)) t(s, p)) +TO '${WORK_DIR}/like-pushdown.vortex'; + +# contains, prefix and suffix match their argument literally +query T +SELECT s FROM '${WORK_DIR}/like-pushdown.vortex' WHERE contains(s, 'a_c'); +---- +a_c + +query T +SELECT s FROM '${WORK_DIR}/like-pushdown.vortex' WHERE prefix(s, 'page_'); +---- +page_view + +query T +SELECT s FROM '${WORK_DIR}/like-pushdown.vortex' WHERE s GLOB 'page_*'; +---- +page_view + +query T +SELECT s FROM '${WORK_DIR}/like-pushdown.vortex' WHERE suffix(s, '0%'); +---- +100% + +query TT +EXPLAIN (FORMAT json) SELECT s FROM '${WORK_DIR}/like-pushdown.vortex' WHERE prefix(s, 'page_'); +---- +:"Filters": "[^"]* like + +# A backslash in a LIKE pattern matches itself +query T +SELECT replace(s, chr(92), '/') FROM '${WORK_DIR}/like-pushdown.vortex' WHERE s LIKE 'C:' || chr(92) || '%'; +---- +C:/x + +query T +SELECT replace(s, chr(92), '/') FROM '${WORK_DIR}/like-pushdown.vortex' WHERE s LIKE 'C:' || chr(92) || '_'; +---- +C:/x + +# A pattern that isn't a constant is evaluated by DuckDB +query T +SELECT replace(s, chr(92), '/') FROM '${WORK_DIR}/like-pushdown.vortex' WHERE s LIKE p ORDER BY s; +---- +100% +1000 +C:% +C:/x +a_c +pageXview +page_view + +query T +SELECT replace(s, chr(92), '/') FROM '${WORK_DIR}/like-pushdown.vortex' WHERE s LIKE s ORDER BY s; +---- +100% +1000 +C:% +C:/x +a_c +abc +pageXview +page_view