Repository: unionai-oss/pandera

Issue:
Columns of type Decimal with scale=0 don't get handled correctly
**Describe the bug**
When setting the scale of a Decimal to 0 (also the default), many checks and coercions fail.

- [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.

#### Code Sample

```python
from decimal import Decimal

import pandera as pa
import pandas as pd


foo = pa.DataFrameSchema({"x": pa.Column(Decimal)})
bar = pa.DataFrameSchema({"x": pa.Column(Decimal, coerce=True)})

foo.validate(pd.DataFrame({"x": [Decimal('1')]}))    # KeyError: 1 -- expected valid schema
bar.validate(pd.DataFrame({"x": [Decimal('1')]}))    # KeyError: 1 -- expected valid schema
foo.validate(pd.DataFrame({"x": [Decimal('1.1')]}))  # SchemaError -- as expected
bar.validate(pd.DataFrame({"x": [Decimal('1.1')]}))  # SchemaError -- expected successful coercion
foo.validate(pd.DataFrame({"x": ['1']}))             # KeyError: 0 -- expected SchemaError
bar.validate(pd.DataFrame({"x": ['1']}))             # KeyError: 1 -- expected successful coercion
foo.validate(pd.DataFrame({"x": ['1.1']}))           # KeyError: 0 -- expected SchemaError
bar.validate(pd.DataFrame({"x": ['1.1']}))           # SchemaError -- expected successful coercion
foo.validate(pd.DataFrame({"x": [1]}))               # SchemaError -- as expected 
bar.validate(pd.DataFrame({"x": [1]}))               # KeyError: 1 -- expected successful coercion
foo.validate(pd.DataFrame({"x": [1.1]}))             # TypeError: confusing -- expected SchemaError
bar.validate(pd.DataFrame({"x": [1.1]}))             # SchemaError --expected successful coercion
```

#### Expected behavior
I expect all coercions to work, non-coercions to result in a schema error, and good input like `Decimal('1')` to validate successfully. Note that with a scale > 0, all coercions work, but many non-coercive validations still lead to different errors.

#### Env
 - OS: ubuntu, python3.8
 - Version: current main (eb6de711d76e26a712fa901f173bb790ae4431ab)




Discussion:

Resolving patch:
diff --git a/pandera/dtypes.py b/pandera/dtypes.py
--- a/pandera/dtypes.py
+++ b/pandera/dtypes.py
@@ -436,7 +436,9 @@ def __init__(
         object.__setattr__(self, "scale", scale)
         object.__setattr__(self, "rounding", rounding)
         object.__setattr__(
-            self, "_exp", _scale_to_exp(scale) if scale else None
+            self,
+            "_exp",
+            _scale_to_exp(scale) if scale else decimal.Decimal("1."),
         )
         object.__setattr__(
             self,
diff --git a/pandera/engines/pandas_engine.py b/pandera/engines/pandas_engine.py
--- a/pandera/engines/pandas_engine.py
+++ b/pandera/engines/pandas_engine.py
@@ -440,6 +440,8 @@ def _check_decimal(
     is_decimal = pandas_obj.apply(
         lambda x: isinstance(x, decimal.Decimal)
     ).astype("bool") | pd.isnull(pandas_obj)
+    if not is_decimal.any():
+        return is_decimal
 
     decimals = pandas_obj[is_decimal]
     # fix for modin unamed series raises KeyError
@@ -447,6 +449,8 @@ def _check_decimal(
     decimals.name = "decimals"
 
     splitted = decimals.astype("string").str.split(".", n=1, expand=True)
+    if splitted.shape[1] < 2:
+        splitted[1] = ""
     len_left = splitted[0].str.len().fillna(0)
     len_right = splitted[1].str.len().fillna(0)
     precisions = len_left + len_right
@@ -502,15 +506,13 @@ def __init__(  # pylint:disable=super-init-not-called
         dtypes.Decimal.__init__(self, precision, scale, rounding)
 
     def coerce_value(self, value: Any) -> decimal.Decimal:
-        """Coerce an value to a particular type."""
+        """Coerce a value to a particular type."""
 
         if pd.isna(value):
             return pd.NA
 
         dec = decimal.Decimal(str(value))
-        if self._exp:
-            return dec.quantize(self._exp, context=self._ctx)
-        return dec
+        return dec.quantize(self._exp, context=self._ctx)
 
     def coerce(self, data_container: pd.Series) -> pd.Series:
         return data_container.apply(self.coerce_value)
@@ -528,7 +530,7 @@ def check(  # type: ignore
             if data_container is None:
                 return False
             else:
-                return np.full_like(data_container, False)
+                return np.full_like(data_container, False, dtype=bool)
         if data_container is None:
             return True
         return _check_decimal(


Test patch:
diff --git a/tests/core/test_logical_dtypes.py b/tests/core/test_logical_dtypes.py
--- a/tests/core/test_logical_dtypes.py
+++ b/tests/core/test_logical_dtypes.py
@@ -223,3 +223,80 @@ def test_invalid_decimal_params(precision: int, scale: int):
     """Test invalid decimal params."""
     with pytest.raises(ValueError):
         pa.Decimal(precision, scale)
+
+
+@pytest.mark.parametrize(
+    "value",
+    [
+        pd.Series([Decimal("1")]),
+        pd.Series([Decimal("10")]),
+        pd.Series([Decimal("100000")]),
+    ],
+)
+def test_decimal_scale_zero(value):
+    """Testing if a scale of 0 works."""
+    check_type = pandas_engine.Decimal(28, 0)
+
+    result = check_type.check(value.dtype, value)
+
+    assert result.all()
+
+
+@pytest.mark.parametrize(
+    "value",
+    [
+        pd.Series([Decimal("1.1")]),
+        pd.Series([Decimal("1.11")]),
+        pd.Series(["1"]),
+        pd.Series(["1.1"]),
+        pd.Series([1]),
+        pd.Series([1.1]),
+    ],
+)
+def test_decimal_scale_zero_violations(value):
+    """Make sure we get proper violations here.
+
+    First half of regression test for #1008.
+    """
+    check_type = pandas_engine.Decimal(28, 0)
+
+    result = check_type.check(value.dtype, value)
+
+    assert not result.any()
+
+
+def test_decimal_scale_zero_missing_violation():
+    """Additional regression test for #1008: `Decimal.check` returned non-bool Series."""
+    check_type = pandas_engine.Decimal(28, 0)
+    value = pd.Series([1.1])
+
+    result = check_type.check(value.dtype, value)
+
+    assert result.dtype == bool
+
+
+@pytest.mark.parametrize(
+    "value",
+    [
+        pd.Series([Decimal("1")]),
+        pd.Series([Decimal("1.1")]),
+        pd.Series([Decimal("1.11")]),
+        pd.Series(["1"]),
+        pd.Series(["1.1"]),
+        pd.Series([1]),
+        pd.Series([1.1]),
+    ],
+)
+def test_decimal_scale_zero_coercions(value):
+    """Make sure coercions work.
+
+    Other half of regression test for #1008.
+    """
+    check_type = pandas_engine.Decimal(28, 0)
+
+    coerced = check_type.coerce(value)
+    result = check_type.check(
+        pandas_engine.Engine.dtype(coerced.dtype), coerced
+    )
+
+    assert result.all()