Repository: ibis-project/ibis

Issue:
bug: decimal type evolution during queries
During my [work](https://github.com/iree-org/iree-llvm-sandbox/pull/551) on representing decimals in relational queries, I have come across behavior where ibis deviates from the SQL standard (as described [here](https://www.ibm.com/docs/en/i/7.1?topic=operators-decimal-arithmetic-in-sql)).

```python
import ibis

table = ibis.table(schema=[("a", "decimal(4, 2)"), ("b", "decimal(5, 3)")],
                   name="t")

res = table.projection((table.a * table.b).name('c'))

print(res.c.type())
```

This code executed with ibis v3.0.0 shows that the type of c is `decimal(prec=5, scale=3)`, whereas I would argue that according to the SQL standard it should be `decimal(prec=9, scale=5)`. Is this a WIP or a deliberate design decision?


Discussion:
Hi @webmiche 👋🏻!

This is most definitely a work in progress. We'd happily accept a PR that implements the rules on the page you link to if you're up for it.

Resolving patch:
diff --git a/ibis/expr/rules.py b/ibis/expr/rules.py
--- a/ibis/expr/rules.py
+++ b/ibis/expr/rules.py
@@ -1,5 +1,6 @@
 import enum
 import functools
+import operator
 from itertools import product, starmap
 
 import ibis.common.exceptions as com
@@ -294,12 +295,48 @@ def _promote_integral_binop(exprs, op):
     return dt.highest_precedence(dtypes)
 
 
+def _promote_decimal_dtype(args, op):
+
+    if len(args) != 2:
+        return highest_precedence_dtype(args)
+
+    # TODO: Add support for setting the maximum precision and maximum scale
+    lhs_prec = args[0].type().precision
+    lhs_scale = args[0].type().scale
+    rhs_prec = args[1].type().precision
+    rhs_scale = args[1].type().scale
+    max_prec = 31 if lhs_prec <= 31 and rhs_prec <= 31 else 63
+    max_scale = 31
+
+    if op is operator.mul:
+        return dt.Decimal(
+            min(max_prec, lhs_prec + rhs_prec),
+            min(max_scale, lhs_scale + rhs_scale),
+        )
+    if op is operator.add or op is operator.sub:
+        return dt.Decimal(
+            min(
+                max_prec,
+                max(
+                    lhs_prec - lhs_scale,
+                    rhs_prec - rhs_scale,
+                )
+                + max(lhs_scale, rhs_scale)
+                + 1,
+            ),
+            max(lhs_scale, rhs_scale),
+        )
+    return highest_precedence_dtype(args)
+
+
 def numeric_like(name, op):
     @immutable_property
     def output_dtype(self):
         args = getattr(self, name)
         if util.all_of(args, ir.IntegerValue):
             result = _promote_integral_binop(args, op)
+        elif util.all_of(args, ir.DecimalValue):
+            result = _promote_decimal_dtype(args, op)
         else:
             result = highest_precedence_dtype(args)
 


Test patch:
diff --git a/ibis/tests/expr/test_decimal.py b/ibis/tests/expr/test_decimal.py
--- a/ibis/tests/expr/test_decimal.py
+++ b/ibis/tests/expr/test_decimal.py
@@ -31,6 +31,37 @@ def test_decimal_sum_type(lineitem):
     assert result.type() == dt.Decimal(38, 2)
 
 
+def test_promote_decimal_type_mul(lineitem):
+    col_1 = lineitem.l_extendedprice
+    col_2 = lineitem.l_discount
+    result = col_1 * col_2
+    assert result.type().precision == 24
+    assert result.type().scale == 4
+
+
+def test_promote_decimal_type_add(lineitem):
+    col_1 = lineitem.l_extendedprice
+    col_2 = lineitem.l_discount
+    result = col_1 + col_2
+    assert result.type().precision == 13
+    assert result.type().scale == 2
+
+
+def test_promote_decimal_type_mod(lineitem):
+    col_1 = lineitem.l_extendedprice
+    col_2 = lineitem.l_discount
+    result = col_1 % col_2
+    assert result.type().precision == 12
+    assert result.type().scale == 2
+
+
+def test_promote_decimal_type_max():
+    t = ibis.table([("a", "decimal(31, 3)"), ("b", "decimal(31, 3)")], "t")
+    result = t.a * t.b
+    assert result.type().precision == 31
+    assert result.type().scale == 6
+
+
 @pytest.mark.parametrize(
     "precision, scale, expected",
     [