mirror of https://github.com/apache/druid.git
Support SearchQueryDimFilter in sql via new methods (#10350)
* Support SearchQueryDimFilter in sql via new methods * Contains is a reserved word * revert unnecessary change * Fix toDruidExpression method * rename methods * java docs * Add native functions * revert change in dockerfile * remove changes from dockerfile * More tests * travis fix * Handle null values better
This commit is contained in:
parent
3d4b48e0aa
commit
f5e2645bbb
|
@ -78,6 +78,8 @@ The following built-in functions are available.
|
|||
|parse_long|parse_long(string[, radix]) parses a string as a long with the given radix, or 10 (decimal) if a radix is not provided.|
|
||||
|regexp_extract|regexp_extract(expr, pattern[, index]) applies a regular expression pattern and extracts a capture group index, or null if there is no match. If index is unspecified or zero, returns the substring that matched the pattern. The pattern may match anywhere inside `expr`; if you want to match the entire string instead, use the `^` and `$` markers at the start and end of your pattern.|
|
||||
|regexp_like|regexp_like(expr, pattern) returns whether `expr` matches regular expression `pattern`. The pattern may match anywhere inside `expr`; if you want to match the entire string instead, use the `^` and `$` markers at the start and end of your pattern. |
|
||||
|contains_string|contains_string(expr, string) returns whether `expr` contains `string` as a substring. This method is case-sensitive.|
|
||||
|icontains_string|contains_string(expr, string) returns whether `expr` contains `string` as a substring. This method is case-insensitive.|
|
||||
|replace|replace(expr, pattern, replacement) replaces pattern with replacement|
|
||||
|substring|substring(expr, index, length) behaves like java.lang.String's substring|
|
||||
|right|right(expr, length) returns the rightmost length characters from a string|
|
||||
|
|
|
@ -397,6 +397,8 @@ String functions accept strings, and return a type appropriate to the function.
|
|||
|`POSITION(needle IN haystack [FROM fromIndex])`|Returns the index of needle within haystack, with indexes starting from 1. The search will begin at fromIndex, or 1 if fromIndex is not specified. If the needle is not found, returns 0.|
|
||||
|`REGEXP_EXTRACT(expr, pattern, [index])`|Apply regular expression `pattern` to `expr` and extract a capture group, or `NULL` if there is no match. If index is unspecified or zero, returns the first substring that matched the pattern. The pattern may match anywhere inside `expr`; if you want to match the entire string instead, use the `^` and `$` markers at the start and end of your pattern. Note: when `druid.generic.useDefaultValueForNull = true`, it is not possible to differentiate an empty-string match from a non-match (both will return `NULL`).|
|
||||
|`REGEXP_LIKE(expr, pattern)`|Returns whether `expr` matches regular expression `pattern`. The pattern may match anywhere inside `expr`; if you want to match the entire string instead, use the `^` and `$` markers at the start and end of your pattern. Similar to [`LIKE`](#comparison-operators), but uses regexps instead of LIKE patterns. Especially useful in WHERE clauses.|
|
||||
|`CONTAINS_STRING(<expr>, str)`|Returns true if the `str` is a substring of `expr`.|
|
||||
|`ICONTAINS_STRING(<expr>, str)`|Returns true if the `str` is a substring of `expr`. The match is case-insensitive.|
|
||||
|`REPLACE(expr, pattern, replacement)`|Replaces pattern with replacement in expr, and returns the result.|
|
||||
|`STRPOS(haystack, needle)`|Returns the index of needle within haystack, with indexes starting from 1. If the needle is not found, returns 0.|
|
||||
|`SUBSTRING(expr, index, [length])`|Returns a substring of expr starting at index, with a max length, both measured in UTF-16 code units.|
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.druid.query.expression;
|
||||
|
||||
import org.apache.druid.java.util.common.IAE;
|
||||
import org.apache.druid.math.expr.Expr;
|
||||
import org.apache.druid.math.expr.ExprMacroTable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class implements a function that checks if one string contains another string. It is required that second
|
||||
* string be a literal. This expression is case-insensitive.
|
||||
* signature:
|
||||
* long contains_string(string, string)
|
||||
* <p>
|
||||
* Examples:
|
||||
* - {@code contains_string("foobar", "bar") - 1 }
|
||||
* - {@code contains_string("foobar", "car") - 0 }
|
||||
* - {@code contains_string("foobar", "Bar") - 1 }
|
||||
* <p>
|
||||
* See {@link ContainsExprMacro} for the case-sensitive version.
|
||||
*/
|
||||
|
||||
public class CaseInsensitiveContainsExprMacro implements ExprMacroTable.ExprMacro
|
||||
{
|
||||
public static final String FN_NAME = "icontains_string";
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return FN_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expr apply(final List<Expr> args)
|
||||
{
|
||||
if (args.size() != 2) {
|
||||
throw new IAE("Function[%s] must have 2 arguments", name());
|
||||
}
|
||||
|
||||
final Expr arg = args.get(0);
|
||||
final Expr searchStr = args.get(1);
|
||||
return new ContainsExpr(FN_NAME, arg, searchStr, false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.druid.query.expression;
|
||||
|
||||
import org.apache.druid.common.config.NullHandling;
|
||||
import org.apache.druid.java.util.common.IAE;
|
||||
import org.apache.druid.java.util.common.StringUtils;
|
||||
import org.apache.druid.math.expr.Expr;
|
||||
import org.apache.druid.math.expr.ExprEval;
|
||||
import org.apache.druid.math.expr.ExprMacroTable;
|
||||
import org.apache.druid.math.expr.ExprType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* {@link Expr} class returned by {@link ContainsExprMacro} and {@link CaseInsensitiveContainsExprMacro} for
|
||||
* evaluating the expression.
|
||||
*/
|
||||
class ContainsExpr extends ExprMacroTable.BaseScalarUnivariateMacroFunctionExpr
|
||||
{
|
||||
private final Function<String, Boolean> searchFunction;
|
||||
private final Expr searchStrExpr;
|
||||
|
||||
ContainsExpr(String functioName, Expr arg, Expr searchStrExpr, boolean caseSensitive)
|
||||
{
|
||||
super(functioName, arg);
|
||||
this.searchStrExpr = validateSearchExpr(searchStrExpr, functioName);
|
||||
// Creates the function eagerly to avoid branching in eval.
|
||||
this.searchFunction = createFunction(searchStrExpr, caseSensitive);
|
||||
}
|
||||
|
||||
private ContainsExpr(String functioName, Expr arg, Expr searchStrExpr, Function<String, Boolean> searchFunction)
|
||||
{
|
||||
super(functioName, arg);
|
||||
this.searchFunction = searchFunction;
|
||||
this.searchStrExpr = validateSearchExpr(searchStrExpr, functioName);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ExprEval eval(final Expr.ObjectBinding bindings)
|
||||
{
|
||||
final String s = NullHandling.nullToEmptyIfNeeded(arg.eval(bindings).asString());
|
||||
|
||||
if (s == null) {
|
||||
// same behavior as regexp_like.
|
||||
return ExprEval.of(false, ExprType.LONG);
|
||||
} else {
|
||||
final boolean doesContain = searchFunction.apply(s);
|
||||
return ExprEval.of(doesContain, ExprType.LONG);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expr visit(Expr.Shuttle shuttle)
|
||||
{
|
||||
Expr newArg = arg.visit(shuttle);
|
||||
return shuttle.visit(new ContainsExpr(name, newArg, searchStrExpr, searchFunction));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String stringify()
|
||||
{
|
||||
return StringUtils.format("%s(%s, %s)", name, arg.stringify(), searchStrExpr.stringify());
|
||||
}
|
||||
|
||||
private Function<String, Boolean> createFunction(Expr searchStrExpr, boolean caseSensitive)
|
||||
{
|
||||
String searchStr = StringUtils.nullToEmptyNonDruidDataString((String) searchStrExpr.getLiteralValue());
|
||||
if (caseSensitive) {
|
||||
return s -> s.contains(searchStr);
|
||||
}
|
||||
return s -> org.apache.commons.lang.StringUtils.containsIgnoreCase(s, searchStr);
|
||||
}
|
||||
|
||||
private Expr validateSearchExpr(Expr searchExpr, String functioName)
|
||||
{
|
||||
if (!ExprUtils.isStringLiteral(searchExpr)) {
|
||||
throw new IAE("Function[%s] substring must be a string literal", functioName);
|
||||
}
|
||||
return searchExpr;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.druid.query.expression;
|
||||
|
||||
import org.apache.druid.java.util.common.IAE;
|
||||
import org.apache.druid.math.expr.Expr;
|
||||
import org.apache.druid.math.expr.ExprMacroTable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class implements a function that checks if one string contains another string. It is required that second
|
||||
* string be a literal. This expression is case-sensitive.
|
||||
* signature:
|
||||
* long contains_string(string, string)
|
||||
* <p>
|
||||
* Examples:
|
||||
* - {@code contains_string("foobar", "bar") - 1 }
|
||||
* - {@code contains_string("foobar", "car") - 0 }
|
||||
* - {@code contains_string("foobar", "Bar") - 0 }
|
||||
* <p>
|
||||
* See {@link CaseInsensitiveContainsExprMacro} for the case-insensitive version.
|
||||
*/
|
||||
public class ContainsExprMacro implements ExprMacroTable.ExprMacro
|
||||
{
|
||||
public static final String FN_NAME = "contains_string";
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return FN_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expr apply(final List<Expr> args)
|
||||
{
|
||||
if (args.size() != 2) {
|
||||
throw new IAE("Function[%s] must have 2 arguments", name());
|
||||
}
|
||||
|
||||
final Expr arg = args.get(0);
|
||||
final Expr searchStr = args.get(1);
|
||||
return new ContainsExpr(FN_NAME, arg, searchStr, true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.druid.query.expression;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.apache.druid.common.config.NullHandling;
|
||||
import org.apache.druid.math.expr.ExprEval;
|
||||
import org.apache.druid.math.expr.ExprType;
|
||||
import org.apache.druid.math.expr.Parser;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CaseInsensitiveExprMacroTest extends MacroTestBase
|
||||
{
|
||||
public CaseInsensitiveExprMacroTest()
|
||||
{
|
||||
super(new CaseInsensitiveContainsExprMacro());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorZeroArguments()
|
||||
{
|
||||
expectException(IllegalArgumentException.class, "Function[icontains_string] must have 2 arguments");
|
||||
eval("icontains_string()", Parser.withMap(ImmutableMap.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorThreeArguments()
|
||||
{
|
||||
expectException(IllegalArgumentException.class, "Function[icontains_string] must have 2 arguments");
|
||||
eval("icontains_string('a', 'b', 'c')", Parser.withMap(ImmutableMap.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchSearchLowerCase()
|
||||
{
|
||||
final ExprEval<?> result = eval("icontains_string(a, 'OBA')", Parser.withMap(ImmutableMap.of("a", "foobar")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchSearchUpperCase()
|
||||
{
|
||||
final ExprEval<?> result = eval("icontains_string(a, 'oba')", Parser.withMap(ImmutableMap.of("a", "FOOBAR")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoMatch()
|
||||
{
|
||||
final ExprEval<?> result = eval("icontains_string(a, 'bar')", Parser.withMap(ImmutableMap.of("a", "foo")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(false, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullSearch()
|
||||
{
|
||||
if (NullHandling.sqlCompatible()) {
|
||||
expectException(IllegalArgumentException.class, "Function[icontains_string] substring must be a string literal");
|
||||
}
|
||||
|
||||
final ExprEval<?> result = eval("icontains_string(a, null)", Parser.withMap(ImmutableMap.of("a", "foo")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyStringSearch()
|
||||
{
|
||||
final ExprEval<?> result = eval("icontains_string(a, '')", Parser.withMap(ImmutableMap.of("a", "foo")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullSearchOnEmptyString()
|
||||
{
|
||||
if (NullHandling.sqlCompatible()) {
|
||||
expectException(IllegalArgumentException.class, "Function[icontains_string] substring must be a string literal");
|
||||
}
|
||||
|
||||
final ExprEval<?> result = eval("icontains_string(a, null)", Parser.withMap(ImmutableMap.of("a", "")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyStringSearchOnEmptyString()
|
||||
{
|
||||
final ExprEval<?> result = eval("icontains_string(a, '')", Parser.withMap(ImmutableMap.of("a", "")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullSearchOnNull()
|
||||
{
|
||||
if (NullHandling.sqlCompatible()) {
|
||||
expectException(IllegalArgumentException.class, "Function[icontains_string] substring must be a string literal");
|
||||
}
|
||||
|
||||
final ExprEval<?> result = eval(
|
||||
"icontains_string(a, null)",
|
||||
Parser.withSuppliers(ImmutableMap.of("a", () -> null))
|
||||
);
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyStringSearchOnNull()
|
||||
{
|
||||
final ExprEval<?> result = eval("icontains_string(a, '')", Parser.withSuppliers(ImmutableMap.of("a", () -> null)));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(!NullHandling.sqlCompatible(), ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.druid.query.expression;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.apache.druid.common.config.NullHandling;
|
||||
import org.apache.druid.math.expr.ExprEval;
|
||||
import org.apache.druid.math.expr.ExprType;
|
||||
import org.apache.druid.math.expr.Parser;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ContainsExprMacroTest extends MacroTestBase
|
||||
{
|
||||
public ContainsExprMacroTest()
|
||||
{
|
||||
super(new ContainsExprMacro());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorZeroArguments()
|
||||
{
|
||||
expectException(IllegalArgumentException.class, "Function[contains_string] must have 2 arguments");
|
||||
eval("contains_string()", Parser.withMap(ImmutableMap.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorThreeArguments()
|
||||
{
|
||||
expectException(IllegalArgumentException.class, "Function[contains_string] must have 2 arguments");
|
||||
eval("contains_string('a', 'b', 'c')", Parser.withMap(ImmutableMap.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatch()
|
||||
{
|
||||
final ExprEval<?> result = eval("contains_string(a, 'oba')", Parser.withMap(ImmutableMap.of("a", "foobar")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoMatch()
|
||||
{
|
||||
final ExprEval<?> result = eval("contains_string(a, 'bar')", Parser.withMap(ImmutableMap.of("a", "foo")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(false, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullSearch()
|
||||
{
|
||||
if (NullHandling.sqlCompatible()) {
|
||||
expectException(IllegalArgumentException.class, "Function[contains_string] substring must be a string literal");
|
||||
}
|
||||
|
||||
final ExprEval<?> result = eval("contains_string(a, null)", Parser.withMap(ImmutableMap.of("a", "foo")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyStringSearch()
|
||||
{
|
||||
final ExprEval<?> result = eval("contains_string(a, '')", Parser.withMap(ImmutableMap.of("a", "foo")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullSearchOnEmptyString()
|
||||
{
|
||||
if (NullHandling.sqlCompatible()) {
|
||||
expectException(IllegalArgumentException.class, "Function[contains_string] substring must be a string literal");
|
||||
}
|
||||
|
||||
final ExprEval<?> result = eval("contains_string(a, null)", Parser.withMap(ImmutableMap.of("a", "")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyStringSearchOnEmptyString()
|
||||
{
|
||||
final ExprEval<?> result = eval("contains_string(a, '')", Parser.withMap(ImmutableMap.of("a", "")));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullSearchOnNull()
|
||||
{
|
||||
if (NullHandling.sqlCompatible()) {
|
||||
expectException(IllegalArgumentException.class, "Function[contains_string] substring must be a string literal");
|
||||
}
|
||||
|
||||
final ExprEval<?> result = eval("contains_string(a, null)", Parser.withSuppliers(ImmutableMap.of("a", () -> null)));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(true, ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyStringSearchOnNull()
|
||||
{
|
||||
final ExprEval<?> result = eval("contains_string(a, '')", Parser.withSuppliers(ImmutableMap.of("a", () -> null)));
|
||||
Assert.assertEquals(
|
||||
ExprEval.of(!NullHandling.sqlCompatible(), ExprType.LONG).value(),
|
||||
result.value()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,6 +25,8 @@ import com.google.inject.Binder;
|
|||
import com.google.inject.multibindings.Multibinder;
|
||||
import org.apache.druid.initialization.DruidModule;
|
||||
import org.apache.druid.math.expr.ExprMacroTable;
|
||||
import org.apache.druid.query.expression.CaseInsensitiveContainsExprMacro;
|
||||
import org.apache.druid.query.expression.ContainsExprMacro;
|
||||
import org.apache.druid.query.expression.GuiceExprMacroTable;
|
||||
import org.apache.druid.query.expression.IPv4AddressMatchExprMacro;
|
||||
import org.apache.druid.query.expression.IPv4AddressParseExprMacro;
|
||||
|
@ -52,6 +54,8 @@ public class ExpressionModule implements DruidModule
|
|||
.add(LikeExprMacro.class)
|
||||
.add(RegexpExtractExprMacro.class)
|
||||
.add(RegexpLikeExprMacro.class)
|
||||
.add(ContainsExprMacro.class)
|
||||
.add(CaseInsensitiveContainsExprMacro.class)
|
||||
.add(TimestampCeilExprMacro.class)
|
||||
.add(TimestampExtractExprMacro.class)
|
||||
.add(TimestampFloorExprMacro.class)
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.druid.sql.calcite.expression.builtin;
|
||||
|
||||
import org.apache.calcite.rex.RexCall;
|
||||
import org.apache.calcite.rex.RexLiteral;
|
||||
import org.apache.calcite.rex.RexNode;
|
||||
import org.apache.calcite.sql.SqlFunction;
|
||||
import org.apache.calcite.sql.SqlFunctionCategory;
|
||||
import org.apache.calcite.sql.SqlOperator;
|
||||
import org.apache.calcite.sql.type.SqlTypeFamily;
|
||||
import org.apache.calcite.sql.type.SqlTypeName;
|
||||
import org.apache.druid.java.util.common.StringUtils;
|
||||
import org.apache.druid.query.expression.CaseInsensitiveContainsExprMacro;
|
||||
import org.apache.druid.query.expression.ContainsExprMacro;
|
||||
import org.apache.druid.query.filter.DimFilter;
|
||||
import org.apache.druid.query.filter.SearchQueryDimFilter;
|
||||
import org.apache.druid.query.search.ContainsSearchQuerySpec;
|
||||
import org.apache.druid.query.search.SearchQuerySpec;
|
||||
import org.apache.druid.segment.VirtualColumn;
|
||||
import org.apache.druid.segment.column.RowSignature;
|
||||
import org.apache.druid.sql.calcite.expression.DruidExpression;
|
||||
import org.apache.druid.sql.calcite.expression.Expressions;
|
||||
import org.apache.druid.sql.calcite.expression.OperatorConversions;
|
||||
import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.planner.PlannerContext;
|
||||
import org.apache.druid.sql.calcite.rel.VirtualColumnRegistry;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Register {@code contains_string} and {@code icontains_string} functions with calcite that internally
|
||||
* translate these functions into {@link SearchQueryDimFilter} with {@link ContainsSearchQuerySpec} as
|
||||
* search query spec.
|
||||
*/
|
||||
public class ContainsOperatorConversion implements SqlOperatorConversion
|
||||
{
|
||||
private final SqlOperator operator;
|
||||
private final boolean caseSensitive;
|
||||
|
||||
private ContainsOperatorConversion(
|
||||
final SqlFunction sqlFunction,
|
||||
final boolean caseSensitive
|
||||
)
|
||||
{
|
||||
this.operator = sqlFunction;
|
||||
this.caseSensitive = caseSensitive;
|
||||
}
|
||||
|
||||
public static SqlOperatorConversion caseSensitive()
|
||||
{
|
||||
final SqlFunction sqlFunction = createSqlFunction(ContainsExprMacro.FN_NAME);
|
||||
return new ContainsOperatorConversion(sqlFunction, true);
|
||||
}
|
||||
|
||||
public static SqlOperatorConversion caseInsensitive()
|
||||
{
|
||||
final SqlFunction sqlFunction = createSqlFunction(CaseInsensitiveContainsExprMacro.FN_NAME);
|
||||
return new ContainsOperatorConversion(sqlFunction, false);
|
||||
}
|
||||
|
||||
private static SqlFunction createSqlFunction(final String functionName)
|
||||
{
|
||||
return OperatorConversions
|
||||
.operatorBuilder(StringUtils.toUpperCase(functionName))
|
||||
.operandTypes(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER)
|
||||
.requiredOperands(2)
|
||||
.literalOperands(1)
|
||||
.returnTypeNonNull(SqlTypeName.BOOLEAN)
|
||||
.functionCategory(SqlFunctionCategory.STRING)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlOperator calciteOperator()
|
||||
{
|
||||
return operator;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DruidExpression toDruidExpression(
|
||||
PlannerContext plannerContext,
|
||||
RowSignature rowSignature,
|
||||
RexNode rexNode
|
||||
)
|
||||
{
|
||||
return OperatorConversions.convertCall(
|
||||
plannerContext,
|
||||
rowSignature,
|
||||
rexNode,
|
||||
operands -> DruidExpression.fromExpression(DruidExpression.functionCall(
|
||||
StringUtils.toLowerCase(operator.getName()),
|
||||
operands
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DimFilter toDruidFilter(
|
||||
PlannerContext plannerContext,
|
||||
RowSignature rowSignature,
|
||||
@Nullable VirtualColumnRegistry virtualColumnRegistry,
|
||||
RexNode rexNode
|
||||
)
|
||||
{
|
||||
final List<RexNode> operands = ((RexCall) rexNode).getOperands();
|
||||
final DruidExpression druidExpression = Expressions.toDruidExpression(
|
||||
plannerContext,
|
||||
rowSignature,
|
||||
operands.get(0)
|
||||
);
|
||||
|
||||
if (druidExpression == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String search = RexLiteral.stringValue(operands.get(1));
|
||||
final SearchQuerySpec spec = new ContainsSearchQuerySpec(search, caseSensitive);
|
||||
|
||||
if (druidExpression.isSimpleExtraction()) {
|
||||
return new SearchQueryDimFilter(
|
||||
druidExpression.getSimpleExtraction().getColumn(),
|
||||
spec,
|
||||
druidExpression.getSimpleExtraction().getExtractionFn(),
|
||||
null
|
||||
);
|
||||
} else if (virtualColumnRegistry != null) {
|
||||
VirtualColumn v = virtualColumnRegistry.getOrCreateVirtualColumnForExpression(
|
||||
plannerContext,
|
||||
druidExpression,
|
||||
operands.get(0).getType()
|
||||
);
|
||||
|
||||
return new SearchQueryDimFilter(
|
||||
v.getOutputName(), spec, null, null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -61,6 +61,7 @@ import org.apache.druid.sql.calcite.expression.builtin.BTrimOperatorConversion;
|
|||
import org.apache.druid.sql.calcite.expression.builtin.CastOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.CeilOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.ConcatOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.ContainsOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.DateTruncOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.ExtractOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.FloorOperatorConversion;
|
||||
|
@ -181,6 +182,8 @@ public class DruidOperatorTable implements SqlOperatorTable
|
|||
.add(new AliasedOperatorConversion(new TruncateOperatorConversion(), "TRUNC"))
|
||||
.add(new LPadOperatorConversion())
|
||||
.add(new RPadOperatorConversion())
|
||||
.add(ContainsOperatorConversion.caseSensitive())
|
||||
.add(ContainsOperatorConversion.caseInsensitive())
|
||||
.build();
|
||||
|
||||
private static final List<SqlOperatorConversion> VALUE_COERCION_OPERATOR_CONVERSIONS =
|
||||
|
|
|
@ -38,7 +38,9 @@ import org.apache.druid.query.filter.ValueMatcher;
|
|||
import org.apache.druid.segment.RowAdapters;
|
||||
import org.apache.druid.segment.RowBasedColumnSelectorFactory;
|
||||
import org.apache.druid.segment.VirtualColumn;
|
||||
import org.apache.druid.segment.VirtualColumns;
|
||||
import org.apache.druid.segment.column.RowSignature;
|
||||
import org.apache.druid.segment.virtual.VirtualizedColumnSelectorFactory;
|
||||
import org.apache.druid.sql.calcite.planner.Calcites;
|
||||
import org.apache.druid.sql.calcite.planner.PlannerConfig;
|
||||
import org.apache.druid.sql.calcite.planner.PlannerContext;
|
||||
|
@ -283,11 +285,14 @@ class ExpressionTestHelper
|
|||
);
|
||||
|
||||
final ValueMatcher matcher = expectedFilter.toFilter().makeMatcher(
|
||||
new VirtualizedColumnSelectorFactory(
|
||||
RowBasedColumnSelectorFactory.create(
|
||||
RowAdapters.standardRow(),
|
||||
() -> new MapBasedRow(0L, bindings),
|
||||
rowSignature,
|
||||
false
|
||||
),
|
||||
VirtualColumns.create(virtualColumns)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -35,9 +35,12 @@ import org.apache.druid.java.util.common.IAE;
|
|||
import org.apache.druid.query.expression.TestExprMacroTable;
|
||||
import org.apache.druid.query.extraction.RegexDimExtractionFn;
|
||||
import org.apache.druid.query.filter.RegexDimFilter;
|
||||
import org.apache.druid.query.filter.SearchQueryDimFilter;
|
||||
import org.apache.druid.query.search.ContainsSearchQuerySpec;
|
||||
import org.apache.druid.segment.column.RowSignature;
|
||||
import org.apache.druid.segment.column.ValueType;
|
||||
import org.apache.druid.segment.virtual.ExpressionVirtualColumn;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.ContainsOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.DateTruncOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.LPadOperatorConversion;
|
||||
import org.apache.druid.sql.calcite.expression.builtin.LeftOperatorConversion;
|
||||
|
@ -1072,6 +1075,231 @@ public class ExpressionsTest extends ExpressionTestBase
|
|||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContains()
|
||||
{
|
||||
testHelper.testExpression(
|
||||
ContainsOperatorConversion.caseSensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeInputRef("spacey"),
|
||||
testHelper.makeLiteral("there")
|
||||
),
|
||||
DruidExpression.fromExpression("contains_string(\"spacey\",'there')"),
|
||||
1L
|
||||
);
|
||||
|
||||
testHelper.testExpression(
|
||||
ContainsOperatorConversion.caseSensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeInputRef("spacey"),
|
||||
testHelper.makeLiteral("There")
|
||||
),
|
||||
DruidExpression.fromExpression("contains_string(\"spacey\",'There')"),
|
||||
0L
|
||||
);
|
||||
|
||||
testHelper.testExpression(
|
||||
ContainsOperatorConversion.caseInsensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeInputRef("spacey"),
|
||||
testHelper.makeLiteral("There")
|
||||
),
|
||||
DruidExpression.fromExpression("icontains_string(\"spacey\",'There')"),
|
||||
1L
|
||||
);
|
||||
|
||||
testHelper.testExpression(
|
||||
ContainsOperatorConversion.caseSensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeCall(
|
||||
SqlStdOperatorTable.CONCAT,
|
||||
testHelper.makeLiteral("what is"),
|
||||
testHelper.makeInputRef("spacey")
|
||||
),
|
||||
testHelper.makeLiteral("what")
|
||||
),
|
||||
DruidExpression.fromExpression("contains_string(concat('what is',\"spacey\"),'what')"),
|
||||
1L
|
||||
);
|
||||
|
||||
testHelper.testExpression(
|
||||
ContainsOperatorConversion.caseSensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeCall(
|
||||
SqlStdOperatorTable.CONCAT,
|
||||
testHelper.makeLiteral("what is"),
|
||||
testHelper.makeInputRef("spacey")
|
||||
),
|
||||
testHelper.makeLiteral("there")
|
||||
),
|
||||
DruidExpression.fromExpression("contains_string(concat('what is',\"spacey\"),'there')"),
|
||||
1L
|
||||
);
|
||||
|
||||
testHelper.testExpression(
|
||||
ContainsOperatorConversion.caseInsensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeCall(
|
||||
SqlStdOperatorTable.CONCAT,
|
||||
testHelper.makeLiteral("what is"),
|
||||
testHelper.makeInputRef("spacey")
|
||||
),
|
||||
testHelper.makeLiteral("There")
|
||||
),
|
||||
DruidExpression.fromExpression("icontains_string(concat('what is',\"spacey\"),'There')"),
|
||||
1L
|
||||
);
|
||||
|
||||
testHelper.testExpression(
|
||||
SqlStdOperatorTable.AND,
|
||||
ImmutableList.of(
|
||||
testHelper.makeCall(
|
||||
ContainsOperatorConversion.caseSensitive().calciteOperator(),
|
||||
testHelper.makeInputRef("spacey"),
|
||||
testHelper.makeLiteral("there")
|
||||
),
|
||||
testHelper.makeCall(
|
||||
SqlStdOperatorTable.EQUALS,
|
||||
testHelper.makeLiteral("yes"),
|
||||
testHelper.makeLiteral("yes")
|
||||
)
|
||||
),
|
||||
DruidExpression.fromExpression("(contains_string(\"spacey\",'there') && ('yes' == 'yes'))"),
|
||||
1L
|
||||
);
|
||||
|
||||
testHelper.testExpression(
|
||||
SqlStdOperatorTable.AND,
|
||||
ImmutableList.of(
|
||||
testHelper.makeCall(
|
||||
ContainsOperatorConversion.caseInsensitive().calciteOperator(),
|
||||
testHelper.makeInputRef("spacey"),
|
||||
testHelper.makeLiteral("There")
|
||||
),
|
||||
testHelper.makeCall(
|
||||
SqlStdOperatorTable.EQUALS,
|
||||
testHelper.makeLiteral("yes"),
|
||||
testHelper.makeLiteral("yes")
|
||||
)
|
||||
),
|
||||
DruidExpression.fromExpression("(icontains_string(\"spacey\",'There') && ('yes' == 'yes'))"),
|
||||
1L
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContainsAsFilter()
|
||||
{
|
||||
testHelper.testFilter(
|
||||
ContainsOperatorConversion.caseSensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeInputRef("spacey"),
|
||||
testHelper.makeLiteral("there")
|
||||
),
|
||||
Collections.emptyList(),
|
||||
new SearchQueryDimFilter("spacey", new ContainsSearchQuerySpec("there", true), null),
|
||||
true
|
||||
);
|
||||
|
||||
testHelper.testFilter(
|
||||
ContainsOperatorConversion.caseSensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeInputRef("spacey"),
|
||||
testHelper.makeLiteral("There")
|
||||
),
|
||||
Collections.emptyList(),
|
||||
new SearchQueryDimFilter("spacey", new ContainsSearchQuerySpec("There", true), null),
|
||||
false
|
||||
);
|
||||
|
||||
testHelper.testFilter(
|
||||
ContainsOperatorConversion.caseInsensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeInputRef("spacey"),
|
||||
testHelper.makeLiteral("There")
|
||||
),
|
||||
Collections.emptyList(),
|
||||
new SearchQueryDimFilter("spacey", new ContainsSearchQuerySpec("There", false), null),
|
||||
true
|
||||
);
|
||||
|
||||
testHelper.testFilter(
|
||||
ContainsOperatorConversion.caseSensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeCall(
|
||||
SqlStdOperatorTable.CONCAT,
|
||||
testHelper.makeLiteral("what is"),
|
||||
testHelper.makeInputRef("spacey")
|
||||
),
|
||||
testHelper.makeLiteral("what")
|
||||
),
|
||||
ImmutableList.of(
|
||||
new ExpressionVirtualColumn(
|
||||
"v0",
|
||||
"concat('what is',\"spacey\")",
|
||||
ValueType.STRING,
|
||||
TestExprMacroTable.INSTANCE
|
||||
)
|
||||
),
|
||||
new SearchQueryDimFilter("v0", new ContainsSearchQuerySpec("what", true), null),
|
||||
true
|
||||
);
|
||||
|
||||
testHelper.testFilter(
|
||||
ContainsOperatorConversion.caseSensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeCall(
|
||||
SqlStdOperatorTable.CONCAT,
|
||||
testHelper.makeLiteral("what is"),
|
||||
testHelper.makeInputRef("spacey")
|
||||
),
|
||||
testHelper.makeLiteral("there")
|
||||
),
|
||||
ImmutableList.of(
|
||||
new ExpressionVirtualColumn(
|
||||
"v0",
|
||||
"concat('what is',\"spacey\")",
|
||||
ValueType.STRING,
|
||||
TestExprMacroTable.INSTANCE
|
||||
)
|
||||
),
|
||||
new SearchQueryDimFilter("v0", new ContainsSearchQuerySpec("there", true), null),
|
||||
true
|
||||
);
|
||||
|
||||
testHelper.testFilter(
|
||||
ContainsOperatorConversion.caseInsensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeCall(
|
||||
SqlStdOperatorTable.CONCAT,
|
||||
testHelper.makeLiteral("what is"),
|
||||
testHelper.makeInputRef("spacey")
|
||||
),
|
||||
testHelper.makeLiteral("What")
|
||||
),
|
||||
ImmutableList.of(
|
||||
new ExpressionVirtualColumn(
|
||||
"v0",
|
||||
"concat('what is',\"spacey\")",
|
||||
ValueType.STRING,
|
||||
TestExprMacroTable.INSTANCE
|
||||
)
|
||||
),
|
||||
new SearchQueryDimFilter("v0", new ContainsSearchQuerySpec("What", false), null),
|
||||
true
|
||||
);
|
||||
|
||||
testHelper.testFilter(
|
||||
ContainsOperatorConversion.caseSensitive().calciteOperator(),
|
||||
ImmutableList.of(
|
||||
testHelper.makeInputRef("spacey"),
|
||||
testHelper.makeLiteral("")
|
||||
),
|
||||
Collections.emptyList(),
|
||||
new SearchQueryDimFilter("spacey", new ContainsSearchQuerySpec("", true), null),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimeFloor()
|
||||
|
|
|
@ -1095,6 +1095,8 @@ nvl
|
|||
parse_long
|
||||
regexp_extract
|
||||
regexp_like
|
||||
contains_string
|
||||
icontains_string
|
||||
result1
|
||||
result2
|
||||
rint
|
||||
|
|
Loading…
Reference in New Issue