You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by GitBox <gi...@apache.org> on 2020/09/03 20:07:18 UTC

[GitHub] [druid] abhishekagarwal87 opened a new pull request #10350: Support SearchQueryDimFilter in sql via new methods

abhishekagarwal87 opened a new pull request #10350:
URL: https://github.com/apache/druid/pull/10350


   ### Description
   
   Native druid query has `SearchQueryDimFilter` that is faster and supports case-insensitive search. This patch adds the `SearchQueryDimFilter` via two new functions - `contains` and `icontains` 
   
   <hr>
   
   This PR has:
   - [x] been self-reviewed.
   - [ ] added documentation for new or modified features or behaviors.
   - [ ] added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
   - [ ] added or updated version, license, or notice information in [licenses.yaml](https://github.com/apache/druid/blob/master/licenses.yaml)
   - [ ] added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
   - [ ] added unit tests or modified existing tests to cover new code paths, ensuring the threshold for [code coverage](https://github.com/apache/druid/blob/master/dev/code-review/code-coverage.md) is met.
   - [ ] added integration tests.
   - [ ] been tested in a test Druid cluster.
   
   <hr>
   
   ##### Key added classes in this PR
    * `ContainsOperatorConversion`
   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486929873



##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       `searchStrExpr` is required in `stringify` method. 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Added two tests for the same. Learnt a few things along the way. I was not at all expecting an empty string to be turned to `null`. Do you know why that is done? 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       hmm. what do you think should be the output of following in the default mode assuming `myColumn` has empty value?
   ```
   CONTAINS("myColumn", '')
   ```
   

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       I was not handling the null values similar to other expressions. It should work now. code coverage bot was complaining as tests were added in `sql` module and these `Expr*` classes are in the `processing` module. After I added the tests in the processing module, I realized what the problem was.  When an empty string is passed, the functions get a null value that they again convert to an empty value. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`. 
   In SQL compatible mode, we would see an exception since `null` is not a valid literal. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       though I am still evaluating if the SQL operator behaves differently in non-sql compatible mode. That is, If `null` is not translated into empty string in the SQL operator, we may see a behaviour mismatch. 

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       `searchStrExpr` is required in `stringify` method. 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Added two tests for the same. Learnt a few things along the way. I was not at all expecting an empty string to be turned to `null`. Do you know why that is done? 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       hmm. what do you think should be the output of following in the default mode assuming `myColumn` has empty value?
   ```
   CONTAINS("myColumn", '')
   ```
   

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       I was not handling the null values similar to other expressions. It should work now. code coverage bot was complaining as tests were added in `sql` module and these `Expr*` classes are in the `processing` module. After I added the tests in the processing module, I realized what the problem was.  When an empty string is passed, the functions get a null value that they again convert to an empty value. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`. 
   In SQL compatible mode, we would see an exception since `null` is not a valid literal. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       though I am still evaluating if the SQL operator behaves differently in non-sql compatible mode. That is, If `null` is not translated into empty string in the SQL operator, we may see a behaviour mismatch. 

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       `searchStrExpr` is required in `stringify` method. 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Added two tests for the same. Learnt a few things along the way. I was not at all expecting an empty string to be turned to `null`. Do you know why that is done? 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       hmm. what do you think should be the output of following in the default mode assuming `myColumn` has empty value?
   ```
   CONTAINS("myColumn", '')
   ```
   

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       I was not handling the null values similar to other expressions. It should work now. code coverage bot was complaining as tests were added in `sql` module and these `Expr*` classes are in the `processing` module. After I added the tests in the processing module, I realized what the problem was.  When an empty string is passed, the functions get a null value that they again convert to an empty value. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`. 
   In SQL compatible mode, we would see an exception since `null` is not a valid literal. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       though I am still evaluating if the SQL operator behaves differently in non-sql compatible mode. That is, If `null` is not translated into empty string in the SQL operator, we may see a behaviour mismatch. 

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       `searchStrExpr` is required in `stringify` method. 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Added two tests for the same. Learnt a few things along the way. I was not at all expecting an empty string to be turned to `null`. Do you know why that is done? 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       hmm. what do you think should be the output of following in the default mode assuming `myColumn` has empty value?
   ```
   CONTAINS("myColumn", '')
   ```
   

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       I was not handling the null values similar to other expressions. It should work now. code coverage bot was complaining as tests were added in `sql` module and these `Expr*` classes are in the `processing` module. After I added the tests in the processing module, I realized what the problem was.  When an empty string is passed, the functions get a null value that they again convert to an empty value. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`. 
   In SQL compatible mode, we would see an exception since `null` is not a valid literal. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       though I am still evaluating if the SQL operator behaves differently in non-sql compatible mode. That is, If `null` is not translated into empty string in the SQL operator, we may see a behaviour mismatch. 

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       `searchStrExpr` is required in `stringify` method. 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Added two tests for the same. Learnt a few things along the way. I was not at all expecting an empty string to be turned to `null`. Do you know why that is done? 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       hmm. what do you think should be the output of following in the default mode assuming `myColumn` has empty value?
   ```
   CONTAINS("myColumn", '')
   ```
   

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       I was not handling the null values similar to other expressions. It should work now. code coverage bot was complaining as tests were added in `sql` module and these `Expr*` classes are in the `processing` module. After I added the tests in the processing module, I realized what the problem was.  When an empty string is passed, the functions get a null value that they again convert to an empty value. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`. 
   In SQL compatible mode, we would see an exception since `null` is not a valid literal. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       though I am still evaluating if the SQL operator behaves differently in non-sql compatible mode. That is, If `null` is not translated into empty string in the SQL operator, we may see a behaviour mismatch. 

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       `searchStrExpr` is required in `stringify` method. 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Added two tests for the same. Learnt a few things along the way. I was not at all expecting an empty string to be turned to `null`. Do you know why that is done? 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       hmm. what do you think should be the output of following in the default mode assuming `myColumn` has empty value?
   ```
   CONTAINS("myColumn", '')
   ```
   

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       I was not handling the null values similar to other expressions. It should work now. code coverage bot was complaining as tests were added in `sql` module and these `Expr*` classes are in the `processing` module. After I added the tests in the processing module, I realized what the problem was.  When an empty string is passed, the functions get a null value that they again convert to an empty value. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`. 
   In SQL compatible mode, we would see an exception since `null` is not a valid literal. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       though I am still evaluating if the SQL operator behaves differently in non-sql compatible mode. That is, If `null` is not translated into empty string in the SQL operator, we may see a behaviour mismatch. 

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       `searchStrExpr` is required in `stringify` method. 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Added two tests for the same. Learnt a few things along the way. I was not at all expecting an empty string to be turned to `null`. Do you know why that is done? 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       hmm. what do you think should be the output of following in the default mode assuming `myColumn` has empty value?
   ```
   CONTAINS("myColumn", '')
   ```
   

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       I was not handling the null values similar to other expressions. It should work now. code coverage bot was complaining as tests were added in `sql` module and these `Expr*` classes are in the `processing` module. After I added the tests in the processing module, I realized what the problem was.  When an empty string is passed, the functions get a null value that they again convert to an empty value. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`. 
   In SQL compatible mode, we would see an exception since `null` is not a valid literal. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       though I am still evaluating if the SQL operator behaves differently in non-sql compatible mode. That is, If `null` is not translated into empty string in the SQL operator, we may see a behaviour mismatch. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] gianm commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
gianm commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486193164



##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ContainsOperatorConversion.java
##########
@@ -0,0 +1,126 @@
+/*
+ * 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.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.java.util.common.StringUtils;
+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.DirectOperatorConversion;
+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;
+
+public class ContainsOperatorConversion extends DirectOperatorConversion
+{
+  private static final String CASE_SENSITIVE_FN_NAME = "contains_str";
+  private static final String CASE_INSENSITIVE_FN_NAME = "icontains_str";
+  private final boolean caseSensitive;
+
+  public ContainsOperatorConversion(
+      final SqlFunction sqlFunction,
+      final String functionName,
+      final boolean caseSensitive
+  )
+  {
+    super(sqlFunction, functionName);

Review comment:
       Yes, because we should let people use this function in any context, not just as a leaf filter, and we'll need a native expression in order to do that. Imagine a query like:
   
   ```
   SELECT CASE WHEN CONTAINS_STR(domain, '.com') THEN 'dot-com' ELSE 'other' END
   FROM tbl
   ```
   
   That would need to get translated using a virtual column + a native expression.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r487073412



##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       https://druid.apache.org/docs/latest/querying/sql.html#null-values
   
   This is because initially there was no null and druid was not sql compatible so nulls were treated as empty strings or 0s. This was done at ingestion time as well, so once the data was ingested, we can't distinguish between a null and a 0.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486070888



##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,108 @@ public void testPad()
     );
   }
 
+  @Test
+  public void testContains()
+  {
+    testHelper.testFilter(
+        ContainsOperatorConversion.createOperatorConversion(true).calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("spacey"),
+            testHelper.makeLiteral("there")
+        ),
+        Collections.emptyList(),
+        new SearchQueryDimFilter("spacey", new ContainsSearchQuerySpec("there", true), null),
+        true
+    );
+
+    testHelper.testFilter(
+        ContainsOperatorConversion.createOperatorConversion(true).calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("spacey"),
+            testHelper.makeLiteral("There")
+        ),
+        Collections.emptyList(),
+        new SearchQueryDimFilter("spacey", new ContainsSearchQuerySpec("There", true), null),
+        false
+    );
+
+    testHelper.testFilter(
+        ContainsOperatorConversion.createOperatorConversion(false).calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("spacey"),
+            testHelper.makeLiteral("There")
+        ),
+        Collections.emptyList(),
+        new SearchQueryDimFilter("spacey", new ContainsSearchQuerySpec("There", false), null),
+        true
+    );
+
+    testHelper.testFilter(
+        ContainsOperatorConversion.createOperatorConversion(true).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.createOperatorConversion(true).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.createOperatorConversion(false).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
+    );
+  }

Review comment:
       Can you also add some tests similar to the ones in `FunctionTest` to test that we can correctly parse the expression similar to `testLpad`




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486074700



##########
File path: docs/querying/sql.md
##########
@@ -561,6 +561,8 @@ The [DataSketches extension](../development/extensions-core/datasketches-extensi
 |`COALESCE(value1, value2, ...)`|Returns the first value that is neither NULL nor empty string.|
 |`NVL(expr,expr-for-null)`|Returns 'expr-for-null' if 'expr' is null (or empty string for string type).|
 |`BLOOM_FILTER_TEST(<expr>, <serialized-filter>)`|Returns true if the value is contained in a Base64-serialized bloom filter. See the [Bloom filter extension](../development/extensions-core/bloom-filter.html) documentation for additional details.|
+|`CONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`.|
+|`ICONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`. The match is case-insensitive.|

Review comment:
       OK thinking about this some more, I think I like the idea of one function name more since it maps to the `SearchQuerySpec`. Then, over time, this function can become smarter and support the different types of search query specs, so if a user passes in a list of values, it knows to use the `FragmentSearchQuerySpec` and if they pass in some other flag, it knows to use the `RegexSearchQuerySpec` 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486251779



##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,108 @@ public void testPad()
     );
   }
 
+  @Test
+  public void testContains()
+  {

Review comment:
       will add those as I add corresponding native functions. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r487073412



##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       https://druid.apache.org/docs/latest/querying/sql.html#null-values
   
   This is because initially there was no null and druid was not sql compatible so nulls were treated as empty strings or 0s. This was done at ingestion time as well, so once the data was ingested, we can't distinguish between a null and a 0.

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       I think that makes sense. I tried to walk through the possible values of `expr.stringify()` given the fact that it is guaranteed to be a string literal, but it was a little tricky for me to reason about, so keeping it an Expr is cool

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.

Review comment:
       nit
   ```suggestion
         // same behavior as ContainsSearchQuerySpec#accept
   ```
   
   Luckily the behavior is the same as regexp_like
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.
+ */

Review comment:
       nit: It would be good to link to the DimFilter whose behavior we are trying to mimic here `ContainsSearchQuerySpec`
   
   since we want the logic between these 2 classes to stay the same. I wonder if we can future proof this so they stay in sync if someone makes an update to `ContainsSearchQuerySpec`

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       According to `ContainsSearchQuerySpec#accept` searching for anything on null should be false
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       https://druid.apache.org/docs/latest/querying/sql.html#null-values
   
   This is because initially there was no null and druid was not sql compatible so nulls were treated as empty strings or 0s. This was done at ingestion time as well, so once the data was ingested, we can't distinguish between a null and a 0.

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       I think that makes sense. I tried to walk through the possible values of `expr.stringify()` given the fact that it is guaranteed to be a string literal, but it was a little tricky for me to reason about, so keeping it an Expr is cool

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.

Review comment:
       nit
   ```suggestion
         // same behavior as ContainsSearchQuerySpec#accept
   ```
   
   Luckily the behavior is the same as regexp_like
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.
+ */

Review comment:
       nit: It would be good to link to the DimFilter whose behavior we are trying to mimic here `ContainsSearchQuerySpec`
   
   since we want the logic between these 2 classes to stay the same. I wonder if we can future proof this so they stay in sync if someone makes an update to `ContainsSearchQuerySpec`

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       According to `ContainsSearchQuerySpec#accept` searching for anything on null should be false
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       https://druid.apache.org/docs/latest/querying/sql.html#null-values
   
   This is because initially there was no null and druid was not sql compatible so nulls were treated as empty strings or 0s. This was done at ingestion time as well, so once the data was ingested, we can't distinguish between a null and a 0.

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       I think that makes sense. I tried to walk through the possible values of `expr.stringify()` given the fact that it is guaranteed to be a string literal, but it was a little tricky for me to reason about, so keeping it an Expr is cool

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.

Review comment:
       nit
   ```suggestion
         // same behavior as ContainsSearchQuerySpec#accept
   ```
   
   Luckily the behavior is the same as regexp_like
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.
+ */

Review comment:
       nit: It would be good to link to the DimFilter whose behavior we are trying to mimic here `ContainsSearchQuerySpec`
   
   since we want the logic between these 2 classes to stay the same. I wonder if we can future proof this so they stay in sync if someone makes an update to `ContainsSearchQuerySpec`

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       According to `ContainsSearchQuerySpec#accept` searching for anything on null should be false
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       https://druid.apache.org/docs/latest/querying/sql.html#null-values
   
   This is because initially there was no null and druid was not sql compatible so nulls were treated as empty strings or 0s. This was done at ingestion time as well, so once the data was ingested, we can't distinguish between a null and a 0.

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       I think that makes sense. I tried to walk through the possible values of `expr.stringify()` given the fact that it is guaranteed to be a string literal, but it was a little tricky for me to reason about, so keeping it an Expr is cool

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.

Review comment:
       nit
   ```suggestion
         // same behavior as ContainsSearchQuerySpec#accept
   ```
   
   Luckily the behavior is the same as regexp_like
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.
+ */

Review comment:
       nit: It would be good to link to the DimFilter whose behavior we are trying to mimic here `ContainsSearchQuerySpec`
   
   since we want the logic between these 2 classes to stay the same. I wonder if we can future proof this so they stay in sync if someone makes an update to `ContainsSearchQuerySpec`

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       According to `ContainsSearchQuerySpec#accept` searching for anything on null should be false
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       https://druid.apache.org/docs/latest/querying/sql.html#null-values
   
   This is because initially there was no null and druid was not sql compatible so nulls were treated as empty strings or 0s. This was done at ingestion time as well, so once the data was ingested, we can't distinguish between a null and a 0.

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       I think that makes sense. I tried to walk through the possible values of `expr.stringify()` given the fact that it is guaranteed to be a string literal, but it was a little tricky for me to reason about, so keeping it an Expr is cool

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.

Review comment:
       nit
   ```suggestion
         // same behavior as ContainsSearchQuerySpec#accept
   ```
   
   Luckily the behavior is the same as regexp_like
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.
+ */

Review comment:
       nit: It would be good to link to the DimFilter whose behavior we are trying to mimic here `ContainsSearchQuerySpec`
   
   since we want the logic between these 2 classes to stay the same. I wonder if we can future proof this so they stay in sync if someone makes an update to `ContainsSearchQuerySpec`

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       According to `ContainsSearchQuerySpec#accept` searching for anything on null should be false
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       https://druid.apache.org/docs/latest/querying/sql.html#null-values
   
   This is because initially there was no null and druid was not sql compatible so nulls were treated as empty strings or 0s. This was done at ingestion time as well, so once the data was ingested, we can't distinguish between a null and a 0.

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       I think that makes sense. I tried to walk through the possible values of `expr.stringify()` given the fact that it is guaranteed to be a string literal, but it was a little tricky for me to reason about, so keeping it an Expr is cool

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.

Review comment:
       nit
   ```suggestion
         // same behavior as ContainsSearchQuerySpec#accept
   ```
   
   Luckily the behavior is the same as regexp_like
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.
+ */

Review comment:
       nit: It would be good to link to the DimFilter whose behavior we are trying to mimic here `ContainsSearchQuerySpec`
   
   since we want the logic between these 2 classes to stay the same. I wonder if we can future proof this so they stay in sync if someone makes an update to `ContainsSearchQuerySpec`

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       According to `ContainsSearchQuerySpec#accept` searching for anything on null should be false
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       https://druid.apache.org/docs/latest/querying/sql.html#null-values
   
   This is because initially there was no null and druid was not sql compatible so nulls were treated as empty strings or 0s. This was done at ingestion time as well, so once the data was ingested, we can't distinguish between a null and a 0.

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       I think that makes sense. I tried to walk through the possible values of `expr.stringify()` given the fact that it is guaranteed to be a string literal, but it was a little tricky for me to reason about, so keeping it an Expr is cool

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.

Review comment:
       nit
   ```suggestion
         // same behavior as ContainsSearchQuerySpec#accept
   ```
   
   Luckily the behavior is the same as regexp_like
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.
+ */

Review comment:
       nit: It would be good to link to the DimFilter whose behavior we are trying to mimic here `ContainsSearchQuerySpec`
   
   since we want the logic between these 2 classes to stay the same. I wonder if we can future proof this so they stay in sync if someone makes an update to `ContainsSearchQuerySpec`

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       According to `ContainsSearchQuerySpec#accept` searching for anything on null should be false
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r487073412



##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       https://druid.apache.org/docs/latest/querying/sql.html#null-values
   
   This is because initially there was no null and druid was not sql compatible so nulls were treated as empty strings or 0s. This was done at ingestion time as well, so once the data was ingested, we can't distinguish between a null and a 0.

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       I think that makes sense. I tried to walk through the possible values of `expr.stringify()` given the fact that it is guaranteed to be a string literal, but it was a little tricky for me to reason about, so keeping it an Expr is cool

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.

Review comment:
       nit
   ```suggestion
         // same behavior as ContainsSearchQuerySpec#accept
   ```
   
   Luckily the behavior is the same as regexp_like
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.
+ */

Review comment:
       nit: It would be good to link to the DimFilter whose behavior we are trying to mimic here `ContainsSearchQuerySpec`
   
   since we want the logic between these 2 classes to stay the same. I wonder if we can future proof this so they stay in sync if someone makes an update to `ContainsSearchQuerySpec`

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       According to `ContainsSearchQuerySpec#accept` searching for anything on null should be false
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] gianm commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
gianm commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486068975



##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ContainsOperatorConversion.java
##########
@@ -0,0 +1,126 @@
+/*
+ * 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.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.java.util.common.StringUtils;
+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.DirectOperatorConversion;
+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;
+
+public class ContainsOperatorConversion extends DirectOperatorConversion
+{
+  private static final String CASE_SENSITIVE_FN_NAME = "contains_str";
+  private static final String CASE_INSENSITIVE_FN_NAME = "icontains_str";
+  private final boolean caseSensitive;
+
+  public ContainsOperatorConversion(
+      final SqlFunction sqlFunction,
+      final String functionName,
+      final boolean caseSensitive
+  )
+  {
+    super(sqlFunction, functionName);

Review comment:
       I don't see native functions named "contains_str" and "icontains_str", do they exist? (DirectOperatorConversion assumes there is a native function with the functionName you pass in here.)

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,108 @@ public void testPad()
     );
   }
 
+  @Test
+  public void testContains()
+  {

Review comment:
       You should include tests here using `testHelper.testExpression` too. It'll test that `toDruidExpression` works properly, which will be used if someone uses this operator outside of a leaf filter.

##########
File path: docs/querying/sql.md
##########
@@ -561,6 +561,8 @@ The [DataSketches extension](../development/extensions-core/datasketches-extensi
 |`COALESCE(value1, value2, ...)`|Returns the first value that is neither NULL nor empty string.|
 |`NVL(expr,expr-for-null)`|Returns 'expr-for-null' if 'expr' is null (or empty string for string type).|
 |`BLOOM_FILTER_TEST(<expr>, <serialized-filter>)`|Returns true if the value is contained in a Base64-serialized bloom filter. See the [Bloom filter extension](../development/extensions-core/bloom-filter.html) documentation for additional details.|
+|`CONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`.|
+|`ICONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`. The match is case-insensitive.|

Review comment:
       These should be in the "String functions" section.
   
   I don't have an opinion on the names right now, but to form an opinion I like to do the following.
   
   1. Check Calcite's SqlStdOperatorTable, which contains standard operators. If there's something matching there, we should use that.
   2. If there isn't a standard operator, survey some other databases. I like to check the golden oldies: MySQL, PostgreSQL, Oracle, SQL Server, as well as some of the newer ones like Presto, Snowflake, BigQuery. If a few of them seem to agree on a name and behavior it's good to go with that.
   3. If there doesn't seem to be any agreement, or if you don't want to implement what they seem to agree on for some reason, then make up a new operator.

##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
##########
@@ -181,6 +182,8 @@
           .add(new AliasedOperatorConversion(new TruncateOperatorConversion(), "TRUNC"))
           .add(new LPadOperatorConversion())
           .add(new RPadOperatorConversion())
+          .add(ContainsOperatorConversion.createOperatorConversion(true))
+          .add(ContainsOperatorConversion.createOperatorConversion(false))

Review comment:
       Might be more readable to give these methods nice names, like `caseInsensitive()` and `caseSensitive()`. Small nit though.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486929873



##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       `searchStrExpr` is required in `stringify` method. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s merged pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s merged pull request #10350:
URL: https://github.com/apache/druid/pull/10350


   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r487371273



##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`. 
   In SQL compatible mode, we would see an exception since `null` is not a valid literal. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486712620



##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Can you add some tests for how the function deals with nulls and empty strings - these tend to surface some issues because we need to support 2 modes - default mode and sql compatible mode, and sometimes the behavior of these functions may be slightly different than we expect. I remember finding issues when I changed the behavior of lpad / rpad functions - https://github.com/apache/druid/pull/10006/files#diff-fd48c48432e4977332aa9806d2d308faR150-R151 

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExprMacro.java
##########
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */

Review comment:
       Thanks for the awesome javadocs 🤘 

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       Since ContainsExpr always expects this to be a literal value, should this class just accept a String instead?

##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ContainsOperatorConversion.java
##########
@@ -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;
+
+  public ContainsOperatorConversion(

Review comment:
       super nit
   ```suggestion
     private ContainsOperatorConversion(
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486929873



##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       `searchStrExpr` is required in `stringify` method. 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Added two tests for the same. Learnt a few things along the way. I was not at all expecting an empty string to be turned to `null`. Do you know why that is done? 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       hmm. what do you think should be the output of following in the default mode assuming `myColumn` has empty value?
   ```
   CONTAINS("myColumn", '')
   ```
   

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       I was not handling the null values similar to other expressions. It should work now. code coverage bot was complaining as tests were added in `sql` module and these `Expr*` classes are in the `processing` module. After I added the tests in the processing module, I realized what the problem was.  When an empty string is passed, the functions get a null value that they again convert to an empty value. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`. 
   In SQL compatible mode, we would see an exception since `null` is not a valid literal. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       though I am still evaluating if the SQL operator behaves differently in non-sql compatible mode. That is, If `null` is not translated into empty string in the SQL operator, we may see a behaviour mismatch. 

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       `searchStrExpr` is required in `stringify` method. 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Added two tests for the same. Learnt a few things along the way. I was not at all expecting an empty string to be turned to `null`. Do you know why that is done? 

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       hmm. what do you think should be the output of following in the default mode assuming `myColumn` has empty value?
   ```
   CONTAINS("myColumn", '')
   ```
   

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       I was not handling the null values similar to other expressions. It should work now. code coverage bot was complaining as tests were added in `sql` module and these `Expr*` classes are in the `processing` module. After I added the tests in the processing module, I realized what the problem was.  When an empty string is passed, the functions get a null value that they again convert to an empty value. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`. 
   In SQL compatible mode, we would see an exception since `null` is not a valid literal. 

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       though I am still evaluating if the SQL operator behaves differently in non-sql compatible mode. That is, If `null` is not translated into empty string in the SQL operator, we may see a behaviour mismatch. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486249916



##########
File path: docs/querying/sql.md
##########
@@ -561,6 +561,8 @@ The [DataSketches extension](../development/extensions-core/datasketches-extensi
 |`COALESCE(value1, value2, ...)`|Returns the first value that is neither NULL nor empty string.|
 |`NVL(expr,expr-for-null)`|Returns 'expr-for-null' if 'expr' is null (or empty string for string type).|
 |`BLOOM_FILTER_TEST(<expr>, <serialized-filter>)`|Returns true if the value is contained in a Base64-serialized bloom filter. See the [Bloom filter extension](../development/extensions-core/bloom-filter.html) documentation for additional details.|
+|`CONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`.|
+|`ICONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`. The match is case-insensitive.|

Review comment:
       mysql and postgres - They have substring functions that return the starting index of a search string. I did not find any function similar to `contains`. Oracle has a similar function which is called `instr` 
   Snowflake has a `contains` function similar to ours though there is no case insensitive flavor. 
   Presto does not seem to have a `contains` method but does have the methods to find the starting index of another string.
   BigQuery has `REGEXP_CONTAINS` which takes a regular expression as an input. 
   
   `CONTAINS` would have been an ideal choice for the name but alas, that conflicts with the existing calcite operator.  I can certainly rename them to `CONTAINS_STRING` 
   
   




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r487077896



##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       hmm. what do you think should be the output of following in the default mode assuming `myColumn` has empty value?
   ```
   CONTAINS("myColumn", '')
   ```
   




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486438037



##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ContainsOperatorConversion.java
##########
@@ -0,0 +1,126 @@
+/*
+ * 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.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.java.util.common.StringUtils;
+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.DirectOperatorConversion;
+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;
+
+public class ContainsOperatorConversion extends DirectOperatorConversion
+{
+  private static final String CASE_SENSITIVE_FN_NAME = "contains_str";
+  private static final String CASE_INSENSITIVE_FN_NAME = "icontains_str";
+  private final boolean caseSensitive;
+
+  public ContainsOperatorConversion(
+      final SqlFunction sqlFunction,
+      final String functionName,
+      final boolean caseSensitive
+  )
+  {
+    super(sqlFunction, functionName);

Review comment:
       This would be a nice comment to add on DirectOperatorConversion. I always get confused about which one to add to.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486066973



##########
File path: docs/querying/sql.md
##########
@@ -561,6 +561,8 @@ The [DataSketches extension](../development/extensions-core/datasketches-extensi
 |`COALESCE(value1, value2, ...)`|Returns the first value that is neither NULL nor empty string.|
 |`NVL(expr,expr-for-null)`|Returns 'expr-for-null' if 'expr' is null (or empty string for string type).|
 |`BLOOM_FILTER_TEST(<expr>, <serialized-filter>)`|Returns true if the value is contained in a Base64-serialized bloom filter. See the [Bloom filter extension](../development/extensions-core/bloom-filter.html) documentation for additional details.|
+|`CONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`.|
+|`ICONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`. The match is case-insensitive.|

Review comment:
       The PR description says the functions are called `contains` and `icontains`, but it looks like this patch uses the names with `_str` appended. Was there any reason you chose these names instead of others?
   
   I couldn't find an equivalent query in postgresql, and when I googled for a contains operator: Oracle and SQL server appear to have one - https://docs.oracle.com/cd/B28359_01/text.111/b28303/query.htm#g1016054 and https://docs.microsoft.com/en-us/sql/t-sql/queries/contains-transact-sql?view=sql-server-2017. 
   
   Also, was there any reasoning behind having 2 functions instead of 1 with a parameter to ignore the case? I don't think I have a strong preference either way yet, just curious




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r487129886



##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       I was not handling the null values similar to other expressions. It should work now. code coverage bot was complaining as tests were added in `sql` module and these `Expr*` classes are in the `processing` module. After I added the tests in the processing module, I realized what the problem was.  When an empty string is passed, the functions get a null value that they again convert to an empty value. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486117457



##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ContainsOperatorConversion.java
##########
@@ -0,0 +1,126 @@
+/*
+ * 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.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.java.util.common.StringUtils;
+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.DirectOperatorConversion;
+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;
+
+public class ContainsOperatorConversion extends DirectOperatorConversion
+{
+  private static final String CASE_SENSITIVE_FN_NAME = "contains_str";
+  private static final String CASE_INSENSITIVE_FN_NAME = "icontains_str";
+  private final boolean caseSensitive;
+
+  public ContainsOperatorConversion(
+      final SqlFunction sqlFunction,
+      final String functionName,
+      final boolean caseSensitive
+  )
+  {
+    super(sqlFunction, functionName);

Review comment:
       They don't exist. I am going to fix this by returning `null` in `toDruidExpression` method. do you think it will be useful to add corresponding native functions as well that can be used outside leaf filters? 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486115540



##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
##########
@@ -181,6 +182,8 @@
           .add(new AliasedOperatorConversion(new TruncateOperatorConversion(), "TRUNC"))
           .add(new LPadOperatorConversion())
           .add(new RPadOperatorConversion())
+          .add(ContainsOperatorConversion.createOperatorConversion(true))
+          .add(ContainsOperatorConversion.createOperatorConversion(false))

Review comment:
       Ack. Those indeed sound better. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on pull request #10350:
URL: https://github.com/apache/druid/pull/10350#issuecomment-689993153


   Overall I think this looks reasonable - I'm just not certain about what the names of the functions should be and whether we need a separate name for ignoring the case. 


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r487322688



##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.

Review comment:
       nit
   ```suggestion
         // same behavior as ContainsSearchQuerySpec#accept
   ```
   
   Luckily the behavior is the same as regexp_like
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.
+ */

Review comment:
       nit: It would be good to link to the DimFilter whose behavior we are trying to mimic here `ContainsSearchQuerySpec`
   
   since we want the logic between these 2 classes to stay the same. I wonder if we can future proof this so they stay in sync if someone makes an update to `ContainsSearchQuerySpec`

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       According to `ContainsSearchQuerySpec#accept` searching for anything on null should be false
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486510700



##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ContainsOperatorConversion.java
##########
@@ -0,0 +1,126 @@
+/*
+ * 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.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.java.util.common.StringUtils;
+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.DirectOperatorConversion;
+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;
+
+public class ContainsOperatorConversion extends DirectOperatorConversion

Review comment:
       Added. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486067530



##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ContainsOperatorConversion.java
##########
@@ -0,0 +1,126 @@
+/*
+ * 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.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.java.util.common.StringUtils;
+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.DirectOperatorConversion;
+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;
+
+public class ContainsOperatorConversion extends DirectOperatorConversion

Review comment:
       nit: javadocs please




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r487371668



##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       though I am still evaluating if the SQL operator behaves differently in non-sql compatible mode. That is, If `null` is not translated into empty string in the SQL operator, we may see a behaviour mismatch. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] gianm commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
gianm commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486199959



##########
File path: docs/querying/sql.md
##########
@@ -561,6 +561,8 @@ The [DataSketches extension](../development/extensions-core/datasketches-extensi
 |`COALESCE(value1, value2, ...)`|Returns the first value that is neither NULL nor empty string.|
 |`NVL(expr,expr-for-null)`|Returns 'expr-for-null' if 'expr' is null (or empty string for string type).|
 |`BLOOM_FILTER_TEST(<expr>, <serialized-filter>)`|Returns true if the value is contained in a Base64-serialized bloom filter. See the [Bloom filter extension](../development/extensions-core/bloom-filter.html) documentation for additional details.|
+|`CONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`.|
+|`ICONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`. The match is case-insensitive.|

Review comment:
       Did some collection of those other databases I mentioned seem to agree on a way to do this?
   
   If not, I think your reasoning makes sense. The only thing I'd consider changing, in that case, is spelling out STRING rather than STR. We have a few other functions that have STRING in the name and none that have STR: https://druid.apache.org/docs/latest/querying/sql
   
   On the two-functions vs extra-parameter thing: IMO it feels more SQL-ish to have two functions. It doesn't map as directly onto the native construct, but I think that's okay. The native constructs were designed for a JSON API, where it's easy and natural to add a bunch of named parameters. That isn't the case with SQL, where parameters are positional, and so adding a lot of them doesn't work as well.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486251537



##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ContainsOperatorConversion.java
##########
@@ -0,0 +1,126 @@
+/*
+ * 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.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.java.util.common.StringUtils;
+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.DirectOperatorConversion;
+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;
+
+public class ContainsOperatorConversion extends DirectOperatorConversion
+{
+  private static final String CASE_SENSITIVE_FN_NAME = "contains_str";
+  private static final String CASE_INSENSITIVE_FN_NAME = "icontains_str";
+  private final boolean caseSensitive;
+
+  public ContainsOperatorConversion(
+      final SqlFunction sqlFunction,
+      final String functionName,
+      final boolean caseSensitive
+  )
+  {
+    super(sqlFunction, functionName);

Review comment:
       makes sense. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486709263



##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       Since ContainsExpr always expects this to be a literal value, should this class just accept a String instead?

##########
File path: sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ContainsOperatorConversion.java
##########
@@ -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;
+
+  public ContainsOperatorConversion(

Review comment:
       super nit
   ```suggestion
     private ContainsOperatorConversion(
   ```

##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       https://druid.apache.org/docs/latest/querying/sql.html#null-values
   
   This is because initially there was no null and druid was not sql compatible so nulls were treated as empty strings or 0s. This was done at ingestion time as well, so once the data was ingested, we can't distinguish between a null and a 0.

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       I think that makes sense. I tried to walk through the possible values of `expr.stringify()` given the fact that it is guaranteed to be a string literal, but it was a little tricky for me to reason about, so keeping it an Expr is cool

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.

Review comment:
       nit
   ```suggestion
         // same behavior as ContainsSearchQuerySpec#accept
   ```
   
   Luckily the behavior is the same as regexp_like
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```

##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -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.
+ */

Review comment:
       nit: It would be good to link to the DimFilter whose behavior we are trying to mimic here `ContainsSearchQuerySpec`
   
   since we want the logic between these 2 classes to stay the same. I wonder if we can future proof this so they stay in sync if someone makes an update to `ContainsSearchQuerySpec`

##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       According to `ContainsSearchQuerySpec#accept` searching for anything on null should be false
   
   ```
       if (dimVal == null || value == null) {
         return false;
       }
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486249916



##########
File path: docs/querying/sql.md
##########
@@ -561,6 +561,8 @@ The [DataSketches extension](../development/extensions-core/datasketches-extensi
 |`COALESCE(value1, value2, ...)`|Returns the first value that is neither NULL nor empty string.|
 |`NVL(expr,expr-for-null)`|Returns 'expr-for-null' if 'expr' is null (or empty string for string type).|
 |`BLOOM_FILTER_TEST(<expr>, <serialized-filter>)`|Returns true if the value is contained in a Base64-serialized bloom filter. See the [Bloom filter extension](../development/extensions-core/bloom-filter.html) documentation for additional details.|
+|`CONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`.|
+|`ICONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`. The match is case-insensitive.|

Review comment:
         - mysql and postgres - They have substring functions that return the starting index of a search string. I did not find any function similar to `contains`. Oracle has a similar function which is called `instr` 
     - Snowflake has a `contains` function similar to ours though there is no case insensitive flavor. 
     - Presto does not seem to have a `contains` method but does have the methods to find the starting index of another string.
     - BigQuery has `REGEXP_CONTAINS` which takes a regular expression as an input. 
   
   `CONTAINS` would have been an ideal choice for the name but alas, that conflicts with the existing calcite operator.  I can certainly rename them to `CONTAINS_STRING` 
   
   




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r488085625



##########
File path: processing/src/test/java/org/apache/druid/query/expression/ContainsExprMacroTest.java
##########
@@ -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(),

Review comment:
       > In non-sql compatible mode, both of these will be empty instead of null and hence the function would return `true`.
   > In SQL compatible mode, we would see an exception since `null` is not a valid literal.
   
   Thanks for the explanation




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r487065202



##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,221 @@ public void testPad()
     );
   }
 
+  @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'))"),

Review comment:
       Added two tests for the same. Learnt a few things along the way. I was not at all expecting an empty string to be turned to `null`. Do you know why that is done? 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486197897



##########
File path: sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
##########
@@ -1072,6 +1075,108 @@ public void testPad()
     );
   }
 
+  @Test
+  public void testContains()
+  {
+    testHelper.testFilter(
+        ContainsOperatorConversion.createOperatorConversion(true).calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("spacey"),
+            testHelper.makeLiteral("there")
+        ),
+        Collections.emptyList(),
+        new SearchQueryDimFilter("spacey", new ContainsSearchQuerySpec("there", true), null),
+        true
+    );
+
+    testHelper.testFilter(
+        ContainsOperatorConversion.createOperatorConversion(true).calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("spacey"),
+            testHelper.makeLiteral("There")
+        ),
+        Collections.emptyList(),
+        new SearchQueryDimFilter("spacey", new ContainsSearchQuerySpec("There", true), null),
+        false
+    );
+
+    testHelper.testFilter(
+        ContainsOperatorConversion.createOperatorConversion(false).calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("spacey"),
+            testHelper.makeLiteral("There")
+        ),
+        Collections.emptyList(),
+        new SearchQueryDimFilter("spacey", new ContainsSearchQuerySpec("There", false), null),
+        true
+    );
+
+    testHelper.testFilter(
+        ContainsOperatorConversion.createOperatorConversion(true).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.createOperatorConversion(true).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.createOperatorConversion(false).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
+    );
+  }

Review comment:
       I have not added native functions for these methods. Yet. similar discussion - https://github.com/apache/druid/pull/10350/files#r486068975




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] suneet-s commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
suneet-s commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r487319466



##########
File path: processing/src/main/java/org/apache/druid/query/expression/ContainsExpr.java
##########
@@ -0,0 +1,91 @@
+/*
+ * 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.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;

Review comment:
       I think that makes sense. I tried to walk through the possible values of `expr.stringify()` given the fact that it is guaranteed to be a string literal, but it was a little tricky for me to reason about, so keeping it an Expr is cool




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


[GitHub] [druid] abhishekagarwal87 commented on a change in pull request #10350: Support SearchQueryDimFilter in sql via new methods

Posted by GitBox <gi...@apache.org>.
abhishekagarwal87 commented on a change in pull request #10350:
URL: https://github.com/apache/druid/pull/10350#discussion_r486103951



##########
File path: docs/querying/sql.md
##########
@@ -561,6 +561,8 @@ The [DataSketches extension](../development/extensions-core/datasketches-extensi
 |`COALESCE(value1, value2, ...)`|Returns the first value that is neither NULL nor empty string.|
 |`NVL(expr,expr-for-null)`|Returns 'expr-for-null' if 'expr' is null (or empty string for string type).|
 |`BLOOM_FILTER_TEST(<expr>, <serialized-filter>)`|Returns true if the value is contained in a Base64-serialized bloom filter. See the [Bloom filter extension](../development/extensions-core/bloom-filter.html) documentation for additional details.|
+|`CONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`.|
+|`ICONTAINS_STR(<expr>, str)`|Returns true if the `str` is a substring of `expr`. The match is case-insensitive.|

Review comment:
       These are really good points and it's my bad to not put my reasonings in the description earlier.  The description has more details but in short, 
    - `contains` is a standard operator in calcite, built with non-string input types. 
    - The function names are different for case insensitive flavour, to not overload the function signature and follow standard MySQL convention (`like`/`ilike`) 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org