Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
22 changes: 18 additions & 4 deletions vortex-duckdb/src/convert/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ fn string_matcher_pattern(children: &[&ExpressionRef]) -> Option<String> {
}
}

/// 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!(
Expand Down Expand Up @@ -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();
Expand All @@ -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}%"),
Expand Down Expand Up @@ -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,
Expand Down
71 changes: 71 additions & 0 deletions vortex-sqllogictest/slt/duckdb/like_pushdown.slt
Original file line number Diff line number Diff line change
@@ -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_');
----
<REGEX>:"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