Repository: ibis-project/ibis

Issue:
bug(duckdb): duckdb backend should add in CAST for some bind parameters
DuckDB casts bind parameters `?` to strings which leads to binder errors with some queries

If we have a small tpch dataset:

```python
import duckdb
con = duckdb.connect("tpch.ddb")
con.execute("CALL dbgen(sf=0.1)")

import ibis
con = ibis.duckdb.connect("tpch.ddb")

t = con.table('orders')
expr = t.aggregate(high_line_count=(t.o_orderpriority.case().when('1-URGENT', 1).else_(0).end().sum()

expr.execute()
```

raises

```
RuntimeError: Binder Error: No function matches the given name and argument types 'sum(VARCHAR)'. You might need to add explicit type casts.
	Candidate functions:
	sum(DECIMAL) -> DECIMAL
	sum(SMALLINT) -> HUGEINT
	sum(INTEGER) -> HUGEINT
	sum(BIGINT) -> HUGEINT
	sum(HUGEINT) -> HUGEINT
	sum(DOUBLE) -> DOUBLE

LINE 1: SELECT sum(CASE WHEN (t0.o_orderpriority = ?) ...
```

because our generated SQL doesn't have explicit casts:

```
print(expr.compile())
SELECT sum(CASE WHEN (t0.o_orderpriority = ?) THEN ? ELSE ? END) AS high_line_count 
FROM orders AS t0
```

we want to generate

```
SELECT sum(CASE WHEN (t0.o_orderpriority = ?) THEN cast(? as INTEGER) ELSE cast(? as INTEGER) END) AS high_line_count FROM orders as t0
```


Discussion:
Interestingly enough, postgres doesn't seem to have a problem with a similar query:

```
def test_case_agg(alltypes):
    expr = alltypes.aggregate(
        high_line_count=(
            alltypes.string_col.case().when("1", 1).else_(0).end().sum()
        )
    )
    expr.execute()
```
Ah that's probably because of https://github.com/duckdb/duckdb/issues/2972#issuecomment-1019283797

Resolving patch:
diff --git a/ibis/backends/duckdb/registry.py b/ibis/backends/duckdb/registry.py
--- a/ibis/backends/duckdb/registry.py
+++ b/ibis/backends/duckdb/registry.py
@@ -99,7 +99,7 @@ def _literal(_, expr):
             f"`{type(value).__name__}` isn't yet supported with the duckdb "
             "backend"
         )
-    return sa.literal(value)
+    return sa.cast(sa.literal(value), sqla_type)
 
 
 def _array_column(t, expr):


Test patch:
diff --git a/ibis/backends/tests/test_aggregation.py b/ibis/backends/tests/test_aggregation.py
--- a/ibis/backends/tests/test_aggregation.py
+++ b/ibis/backends/tests/test_aggregation.py
@@ -461,3 +461,14 @@ def collect_udf(v):
     )
 
     backend.assert_frame_equal(result, expected, check_like=True)
+
+
+@pytest.mark.notimpl(["datafusion", "pyspark"])
+def test_binds_are_cast(alltypes):
+    expr = alltypes.aggregate(
+        high_line_count=(
+            alltypes.string_col.case().when('1-URGENT', 1).else_(0).end().sum()
+        )
+    )
+
+    expr.execute()
diff --git a/ibis/backends/tests/test_generic.py b/ibis/backends/tests/test_generic.py
--- a/ibis/backends/tests/test_generic.py
+++ b/ibis/backends/tests/test_generic.py
@@ -111,7 +111,7 @@ def test_coalesce(backend, con, expr, expected):
         # False
         assert result == decimal.Decimal(str(expected))
     else:
-        assert result == expected
+        assert result == pytest.approx(expected)
 
 
 # TODO(dask) - identicalTo - #2553
diff --git a/ibis/backends/tests/test_temporal.py b/ibis/backends/tests/test_temporal.py
--- a/ibis/backends/tests/test_temporal.py
+++ b/ibis/backends/tests/test_temporal.py
@@ -362,7 +362,7 @@ def convert_to_offset(x):
             lambda t, be: t.timestamp_col.date() - ibis.date(date_value),
             lambda t, be: t.timestamp_col.dt.floor('d') - date_value,
             id='date-subtract-date',
-            marks=pytest.mark.notimpl(["duckdb", 'pyspark']),
+            marks=pytest.mark.notimpl(["pyspark"]),
         ),
     ],
 )