Repository: unionai-oss/pandera

Issue:
Lazy schema validation does not raise expected errors with polars dataframes
**Describe the bug**

I have a polars dataframe that should raise multiple schema validation errors. I want to see all of the errors at once, so I'm setting `lazy=True` when performing schema validation. Currently, none of the expected errors are returned. Additionally, an unexpected attribute error is returned when switching from LazyFrame to DataFrame.

- [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.
- [x] (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

This code sample should raise schema validation errors on multiple columns but nothing is returned. 

```python
import pandera.polars as pa
from pandera.polars import Check, Column
import polars as pl

x = pl.LazyFrame(
    {
        "foo": ["bar", "baz", "test", "tester"],
        "fruit": ["strawberry", "pear", "banana", "apple"],
        "fruit2": ["strawberry", "pear", "banana", None],
    }
)

s = pa.DataFrameSchema(
    {
        "foo": Column(str, Check.str_length(max_value=4), required=True),
        "fruit": Column(
            str,
            checks=Check.isin(["apple", "strawberry", "pear"]),
            nullable=False,
        ),
        "fruit2": Column(
            str,
            checks=Check.isin(["apple", "strawberry", "pear"]),
            nullable=False,
        ),
    }
)

try:
    s.validate(x, lazy=True).collect() # should raise errors on all three columns 
except pa.errors.SchemaErrors as e:
    print(e.failure_cases)
    
# Nothing is returned...
```

When I switch from using a LazyFrame to a DataFrame, I see this error: `AttributeError: 'NoneType' object has no attribute 'with_row_count'`

```python
import pandera.polars as pa
from pandera.polars import Check, Column
import polars as pl

x = pl.DataFrame(
    {
        "foo": ["bar", "baz", "test", "tester"],
        "fruit": ["strawberry", "pear", "banana", "apple"],
        "fruit2": ["strawberry", "pear", "banana", None],
    }
)

s = pa.DataFrameSchema(
    {
        "foo": Column(str, Check.str_length(max_value=4), required=True),
        "fruit": Column(
            str,
            checks=Check.isin(["apple", "strawberry", "pear"]),
            nullable=False,
        ),
        "fruit2": Column(
            str,
            checks=Check.isin(["apple", "strawberry", "pear"]),
            nullable=False,
        ),
    }
)

try:
    s.validate(x, lazy=True).collect() # should raise errors on all three columns 
except pa.errors.SchemaErrors as e:
    print(e.failure_cases)
    
    
# Traceback (most recent call last):
#   File "C:\local\project\test_polars_validate_lazy_true.py", line 26, in <module>
#     s.validate(x, lazy=True).collect() # should raise errors on all three columns
#     ^^^^^^^^^^^^^^^^^^^^^^^^
#   File "C:\local\project\.venv\Lib\site-packages\pandera\api\polars\container.py", line 58, in validate
#     output = self.get_backend(check_obj).validate(
#              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "C:\local\project\.venv\Lib\site-packages\pandera\backends\polars\container.py", line 92, in validate
#     results = check(*args)  # type: ignore[operator]
#               ^^^^^^^^^^^^
#   File "C:\local\project\.venv\Lib\site-packages\pandera\backends\polars\container.py", line 182, in run_schema_component_checks
#     result = schema_component.validate(check_obj, lazy=lazy)
#              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "C:\local\project\.venv\Lib\site-packages\pandera\api\polars\components.py", line 143, in validate
#     output = self.get_backend(check_obj).validate(
#              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "C:\local\project\.venv\Lib\site-packages\pandera\backends\polars\components.py", line 95, in validate
#     raise SchemaErrors(
#           ^^^^^^^^^^^^^
#   File "C:\local\project\.venv\Lib\site-packages\pandera\errors.py", line 183, in __init__
#     ).failure_cases_metadata(schema.name, schema_errors)
#       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "C:\local\project\.venv\Lib\site-packages\pandera\backends\polars\base.py", line 151, in failure_cases_metadata
#     index = err.check_output.with_row_count("index").filter(
#             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# AttributeError: 'NoneType' object has no attribute 'with_row_count'
```

#### Expected behavior

I would expect the output to match what the pandas dataframe returns:

```python
import pandas as pd
import pandera as pa
from pandera import Check, Column

x = pd.DataFrame(
    {
        "foo": ["bar", "baz", "test", "tester"],
        "fruit": ["strawberry", "pear", "banana", "apple"],
        "fruit2": ["strawberry", "pear", "banana", None],
    }
)

s = pa.DataFrameSchema(
    {
        "foo": Column(str, Check.str_length(max_value=4), required=True),
        "fruit": Column(
            str,
            checks=Check.isin(["apple", "strawberry", "pear"]),
            nullable=False,
        ),
        "fruit2": Column(
            str,
            checks=Check.isin(["apple", "strawberry", "pear"]),
            nullable=False,
        ),
    }
)

try:
    s.validate(x, lazy=True)
except pa.errors.SchemaErrors as e:
    print(e.failure_cases)

#   schema_context  column                                  check check_number failure_case  index
# 0         Column     foo                    str_length(None, 4)            0       tester      3
# 1         Column   fruit  isin(['apple', 'strawberry', 'pear'])            0       banana      2
# 2         Column  fruit2                           not_nullable         None         None      3
# 3         Column  fruit2  isin(['apple', 'strawberry', 'pear'])            0       banana      2
```

#### Desktop (please complete the following information):

 - OS: `Windows 10`
 - Browser: `Chrome`
 - Version: `pandera@git+https://github.com/unionai-oss/pandera.git@870d74a79546ee0ac019d3c347bd31643a66f1cc`

#### Screenshots
If applicable, add screenshots to help explain your problem.

#### Additional context
Add any other context about the problem here.



Discussion:

Resolving patch:
diff --git a/pandera/backends/polars/base.py b/pandera/backends/polars/base.py
--- a/pandera/backends/polars/base.py
+++ b/pandera/backends/polars/base.py
@@ -170,7 +170,13 @@ def failure_cases_metadata(
                     check=pl.lit(check_identifier),
                     check_number=pl.lit(err.check_index),
                     index=index,
-                ).cast({"failure_case": pl.Utf8, "index": pl.Int32})
+                ).cast(
+                    {
+                        "failure_case": pl.Utf8,
+                        "index": pl.Int32,
+                        "check_number": pl.Int32,
+                    }
+                )
 
             else:
                 scalar_failure_cases = defaultdict(list)
diff --git a/pandera/backends/polars/components.py b/pandera/backends/polars/components.py
--- a/pandera/backends/polars/components.py
+++ b/pandera/backends/polars/components.py
@@ -10,6 +10,7 @@
 from pandera.backends.base import CoreCheckResult
 from pandera.backends.polars.base import PolarsSchemaBackend, is_float_dtype
 from pandera.config import ValidationScope, ValidationDepth, get_config_context
+from pandera.constants import CHECK_OUTPUT_KEY
 from pandera.errors import (
     ParserError,
     SchemaDefinitionError,
@@ -221,15 +222,18 @@ def check_nullable(
                 continue
             failure_cases = (
                 check_obj.with_context(
-                    isna.select(pl.col(column).alias("_isna"))
+                    isna.select(pl.col(column).alias(CHECK_OUTPUT_KEY))
                 )
-                .filter(pl.col("_isna").not_())
+                .filter(pl.col(CHECK_OUTPUT_KEY).not_())
                 .select(column)
                 .collect()
             )
             results.append(
                 CoreCheckResult(
                     passed=cast(bool, passed.select(column).item()),
+                    check_output=isna.collect().rename(
+                        {column: CHECK_OUTPUT_KEY}
+                    ),
                     check="not_nullable",
                     reason_code=SchemaErrorReason.SERIES_CONTAINS_NULLS,
                     message=(
@@ -279,6 +283,9 @@ def check_unique(
                     CoreCheckResult(
                         passed=False,
                         check=check_name,
+                        check_output=duplicates.select(
+                            pl.col(column).not_().alias(CHECK_OUTPUT_KEY)
+                        ),
                         reason_code=SchemaErrorReason.SERIES_CONTAINS_DUPLICATES,
                         message=(
                             f"column '{schema.selector}' "


Test patch:
diff --git a/tests/polars/test_polars_container.py b/tests/polars/test_polars_container.py
--- a/tests/polars/test_polars_container.py
+++ b/tests/polars/test_polars_container.py
@@ -444,6 +444,32 @@ def test_lazy_validation_errors():
         assert exc.failure_cases.shape[0] == 6
 
 
+def test_dataframe_validation_errors_nullable():
+    schema = DataFrameSchema(
+        {"a": Column(str, pa.Check.isin([*"abc"]), nullable=False)}
+    )
+    invalid_df = pl.DataFrame(
+        {"a": ["a", "b", "f", None]}
+    )  # 2 errors: "f" and None
+    try:
+        schema.validate(invalid_df, lazy=True)
+    except pa.errors.SchemaErrors as exc:
+        assert exc.failure_cases.shape[0] == 2
+
+
+def test_dataframe_validation_errors_unique():
+    schema = DataFrameSchema(
+        {"a": Column(str, pa.Check.isin([*"abc"]), unique=True)}
+    )
+    invalid_df = pl.DataFrame(
+        {"a": ["a", "b", "b", "a"]}
+    )  # 4 errors, polars reports all duplicates
+    try:
+        schema.validate(invalid_df, lazy=True)
+    except pa.errors.SchemaErrors as exc:
+        assert exc.failure_cases.shape[0] == 4
+
+
 @pytest.fixture
 def lf_with_nested_types():
     return pl.LazyFrame(