new SCALAR_IN_ARRAY function analogous to DRUID_IN (#16306)

* scalar_in function

* api doc

* refactor
diff --git a/docs/querying/math-expr.md b/docs/querying/math-expr.md
index ee47fc7..d525554 100644
--- a/docs/querying/math-expr.md
+++ b/docs/querying/math-expr.md
@@ -184,6 +184,7 @@
 | array_ordinal(arr,long) | returns the array element at the 1 based index supplied, or null for an out of range index |
 | array_contains(arr,expr) | returns 1 if the array contains the element specified by expr, or contains all elements specified by expr if expr is an array, else 0 |
 | array_overlap(arr1,arr2) | returns 1 if arr1 and arr2 have any elements in common, else 0 |
+| scalar_in_array(expr, arr) | returns 1 if the scalar is present in the array, else 0 |
 | array_offset_of(arr,expr) | returns the 0 based index of the first occurrence of expr in the array, or `null` or `-1` if `druid.generic.useDefaultValueForNull=true` (deprecated legacy mode) if no matching elements exist in the array. |
 | array_ordinal_of(arr,expr) | returns the 1 based index of the first occurrence of expr in the array, or `null` or `-1` if `druid.generic.useDefaultValueForNull=true` (deprecated legacy mode) if no matching elements exist in the array. |
 | array_prepend(expr,arr) | adds expr to arr at the beginning, the resulting array type determined by the type of the array |
diff --git a/docs/querying/sql-array-functions.md b/docs/querying/sql-array-functions.md
index 203b0e0..ab84c66 100644
--- a/docs/querying/sql-array-functions.md
+++ b/docs/querying/sql-array-functions.md
@@ -54,6 +54,7 @@
 |`ARRAY_ORDINAL(arr, long)`|Returns the array element at the 1-based index supplied, or null for an out of range index.|
 |`ARRAY_CONTAINS(arr, expr)`|If `expr` is a scalar type, returns 1 if `arr` contains `expr`. If `expr` is an array, returns 1 if `arr` contains all elements of `expr`. Otherwise returns 0.|
 |`ARRAY_OVERLAP(arr1, arr2)`|Returns 1 if `arr1` and `arr2` have any elements in common, else 0.|
+| `SCALAR_IN_ARRAY(expr, arr)`|Returns 1 if the scalar `expr` is present in `arr`. else 0.|
 |`ARRAY_OFFSET_OF(arr, expr)`|Returns the 0-based index of the first occurrence of `expr` in the array. If no matching elements exist in the array, returns `null` or `-1` if `druid.generic.useDefaultValueForNull=true` (deprecated legacy mode).|
 |`ARRAY_ORDINAL_OF(arr, expr)`|Returns the 1-based index of the first occurrence of `expr` in the array. If no matching elements exist in the array, returns `null` or `-1` if `druid.generic.useDefaultValueForNull=true` (deprecated legacy mode).|
 |`ARRAY_PREPEND(expr, arr)`|Adds `expr` to the beginning of `arr`, the resulting array type determined by the type of `arr`.|
diff --git a/docs/querying/sql-functions.md b/docs/querying/sql-functions.md
index 9a43afa..093e7ce 100644
--- a/docs/querying/sql-functions.md
+++ b/docs/querying/sql-functions.md
@@ -206,6 +206,14 @@
 
 Returns 1 if `arr1` and `arr2` have any elements in common, else 0.|
 
+## SCALAR_IN_ARRAY
+
+`SCALAR_IN_ARRAY(expr, arr)`
+
+**Function type:** [Array](./sql-array-functions.md)
+
+Returns 1 if the scalar `expr` is present in `arr`, else 0.|
+
 ## ARRAY_PREPEND
 
 `ARRAY_PREPEND(expr, arr)`
diff --git a/processing/src/main/java/org/apache/druid/math/expr/Function.java b/processing/src/main/java/org/apache/druid/math/expr/Function.java
index 4c07311..aa54409 100644
--- a/processing/src/main/java/org/apache/druid/math/expr/Function.java
+++ b/processing/src/main/java/org/apache/druid/math/expr/Function.java
@@ -3724,6 +3724,44 @@
     }
   }
 
+  class ArrayScalarInFunction extends ArrayScalarFunction
+  {
+    @Override
+    public String name()
+    {
+      return "scalar_in_array";
+    }
+
+    @Nullable
+    @Override
+    public ExpressionType getOutputType(Expr.InputBindingInspector inspector, List<Expr> args)
+    {
+      return ExpressionType.LONG;
+    }
+
+    @Override
+    Expr getScalarArgument(List<Expr> args)
+    {
+      return args.get(0);
+    }
+
+    @Override
+    Expr getArrayArgument(List<Expr> args)
+    {
+      return args.get(1);
+    }
+
+    @Override
+    ExprEval doApply(ExprEval arrayExpr, ExprEval scalarExpr)
+    {
+      final Object[] array = arrayExpr.castTo(scalarExpr.asArrayType()).asArray();
+      if (array == null) {
+        return ExprEval.ofLong(null);
+      }
+      return ExprEval.ofLongBoolean(Arrays.asList(array).contains(scalarExpr.value()));
+    }
+  }
+
   class ArrayAppendFunction extends ArrayAddElementFunction
   {
     @Override
diff --git a/processing/src/test/java/org/apache/druid/math/expr/FunctionTest.java b/processing/src/test/java/org/apache/druid/math/expr/FunctionTest.java
index 30d549d..da81a55 100644
--- a/processing/src/test/java/org/apache/druid/math/expr/FunctionTest.java
+++ b/processing/src/test/java/org/apache/druid/math/expr/FunctionTest.java
@@ -370,6 +370,18 @@
   }
 
   @Test
+  public void testScalarInArray()
+  {
+    assertExpr("scalar_in_array(2, [1, 2, 3])", 1L);
+    assertExpr("scalar_in_array(4, [1, 2, 3])", 0L);
+    assertExpr("scalar_in_array(b, [3, 4])", 0L);
+    assertExpr("scalar_in_array(1, null)", null);
+    assertExpr("scalar_in_array(null, null)", null);
+    assertExpr("scalar_in_array(null, [1, null, 2])", 1L);
+    assertExpr("scalar_in_array(null, [1, 2])", 0L);
+  }
+
+  @Test
   public void testArrayContains()
   {
     assertExpr("array_contains([1, 2, 3], 2)", 1L);
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ScalarInArrayOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ScalarInArrayOperatorConversion.java
new file mode 100644
index 0000000..f6e3dce
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/ScalarInArrayOperatorConversion.java
@@ -0,0 +1,50 @@
+/*
+ * 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.sql.SqlFunction;
+import org.apache.calcite.sql.type.OperandTypes;
+import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.druid.sql.calcite.expression.DirectOperatorConversion;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
+
+public class ScalarInArrayOperatorConversion extends DirectOperatorConversion
+{
+  private static final SqlFunction SQL_FUNCTION = OperatorConversions
+          .operatorBuilder("SCALAR_IN_ARRAY")
+          .operandTypeChecker(
+                  OperandTypes.sequence(
+                          "'SCALAR_IN_ARRAY(expr, array)'",
+                          OperandTypes.or(
+                                  OperandTypes.family(SqlTypeFamily.CHARACTER),
+                                  OperandTypes.family(SqlTypeFamily.NUMERIC)
+                          ),
+                          OperandTypes.family(SqlTypeFamily.ARRAY)
+                  )
+          )
+          .returnTypeInference(ReturnTypes.BOOLEAN_NULLABLE)
+          .build();
+
+  public ScalarInArrayOperatorConversion()
+  {
+    super(SQL_FUNCTION, "scalar_in_array");
+  }
+}
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
index 25c2b03..723d5f4 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
@@ -110,6 +110,7 @@
 import org.apache.druid.sql.calcite.expression.builtin.ReverseOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.RightOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.SafeDivideOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.ScalarInArrayOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.SearchOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.StringFormatOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.StringToArrayOperatorConversion;
@@ -262,6 +263,7 @@
                    .add(new ArrayToStringOperatorConversion())
                    .add(new StringToArrayOperatorConversion())
                    .add(new ArrayToMultiValueStringOperatorConversion())
+                   .add(new ScalarInArrayOperatorConversion())
                    .build();
 
   private static final List<SqlOperatorConversion> MULTIVALUE_STRING_OPERATOR_CONVERSIONS =
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteArraysQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteArraysQueryTest.java
index 7c388f6..949e6b9 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteArraysQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteArraysQueryTest.java
@@ -1338,6 +1338,73 @@
     );
   }
 
+  @Test
+  public void testScalarInArrayFilter()
+  {
+    msqIncompatible();
+    testQuery(
+            "SELECT dim2 FROM druid.numfoo WHERE SCALAR_IN_ARRAY(dim2, ARRAY['a', 'd']) LIMIT 5",
+            ImmutableList.of(
+                    newScanQueryBuilder()
+                            .dataSource(CalciteTests.DATASOURCE3)
+                            .intervals(querySegmentSpec(Filtration.eternity()))
+                            .filters(
+                                    new ExpressionDimFilter("scalar_in_array(\"dim2\",array('a','d'))", ExprMacroTable.nil())
+                            )
+                            .columns("dim2")
+                            .resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+                            .limit(5)
+                            .context(QUERY_CONTEXT_DEFAULT)
+                            .build()
+            ),
+            ImmutableList.of(
+                    new Object[]{"a"},
+                    new Object[]{"a"}
+            )
+    );
+  }
+
+  @Test
+  public void testArrayScalarInFilter_MVD()
+  {
+    msqIncompatible();
+    testBuilder()
+            .sql(
+                    "SELECT dim3, (CASE WHEN scalar_in_array(dim3, Array['a', 'b', 'd']) THEN 'abd' ELSE 'not abd' END) " +
+                            "FROM druid.numfoo"
+            )
+            .expectedQueries(
+                    ImmutableList.of(
+                            newScanQueryBuilder()
+                                    .dataSource(CalciteTests.DATASOURCE3)
+                                    .intervals(querySegmentSpec(Filtration.eternity()))
+                                    .virtualColumns(
+                                            new ExpressionVirtualColumn(
+                                                    "v0",
+                                                    "case_searched(scalar_in_array(\"dim3\",array('a','b','d')),'abd','not abd')",
+                                                    ColumnType.STRING,
+                                                    ExprMacroTable.nil()
+                                            )
+                                    )
+                                    .columns("dim3", "v0")
+                                    .resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+                                    .context(QUERY_CONTEXT_DEFAULT)
+                                    .build()
+                    )
+            )
+            .expectedResults(ResultMatchMode.RELAX_NULLS,
+                    ImmutableList.of(
+                            new Object[]{"[\"a\",\"b\"]", "[\"abd\",\"abd\"]"},
+                            new Object[]{"[\"b\",\"c\"]", "[\"abd\",\"not abd\"]"},
+                            new Object[]{"d", "abd"},
+                            new Object[]{"", "not abd"},
+                            new Object[]{null, "not abd"},
+                            new Object[]{null, "not abd"}
+                    )
+            )
+            .run();
+
+  }
 
   @Test
   public void testArraySlice()
diff --git a/website/.spelling b/website/.spelling
index 3ceea92..468c6f9 100644
--- a/website/.spelling
+++ b/website/.spelling
@@ -1508,6 +1508,7 @@
 array_prepend
 array_slice
 array_to_string
+scalar_in_array
 asin
 atan
 atan2