Repository: unionai-oss/pandera

Issue:
Regression in pandera>=0.14.0 : missing columns appear to be grouped.
**Describe the bug**

Pandera only reports one missing column when there are multiple.

- [x] I have checked that this issue has not already been reported.
- [x] I have confirmed this bug exists on the latest version of pandera.
- [ ] (optional) I have confirmed this bug exists on the master branch of pandera.

**Note**: Please read [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) detailing how to provide the necessary information for us to reproduce your bug.

#### Code Sample, a copy-pastable example

```python
import pandera as pa
import numpy as np
import pandas as pd

schema = pa.DataFrameSchema(
    {"column3": pa.Column(int),
     "column2": pa.Column(float)}
)

df = pd.DataFrame({"column1": [1]})
try:
    schema.validate(df, lazy=True)
except pa.errors.SchemaErrors as e:
    assert len(e.schema_errors) == 2
```

#### Expected behavior

There should be two schema errors (column 2/3 missing), and there is only one. This works on `pandera<0.14.0`.

#### Desktop (please complete the following information):

 - OS: Mac OSX 12.6 monterey

#### Screenshots
If applicable, add screenshots to help explain your problem.

#### Additional context
Automated integration testing for [hamilton](https://github.com/DAGWorks-Inc/hamilton) caught this. Resulted in CI error here: https://github.com/DAGWorks-Inc/hamilton/pull/110. 

Can't immediately see what might have caused this, but I imagine its one of these: https://github.com/unionai-oss/pandera/releases/tag/v0.14.0. 

Thanks folks!


Discussion:
thanks! taking a look
> thanks! taking a look

Thank you 🙌. Will be happy to test out RC releases/other ways of versioning branches if you want :)
@elijahbenizzy is it possible to share the error message prior to the regression?
> @elijahbenizzy is it possible to share the error message prior to the regression?

So the script above produced no error message -- it passed (the assert statement was fine). Prior to the regression, however, the schema errors looked like this:

```python
Error Counts
------------
- column_not_in_dataframe: 2

Schema Error Summary
--------------------
                                                 failure_cases  n_failure_cases
schema_context  column check
DataFrameSchema <NA>   column_in_dataframe  [column3, column2]                2

Usage Tip
---------

Directly inspect all errors by catching the exception:
```

Now it looks like this:

```python
Error Counts
------------
- column_not_in_dataframe: 1

Schema Error Summary
--------------------
                                           failure_cases  n_failure_cases
schema_context  column check
DataFrameSchema <NA>   column_in_dataframe     [column3]                1

Usage Tip
---------

Directly inspect all errors by catching the exception:
```




Resolving patch:
diff --git a/pandera/backends/pandas/container.py b/pandera/backends/pandas/container.py
--- a/pandera/backends/pandas/container.py
+++ b/pandera/backends/pandas/container.py
@@ -67,8 +67,12 @@ def validate(
 
         try:
             self.check_column_presence(check_obj, schema, column_info)
-        except SchemaError as exc:
-            error_handler.collect_error(exc.reason_code, exc)
+        except SchemaErrors as exc:
+            for schema_error in exc.schema_errors:
+                error_handler.collect_error(
+                    schema_error["reason_code"],
+                    schema_error["error"],
+                )
 
         # strictness check and filter
         try:
@@ -471,18 +475,27 @@ def check_column_presence(
     ):
         """Check for presence of specified columns in the data object."""
         if column_info.absent_column_names:
-            # NOTE: only report the first absent column for now, need to update
-            # this when backend stuff is complete
-            colname, *_ = column_info.absent_column_names
-            raise SchemaError(
+            reason_code = "column_not_in_dataframe"
+            raise SchemaErrors(
                 schema=schema,
+                schema_errors=[
+                    {
+                        "reason_code": reason_code,
+                        "error": SchemaError(
+                            schema=schema,
+                            data=check_obj,
+                            message=(
+                                f"column '{colname}' not in dataframe"
+                                f"\n{check_obj.head()}"
+                            ),
+                            failure_cases=scalar_failure_case(colname),
+                            check="column_in_dataframe",
+                            reason_code=reason_code,
+                        ),
+                    }
+                    for colname in column_info.absent_column_names
+                ],
                 data=check_obj,
-                message=(
-                    f"column '{colname}' not in dataframe\n{check_obj.head()}"
-                ),
-                failure_cases=scalar_failure_case(colname),
-                check="column_in_dataframe",
-                reason_code="column_not_in_dataframe",
             )
 
     def check_column_values_are_unique(self, check_obj: pd.DataFrame, schema):


Test patch:
diff --git a/tests/core/test_schemas.py b/tests/core/test_schemas.py
--- a/tests/core/test_schemas.py
+++ b/tests/core/test_schemas.py
@@ -1965,3 +1965,24 @@ def test_column_set_unique():
     assert not test_schema.columns["a"].unique
     test_schema = test_schema.update_column("a", unique=True)
     assert test_schema.columns["a"].unique
+
+
+def test_missing_columns():
+    """Test that multiple missing columns is correctly reported."""
+    schema = DataFrameSchema(
+        {
+            "column3": Column(int),
+            "column2": Column(float),
+        }
+    )
+
+    df = pd.DataFrame({"column1": [1]})
+
+    try:
+        schema.validate(df, lazy=True)
+    except errors.SchemaErrors as exc:
+        assert (exc.failure_cases["check"] == "column_in_dataframe").all()
+        assert exc.failure_cases["failure_case"].tolist() == [
+            "column3",
+            "column2",
+        ]