This is an automated email from the ASF dual-hosted git repository.

cloud-fan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new cc75de63a456 [SPARK-58831][SQL] Add bitmap scalar set operation 
functions
cc75de63a456 is described below

commit cc75de63a45694f8e70c60cd3387ddf8a83cf80b
Author: StormSpirit <[email protected]>
AuthorDate: Fri Aug 21 10:33:58 2026 +0800

    [SPARK-58831][SQL] Add bitmap scalar set operation functions
    
    ### What changes were proposed in this pull request?
    
    This PR adds four scalar set operations for Spark's flat bitmap 
representation:
    
    - `bitmap_and`
    - `bitmap_or`
    - `bitmap_andnot`
    - `bitmap_xor`
    
    Each function operates on two `BINARY` values from the same row. The 
implementation:
    
    - accepts inputs up to 4096 bytes;
    - treats missing trailing bytes as zero;
    - returns a newly allocated, fixed-size 4096-byte bitmap;
    - propagates `NULL` inputs;
    - rejects oversized inputs with the structured `BITMAP_INPUT_TOO_LARGE` 
error;
    - does not mutate either input.
    
    The functions are implemented as Catalyst-native binary expressions with 
both interpreted and whole-stage codegen paths. They are registered in the SQL 
function registry and exposed through the Scala API, PySpark classic, and Spark 
Connect.
    
    This PR also adds SQL documentation, Python documentation, a structured 
error definition, Connect plan and schema golden files, and tests for the new 
APIs.
    
    ### Why are the changes needed?
    
    Spark provides functions for constructing, aggregating, and counting flat 
bitmaps, but it does not provide scalar set operations for combining two bitmap 
values from the same row.
    
    Without native scalar functions, users need UDFs or external bitmap 
processing to compute intersection, union, difference, or symmetric difference 
between precomputed bitmap columns. Native Catalyst expressions provide 
consistent SQL semantics, structured validation, and code generation, and allow 
these operations to compose naturally with the existing bitmap aggregate and 
count functions.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. It adds four new SQL, Scala, PySpark classic, and Spark Connect 
functions:
    
    ```sql
    SELECT bitmap_and(left_bitmap, right_bitmap);
    SELECT bitmap_or(left_bitmap, right_bitmap);
    SELECT bitmap_andnot(left_bitmap, right_bitmap);
    SELECT bitmap_xor(left_bitmap, right_bitmap);
    ```
    
    `bitmap_andnot(left, right)` is directional and returns the bits present in 
`left` but not in `right`.
    
    No existing function behavior is changed.
    
    ### How was this patch tested?
    
    The following targeted suites and build checks passed:
    
    - `build/sbt "catalyst/testOnly 
org.apache.spark.sql.catalyst.expressions.BitmapExpressionUtilsSuite"` (10 
tests)
    - `build/sbt "sql/testOnly 
org.apache.spark.sql.BitmapExpressionsQuerySuite"` (19 tests, including 
interpreted and codegen execution)
    - `build/sbt "sql/testOnly org.apache.spark.sql.ExpressionsSchemaSuite"`
    - `build/sbt "connect-client-jvm/testOnly 
org.apache.spark.sql.PlanGenerationTestSuite -- -z 'function bitmap'"` (9 tests)
    - `build/sbt -Phive package`
    - `python/run-tests --testnames "pyspark.sql.tests.test_functions 
FunctionsTests.test_bitmap_scalar_functions"`
    - `python/run-tests --testnames 
"pyspark.sql.tests.connect.test_connect_plan 
SparkConnectPlanTests.test_bitmap_scalar_functions"`
    - Targeted PySpark doctests for the four new functions (12 tests)
    
    The tests cover variable-length and empty inputs, the 4096-byte boundary, 
input immutability, null propagation, invalid types, oversized inputs on either 
side, interpreted and codegen execution, aggregation composition, grouped 
queries, PySpark classic, and Spark Connect.
    
    Scalafmt, Ruff, JSON validation, golden-file consistency checks, and `git 
diff --check` also passed.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Codex and Claude AI
    
    Closes #58066 from jiangxt2/feat/bitmap-scalar-set-operations.
    
    Authored-by: StormSpirit <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../src/main/resources/error/error-conditions.json |   6 +
 .../source/reference/pyspark.sql/functions.rst     |   4 +
 python/pyspark/sql/connect/functions/builtin.py    |  28 +++
 python/pyspark/sql/functions/__init__.py           |   4 +
 python/pyspark/sql/functions/builtin.py            | 188 ++++++++++++++++++
 .../pyspark/sql/tests/connect/test_connect_plan.py |  26 ++-
 python/pyspark/sql/tests/test_functions.py         |  12 ++
 .../scala/org/apache/spark/sql/functions.scala     |  85 ++++++++
 .../expressions/BitmapExpressionUtils.java         |  43 ++++
 .../sql/catalyst/analysis/FunctionRegistry.scala   |   4 +
 .../catalyst/expressions/bitmapExpressions.scala   | 217 +++++++++++++++++++++
 .../spark/sql/errors/QueryExecutionErrors.scala    |   9 +
 .../expressions/BitmapExpressionUtilsSuite.scala   |  91 +++++++++
 .../apache/spark/sql/PlanGenerationTestSuite.scala |  16 ++
 .../explain-results/function_bitmap_and.explain    |   2 +
 .../explain-results/function_bitmap_andnot.explain |   2 +
 .../explain-results/function_bitmap_or.explain     |   2 +
 .../explain-results/function_bitmap_xor.explain    |   2 +
 .../query-tests/queries/function_bitmap_and.json   |  81 ++++++++
 .../queries/function_bitmap_and.proto.bin          | Bin 0 -> 629 bytes
 .../queries/function_bitmap_andnot.json            |  81 ++++++++
 .../queries/function_bitmap_andnot.proto.bin       | Bin 0 -> 635 bytes
 .../query-tests/queries/function_bitmap_or.json    |  81 ++++++++
 .../queries/function_bitmap_or.proto.bin           | Bin 0 -> 627 bytes
 .../query-tests/queries/function_bitmap_xor.json   |  81 ++++++++
 .../queries/function_bitmap_xor.proto.bin          | Bin 0 -> 629 bytes
 .../sql-functions/sql-expression-schema.md         |   4 +
 .../spark/sql/BitmapExpressionsQuerySuite.scala    | 168 ++++++++++++++++
 28 files changed, 1236 insertions(+), 1 deletion(-)

diff --git a/common/utils/src/main/resources/error/error-conditions.json 
b/common/utils/src/main/resources/error/error-conditions.json
index 59da38fdc8dd..df1a1f3a5b8f 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -489,6 +489,12 @@
     ],
     "sqlState" : "0A000"
   },
+  "BITMAP_INPUT_TOO_LARGE" : {
+    "message" : [
+      "The input bitmap has <inputNumBytes> bytes, which exceeds the maximum 
supported size of <maxNumBytes> bytes."
+    ],
+    "sqlState" : "22001"
+  },
   "CALL_ON_STREAMING_DATASET_UNSUPPORTED" : {
     "message" : [
       "The method <methodName> can not be called on streaming 
Dataset/DataFrame."
diff --git a/python/docs/source/reference/pyspark.sql/functions.rst 
b/python/docs/source/reference/pyspark.sql/functions.rst
index 661fc086c4e5..031c48734c49 100644
--- a/python/docs/source/reference/pyspark.sql/functions.rst
+++ b/python/docs/source/reference/pyspark.sql/functions.rst
@@ -665,9 +665,13 @@ Misc Functions
     aes_decrypt
     aes_encrypt
     assert_true
+    bitmap_and
+    bitmap_andnot
     bitmap_bit_position
     bitmap_bucket_number
     bitmap_count
+    bitmap_or
+    bitmap_xor
     current_catalog
     current_database
     current_path
diff --git a/python/pyspark/sql/connect/functions/builtin.py 
b/python/pyspark/sql/connect/functions/builtin.py
index 2e696f344b12..9d622925bf19 100644
--- a/python/pyspark/sql/connect/functions/builtin.py
+++ b/python/pyspark/sql/connect/functions/builtin.py
@@ -5663,6 +5663,34 @@ def bitmap_count(col: "ColumnOrName") -> Column:
 bitmap_count.__doc__ = pysparkfuncs.bitmap_count.__doc__
 
 
+def bitmap_and(left: "ColumnOrName", right: "ColumnOrName") -> Column:
+    return _invoke_function_over_columns("bitmap_and", left, right)
+
+
+bitmap_and.__doc__ = pysparkfuncs.bitmap_and.__doc__
+
+
+def bitmap_or(left: "ColumnOrName", right: "ColumnOrName") -> Column:
+    return _invoke_function_over_columns("bitmap_or", left, right)
+
+
+bitmap_or.__doc__ = pysparkfuncs.bitmap_or.__doc__
+
+
+def bitmap_andnot(left: "ColumnOrName", right: "ColumnOrName") -> Column:
+    return _invoke_function_over_columns("bitmap_andnot", left, right)
+
+
+bitmap_andnot.__doc__ = pysparkfuncs.bitmap_andnot.__doc__
+
+
+def bitmap_xor(left: "ColumnOrName", right: "ColumnOrName") -> Column:
+    return _invoke_function_over_columns("bitmap_xor", left, right)
+
+
+bitmap_xor.__doc__ = pysparkfuncs.bitmap_xor.__doc__
+
+
 def bitmap_or_agg(col: "ColumnOrName") -> Column:
     return _invoke_function_over_columns("bitmap_or_agg", col)
 
diff --git a/python/pyspark/sql/functions/__init__.py 
b/python/pyspark/sql/functions/__init__.py
index a3c0b7016942..462509a5d7a2 100644
--- a/python/pyspark/sql/functions/__init__.py
+++ b/python/pyspark/sql/functions/__init__.py
@@ -526,9 +526,13 @@ __all__ = [  # noqa: F405
     "aes_decrypt",
     "aes_encrypt",
     "assert_true",
+    "bitmap_and",
+    "bitmap_andnot",
     "bitmap_bit_position",
     "bitmap_bucket_number",
     "bitmap_count",
+    "bitmap_or",
+    "bitmap_xor",
     "current_catalog",
     "current_database",
     "current_path",
diff --git a/python/pyspark/sql/functions/builtin.py 
b/python/pyspark/sql/functions/builtin.py
index 1d2c6355e9e2..5acb3301be4d 100644
--- a/python/pyspark/sql/functions/builtin.py
+++ b/python/pyspark/sql/functions/builtin.py
@@ -33043,6 +33043,194 @@ def bitmap_count(col: "ColumnOrName") -> Column:
     return _invoke_function_over_columns("bitmap_count", col)
 
 
+@_try_remote_functions
+def bitmap_and(left: "ColumnOrName", right: "ColumnOrName") -> Column:
+    """
+    Returns a bitmap that is the bitwise AND of two input bitmaps.
+
+    .. versionadded:: 4.4.0
+
+    Parameters
+    ----------
+    left : :class:`~pyspark.sql.Column` or column name
+        The left input bitmap.
+    right : :class:`~pyspark.sql.Column` or column name
+        The right input bitmap.
+
+    See Also
+    --------
+    :meth:`pyspark.sql.functions.bitmap_andnot`
+    :meth:`pyspark.sql.functions.bitmap_or`
+    :meth:`pyspark.sql.functions.bitmap_xor`
+
+    Notes
+    -----
+    Inputs use Spark's Binary bitmap representation, not a RoaringBitmap 
serialization. Each
+    input may contain 0 to 4096 bytes; missing bytes are treated as zero. The 
result is always a
+    4096-byte Binary value. NULL input returns NULL, and inputs longer than 
4096 bytes raise
+    ``BITMAP_INPUT_TOO_LARGE``. Both inputs must use the same bit-position 
mapping. If they were
+    constructed by grouping ``bitmap_bit_position`` values by 
``bitmap_bucket_number``, they must
+    represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+    function combines two bitmaps from the same row; use ``bitmap_*_agg`` to 
combine bitmaps across
+    rows.
+
+    Examples
+    --------
+    >>> from pyspark.sql import functions as sf
+    >>> df = spark.createDataFrame([("F0", "70")], ["left", "right"])
+    >>> df.select(sf.bitmap_and(
+    ...     sf.to_binary("left", sf.lit("hex")),
+    ...     sf.to_binary("right", sf.lit("hex"))).alias("bitmap")).show()
+    +--------------------+
+    |              bitmap|
+    +--------------------+
+    |[70 00 00 00 00 0...|
+    +--------------------+
+    """
+    return _invoke_function_over_columns("bitmap_and", left, right)
+
+
+@_try_remote_functions
+def bitmap_or(left: "ColumnOrName", right: "ColumnOrName") -> Column:
+    """
+    Returns a bitmap that is the bitwise OR of two input bitmaps.
+
+    .. versionadded:: 4.4.0
+
+    Parameters
+    ----------
+    left : :class:`~pyspark.sql.Column` or column name
+        The left input bitmap.
+    right : :class:`~pyspark.sql.Column` or column name
+        The right input bitmap.
+
+    See Also
+    --------
+    :meth:`pyspark.sql.functions.bitmap_and`
+    :meth:`pyspark.sql.functions.bitmap_andnot`
+    :meth:`pyspark.sql.functions.bitmap_xor`
+
+    Notes
+    -----
+    Inputs use Spark's Binary bitmap representation, not a RoaringBitmap 
serialization. Each
+    input may contain 0 to 4096 bytes; missing bytes are treated as zero. The 
result is always a
+    4096-byte Binary value. NULL input returns NULL, and inputs longer than 
4096 bytes raise
+    ``BITMAP_INPUT_TOO_LARGE``. Both inputs must use the same bit-position 
mapping. If they were
+    constructed by grouping ``bitmap_bit_position`` values by 
``bitmap_bucket_number``, they must
+    represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+    function combines two bitmaps from the same row; use ``bitmap_*_agg`` to 
combine bitmaps across
+    rows.
+
+    Examples
+    --------
+    >>> from pyspark.sql import functions as sf
+    >>> df = spark.createDataFrame([("10", "20")], ["left", "right"])
+    >>> df.select(sf.bitmap_or(
+    ...     sf.to_binary("left", sf.lit("hex")),
+    ...     sf.to_binary("right", sf.lit("hex"))).alias("bitmap")).show()
+    +--------------------+
+    |              bitmap|
+    +--------------------+
+    |[30 00 00 00 00 0...|
+    +--------------------+
+    """
+    return _invoke_function_over_columns("bitmap_or", left, right)
+
+
+@_try_remote_functions
+def bitmap_andnot(left: "ColumnOrName", right: "ColumnOrName") -> Column:
+    """
+    Returns a bitmap that is the bitwise AND NOT of two input bitmaps.
+
+    .. versionadded:: 4.4.0
+
+    Parameters
+    ----------
+    left : :class:`~pyspark.sql.Column` or column name
+        The left input bitmap.
+    right : :class:`~pyspark.sql.Column` or column name
+        The right input bitmap.
+
+    See Also
+    --------
+    :meth:`pyspark.sql.functions.bitmap_and`
+    :meth:`pyspark.sql.functions.bitmap_or`
+    :meth:`pyspark.sql.functions.bitmap_xor`
+
+    Notes
+    -----
+    Inputs use Spark's Binary bitmap representation, not a RoaringBitmap 
serialization. Each
+    input may contain 0 to 4096 bytes; missing bytes are treated as zero. The 
result is always a
+    4096-byte Binary value. NULL input returns NULL, and inputs longer than 
4096 bytes raise
+    ``BITMAP_INPUT_TOO_LARGE``. Both inputs must use the same bit-position 
mapping. If they were
+    constructed by grouping ``bitmap_bit_position`` values by 
``bitmap_bucket_number``, they must
+    represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+    function combines two bitmaps from the same row; use ``bitmap_*_agg`` to 
combine bitmaps across
+    rows.
+
+    Examples
+    --------
+    >>> from pyspark.sql import functions as sf
+    >>> df = spark.createDataFrame([("F0", "70")], ["left", "right"])
+    >>> df.select(sf.bitmap_andnot(
+    ...     sf.to_binary("left", sf.lit("hex")),
+    ...     sf.to_binary("right", sf.lit("hex"))).alias("bitmap")).show()
+    +--------------------+
+    |              bitmap|
+    +--------------------+
+    |[80 00 00 00 00 0...|
+    +--------------------+
+    """
+    return _invoke_function_over_columns("bitmap_andnot", left, right)
+
+
+@_try_remote_functions
+def bitmap_xor(left: "ColumnOrName", right: "ColumnOrName") -> Column:
+    """
+    Returns a bitmap that is the bitwise XOR of two input bitmaps.
+
+    .. versionadded:: 4.4.0
+
+    Parameters
+    ----------
+    left : :class:`~pyspark.sql.Column` or column name
+        The left input bitmap.
+    right : :class:`~pyspark.sql.Column` or column name
+        The right input bitmap.
+
+    See Also
+    --------
+    :meth:`pyspark.sql.functions.bitmap_and`
+    :meth:`pyspark.sql.functions.bitmap_andnot`
+    :meth:`pyspark.sql.functions.bitmap_or`
+
+    Notes
+    -----
+    Inputs use Spark's Binary bitmap representation, not a RoaringBitmap 
serialization. Each
+    input may contain 0 to 4096 bytes; missing bytes are treated as zero. The 
result is always a
+    4096-byte Binary value. NULL input returns NULL, and inputs longer than 
4096 bytes raise
+    ``BITMAP_INPUT_TOO_LARGE``. Both inputs must use the same bit-position 
mapping. If they were
+    constructed by grouping ``bitmap_bit_position`` values by 
``bitmap_bucket_number``, they must
+    represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+    function combines two bitmaps from the same row; use ``bitmap_*_agg`` to 
combine bitmaps across
+    rows.
+
+    Examples
+    --------
+    >>> from pyspark.sql import functions as sf
+    >>> df = spark.createDataFrame([("F0", "70")], ["left", "right"])
+    >>> df.select(sf.bitmap_xor(
+    ...     sf.to_binary("left", sf.lit("hex")),
+    ...     sf.to_binary("right", sf.lit("hex"))).alias("bitmap")).show()
+    +--------------------+
+    |              bitmap|
+    +--------------------+
+    |[80 00 00 00 00 0...|
+    +--------------------+
+    """
+    return _invoke_function_over_columns("bitmap_xor", left, right)
+
+
 @_try_remote_functions
 def bitmap_or_agg(col: "ColumnOrName") -> Column:
     """
diff --git a/python/pyspark/sql/tests/connect/test_connect_plan.py 
b/python/pyspark/sql/tests/connect/test_connect_plan.py
index 5105a58b400b..d1f09c37cf93 100644
--- a/python/pyspark/sql/tests/connect/test_connect_plan.py
+++ b/python/pyspark/sql/tests/connect/test_connect_plan.py
@@ -44,7 +44,17 @@ if should_test_connect:
     from pyspark.sql.connect.observation import Observation
     from pyspark.sql.connect.readwriter import DataFrameReader
     from pyspark.sql.connect.expressions import LiteralExpression
-    from pyspark.sql.connect.functions import col, lit, max, min, sum
+    from pyspark.sql.connect.functions import (
+        bitmap_and,
+        bitmap_andnot,
+        bitmap_or,
+        bitmap_xor,
+        col,
+        lit,
+        max,
+        min,
+        sum,
+    )
     from pyspark.sql.connect.types import pyspark_types_to_proto_types
     from pyspark.sql.types import (
         StringType,
@@ -71,6 +81,20 @@ class SparkConnectPlanTests(PlanOnlyTestFixture):
         self.assertIsNotNone(plan.root, "Root relation must be set")
         self.assertIsNotNone(plan.root.read)
 
+    def test_bitmap_scalar_functions(self):
+        df = self.connect.readTable(table_name=self.tbl_name)
+        plan = df.select(
+            bitmap_and(col("bytes"), col("bytes")),
+            bitmap_or(col("bytes"), col("bytes")),
+            bitmap_andnot(col("bytes"), col("bytes")),
+            bitmap_xor(col("bytes"), col("bytes")),
+        )._plan.to_proto(self.connect)
+        function_names = [
+            expression.unresolved_function.function_name
+            for expression in plan.root.project.expressions
+        ]
+        self.assertEqual(function_names, ["bitmap_and", "bitmap_or", 
"bitmap_andnot", "bitmap_xor"])
+
     def test_join_using_columns(self):
         left_input = self.connect.readTable(table_name=self.tbl_name)
         right_input = self.connect.readTable(table_name=self.tbl_name)
diff --git a/python/pyspark/sql/tests/test_functions.py 
b/python/pyspark/sql/tests/test_functions.py
index da8c13537ab8..cc7463d87a16 100644
--- a/python/pyspark/sql/tests/test_functions.py
+++ b/python/pyspark/sql/tests/test_functions.py
@@ -938,6 +938,18 @@ class FunctionsTestsMixin:
                 df.select(getattr(F, name)(F.col("name"))),
             )
 
+    def test_bitmap_scalar_functions(self):
+        df = self.spark.createDataFrame([("F00F", "70")], ["left", "right"])
+        left = F.to_binary("left", F.lit("hex"))
+        right = F.to_binary("right", F.lit("hex"))
+        actual = df.select(
+            F.substring(F.hex(F.bitmap_and(left, right)), 0, 4),
+            F.substring(F.hex(F.bitmap_or(left, right)), 0, 4),
+            F.substring(F.hex(F.bitmap_andnot(left, right)), 0, 4),
+            F.substring(F.hex(F.bitmap_xor(left, right)), 0, 4),
+        )
+        assertDataFrameEqual([Row("7000", "F00F", "800F", "800F")], actual)
+
     def test_collation(self):
         df = self.spark.createDataFrame([("a",), ("b",)], ["name"])
         actual = df.select(F.collation(F.collate("name", 
"UNICODE"))).distinct()
diff --git a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala 
b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
index 3ac36551b0ed..9c79e1117332 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
@@ -7338,6 +7338,91 @@ object functions {
    */
   def bitmap_count(col: Column): Column = Column.fn("bitmap_count", col)
 
+  /**
+   * Returns a bitmap that is the bitwise AND of two input bitmaps. The result 
is always a
+   * 4096-byte Spark Binary bitmap. If either input is NULL, the result is 
NULL. Missing bytes in
+   * shorter inputs are treated as zero, and inputs longer than 4096 bytes 
raise
+   * `BITMAP_INPUT_TOO_LARGE`. Both inputs must use the same bit-position 
mapping. If they were
+   * constructed by grouping `bitmap_bit_position` values by 
`bitmap_bucket_number`, they must
+   * represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+   * function operates on two bitmaps from the same row; use `bitmap_*_agg` to 
combine bitmaps
+   * across rows. The representation is not a RoaringBitmap serialization.
+   *
+   * @param left
+   *   A column that evaluates to a binary bitmap.
+   * @param right
+   *   A column that evaluates to a binary bitmap.
+   * @group misc_funcs
+   * @since 4.4.0
+   * @return
+   *   Returns a column that evaluates to a binary bitmap.
+   */
+  def bitmap_and(left: Column, right: Column): Column = 
Column.fn("bitmap_and", left, right)
+
+  /**
+   * Returns a bitmap that is the bitwise OR of two input bitmaps. The result 
is always a
+   * 4096-byte Spark Binary bitmap. If either input is NULL, the result is 
NULL. Missing bytes in
+   * shorter inputs are treated as zero, and inputs longer than 4096 bytes 
raise
+   * `BITMAP_INPUT_TOO_LARGE`. Both inputs must use the same bit-position 
mapping. If they were
+   * constructed by grouping `bitmap_bit_position` values by 
`bitmap_bucket_number`, they must
+   * represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+   * function operates on two bitmaps from the same row; use `bitmap_*_agg` to 
combine bitmaps
+   * across rows. The representation is not a RoaringBitmap serialization.
+   *
+   * @param left
+   *   A column that evaluates to a binary bitmap.
+   * @param right
+   *   A column that evaluates to a binary bitmap.
+   * @group misc_funcs
+   * @since 4.4.0
+   * @return
+   *   Returns a column that evaluates to a binary bitmap.
+   */
+  def bitmap_or(left: Column, right: Column): Column = Column.fn("bitmap_or", 
left, right)
+
+  /**
+   * Returns a bitmap that is the bitwise AND NOT of two input bitmaps. The 
result is always a
+   * 4096-byte Spark Binary bitmap. If either input is NULL, the result is 
NULL. Missing bytes in
+   * shorter inputs are treated as zero, and inputs longer than 4096 bytes 
raise
+   * `BITMAP_INPUT_TOO_LARGE`. Both inputs must use the same bit-position 
mapping. If they were
+   * constructed by grouping `bitmap_bit_position` values by 
`bitmap_bucket_number`, they must
+   * represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+   * function operates on two bitmaps from the same row; use `bitmap_*_agg` to 
combine bitmaps
+   * across rows. The representation is not a RoaringBitmap serialization.
+   *
+   * @param left
+   *   A column that evaluates to a binary bitmap.
+   * @param right
+   *   A column that evaluates to a binary bitmap.
+   * @group misc_funcs
+   * @since 4.4.0
+   * @return
+   *   Returns a column that evaluates to a binary bitmap.
+   */
+  def bitmap_andnot(left: Column, right: Column): Column =
+    Column.fn("bitmap_andnot", left, right)
+
+  /**
+   * Returns a bitmap that is the bitwise XOR of two input bitmaps. The result 
is always a
+   * 4096-byte Spark Binary bitmap. If either input is NULL, the result is 
NULL. Missing bytes in
+   * shorter inputs are treated as zero, and inputs longer than 4096 bytes 
raise
+   * `BITMAP_INPUT_TOO_LARGE`. Both inputs must use the same bit-position 
mapping. If they were
+   * constructed by grouping `bitmap_bit_position` values by 
`bitmap_bucket_number`, they must
+   * represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+   * function operates on two bitmaps from the same row; use `bitmap_*_agg` to 
combine bitmaps
+   * across rows. The representation is not a RoaringBitmap serialization.
+   *
+   * @param left
+   *   A column that evaluates to a binary bitmap.
+   * @param right
+   *   A column that evaluates to a binary bitmap.
+   * @group misc_funcs
+   * @since 4.4.0
+   * @return
+   *   Returns a column that evaluates to a binary bitmap.
+   */
+  def bitmap_xor(left: Column, right: Column): Column = 
Column.fn("bitmap_xor", left, right)
+
   /**
    * Returns a bitmap that is the bitwise OR of all of the bitmaps from the 
input column. The
    * input column should be bitmaps created from bitmap_construct_agg().
diff --git 
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/BitmapExpressionUtils.java
 
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/BitmapExpressionUtils.java
index d03db7d952a5..714a3c28b476 100644
--- 
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/BitmapExpressionUtils.java
+++ 
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/BitmapExpressionUtils.java
@@ -78,4 +78,47 @@ public class BitmapExpressionUtils {
       bitmap1[i] = 0;
     }
   }
+
+  /** Performs bitwise AND on both bitmaps and returns a new fixed-size 
bitmap. */
+  public static byte[] bitmapAnd(byte[] bitmap1, byte[] bitmap2) {
+    byte[] result = new byte[NUM_BYTES];
+    int numBytes = java.lang.Math.min(
+        NUM_BYTES, java.lang.Math.min(bitmap1.length, bitmap2.length));
+    for (int i = 0; i < numBytes; ++i) {
+      result[i] = (byte) ((bitmap1[i] & 0x0FF) & (bitmap2[i] & 0x0FF));
+    }
+    return result;
+  }
+
+  /** Performs bitwise OR on both bitmaps and returns a new fixed-size bitmap. 
*/
+  public static byte[] bitmapOr(byte[] bitmap1, byte[] bitmap2) {
+    byte[] longer = bitmap1.length >= bitmap2.length ? bitmap1 : bitmap2;
+    byte[] shorter = bitmap1.length >= bitmap2.length ? bitmap2 : bitmap1;
+    byte[] result = java.util.Arrays.copyOf(longer, NUM_BYTES);
+    bitmapMerge(result, shorter);
+    return result;
+  }
+
+  /** Performs bitwise AND NOT on both bitmaps and returns a new fixed-size 
bitmap. */
+  public static byte[] bitmapAndNot(byte[] bitmap1, byte[] bitmap2) {
+    byte[] result = java.util.Arrays.copyOf(bitmap1, NUM_BYTES);
+    int numBytes = java.lang.Math.min(
+        NUM_BYTES, java.lang.Math.min(bitmap1.length, bitmap2.length));
+    for (int i = 0; i < numBytes; ++i) {
+      result[i] = (byte) ((result[i] & 0x0FF) & ~(bitmap2[i] & 0x0FF));
+    }
+    return result;
+  }
+
+  /** Performs bitwise XOR on both bitmaps and returns a new fixed-size 
bitmap. */
+  public static byte[] bitmapXor(byte[] bitmap1, byte[] bitmap2) {
+    byte[] longer = bitmap1.length >= bitmap2.length ? bitmap1 : bitmap2;
+    byte[] shorter = bitmap1.length >= bitmap2.length ? bitmap2 : bitmap1;
+    byte[] result = java.util.Arrays.copyOf(longer, NUM_BYTES);
+    int numBytes = java.lang.Math.min(NUM_BYTES, shorter.length);
+    for (int i = 0; i < numBytes; ++i) {
+      result[i] = (byte) ((result[i] & 0x0FF) ^ (shorter[i] & 0x0FF));
+    }
+    return result;
+  }
 }
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
index a23e70188622..dd70ea9d177a 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
@@ -974,6 +974,10 @@ object FunctionRegistry {
     expression[BitmapBitPosition]("bitmap_bit_position"),
     expression[BitmapConstructAgg]("bitmap_construct_agg"),
     expression[BitmapCount]("bitmap_count"),
+    expression[BitmapAnd]("bitmap_and"),
+    expression[BitmapOr]("bitmap_or"),
+    expression[BitmapAndNot]("bitmap_andnot"),
+    expression[BitmapXor]("bitmap_xor"),
     expression[BitmapOrAgg]("bitmap_or_agg"),
     expression[BitmapAndAgg]("bitmap_and_agg"),
     expression[BitmapXorAgg]("bitmap_xor_agg"),
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/bitmapExpressions.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/bitmapExpressions.scala
index 02576f051979..806d8bf51010 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/bitmapExpressions.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/bitmapExpressions.scala
@@ -21,6 +21,7 @@ import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.catalyst.analysis.TypeCheckResult
 import 
org.apache.spark.sql.catalyst.analysis.TypeCheckResult.{DataTypeMismatch, 
TypeCheckSuccess}
 import org.apache.spark.sql.catalyst.expressions.aggregate.ImperativeAggregate
+import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, 
ExprCode}
 import org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke
 import org.apache.spark.sql.catalyst.trees.UnaryLike
 import org.apache.spark.sql.catalyst.types.DataTypeUtils
@@ -158,6 +159,222 @@ case class BitmapCount(child: Expression)
     copy(child = newChild)
 }
 
+/** Base class for scalar bitmap binary operations. */
+abstract class BitmapBinaryExpression extends BinaryExpression with 
ExpectsInputTypes {
+
+  override def inputTypes: Seq[AbstractDataType] = Seq(BinaryType, BinaryType)
+
+  override def dataType: DataType = BinaryType
+
+  override def nullIntolerant: Boolean = true
+
+  protected def applyOperation(bitmap1: Array[Byte], bitmap2: Array[Byte]): 
Array[Byte]
+
+  protected def genCodeOperation(
+      bitmapUtils: String, bitmap1: String, bitmap2: String): String
+
+  private def checkBitmapLength(bitmap: Array[Byte]): Unit = {
+    if (bitmap.length > BitmapExpressionUtils.NUM_BYTES) {
+      throw QueryExecutionErrors.bitmapInputTooLargeError(
+        bitmap.length, BitmapExpressionUtils.NUM_BYTES)
+    }
+  }
+
+  override protected def nullSafeEval(input1: Any, input2: Any): Any = {
+    val bitmap1 = input1.asInstanceOf[Array[Byte]]
+    val bitmap2 = input2.asInstanceOf[Array[Byte]]
+    checkBitmapLength(bitmap1)
+    checkBitmapLength(bitmap2)
+    applyOperation(bitmap1, bitmap2)
+  }
+
+  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): 
ExprCode = {
+    val bitmapUtils = classOf[BitmapExpressionUtils].getName
+    val errors = QueryExecutionErrors.getClass.getName.stripSuffix("$")
+    nullSafeCodeGen(ctx, ev, (bitmap1, bitmap2) => {
+      s"""
+         |if ($bitmap1.length > ${BitmapExpressionUtils.NUM_BYTES}) {
+         |  throw $errors.bitmapInputTooLargeError(
+         |    $bitmap1.length, ${BitmapExpressionUtils.NUM_BYTES});
+         |}
+         |if ($bitmap2.length > ${BitmapExpressionUtils.NUM_BYTES}) {
+         |  throw $errors.bitmapInputTooLargeError(
+         |    $bitmap2.length, ${BitmapExpressionUtils.NUM_BYTES});
+         |}
+         |${ev.value} = ${genCodeOperation(bitmapUtils, bitmap1, bitmap2)};
+         |""".stripMargin
+    })
+  }
+}
+
+@ExpressionDescription(
+  usage = "_FUNC_(left, right) - Returns a bitmap that is the bitwise AND of 
two input bitmaps.",
+  arguments = """
+    Arguments:
+      * left - A binary bitmap.
+      * right - A binary bitmap.
+  """,
+  examples = """
+    Examples:
+      > SELECT substring(hex(_FUNC_(X 'F0', X '70')), 0, 2);
+       70
+  """,
+  note = """
+    Inputs use Spark's Binary bitmap representation, not a RoaringBitmap 
serialization. Each
+    input may contain 0 to 4096 bytes; missing bytes are treated as zero. The 
result is always a
+    4096-byte Binary value. NULL input returns NULL, and inputs longer than 
4096 bytes raise
+    BITMAP_INPUT_TOO_LARGE. Both inputs must use the same bit-position 
mapping. If they were
+    constructed by grouping bitmap_bit_position values by 
bitmap_bucket_number, they must
+    represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+    function combines two bitmaps from the same row; use bitmap_*_agg to 
combine bitmaps across
+    rows.
+  """,
+  since = "4.4.0",
+  group = "misc_funcs"
+)
+case class BitmapAnd(left: Expression, right: Expression) extends 
BitmapBinaryExpression {
+
+  override def prettyName: String = "bitmap_and"
+
+  override protected def applyOperation(
+      bitmap1: Array[Byte], bitmap2: Array[Byte]): Array[Byte] =
+    BitmapExpressionUtils.bitmapAnd(bitmap1, bitmap2)
+
+  override protected def genCodeOperation(
+      bitmapUtils: String, bitmap1: String, bitmap2: String): String =
+    s"$bitmapUtils.bitmapAnd($bitmap1, $bitmap2)"
+
+  override protected def withNewChildrenInternal(
+      newLeft: Expression, newRight: Expression): BitmapAnd =
+    copy(left = newLeft, right = newRight)
+}
+
+@ExpressionDescription(
+  usage = "_FUNC_(left, right) - Returns a bitmap that is the bitwise OR of 
two input bitmaps.",
+  arguments = """
+    Arguments:
+      * left - A binary bitmap.
+      * right - A binary bitmap.
+  """,
+  examples = """
+    Examples:
+      > SELECT substring(hex(_FUNC_(X '10', X '20')), 0, 2);
+       30
+  """,
+  note = """
+    Inputs use Spark's Binary bitmap representation, not a RoaringBitmap 
serialization. Each
+    input may contain 0 to 4096 bytes; missing bytes are treated as zero. The 
result is always a
+    4096-byte Binary value. NULL input returns NULL, and inputs longer than 
4096 bytes raise
+    BITMAP_INPUT_TOO_LARGE. Both inputs must use the same bit-position 
mapping. If they were
+    constructed by grouping bitmap_bit_position values by 
bitmap_bucket_number, they must
+    represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+    function combines two bitmaps from the same row; use bitmap_*_agg to 
combine bitmaps across
+    rows.
+  """,
+  since = "4.4.0",
+  group = "misc_funcs"
+)
+case class BitmapOr(left: Expression, right: Expression) extends 
BitmapBinaryExpression {
+
+  override def prettyName: String = "bitmap_or"
+
+  override protected def applyOperation(
+      bitmap1: Array[Byte], bitmap2: Array[Byte]): Array[Byte] =
+    BitmapExpressionUtils.bitmapOr(bitmap1, bitmap2)
+
+  override protected def genCodeOperation(
+      bitmapUtils: String, bitmap1: String, bitmap2: String): String =
+    s"$bitmapUtils.bitmapOr($bitmap1, $bitmap2)"
+
+  override protected def withNewChildrenInternal(
+      newLeft: Expression, newRight: Expression): BitmapOr =
+    copy(left = newLeft, right = newRight)
+}
+
+@ExpressionDescription(
+  usage = "_FUNC_(left, right) - Returns a bitmap that is the bitwise AND NOT 
of two bitmaps.",
+  arguments = """
+    Arguments:
+      * left - A binary bitmap.
+      * right - A binary bitmap.
+  """,
+  examples = """
+    Examples:
+      > SELECT substring(hex(_FUNC_(X 'F0', X '70')), 0, 2);
+       80
+  """,
+  note = """
+    Inputs use Spark's Binary bitmap representation, not a RoaringBitmap 
serialization. Each
+    input may contain 0 to 4096 bytes; missing bytes are treated as zero. The 
result is always a
+    4096-byte Binary value. NULL input returns NULL, and inputs longer than 
4096 bytes raise
+    BITMAP_INPUT_TOO_LARGE. Both inputs must use the same bit-position 
mapping. If they were
+    constructed by grouping bitmap_bit_position values by 
bitmap_bucket_number, they must
+    represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+    function combines two bitmaps from the same row; use bitmap_*_agg to 
combine bitmaps across
+    rows.
+  """,
+  since = "4.4.0",
+  group = "misc_funcs"
+)
+case class BitmapAndNot(left: Expression, right: Expression) extends 
BitmapBinaryExpression {
+
+  override def prettyName: String = "bitmap_andnot"
+
+  override protected def applyOperation(
+      bitmap1: Array[Byte], bitmap2: Array[Byte]): Array[Byte] =
+    BitmapExpressionUtils.bitmapAndNot(bitmap1, bitmap2)
+
+  override protected def genCodeOperation(
+      bitmapUtils: String, bitmap1: String, bitmap2: String): String =
+    s"$bitmapUtils.bitmapAndNot($bitmap1, $bitmap2)"
+
+  override protected def withNewChildrenInternal(
+      newLeft: Expression, newRight: Expression): BitmapAndNot =
+    copy(left = newLeft, right = newRight)
+}
+
+@ExpressionDescription(
+  usage = "_FUNC_(left, right) - Returns a bitmap that is the bitwise XOR of 
two input bitmaps.",
+  arguments = """
+    Arguments:
+      * left - A binary bitmap.
+      * right - A binary bitmap.
+  """,
+  examples = """
+    Examples:
+      > SELECT substring(hex(_FUNC_(X 'F0', X '70')), 0, 2);
+       80
+  """,
+  note = """
+    Inputs use Spark's Binary bitmap representation, not a RoaringBitmap 
serialization. Each
+    input may contain 0 to 4096 bytes; missing bytes are treated as zero. The 
result is always a
+    4096-byte Binary value. NULL input returns NULL, and inputs longer than 
4096 bytes raise
+    BITMAP_INPUT_TOO_LARGE. Both inputs must use the same bit-position 
mapping. If they were
+    constructed by grouping bitmap_bit_position values by 
bitmap_bucket_number, they must
+    represent the same bucket because the bitmap bytes do not retain bucket 
metadata. This scalar
+    function combines two bitmaps from the same row; use bitmap_*_agg to 
combine bitmaps across
+    rows.
+  """,
+  since = "4.4.0",
+  group = "misc_funcs"
+)
+case class BitmapXor(left: Expression, right: Expression) extends 
BitmapBinaryExpression {
+
+  override def prettyName: String = "bitmap_xor"
+
+  override protected def applyOperation(
+      bitmap1: Array[Byte], bitmap2: Array[Byte]): Array[Byte] =
+    BitmapExpressionUtils.bitmapXor(bitmap1, bitmap2)
+
+  override protected def genCodeOperation(
+      bitmapUtils: String, bitmap1: String, bitmap2: String): String =
+    s"$bitmapUtils.bitmapXor($bitmap1, $bitmap2)"
+
+  override protected def withNewChildrenInternal(
+      newLeft: Expression, newRight: Expression): BitmapXor =
+    copy(left = newLeft, right = newRight)
+}
+
 @ExpressionDescription(
   usage = """
     _FUNC_(child) - Returns a bitmap with the positions of the bits set from 
all the values from
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
index a17b1ca7e9bb..63ab10b69422 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
@@ -269,6 +269,15 @@ private[sql] object QueryExecutionErrors extends 
QueryErrorsBase with ExecutionE
       summary = "")
   }
 
+  def bitmapInputTooLargeError(inputNumBytes: Int, maxNumBytes: Int): 
SparkRuntimeException = {
+    new SparkRuntimeException(
+      errorClass = "BITMAP_INPUT_TOO_LARGE",
+      messageParameters = Map(
+        "inputNumBytes" -> inputNumBytes.toString,
+        "maxNumBytes" -> maxNumBytes.toString),
+      cause = null)
+  }
+
   def invalidFractionOfSecondError(secAndMicros: Double): DateTimeException = {
     new SparkDateTimeException(
       errorClass = "INVALID_FRACTION_OF_SECOND",
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/BitmapExpressionUtilsSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/BitmapExpressionUtilsSuite.scala
index c33a14aa7b42..3cd4eea2e0ef 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/BitmapExpressionUtilsSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/BitmapExpressionUtilsSuite.scala
@@ -93,6 +93,97 @@ class BitmapExpressionUtilsSuite extends SparkFunSuite {
     assert(BitmapExpressionUtils.bitmapCount(bitmap) == 15L)
   }
 
+  test("scalar bitmap binary operations") {
+    val bitmap1 = Array(0xf0.toByte, 0x0f.toByte)
+    val bitmap2 = Array(0x70.toByte)
+    val originalBitmap1 = bitmap1.clone()
+    val originalBitmap2 = bitmap2.clone()
+
+    val results = Seq(
+      BitmapExpressionUtils.bitmapAnd(bitmap1, bitmap2),
+      BitmapExpressionUtils.bitmapOr(bitmap1, bitmap2),
+      BitmapExpressionUtils.bitmapAndNot(bitmap1, bitmap2),
+      BitmapExpressionUtils.bitmapXor(bitmap1, bitmap2))
+    val expected = Seq(
+      Seq(0x70.toByte, 0x00.toByte),
+      Seq(0xf0.toByte, 0x0f.toByte),
+      Seq(0x80.toByte, 0x0f.toByte),
+      Seq(0x80.toByte, 0x0f.toByte))
+
+    results.zip(expected).foreach { case (result, expectedBytes) =>
+      assert(result.length == BitmapExpressionUtils.NUM_BYTES)
+      assert(result.take(expectedBytes.length).toSeq == expectedBytes)
+      assert(result.drop(expectedBytes.length).forall(_ == 0))
+    }
+    assert(bitmap1.sameElements(originalBitmap1))
+    assert(bitmap2.sameElements(originalBitmap2))
+  }
+
+  test("scalar bitmap binary operations with a longer right input") {
+    val bitmap1 = Array(0xf0.toByte)
+    val bitmap2 = Array(0x70.toByte, 0x0f.toByte)
+    val originalBitmap1 = bitmap1.clone()
+    val originalBitmap2 = bitmap2.clone()
+
+    val results = Seq(
+      BitmapExpressionUtils.bitmapAnd(bitmap1, bitmap2),
+      BitmapExpressionUtils.bitmapOr(bitmap1, bitmap2),
+      BitmapExpressionUtils.bitmapAndNot(bitmap1, bitmap2),
+      BitmapExpressionUtils.bitmapXor(bitmap1, bitmap2))
+    val expected = Seq(
+      Seq(0x70.toByte, 0x00.toByte),
+      Seq(0xf0.toByte, 0x0f.toByte),
+      Seq(0x80.toByte, 0x00.toByte),
+      Seq(0x80.toByte, 0x0f.toByte))
+
+    results.zip(expected).foreach { case (result, expectedBytes) =>
+      assert(result.length == BitmapExpressionUtils.NUM_BYTES)
+      assert(result.take(expectedBytes.length).toSeq == expectedBytes)
+      assert(result.drop(expectedBytes.length).forall(_ == 0))
+    }
+    assert(bitmap1.sameElements(originalBitmap1))
+    assert(bitmap2.sameElements(originalBitmap2))
+  }
+
+  test("scalar bitmap binary operations with boundary lengths") {
+    val expectedBytes = Seq(0x80.toByte, 0xff.toByte, 0x00.toByte, 0x7f.toByte)
+    Seq(0, 1, BitmapExpressionUtils.NUM_BYTES - 1, 
BitmapExpressionUtils.NUM_BYTES).foreach {
+      size =>
+        val bitmap1 = Array.fill[Byte](size)(0x80.toByte)
+        val bitmap2 = Array.fill[Byte](size)(0xff.toByte)
+        val results = Seq(
+          BitmapExpressionUtils.bitmapAnd(bitmap1, bitmap2),
+          BitmapExpressionUtils.bitmapOr(bitmap1, bitmap2),
+          BitmapExpressionUtils.bitmapAndNot(bitmap1, bitmap2),
+          BitmapExpressionUtils.bitmapXor(bitmap1, bitmap2))
+
+        results.zip(expectedBytes).foreach { case (result, expectedByte) =>
+          assert(result.length == BitmapExpressionUtils.NUM_BYTES)
+          assert(result.take(size).forall(_ == expectedByte))
+          assert(result.drop(size).forall(_ == 0))
+        }
+    }
+  }
+
+  test("scalar bitmap binary operations satisfy set operation properties") {
+    val bitmap = Array.fill[Byte](BitmapExpressionUtils.NUM_BYTES)(0x5a.toByte)
+    val other = Array.fill[Byte](BitmapExpressionUtils.NUM_BYTES)(0xa5.toByte)
+    val empty = Array.fill[Byte](BitmapExpressionUtils.NUM_BYTES)(0)
+
+    assert(BitmapExpressionUtils.bitmapAnd(bitmap, 
bitmap).sameElements(bitmap))
+    assert(BitmapExpressionUtils.bitmapOr(bitmap, bitmap).sameElements(bitmap))
+    assert(BitmapExpressionUtils.bitmapXor(bitmap, bitmap).sameElements(empty))
+    assert(BitmapExpressionUtils.bitmapAndNot(bitmap, 
bitmap).sameElements(empty))
+    assert(BitmapExpressionUtils.bitmapAndNot(bitmap, 
empty).sameElements(bitmap))
+    assert(BitmapExpressionUtils.bitmapAndNot(empty, 
bitmap).sameElements(empty))
+    assert(BitmapExpressionUtils.bitmapOr(bitmap, empty).sameElements(bitmap))
+    assert(BitmapExpressionUtils.bitmapXor(bitmap, empty).sameElements(bitmap))
+    assert(BitmapExpressionUtils.bitmapOr(bitmap, other).sameElements(
+      BitmapExpressionUtils.bitmapOr(other, bitmap)))
+    assert(BitmapExpressionUtils.bitmapXor(bitmap, other).sameElements(
+      BitmapExpressionUtils.bitmapXor(other, bitmap)))
+  }
+
   test("bitmap_xor_merge equal length") {
     val bitmap1 = Array[Byte](0x10, 0x30, 0x40)
     val bitmap2 = Array[Byte](0x10, 0x20, 0x40)
diff --git 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
index 0c6ff268aaf2..6346cf4b5cd8 100644
--- 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
+++ 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
@@ -2266,6 +2266,22 @@ class PlanGenerationTestSuite extends ConnectFunSuite 
with Logging {
     binary.select(fn.bitmap_or_agg(fn.col("bytes")))
   }
 
+  test("function bitmap_and") {
+    binary.select(fn.bitmap_and(fn.col("bytes"), fn.col("bytes")))
+  }
+
+  test("function bitmap_or") {
+    binary.select(fn.bitmap_or(fn.col("bytes"), fn.col("bytes")))
+  }
+
+  test("function bitmap_andnot") {
+    binary.select(fn.bitmap_andnot(fn.col("bytes"), fn.col("bytes")))
+  }
+
+  test("function bitmap_xor") {
+    binary.select(fn.bitmap_xor(fn.col("bytes"), fn.col("bytes")))
+  }
+
   test("function bitmap_xor_agg") {
     binary.select(fn.bitmap_xor_agg(fn.col("bytes")))
   }
diff --git 
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_and.explain
 
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_and.explain
new file mode 100644
index 000000000000..d8892a7cfa82
--- /dev/null
+++ 
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_and.explain
@@ -0,0 +1,2 @@
+Project [bitmap_and(bytes#0, bytes#0) AS bitmap_and(bytes, bytes)#0]
++- LocalRelation <empty>, [id#0L, bytes#0]
diff --git 
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_andnot.explain
 
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_andnot.explain
new file mode 100644
index 000000000000..7a081fa44975
--- /dev/null
+++ 
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_andnot.explain
@@ -0,0 +1,2 @@
+Project [bitmap_andnot(bytes#0, bytes#0) AS bitmap_andnot(bytes, bytes)#0]
++- LocalRelation <empty>, [id#0L, bytes#0]
diff --git 
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_or.explain
 
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_or.explain
new file mode 100644
index 000000000000..f58db6eb6908
--- /dev/null
+++ 
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_or.explain
@@ -0,0 +1,2 @@
+Project [bitmap_or(bytes#0, bytes#0) AS bitmap_or(bytes, bytes)#0]
++- LocalRelation <empty>, [id#0L, bytes#0]
diff --git 
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_xor.explain
 
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_xor.explain
new file mode 100644
index 000000000000..a6f81bbc9a2b
--- /dev/null
+++ 
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_bitmap_xor.explain
@@ -0,0 +1,2 @@
+Project [bitmap_xor(bytes#0, bytes#0) AS bitmap_xor(bytes, bytes)#0]
++- LocalRelation <empty>, [id#0L, bytes#0]
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_and.json
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_and.json
new file mode 100644
index 000000000000..1abf52ef603e
--- /dev/null
+++ 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_and.json
@@ -0,0 +1,81 @@
+{
+  "common": {
+    "planId": "1"
+  },
+  "project": {
+    "input": {
+      "common": {
+        "planId": "0"
+      },
+      "localRelation": {
+        "schema": "struct\u003cid:bigint,bytes:binary\u003e"
+      }
+    },
+    "expressions": [{
+      "unresolvedFunction": {
+        "functionName": "bitmap_and",
+        "arguments": [{
+          "unresolvedAttribute": {
+            "unparsedIdentifier": "bytes"
+          },
+          "common": {
+            "origin": {
+              "jvmOrigin": {
+                "stackTrace": [{
+                  "classLoaderName": "app",
+                  "declaringClass": "org.apache.spark.sql.functions$",
+                  "methodName": "col",
+                  "fileName": "functions.scala"
+                }, {
+                  "classLoaderName": "app",
+                  "declaringClass": 
"org.apache.spark.sql.PlanGenerationTestSuite",
+                  "methodName": "~~trimmed~anonfun~~",
+                  "fileName": "PlanGenerationTestSuite.scala"
+                }]
+              }
+            }
+          }
+        }, {
+          "unresolvedAttribute": {
+            "unparsedIdentifier": "bytes"
+          },
+          "common": {
+            "origin": {
+              "jvmOrigin": {
+                "stackTrace": [{
+                  "classLoaderName": "app",
+                  "declaringClass": "org.apache.spark.sql.functions$",
+                  "methodName": "col",
+                  "fileName": "functions.scala"
+                }, {
+                  "classLoaderName": "app",
+                  "declaringClass": 
"org.apache.spark.sql.PlanGenerationTestSuite",
+                  "methodName": "~~trimmed~anonfun~~",
+                  "fileName": "PlanGenerationTestSuite.scala"
+                }]
+              }
+            }
+          }
+        }],
+        "isInternal": false
+      },
+      "common": {
+        "origin": {
+          "jvmOrigin": {
+            "stackTrace": [{
+              "classLoaderName": "app",
+              "declaringClass": "org.apache.spark.sql.functions$",
+              "methodName": "bitmap_and",
+              "fileName": "functions.scala"
+            }, {
+              "classLoaderName": "app",
+              "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite",
+              "methodName": "~~trimmed~anonfun~~",
+              "fileName": "PlanGenerationTestSuite.scala"
+            }]
+          }
+        }
+      }
+    }]
+  }
+}
\ No newline at end of file
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_and.proto.bin
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_and.proto.bin
new file mode 100644
index 000000000000..4ed1b96834a2
Binary files /dev/null and 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_and.proto.bin
 differ
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_andnot.json
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_andnot.json
new file mode 100644
index 000000000000..a3363b09d430
--- /dev/null
+++ 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_andnot.json
@@ -0,0 +1,81 @@
+{
+  "common": {
+    "planId": "1"
+  },
+  "project": {
+    "input": {
+      "common": {
+        "planId": "0"
+      },
+      "localRelation": {
+        "schema": "struct\u003cid:bigint,bytes:binary\u003e"
+      }
+    },
+    "expressions": [{
+      "unresolvedFunction": {
+        "functionName": "bitmap_andnot",
+        "arguments": [{
+          "unresolvedAttribute": {
+            "unparsedIdentifier": "bytes"
+          },
+          "common": {
+            "origin": {
+              "jvmOrigin": {
+                "stackTrace": [{
+                  "classLoaderName": "app",
+                  "declaringClass": "org.apache.spark.sql.functions$",
+                  "methodName": "col",
+                  "fileName": "functions.scala"
+                }, {
+                  "classLoaderName": "app",
+                  "declaringClass": 
"org.apache.spark.sql.PlanGenerationTestSuite",
+                  "methodName": "~~trimmed~anonfun~~",
+                  "fileName": "PlanGenerationTestSuite.scala"
+                }]
+              }
+            }
+          }
+        }, {
+          "unresolvedAttribute": {
+            "unparsedIdentifier": "bytes"
+          },
+          "common": {
+            "origin": {
+              "jvmOrigin": {
+                "stackTrace": [{
+                  "classLoaderName": "app",
+                  "declaringClass": "org.apache.spark.sql.functions$",
+                  "methodName": "col",
+                  "fileName": "functions.scala"
+                }, {
+                  "classLoaderName": "app",
+                  "declaringClass": 
"org.apache.spark.sql.PlanGenerationTestSuite",
+                  "methodName": "~~trimmed~anonfun~~",
+                  "fileName": "PlanGenerationTestSuite.scala"
+                }]
+              }
+            }
+          }
+        }],
+        "isInternal": false
+      },
+      "common": {
+        "origin": {
+          "jvmOrigin": {
+            "stackTrace": [{
+              "classLoaderName": "app",
+              "declaringClass": "org.apache.spark.sql.functions$",
+              "methodName": "bitmap_andnot",
+              "fileName": "functions.scala"
+            }, {
+              "classLoaderName": "app",
+              "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite",
+              "methodName": "~~trimmed~anonfun~~",
+              "fileName": "PlanGenerationTestSuite.scala"
+            }]
+          }
+        }
+      }
+    }]
+  }
+}
\ No newline at end of file
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_andnot.proto.bin
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_andnot.proto.bin
new file mode 100644
index 000000000000..b0cdbd7ea9bd
Binary files /dev/null and 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_andnot.proto.bin
 differ
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_or.json
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_or.json
new file mode 100644
index 000000000000..7c0acff13b79
--- /dev/null
+++ 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_or.json
@@ -0,0 +1,81 @@
+{
+  "common": {
+    "planId": "1"
+  },
+  "project": {
+    "input": {
+      "common": {
+        "planId": "0"
+      },
+      "localRelation": {
+        "schema": "struct\u003cid:bigint,bytes:binary\u003e"
+      }
+    },
+    "expressions": [{
+      "unresolvedFunction": {
+        "functionName": "bitmap_or",
+        "arguments": [{
+          "unresolvedAttribute": {
+            "unparsedIdentifier": "bytes"
+          },
+          "common": {
+            "origin": {
+              "jvmOrigin": {
+                "stackTrace": [{
+                  "classLoaderName": "app",
+                  "declaringClass": "org.apache.spark.sql.functions$",
+                  "methodName": "col",
+                  "fileName": "functions.scala"
+                }, {
+                  "classLoaderName": "app",
+                  "declaringClass": 
"org.apache.spark.sql.PlanGenerationTestSuite",
+                  "methodName": "~~trimmed~anonfun~~",
+                  "fileName": "PlanGenerationTestSuite.scala"
+                }]
+              }
+            }
+          }
+        }, {
+          "unresolvedAttribute": {
+            "unparsedIdentifier": "bytes"
+          },
+          "common": {
+            "origin": {
+              "jvmOrigin": {
+                "stackTrace": [{
+                  "classLoaderName": "app",
+                  "declaringClass": "org.apache.spark.sql.functions$",
+                  "methodName": "col",
+                  "fileName": "functions.scala"
+                }, {
+                  "classLoaderName": "app",
+                  "declaringClass": 
"org.apache.spark.sql.PlanGenerationTestSuite",
+                  "methodName": "~~trimmed~anonfun~~",
+                  "fileName": "PlanGenerationTestSuite.scala"
+                }]
+              }
+            }
+          }
+        }],
+        "isInternal": false
+      },
+      "common": {
+        "origin": {
+          "jvmOrigin": {
+            "stackTrace": [{
+              "classLoaderName": "app",
+              "declaringClass": "org.apache.spark.sql.functions$",
+              "methodName": "bitmap_or",
+              "fileName": "functions.scala"
+            }, {
+              "classLoaderName": "app",
+              "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite",
+              "methodName": "~~trimmed~anonfun~~",
+              "fileName": "PlanGenerationTestSuite.scala"
+            }]
+          }
+        }
+      }
+    }]
+  }
+}
\ No newline at end of file
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_or.proto.bin
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_or.proto.bin
new file mode 100644
index 000000000000..b8dbed026128
Binary files /dev/null and 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_or.proto.bin
 differ
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_xor.json
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_xor.json
new file mode 100644
index 000000000000..59c78fa16801
--- /dev/null
+++ 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_xor.json
@@ -0,0 +1,81 @@
+{
+  "common": {
+    "planId": "1"
+  },
+  "project": {
+    "input": {
+      "common": {
+        "planId": "0"
+      },
+      "localRelation": {
+        "schema": "struct\u003cid:bigint,bytes:binary\u003e"
+      }
+    },
+    "expressions": [{
+      "unresolvedFunction": {
+        "functionName": "bitmap_xor",
+        "arguments": [{
+          "unresolvedAttribute": {
+            "unparsedIdentifier": "bytes"
+          },
+          "common": {
+            "origin": {
+              "jvmOrigin": {
+                "stackTrace": [{
+                  "classLoaderName": "app",
+                  "declaringClass": "org.apache.spark.sql.functions$",
+                  "methodName": "col",
+                  "fileName": "functions.scala"
+                }, {
+                  "classLoaderName": "app",
+                  "declaringClass": 
"org.apache.spark.sql.PlanGenerationTestSuite",
+                  "methodName": "~~trimmed~anonfun~~",
+                  "fileName": "PlanGenerationTestSuite.scala"
+                }]
+              }
+            }
+          }
+        }, {
+          "unresolvedAttribute": {
+            "unparsedIdentifier": "bytes"
+          },
+          "common": {
+            "origin": {
+              "jvmOrigin": {
+                "stackTrace": [{
+                  "classLoaderName": "app",
+                  "declaringClass": "org.apache.spark.sql.functions$",
+                  "methodName": "col",
+                  "fileName": "functions.scala"
+                }, {
+                  "classLoaderName": "app",
+                  "declaringClass": 
"org.apache.spark.sql.PlanGenerationTestSuite",
+                  "methodName": "~~trimmed~anonfun~~",
+                  "fileName": "PlanGenerationTestSuite.scala"
+                }]
+              }
+            }
+          }
+        }],
+        "isInternal": false
+      },
+      "common": {
+        "origin": {
+          "jvmOrigin": {
+            "stackTrace": [{
+              "classLoaderName": "app",
+              "declaringClass": "org.apache.spark.sql.functions$",
+              "methodName": "bitmap_xor",
+              "fileName": "functions.scala"
+            }, {
+              "classLoaderName": "app",
+              "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite",
+              "methodName": "~~trimmed~anonfun~~",
+              "fileName": "PlanGenerationTestSuite.scala"
+            }]
+          }
+        }
+      }
+    }]
+  }
+}
\ No newline at end of file
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_xor.proto.bin
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_xor.proto.bin
new file mode 100644
index 000000000000..dfc994306a53
Binary files /dev/null and 
b/sql/connect/common/src/test/resources/query-tests/queries/function_bitmap_xor.proto.bin
 differ
diff --git a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md 
b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
index e0d9862bc22a..6f11ca0435ae 100644
--- a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
+++ b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
@@ -49,12 +49,16 @@
 | org.apache.spark.sql.catalyst.expressions.Between | between | SELECT 0.5 
between 0.1 AND 1.0 | struct<between(0.5, 0.1, 1.0):boolean> |
 | org.apache.spark.sql.catalyst.expressions.Bin | bin | SELECT bin(13) | 
struct<bin(13):string> |
 | org.apache.spark.sql.catalyst.expressions.BitLength | bit_length | SELECT 
bit_length('Spark SQL') | struct<bit_length(Spark SQL):int> |
+| org.apache.spark.sql.catalyst.expressions.BitmapAnd | bitmap_and | SELECT 
substring(hex(bitmap_and(X 'F0', X '70')), 0, 2) | 
struct<substring(hex(bitmap_and(X'F0', X'70')), 0, 2):string> |
 | org.apache.spark.sql.catalyst.expressions.BitmapAndAgg | bitmap_and_agg | 
SELECT substring(hex(bitmap_and_agg(col)), 0, 6) FROM VALUES (X 'F0'), (X 
'70'), (X '30') AS tab(col) | struct<substring(hex(bitmap_and_agg(col)), 0, 
6):string> |
+| org.apache.spark.sql.catalyst.expressions.BitmapAndNot | bitmap_andnot | 
SELECT substring(hex(bitmap_andnot(X 'F0', X '70')), 0, 2) | 
struct<substring(hex(bitmap_andnot(X'F0', X'70')), 0, 2):string> |
 | org.apache.spark.sql.catalyst.expressions.BitmapBitPosition | 
bitmap_bit_position | SELECT bitmap_bit_position(1) | 
struct<bitmap_bit_position(1):bigint> |
 | org.apache.spark.sql.catalyst.expressions.BitmapBucketNumber | 
bitmap_bucket_number | SELECT bitmap_bucket_number(123) | 
struct<bitmap_bucket_number(123):bigint> |
 | org.apache.spark.sql.catalyst.expressions.BitmapConstructAgg | 
bitmap_construct_agg | SELECT 
substring(hex(bitmap_construct_agg(bitmap_bit_position(col))), 0, 6) FROM 
VALUES (1), (2), (3) AS tab(col) | 
struct<substring(hex(bitmap_construct_agg(bitmap_bit_position(col))), 0, 
6):string> |
 | org.apache.spark.sql.catalyst.expressions.BitmapCount | bitmap_count | 
SELECT bitmap_count(X '1010') | struct<bitmap_count(X'1010'):bigint> |
+| org.apache.spark.sql.catalyst.expressions.BitmapOr | bitmap_or | SELECT 
substring(hex(bitmap_or(X '10', X '20')), 0, 2) | 
struct<substring(hex(bitmap_or(X'10', X'20')), 0, 2):string> |
 | org.apache.spark.sql.catalyst.expressions.BitmapOrAgg | bitmap_or_agg | 
SELECT substring(hex(bitmap_or_agg(col)), 0, 6) FROM VALUES (X '10'), (X '20'), 
(X '40') AS tab(col) | struct<substring(hex(bitmap_or_agg(col)), 0, 6):string> |
+| org.apache.spark.sql.catalyst.expressions.BitmapXor | bitmap_xor | SELECT 
substring(hex(bitmap_xor(X 'F0', X '70')), 0, 2) | 
struct<substring(hex(bitmap_xor(X'F0', X'70')), 0, 2):string> |
 | org.apache.spark.sql.catalyst.expressions.BitmapXorAgg | bitmap_xor_agg | 
SELECT substring(hex(bitmap_xor_agg(col)), 0, 6) FROM VALUES (X'10'), (X'30'), 
(X'40') AS tab(col) | struct<substring(hex(bitmap_xor_agg(col)), 0, 6):string> |
 | org.apache.spark.sql.catalyst.expressions.BitwiseAnd | & | SELECT 3 & 5 | 
struct<(3 & 5):int> |
 | org.apache.spark.sql.catalyst.expressions.BitwiseCount | bit_count | SELECT 
bit_count(0) | struct<bit_count(0):int> |
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/BitmapExpressionsQuerySuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/BitmapExpressionsQuerySuite.scala
index 8201e2c2f872..7d079fb894e7 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/BitmapExpressionsQuerySuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/BitmapExpressionsQuerySuite.scala
@@ -17,20 +17,27 @@
 
 package org.apache.spark.sql
 
+import org.apache.spark.SparkRuntimeException
 import org.apache.spark.sql.functions.{
+  bitmap_and,
   bitmap_and_agg,
+  bitmap_andnot,
   bitmap_bit_position,
   bitmap_bucket_number,
   bitmap_construct_agg,
   bitmap_count,
+  bitmap_or,
   bitmap_or_agg,
+  bitmap_xor,
   bitmap_xor_agg,
   col,
   expr,
   hex,
   lit,
   substring,
+  sum,
   to_binary}
+import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.test.SharedSparkSession
 
 class BitmapExpressionsQuerySuite extends SharedSparkSession {
@@ -310,6 +317,167 @@ class BitmapExpressionsQuerySuite extends 
SharedSparkSession {
     }
   }
 
+  test("scalar bitmap binary operations") {
+    def checkResults(): Unit = {
+      val df = Seq(("F00F", "70")).toDF("left", "right")
+      checkAnswer(
+        df.selectExpr(
+          "substring(hex(bitmap_and(to_binary(left, 'hex'), to_binary(right, 
'hex'))), 0, 4)",
+          "substring(hex(bitmap_or(to_binary(left, 'hex'), to_binary(right, 
'hex'))), 0, 4)",
+          "substring(hex(bitmap_andnot(to_binary(left, 'hex'), 
to_binary(right, 'hex'))), 0, 4)",
+          "substring(hex(bitmap_xor(to_binary(left, 'hex'), to_binary(right, 
'hex'))), 0, 4)"),
+        Seq(Row("7000", "F00F", "800F", "800F")))
+
+      val leftBitmap = to_binary(col("left"), lit("hex"))
+      val rightBitmap = to_binary(col("right"), lit("hex"))
+      checkAnswer(
+        df.select(
+          substring(hex(bitmap_and(leftBitmap, rightBitmap)), 0, 4),
+          substring(hex(bitmap_or(leftBitmap, rightBitmap)), 0, 4),
+          substring(hex(bitmap_andnot(leftBitmap, rightBitmap)), 0, 4),
+          substring(hex(bitmap_xor(leftBitmap, rightBitmap)), 0, 4)),
+        Seq(Row("7000", "F00F", "800F", "800F")))
+
+      val shortBitmap = Seq(("", "FF")).toDF("left", "right")
+      checkAnswer(
+        shortBitmap.selectExpr(
+          "length(bitmap_and(to_binary(left, 'hex'), to_binary(right, 
'hex')))",
+          "length(bitmap_or(to_binary(left, 'hex'), to_binary(right, 'hex')))",
+          "length(bitmap_andnot(to_binary(left, 'hex'), to_binary(right, 
'hex')))",
+          "length(bitmap_xor(to_binary(left, 'hex'), to_binary(right, 
'hex')))"),
+        Seq(Row(4096, 4096, 4096, 4096)))
+
+      checkAnswer(
+        spark.sql("""
+          |SELECT bitmap_count(bitmap_and(X'', X'FF')),
+          |  bitmap_count(bitmap_or(X'', X'FF')),
+          |  bitmap_count(bitmap_andnot(X'', X'FF')),
+          |  bitmap_count(bitmap_xor(X'', X'FF'))
+          |""".stripMargin),
+        Seq(Row(0, 8, 0, 8)))
+
+      val boundaryBitmap =
+        Seq((Array.fill[Byte](4096)(0), 
Array.fill[Byte](4096)(0))).toDF("left", "right")
+      checkAnswer(
+        boundaryBitmap.selectExpr(
+          "length(bitmap_and(left, right))",
+          "length(bitmap_or(left, right))",
+          "length(bitmap_andnot(left, right))",
+          "length(bitmap_xor(left, right))"),
+        Seq(Row(4096, 4096, 4096, 4096)))
+
+      checkAnswer(
+        spark.sql("SELECT bitmap_count(bitmap_or(bitmap_and(X 'F0', X '70'), X 
'0F'))"),
+        Seq(Row(7)))
+    }
+
+    Seq("CODEGEN_ONLY" -> "true", "NO_CODEGEN" -> "false").foreach {
+      case (codegenMode, wholeStageEnabled) =>
+        withSQLConf(
+          SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> wholeStageEnabled,
+          SQLConf.CODEGEN_FACTORY_MODE.key -> codegenMode) {
+          checkResults()
+        }
+    }
+  }
+
+  test("scalar bitmap binary operations with aggregate bitmaps") {
+    val constructed = Seq((1, 2), (2, 3), (3, 4)).toDF("left", "right")
+    val leftBitmap = bitmap_construct_agg(bitmap_bit_position(col("left")))
+    val rightBitmap = bitmap_construct_agg(bitmap_bit_position(col("right")))
+    checkAnswer(
+      constructed.agg(bitmap_count(bitmap_and(leftBitmap, rightBitmap))),
+      Seq(Row(2)))
+
+    val precomputed = Seq("F0", "70").toDF("bitmap")
+    val orBitmap = bitmap_or_agg(to_binary(col("bitmap"), lit("hex")))
+    val andBitmap = bitmap_and_agg(to_binary(col("bitmap"), lit("hex")))
+    checkAnswer(
+      precomputed.agg(
+        bitmap_count(bitmap_and(orBitmap, andBitmap)),
+        bitmap_count(bitmap_or(orBitmap, andBitmap)),
+        bitmap_count(bitmap_andnot(orBitmap, andBitmap)),
+        bitmap_count(bitmap_xor(orBitmap, andBitmap))),
+      Seq(Row(3, 4, 1, 1)))
+  }
+
+  test("scalar bitmap binary operations in grouped query") {
+    val df = Seq((1, "F0", "70"), (1, "10", "20"), (2, "FF", "0F"))
+      .toDF("group_id", "left", "right")
+    checkAnswer(
+      df.selectExpr(
+        "group_id",
+        "bitmap_count(bitmap_and(to_binary(left, 'hex'), to_binary(right, 
'hex'))) AS count")
+        .groupBy("group_id")
+        .agg(sum("count"))
+        .orderBy("group_id"),
+      Seq(Row(1, 3), Row(2, 4)))
+  }
+
+  test("scalar bitmap binary operations with nulls") {
+    val df = Seq[(String, String)](("F0", null), (null, "70"), (null, 
null)).toDF("left", "right")
+    checkAnswer(
+      df.selectExpr(
+        "bitmap_and(to_binary(left, 'hex'), to_binary(right, 'hex'))",
+        "bitmap_or(to_binary(left, 'hex'), to_binary(right, 'hex'))",
+        "bitmap_andnot(to_binary(left, 'hex'), to_binary(right, 'hex'))",
+        "bitmap_xor(to_binary(left, 'hex'), to_binary(right, 'hex'))"),
+      Seq(
+        Row(null, null, null, null),
+        Row(null, null, null, null),
+        Row(null, null, null, null)))
+  }
+
+  test("scalar bitmap binary operations reject oversized inputs") {
+    val oversizedInputs = Seq(
+      "left" -> Seq((Array.fill[Byte](4097)(0), Array[Byte](0))).toDF("left", 
"right"),
+      "right" -> Seq((Array[Byte](0), Array.fill[Byte](4097)(0))).toDF("left", 
"right"))
+
+    Seq("CODEGEN_ONLY" -> "true", "NO_CODEGEN" -> "false").foreach {
+      case (codegenMode, wholeStageEnabled) =>
+        withSQLConf(
+          SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> wholeStageEnabled,
+          SQLConf.CODEGEN_FACTORY_MODE.key -> codegenMode) {
+          oversizedInputs.foreach { case (inputSide, oversized) =>
+            Seq("bitmap_and", "bitmap_or", "bitmap_andnot", 
"bitmap_xor").foreach { functionName =>
+              withClue(s"$functionName with oversized $inputSide input in 
$codegenMode: ") {
+                checkError(
+                  exception = intercept[SparkRuntimeException] {
+                    oversized.selectExpr(s"$functionName(left, 
right)").collect()
+                  },
+                  condition = "BITMAP_INPUT_TOO_LARGE",
+                  parameters = Map("inputNumBytes" -> "4097", "maxNumBytes" -> 
"4096"))
+              }
+            }
+          }
+        }
+    }
+  }
+
+  test("scalar bitmap binary operations called with non-binary types") {
+    val invalidInputs = Seq(
+      ("first", "left", Seq((12, Array[Byte](0))).toDF("left", "right")),
+      ("second", "right", Seq((Array[Byte](0), 13)).toDF("left", "right")))
+
+    invalidInputs.foreach { case (paramIndex, inputColumn, df) =>
+      Seq("bitmap_and", "bitmap_or", "bitmap_andnot", "bitmap_xor").foreach { 
functionName =>
+        val sqlExpr = s"$functionName(left, right)"
+        checkError(
+          exception = intercept[AnalysisException] {
+            df.selectExpr(sqlExpr)
+          },
+          condition = "DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE",
+          parameters = Map(
+            "sqlExpr" -> s""""$sqlExpr"""",
+            "paramIndex" -> paramIndex,
+            "requiredType" -> "\"BINARY\"",
+            "inputSql" -> s""""$inputColumn"""",
+            "inputType" -> "\"INT\""),
+          context = ExpectedContext(fragment = sqlExpr, start = 0, stop = 
sqlExpr.length - 1))
+      }
+    }
+  }
+
   test("bitmap_count called with non-binary type") {
     val df = Seq(12).toDF("a")
     checkError(


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to