Repository: great-expectations/great_expectations

Issue:
Use cleaner solution for non-truncating division in python 2
Prefer `from __future__ import division` to `1.*x/y`


Discussion:
Done inline during improve_partitions push.

Resolving patch:
diff --git a/great_expectations/expectations/metrics/map_metric_provider.py b/great_expectations/expectations/metrics/map_metric_provider.py
--- a/great_expectations/expectations/metrics/map_metric_provider.py
+++ b/great_expectations/expectations/metrics/map_metric_provider.py
@@ -350,7 +350,7 @@ def inner_func(
             MetricPartialFunctionTypes.WINDOW_CONDITION_FN,
         ]:
             raise ValueError(
-                "SqlAlchemyExecutionEngine only supports map_condition_fn for column_condition_partial partial_fn_type"
+                "SqlAlchemyExecutionEngine only supports map_condition_fn and window_condition_fn for column_condition_partial partial_fn_type"
             )
 
         def wrapper(metric_fn: Callable):
@@ -518,7 +518,6 @@ def column_pair_function_partial(
         An annotated metric_function which will be called with a simplified signature.
 
     """
-    # TODO: <Alex>ALEX -- temporarily only Pandas and SQLAlchemy implementations are provided (Spark to follow).</Alex>
     domain_type = MetricDomainTypes.COLUMN_PAIR
     if issubclass(engine, PandasExecutionEngine):
         if partial_fn_type is None:
@@ -605,7 +604,7 @@ def inner_func(
                 runtime_configuration: Dict,
             ):
                 (
-                    df,
+                    selectable,
                     compute_domain_kwargs,
                     accessor_domain_kwargs,
                 ) = execution_engine.get_compute_domain(
@@ -642,6 +641,69 @@ def inner_func(
 
         return wrapper
 
+    elif issubclass(engine, SparkDFExecutionEngine):
+        if partial_fn_type is None:
+            partial_fn_type = MetricPartialFunctionTypes.MAP_FN
+        partial_fn_type = MetricPartialFunctionTypes(partial_fn_type)
+        if partial_fn_type != MetricPartialFunctionTypes.MAP_FN:
+            raise ValueError(
+                "SparkDFExecutionEngine only supports map_fn for column_pair_function_partial partial_fn_type"
+            )
+
+        def wrapper(metric_fn: Callable):
+            @metric_partial(
+                engine=engine,
+                partial_fn_type=partial_fn_type,
+                domain_type=domain_type,
+                **kwargs,
+            )
+            @wraps(metric_fn)
+            def inner_func(
+                cls,
+                execution_engine: SparkDFExecutionEngine,
+                metric_domain_kwargs: Dict,
+                metric_value_kwargs: Dict,
+                metrics: Dict[str, Any],
+                runtime_configuration: Dict,
+            ):
+                (
+                    data,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                ) = execution_engine.get_compute_domain(
+                    domain_kwargs=metric_domain_kwargs, domain_type=domain_type
+                )
+
+                # noinspection PyPep8Naming
+                column_A_name = accessor_domain_kwargs["column_A"]
+                # noinspection PyPep8Naming
+                column_B_name = accessor_domain_kwargs["column_B"]
+
+                column_list = [column_A_name, column_B_name]
+
+                for column_name in column_list:
+                    if column_name not in metrics["table.columns"]:
+                        raise ge_exceptions.ExecutionEngineError(
+                            message=f'Error: The column "{column_name}" in BatchData does not exist.'
+                        )
+
+                column_pair_function = metric_fn(
+                    cls,
+                    data[column_A_name],
+                    data[column_B_name],
+                    **metric_value_kwargs,
+                    _metrics=metrics,
+                )
+                return (
+                    column_pair_function,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                )
+
+            return inner_func
+
+        return wrapper
+
     else:
         raise ValueError("Unsupported engine for column_pair_function_partial")
 
@@ -667,7 +729,6 @@ def column_pair_condition_partial(
         An annotated metric_function which will be called with a simplified signature.
 
     """
-    # TODO: <Alex>ALEX -- temporarily only Pandas and SQLAlchemy implementations are provided (Spark to follow).</Alex>
     domain_type = MetricDomainTypes.COLUMN_PAIR
     if issubclass(engine, PandasExecutionEngine):
         if partial_fn_type is None:
@@ -741,7 +802,7 @@ def inner_func(
             MetricPartialFunctionTypes.WINDOW_CONDITION_FN,
         ]:
             raise ValueError(
-                "SqlAlchemyExecutionEngine only supports map_condition_fn for column_pair_condition_partial partial_fn_type"
+                "SqlAlchemyExecutionEngine only supports map_condition_fn and window_condition_fn for column_pair_condition_partial partial_fn_type"
             )
 
         def wrapper(metric_fn: Callable):
@@ -765,7 +826,7 @@ def inner_func(
                     compute_domain_kwargs,
                     accessor_domain_kwargs,
                 ) = execution_engine.get_compute_domain(
-                    metric_domain_kwargs, domain_type=domain_type
+                    domain_kwargs=metric_domain_kwargs, domain_type=domain_type
                 )
 
                 # noinspection PyPep8Naming
@@ -806,6 +867,72 @@ def inner_func(
 
         return wrapper
 
+    elif issubclass(engine, SparkDFExecutionEngine):
+        if partial_fn_type is None:
+            partial_fn_type = MetricPartialFunctionTypes.MAP_CONDITION_FN
+        partial_fn_type = MetricPartialFunctionTypes(partial_fn_type)
+        if partial_fn_type not in [
+            MetricPartialFunctionTypes.MAP_CONDITION_FN,
+            MetricPartialFunctionTypes.WINDOW_CONDITION_FN,
+        ]:
+            raise ValueError(
+                "SparkDFExecutionEngine only supports map_condition_fn and window_condition_fn for column_pair_condition_partial partial_fn_type"
+            )
+
+        def wrapper(metric_fn: Callable):
+            @metric_partial(
+                engine=engine,
+                partial_fn_type=partial_fn_type,
+                domain_type=domain_type,
+                **kwargs,
+            )
+            @wraps(metric_fn)
+            def inner_func(
+                cls,
+                execution_engine: SparkDFExecutionEngine,
+                metric_domain_kwargs: Dict,
+                metric_value_kwargs: Dict,
+                metrics: Dict[str, Any],
+                runtime_configuration: Dict,
+            ):
+                (
+                    data,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                ) = execution_engine.get_compute_domain(
+                    domain_kwargs=metric_domain_kwargs, domain_type=domain_type
+                )
+
+                # noinspection PyPep8Naming
+                column_A_name = accessor_domain_kwargs["column_A"]
+                # noinspection PyPep8Naming
+                column_B_name = accessor_domain_kwargs["column_B"]
+
+                column_list = [column_A_name, column_B_name]
+
+                for column_name in column_list:
+                    if column_name not in metrics["table.columns"]:
+                        raise ge_exceptions.ExecutionEngineError(
+                            message=f'Error: The column "{column_name}" in BatchData does not exist.'
+                        )
+
+                expected_condition = metric_fn(
+                    cls,
+                    data[column_A_name],
+                    data[column_B_name],
+                    **metric_value_kwargs,
+                    _metrics=metrics,
+                )
+                return (
+                    ~expected_condition,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                )
+
+            return inner_func
+
+        return wrapper
+
     else:
         raise ValueError("Unsupported engine for column_pair_condition_partial")
 
@@ -827,7 +954,6 @@ def multicolumn_function_partial(
         An annotated metric_function which will be called with a simplified signature.
 
     """
-    # TODO: <Alex>ALEX -- temporarily only Pandas and SQLAlchemy implementations are provided (Spark to follow).</Alex>
     domain_type = MetricDomainTypes.MULTICOLUMN
     if issubclass(engine, PandasExecutionEngine):
         if partial_fn_type is None:
@@ -925,7 +1051,7 @@ def inner_func(
 
                 column_select = [sa.column(column_name) for column_name in column_list]
                 dialect = execution_engine.dialect_module
-                column_function = metric_fn(
+                multicolumn_function = metric_fn(
                     cls,
                     column_select,
                     **metric_value_kwargs,
@@ -933,7 +1059,68 @@ def inner_func(
                     _table=selectable,
                     _metrics=metrics,
                 )
-                return column_function, compute_domain_kwargs, accessor_domain_kwargs
+                return (
+                    multicolumn_function,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                )
+
+            return inner_func
+
+        return wrapper
+
+    elif issubclass(engine, SparkDFExecutionEngine):
+        if partial_fn_type is None:
+            partial_fn_type = MetricPartialFunctionTypes.MAP_FN
+        partial_fn_type = MetricPartialFunctionTypes(partial_fn_type)
+        if partial_fn_type != MetricPartialFunctionTypes.MAP_FN:
+            raise ValueError(
+                "SparkDFExecutionEngine only supports map_fn for multicolumn_function_partial partial_fn_type"
+            )
+
+        def wrapper(metric_fn: Callable):
+            @metric_partial(
+                engine=engine,
+                partial_fn_type=partial_fn_type,
+                domain_type=domain_type,
+                **kwargs,
+            )
+            @wraps(metric_fn)
+            def inner_func(
+                cls,
+                execution_engine: SparkDFExecutionEngine,
+                metric_domain_kwargs: Dict,
+                metric_value_kwargs: Dict,
+                metrics: Dict[str, Any],
+                runtime_configuration: Dict,
+            ):
+                (
+                    data,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                ) = execution_engine.get_compute_domain(
+                    domain_kwargs=metric_domain_kwargs, domain_type=domain_type
+                )
+
+                column_list = accessor_domain_kwargs["column_list"]
+
+                for column_name in column_list:
+                    if column_name not in metrics["table.columns"]:
+                        raise ge_exceptions.ExecutionEngineError(
+                            message=f'Error: The column "{column_name}" in BatchData does not exist.'
+                        )
+
+                multicolumn_function = metric_fn(
+                    cls,
+                    data[column_list],
+                    **metric_value_kwargs,
+                    _metrics=metrics,
+                )
+                return (
+                    multicolumn_function,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                )
 
             return inner_func
 
@@ -964,7 +1151,6 @@ def multicolumn_condition_partial(
         An annotated metric_function which will be called with a simplified signature.
 
     """
-    # TODO: <Alex>ALEX -- temporarily only Pandas and SQLAlchemy implementations are provided (Spark to follow).</Alex>
     domain_type = MetricDomainTypes.MULTICOLUMN
     if issubclass(engine, PandasExecutionEngine):
         if partial_fn_type is None:
@@ -1027,9 +1213,12 @@ def inner_func(
         if partial_fn_type is None:
             partial_fn_type = MetricPartialFunctionTypes.MAP_CONDITION_FN
         partial_fn_type = MetricPartialFunctionTypes(partial_fn_type)
-        if partial_fn_type not in [MetricPartialFunctionTypes.MAP_CONDITION_FN]:
+        if partial_fn_type not in [
+            MetricPartialFunctionTypes.MAP_CONDITION_FN,
+            MetricPartialFunctionTypes.WINDOW_CONDITION_FN,
+        ]:
             raise ValueError(
-                "SqlAlchemyExecutionEngine only supports map_condition_fn for multicolumn_condition_partial partial_fn_type"
+                "SqlAlchemyExecutionEngine only supports map_condition_fn and window_condition_fn for multicolumn_condition_partial partial_fn_type"
             )
 
         def wrapper(metric_fn: Callable):
@@ -1089,6 +1278,66 @@ def inner_func(
 
         return wrapper
 
+    elif issubclass(engine, SparkDFExecutionEngine):
+        if partial_fn_type is None:
+            partial_fn_type = MetricPartialFunctionTypes.MAP_CONDITION_FN
+        partial_fn_type = MetricPartialFunctionTypes(partial_fn_type)
+        if partial_fn_type not in [
+            MetricPartialFunctionTypes.MAP_CONDITION_FN,
+            MetricPartialFunctionTypes.WINDOW_CONDITION_FN,
+        ]:
+            raise ValueError(
+                "SparkDFExecutionEngine only supports map_condition_fn and window_condition_fn for multicolumn_condition_partial partial_fn_type"
+            )
+
+        def wrapper(metric_fn: Callable):
+            @metric_partial(
+                engine=engine,
+                partial_fn_type=partial_fn_type,
+                domain_type=domain_type,
+                **kwargs,
+            )
+            @wraps(metric_fn)
+            def inner_func(
+                cls,
+                execution_engine: SparkDFExecutionEngine,
+                metric_domain_kwargs: Dict,
+                metric_value_kwargs: Dict,
+                metrics: Dict[str, Any],
+                runtime_configuration: Dict,
+            ):
+                (
+                    data,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                ) = execution_engine.get_compute_domain(
+                    domain_kwargs=metric_domain_kwargs, domain_type=domain_type
+                )
+
+                column_list = accessor_domain_kwargs["column_list"]
+
+                for column_name in column_list:
+                    if column_name not in metrics["table.columns"]:
+                        raise ge_exceptions.ExecutionEngineError(
+                            message=f'Error: The column "{column_name}" in BatchData does not exist.'
+                        )
+
+                expected_condition = metric_fn(
+                    cls,
+                    data[column_list],
+                    **metric_value_kwargs,
+                    _metrics=metrics,
+                )
+                return (
+                    ~expected_condition,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                )
+
+            return inner_func
+
+        return wrapper
+
     else:
         raise ValueError("Unsupported engine for multicolumn_condition_partial")
 
@@ -1230,7 +1479,7 @@ def _pandas_column_pair_map_condition_filtered_row_count(
     metrics: Dict[str, Any],
     **kwargs,
 ):
-    """Return values from the specified domain that match the map-style metric in the metrics dictionary."""
+    """Return record counts from the specified domain that match the map-style metric in the metrics dictionary."""
     _, compute_domain_kwargs, accessor_domain_kwargs = metrics["unexpected_condition"]
     """
     In order to invoke the "ignore_row_if" filtering logic, "execution_engine.get_domain_records()" must be supplied
@@ -1324,7 +1573,7 @@ def _pandas_multicolumn_map_condition_filtered_row_count(
     metrics: Dict[str, Any],
     **kwargs,
 ):
-    """Return values from the specified domain that match the map-style metric in the metrics dictionary."""
+    """Return record counts from the specified domain that match the map-style metric in the metrics dictionary."""
     _, compute_domain_kwargs, accessor_domain_kwargs = metrics["unexpected_condition"]
     """
     In order to invoke the "ignore_row_if" filtering logic, "execution_engine.get_domain_records()" must be supplied
@@ -1843,11 +2092,21 @@ def _sqlalchemy_column_pair_map_condition_filtered_row_count(
     metrics: Dict[str, Any],
     **kwargs,
 ):
-    """Return value counts from the specified domain that match the map-style metric in the metrics dictionary."""
+    """Return record counts from the specified domain that match the map-style metric in the metrics dictionary."""
     _, compute_domain_kwargs, accessor_domain_kwargs = metrics["unexpected_condition"]
     selectable = execution_engine.get_domain_records(
         domain_kwargs=compute_domain_kwargs,
     )
+    """Return values from the specified domain that match the map-style metric in the metrics dictionary."""
+    _, compute_domain_kwargs, accessor_domain_kwargs = metrics["unexpected_condition"]
+    """
+    In order to invoke the "ignore_row_if" filtering logic, "execution_engine.get_domain_records()" must be supplied
+    with all of the available "domain_kwargs" keys.
+    """
+    domain_kwargs = dict(**compute_domain_kwargs, **accessor_domain_kwargs)
+    df = execution_engine.get_domain_records(
+        domain_kwargs=domain_kwargs,
+    )
 
     # noinspection PyPep8Naming
     column_A_name = accessor_domain_kwargs["column_A"]
@@ -1927,7 +2186,7 @@ def _sqlalchemy_multicolumn_map_condition_filtered_row_count(
     metrics: Dict[str, Any],
     **kwargs,
 ):
-    """Return value counts from the specified domain that match the map-style metric in the metrics dictionary."""
+    """Return record counts from the specified domain that match the map-style metric in the metrics dictionary."""
     _, compute_domain_kwargs, accessor_domain_kwargs = metrics["unexpected_condition"]
     selectable = execution_engine.get_domain_records(
         domain_kwargs=compute_domain_kwargs,
@@ -2071,7 +2330,7 @@ def _spark_map_condition_unexpected_count_value(
     return filtered.count()
 
 
-def spark_column_map_condition_values(
+def _spark_column_map_condition_values(
     cls,
     execution_engine: SparkDFExecutionEngine,
     metric_domain_kwargs: Dict,
@@ -2079,6 +2338,7 @@ def spark_column_map_condition_values(
     metrics: Dict[str, Any],
     **kwargs,
 ):
+    """Return values from the specified domain that match the map-style metric in the metrics dictionary."""
     unexpected_condition, compute_domain_kwargs, accessor_domain_kwargs = metrics.get(
         "unexpected_condition"
     )
@@ -2090,7 +2350,7 @@ def spark_column_map_condition_values(
     if "column" not in accessor_domain_kwargs:
         raise ValueError(
             """No "column" found in provided metric_domain_kwargs, but it is required for a column map metric
-(spark_column_map_condition_values).
+(_spark_column_map_condition_values).
 """
         )
 
@@ -2181,6 +2441,194 @@ def _spark_map_condition_rows(
         return filtered.limit(result_format["partial_unexpected_count"]).collect()
 
 
+def _spark_column_pair_map_condition_values(
+    cls,
+    execution_engine: SparkDFExecutionEngine,
+    metric_domain_kwargs: Dict,
+    metric_value_kwargs: Dict,
+    metrics: Dict[str, Any],
+    **kwargs,
+):
+    """Return values from the specified domain that match the map-style metric in the metrics dictionary."""
+    (
+        boolean_mapped_unexpected_values,
+        compute_domain_kwargs,
+        accessor_domain_kwargs,
+    ) = metrics["unexpected_condition"]
+    """
+    In order to invoke the "ignore_row_if" filtering logic, "execution_engine.get_domain_records()" must be supplied
+    with all of the available "domain_kwargs" keys.
+    """
+    domain_kwargs = dict(**compute_domain_kwargs, **accessor_domain_kwargs)
+    df = execution_engine.get_domain_records(
+        domain_kwargs=domain_kwargs,
+    )
+
+    # noinspection PyPep8Naming
+    column_A_name = accessor_domain_kwargs["column_A"]
+    # noinspection PyPep8Naming
+    column_B_name = accessor_domain_kwargs["column_B"]
+
+    column_list = [column_A_name, column_B_name]
+
+    for column_name in column_list:
+        if column_name not in metrics["table.columns"]:
+            raise ge_exceptions.ExecutionEngineError(
+                message=f'Error: The column "{column_name}" in BatchData does not exist.'
+            )
+
+    data = df.withColumn("__unexpected", boolean_mapped_unexpected_values)
+
+    filtered = data.filter(F.col("__unexpected") == True).drop(F.col("__unexpected"))
+
+    result_format = metric_value_kwargs["result_format"]
+    if result_format["result_format"] == "COMPLETE":
+        rows = filtered.select([F.col(column_A_name), F.col(column_B_name)]).collect()
+    else:
+        rows = (
+            filtered.select([F.col(column_A_name), F.col(column_B_name)])
+            .limit(result_format["partial_unexpected_count"])
+            .collect()
+        )
+
+    unexpected_list = [(row[column_A_name], row[column_B_name]) for row in rows]
+    return unexpected_list
+
+
+def _spark_column_pair_map_condition_filtered_row_count(
+    cls,
+    execution_engine: SparkDFExecutionEngine,
+    metric_domain_kwargs: Dict,
+    metric_value_kwargs: Dict,
+    metrics: Dict[str, Any],
+    **kwargs,
+):
+    """Return record counts from the specified domain that match the map-style metric in the metrics dictionary."""
+    _, compute_domain_kwargs, accessor_domain_kwargs = metrics["unexpected_condition"]
+    """
+    In order to invoke the "ignore_row_if" filtering logic, "execution_engine.get_domain_records()" must be supplied
+    with all of the available "domain_kwargs" keys.
+    """
+    domain_kwargs = dict(**compute_domain_kwargs, **accessor_domain_kwargs)
+    df = execution_engine.get_domain_records(
+        domain_kwargs=domain_kwargs,
+    )
+
+    # noinspection PyPep8Naming
+    column_A_name = accessor_domain_kwargs["column_A"]
+    # noinspection PyPep8Naming
+    column_B_name = accessor_domain_kwargs["column_B"]
+
+    column_list = [column_A_name, column_B_name]
+
+    for column_name in column_list:
+        if column_name not in metrics["table.columns"]:
+            raise ge_exceptions.ExecutionEngineError(
+                message=f'Error: The column "{column_name}" in BatchData does not exist.'
+            )
+
+    return df.count()
+
+
+def _spark_multicolumn_map_condition_values(
+    cls,
+    execution_engine: SqlAlchemyExecutionEngine,
+    metric_domain_kwargs: Dict,
+    metric_value_kwargs: Dict,
+    metrics: Dict[str, Any],
+    **kwargs,
+):
+    """Return values from the specified domain that match the map-style metric in the metrics dictionary."""
+    (
+        boolean_mapped_unexpected_values,
+        compute_domain_kwargs,
+        accessor_domain_kwargs,
+    ) = metrics["unexpected_condition"]
+    """
+    In order to invoke the "ignore_row_if" filtering logic, "execution_engine.get_domain_records()" must be supplied
+    with all of the available "domain_kwargs" keys.
+    """
+    domain_kwargs = dict(**compute_domain_kwargs, **accessor_domain_kwargs)
+    df = execution_engine.get_domain_records(
+        domain_kwargs=domain_kwargs,
+    )
+
+    if "column_list" not in accessor_domain_kwargs:
+        raise ValueError(
+            """No "column_list" found in provided metric_domain_kwargs, but it is required for a multicolumn map metric
+(_spark_multicolumn_map_condition_values).
+"""
+        )
+
+    column_list = accessor_domain_kwargs["column_list"]
+
+    for column_name in column_list:
+        if column_name not in metrics["table.columns"]:
+            raise ge_exceptions.ExecutionEngineError(
+                message=f'Error: The column "{column_name}" in BatchData does not exist.'
+            )
+
+    data = df.withColumn("__unexpected", boolean_mapped_unexpected_values)
+
+    filtered = data.filter(F.col("__unexpected") == True).drop(F.col("__unexpected"))
+
+    column_select = [F.col(column_name) for column_name in column_list]
+
+    domain_values = filtered.select(column_select)
+
+    result_format = metric_value_kwargs["result_format"]
+    if result_format["result_format"] == "COMPLETE":
+        domain_values = (
+            domain_values.select(column_select).toPandas().to_dict("records")
+        )
+    else:
+        domain_values = (
+            domain_values.select(column_select)
+            .limit(result_format["partial_unexpected_count"])
+            .toPandas()
+            .to_dict("records")
+        )
+
+    return domain_values
+
+
+def _spark_multicolumn_map_condition_filtered_row_count(
+    cls,
+    execution_engine: SparkDFExecutionEngine,
+    metric_domain_kwargs: Dict,
+    metric_value_kwargs: Dict,
+    metrics: Dict[str, Any],
+    **kwargs,
+):
+    """Return record counts from the specified domain that match the map-style metric in the metrics dictionary."""
+    _, compute_domain_kwargs, accessor_domain_kwargs = metrics["unexpected_condition"]
+    """
+    In order to invoke the "ignore_row_if" filtering logic, "execution_engine.get_domain_records()" must be supplied
+    with all of the available "domain_kwargs" keys.
+    """
+    domain_kwargs = dict(**compute_domain_kwargs, **accessor_domain_kwargs)
+    df = execution_engine.get_domain_records(
+        domain_kwargs=domain_kwargs,
+    )
+
+    if "column_list" not in accessor_domain_kwargs:
+        raise ValueError(
+            """No "column_list" found in provided metric_domain_kwargs, but it is required for a multicolumn map metric
+(_spark_multicolumn_map_condition_filtered_row_count).
+"""
+        )
+
+    column_list = accessor_domain_kwargs["column_list"]
+
+    for column_name in column_list:
+        if column_name not in metrics["table.columns"]:
+            raise ge_exceptions.ExecutionEngineError(
+                message=f'Error: The column "{column_name}" in BatchData does not exist.'
+            )
+
+    return df.count()
+
+
 class MapMetricProvider(MetricProvider):
     condition_domain_keys = (
         "batch_id",
@@ -2473,53 +2921,103 @@ def _register_metric_functions(cls):
                         metric_fn_type=MetricFunctionTypes.VALUE,
                     )
                     if metric_fn_type == MetricPartialFunctionTypes.MAP_CONDITION_FN:
+                        if domain_type == MetricDomainTypes.COLUMN:
+                            register_metric(
+                                metric_name=metric_name
+                                + ".unexpected_count.aggregate_fn",
+                                metric_domain_keys=metric_domain_keys,
+                                metric_value_keys=metric_value_keys,
+                                execution_engine=engine,
+                                metric_class=cls,
+                                metric_provider=_spark_map_condition_unexpected_count_aggregate_fn,
+                                metric_fn_type=MetricPartialFunctionTypes.AGGREGATE_FN,
+                            )
+                            register_metric(
+                                metric_name=metric_name + ".unexpected_count",
+                                metric_domain_keys=metric_domain_keys,
+                                metric_value_keys=metric_value_keys,
+                                execution_engine=engine,
+                                metric_class=cls,
+                                metric_provider=None,
+                                metric_fn_type=MetricFunctionTypes.VALUE,
+                            )
+                        else:
+                            register_metric(
+                                metric_name=metric_name + ".unexpected_count",
+                                metric_domain_keys=metric_domain_keys,
+                                metric_value_keys=metric_value_keys,
+                                execution_engine=engine,
+                                metric_class=cls,
+                                metric_provider=_spark_map_condition_unexpected_count_value,
+                                metric_fn_type=MetricFunctionTypes.VALUE,
+                            )
+                    elif (
+                        metric_fn_type == MetricPartialFunctionTypes.WINDOW_CONDITION_FN
+                    ):
                         register_metric(
-                            metric_name=metric_name + ".unexpected_count.aggregate_fn",
+                            metric_name=metric_name + ".unexpected_count",
                             metric_domain_keys=metric_domain_keys,
                             metric_value_keys=metric_value_keys,
                             execution_engine=engine,
                             metric_class=cls,
-                            metric_provider=_spark_map_condition_unexpected_count_aggregate_fn,
-                            metric_fn_type=MetricPartialFunctionTypes.AGGREGATE_FN,
+                            metric_provider=_spark_map_condition_unexpected_count_value,
+                            metric_fn_type=MetricFunctionTypes.VALUE,
                         )
+                    if domain_type == MetricDomainTypes.COLUMN:
                         register_metric(
-                            metric_name=metric_name + ".unexpected_count",
+                            metric_name=metric_name + ".unexpected_values",
                             metric_domain_keys=metric_domain_keys,
-                            metric_value_keys=metric_value_keys,
+                            metric_value_keys=(*metric_value_keys, "result_format"),
                             execution_engine=engine,
                             metric_class=cls,
-                            metric_provider=None,
+                            metric_provider=_spark_column_map_condition_values,
                             metric_fn_type=MetricFunctionTypes.VALUE,
                         )
-                    elif (
-                        metric_fn_type == MetricPartialFunctionTypes.WINDOW_CONDITION_FN
-                    ):
                         register_metric(
-                            metric_name=metric_name + ".unexpected_count",
+                            metric_name=metric_name + ".unexpected_value_counts",
                             metric_domain_keys=metric_domain_keys,
-                            metric_value_keys=metric_value_keys,
+                            metric_value_keys=(*metric_value_keys, "result_format"),
                             execution_engine=engine,
                             metric_class=cls,
-                            metric_provider=_spark_map_condition_unexpected_count_value,
+                            metric_provider=_spark_column_map_condition_value_counts,
                             metric_fn_type=MetricFunctionTypes.VALUE,
                         )
-                    if domain_type == MetricDomainTypes.COLUMN:
+                    elif domain_type == MetricDomainTypes.COLUMN_PAIR:
                         register_metric(
                             metric_name=metric_name + ".unexpected_values",
                             metric_domain_keys=metric_domain_keys,
                             metric_value_keys=(*metric_value_keys, "result_format"),
                             execution_engine=engine,
                             metric_class=cls,
-                            metric_provider=spark_column_map_condition_values,
+                            metric_provider=_spark_column_pair_map_condition_values,
                             metric_fn_type=MetricFunctionTypes.VALUE,
                         )
                         register_metric(
-                            metric_name=metric_name + ".unexpected_value_counts",
+                            metric_name=metric_name + ".filtered_row_count",
                             metric_domain_keys=metric_domain_keys,
                             metric_value_keys=(*metric_value_keys, "result_format"),
                             execution_engine=engine,
                             metric_class=cls,
-                            metric_provider=_spark_column_map_condition_value_counts,
+                            metric_provider=_spark_column_pair_map_condition_filtered_row_count,
+                            metric_fn_type=MetricFunctionTypes.VALUE,
+                        )
+                    elif domain_type == MetricDomainTypes.MULTICOLUMN:
+                        register_metric(
+                            metric_name=metric_name + ".unexpected_values",
+                            metric_domain_keys=metric_domain_keys,
+                            metric_value_keys=(*metric_value_keys, "result_format"),
+                            execution_engine=engine,
+                            metric_class=cls,
+                            metric_provider=_spark_multicolumn_map_condition_values,
+                            metric_fn_type=MetricFunctionTypes.VALUE,
+                        )
+                        register_metric(
+                            metric_name=metric_name + ".filtered_row_count",
+                            metric_domain_keys=metric_domain_keys,
+                            metric_value_keys=(*metric_value_keys, "result_format"),
+                            execution_engine=engine,
+                            metric_class=cls,
+                            metric_provider=_spark_multicolumn_map_condition_filtered_row_count,
                             metric_fn_type=MetricFunctionTypes.VALUE,
                         )
             elif metric_fn_type in [
diff --git a/great_expectations/expectations/metrics/multicolumn_map_metrics/multicolumn_sum_equal.py b/great_expectations/expectations/metrics/multicolumn_map_metrics/multicolumn_sum_equal.py
--- a/great_expectations/expectations/metrics/multicolumn_map_metrics/multicolumn_sum_equal.py
+++ b/great_expectations/expectations/metrics/multicolumn_map_metrics/multicolumn_sum_equal.py
@@ -1,7 +1,9 @@
 from great_expectations.execution_engine import (
     PandasExecutionEngine,
+    SparkDFExecutionEngine,
     SqlAlchemyExecutionEngine,
 )
+from great_expectations.execution_engine.sparkdf_execution_engine import F
 from great_expectations.expectations.metrics.import_manager import sa
 from great_expectations.expectations.metrics.map_metric_provider import (
     MulticolumnMapMetricProvider,
@@ -33,3 +35,12 @@ def _sqlalchemy(cls, column_list, **kwargs):
         sum_total = kwargs.get("sum_total")
         row_wise_cond = sa.case((sum(column_list) == sum_total, True), else_=False)
         return row_wise_cond
+
+    @multicolumn_condition_partial(engine=SparkDFExecutionEngine)
+    def _spark(cls, column_list, **kwargs):
+        sum_total = kwargs.get("sum_total")
+        expression = "+".join(
+            [f"COALESCE({column_name}, 0)" for column_name in column_list.columns]
+        )
+        row_wise_cond = F.expr(expression) == F.lit(sum_total)
+        return row_wise_cond


Test patch:
diff --git a/tests/expectations/metrics/test_core.py b/tests/expectations/metrics/test_core.py
--- a/tests/expectations/metrics/test_core.py
+++ b/tests/expectations/metrics/test_core.py
@@ -2647,6 +2647,202 @@ def test_map_multicolumn_sum_equal_sa(sa):
     assert metrics[unexpected_values_metric.id] == [{"a": 2, "b": 3, "c": 1}]
 
 
+def test_map_multicolumn_sum_equal_spark(spark_session):
+    engine: SparkDFExecutionEngine = build_spark_engine(
+        spark=spark_session,
+        df=pd.DataFrame(
+            data={"a": [0, 1, 2], "b": [5, 4, 3], "c": [0, 0, 1], "d": [7, 8, 9]}
+        ),
+        batch_id="my_id",
+    )
+
+    metrics: dict = {}
+
+    table_columns_metric: MetricConfiguration
+    results: dict
+
+    table_columns_metric, results = get_table_columns_metric(engine=engine)
+    metrics.update(results)
+
+    """
+    Two tests:
+    1. Pass -- no unexpected rows.
+    2. Fail -- one unexpected row.
+    """
+
+    # Save original metrics for testing unexpected results.
+    metrics_save: dict = copy.deepcopy(metrics)
+
+    metric_name: str = "multicolumn_sum.equal"
+    condition_metric_name: str = f"{metric_name}.condition"
+    unexpected_count_metric_name: str = f"{metric_name}.unexpected_count"
+    unexpected_rows_metric_name: str = f"{metric_name}.unexpected_rows"
+    unexpected_values_metric_name: str = f"{metric_name}.unexpected_values"
+
+    # First, assert Pass (no unexpected results).
+    condition_metric = MetricConfiguration(
+        metric_name=condition_metric_name,
+        metric_domain_kwargs={
+            "column_list": ["a", "b"],
+        },
+        metric_value_kwargs={
+            "sum_total": 5,
+        },
+        metric_dependencies={
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(condition_metric,),
+        metrics=metrics,
+    )
+    metrics.update(results)
+
+    unexpected_count_metric = MetricConfiguration(
+        metric_name=unexpected_count_metric_name,
+        metric_domain_kwargs={
+            "column_list": ["a", "b"],
+        },
+        metric_value_kwargs=None,
+        metric_dependencies={
+            "unexpected_condition": condition_metric,
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(unexpected_count_metric,), metrics=metrics
+    )
+    metrics.update(results)
+
+    # Condition metrics return "negative logic" series.
+    assert metrics[unexpected_count_metric.id] == 0
+
+    unexpected_rows_metric = MetricConfiguration(
+        metric_name=unexpected_rows_metric_name,
+        metric_domain_kwargs={
+            "column_list": ["a", "b"],
+        },
+        metric_value_kwargs={
+            "result_format": {"result_format": "SUMMARY", "partial_unexpected_count": 3}
+        },
+        metric_dependencies={
+            "unexpected_condition": condition_metric,
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(unexpected_rows_metric,), metrics=metrics
+    )
+    metrics.update(results)
+
+    assert len(metrics[unexpected_rows_metric.id]) == 0
+
+    unexpected_values_metric = MetricConfiguration(
+        metric_name=unexpected_values_metric_name,
+        metric_domain_kwargs={
+            "column_list": ["a", "b"],
+        },
+        metric_value_kwargs={
+            "result_format": {"result_format": "SUMMARY", "partial_unexpected_count": 3}
+        },
+        metric_dependencies={
+            "unexpected_condition": condition_metric,
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(unexpected_values_metric,), metrics=metrics
+    )
+    metrics.update(results)
+
+    assert len(metrics[unexpected_values_metric.id]) == 0
+    assert metrics[unexpected_values_metric.id] == []
+
+    # Restore from saved original metrics in order to start fresh on testing for unexpected results.
+    metrics = copy.deepcopy(metrics_save)
+
+    # Second, assert Fail (one unexpected result).
+
+    condition_metric = MetricConfiguration(
+        metric_name=condition_metric_name,
+        metric_domain_kwargs={
+            "column_list": ["a", "b", "c"],
+        },
+        metric_value_kwargs={
+            "sum_total": 5,
+        },
+        metric_dependencies={
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(condition_metric,),
+        metrics=metrics,
+    )
+    metrics.update(results)
+
+    unexpected_count_metric = MetricConfiguration(
+        metric_name=unexpected_count_metric_name,
+        metric_domain_kwargs={
+            "column_list": ["a", "b", "c"],
+        },
+        metric_value_kwargs=None,
+        metric_dependencies={
+            "unexpected_condition": condition_metric,
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(unexpected_count_metric,), metrics=metrics
+    )
+    metrics.update(results)
+
+    # Condition metrics return "negative logic" series.
+    assert metrics[unexpected_count_metric.id] == 1
+
+    unexpected_rows_metric = MetricConfiguration(
+        metric_name=unexpected_rows_metric_name,
+        metric_domain_kwargs={
+            "column_list": ["a", "b", "c"],
+        },
+        metric_value_kwargs={
+            "result_format": {"result_format": "SUMMARY", "partial_unexpected_count": 3}
+        },
+        metric_dependencies={
+            "unexpected_condition": condition_metric,
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(unexpected_rows_metric,), metrics=metrics
+    )
+    metrics.update(results)
+
+    assert metrics[unexpected_rows_metric.id] == [(2, 3, 1, 9)]
+    assert len(metrics[unexpected_rows_metric.id][0]) == 4
+
+    unexpected_values_metric = MetricConfiguration(
+        metric_name=unexpected_values_metric_name,
+        metric_domain_kwargs={
+            "column_list": ["a", "b", "c"],
+        },
+        metric_value_kwargs={
+            "result_format": {"result_format": "SUMMARY", "partial_unexpected_count": 3}
+        },
+        metric_dependencies={
+            "unexpected_condition": condition_metric,
+            "table.columns": table_columns_metric,
+        },
+    )
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(unexpected_values_metric,), metrics=metrics
+    )
+    metrics.update(results)
+
+    assert len(metrics[unexpected_values_metric.id]) == 1
+    assert metrics[unexpected_values_metric.id] == [{"a": 2, "b": 3, "c": 1}]
+
+
 def test_map_compound_columns_unique_pd():
     engine = build_pandas_engine(
         pd.DataFrame(data={"a": [0, 1, 1], "b": [1, 2, 3], "c": [0, 2, 2]})