Repository: ibis-project/ibis

Issue:
bug: execution of struct literals in the pandas and dask backends returns the wrong value
### What happened?

```python
import pandas as pd
from ibis.common.collections import frozendict
pd.Series([frozendict({"a": 5, "b": 6})])
# 0    (a, b)
# dtype: object
```

This is causing casting bugs in the pandas and dask backends:

```python
import ibis

con = ibis.pandas.connect()

s = ibis.struct({"a": 1, "b": 2})
s2 = s.cast("struct<a: float64, b: float64>")
con.execute(s2)
# {'a': 'a'}
```

found this in https://github.com/ibis-project/ibis/pull/8666

I think we want to make it so our builtin frozendict looks exactly like a dict to pandas. Then we get the right behavior here

### What version of ibis are you using?

main

### What backend(s) are you using, if any?

_No response_

### Relevant log output

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:

Resolving patch:
diff --git a/ibis/common/collections.py b/ibis/common/collections.py
--- a/ibis/common/collections.py
+++ b/ibis/common/collections.py
@@ -3,7 +3,6 @@
 import collections.abc
 from abc import abstractmethod
 from itertools import tee
-from types import MappingProxyType
 from typing import TYPE_CHECKING, Any, Generic, TypeVar
 
 from public import public
@@ -276,42 +275,26 @@ def isdisjoint(self, other: collections.abc.Mapping) -> bool:
 
 
 @public
-class FrozenDict(Mapping[K, V], Hashable):
-    """Immutable dictionary with a precomputed hash value."""
-
-    __slots__ = ("__view__", "__precomputed_hash__")
-    __view__: MappingProxyType
+class FrozenDict(dict, Mapping[K, V], Hashable):
+    __slots__ = ("__precomputed_hash__",)
     __precomputed_hash__: int
 
     def __init__(self, *args, **kwargs):
-        dictview = MappingProxyType(dict(*args, **kwargs))
-        dicthash = hash(tuple(dictview.items()))
-        object.__setattr__(self, "__view__", dictview)
-        object.__setattr__(self, "__precomputed_hash__", dicthash)
+        super().__init__(*args, **kwargs)
+        hashable = frozenset(self.items())
+        object.__setattr__(self, "__precomputed_hash__", hash(hashable))
 
-    def __str__(self):
-        return str(self.__view__)
+    def __hash__(self) -> int:
+        return self.__precomputed_hash__
 
-    def __repr__(self):
-        return f"{self.__class__.__name__}({dict(self.__view__)!r})"
+    def __setitem__(self, key: K, value: V) -> None:
+        raise TypeError("'FrozenDict' object does not support item assignment")
 
     def __setattr__(self, name: str, _: Any) -> None:
         raise TypeError(f"Attribute {name!r} cannot be assigned to frozendict")
 
-    def __reduce__(self):
-        return self.__class__, (dict(self.__view__),)
-
-    def __iter__(self):
-        return iter(self.__view__)
-
-    def __len__(self):
-        return len(self.__view__)
-
-    def __getitem__(self, key):
-        return self.__view__[key]
-
-    def __hash__(self):
-        return self.__precomputed_hash__
+    def __reduce__(self) -> tuple:
+        return (self.__class__, (dict(self),))
 
 
 class RewindableIterator(Iterator[V]):


Test patch:
diff --git a/ibis/backends/pandas/tests/test_structs.py b/ibis/backends/pandas/tests/test_structs.py
--- a/ibis/backends/pandas/tests/test_structs.py
+++ b/ibis/backends/pandas/tests/test_structs.py
@@ -57,6 +57,9 @@ def test_struct_field_literal(value):
     result = con.execute(expr)
     assert result == 0
 
+    expr = struct.cast("struct<fruit: string, weight: float64>")
+    assert con.execute(expr) == {"fruit": "pear", "weight": 0.0}
+
 
 def test_struct_field_series(struct_table):
     t = struct_table
diff --git a/ibis/backends/tests/test_param.py b/ibis/backends/tests/test_param.py
--- a/ibis/backends/tests/test_param.py
+++ b/ibis/backends/tests/test_param.py
@@ -202,7 +202,6 @@ def test_scalar_param_date(backend, alltypes, value):
         "risingwave",
         "datafusion",
         "clickhouse",
-        "polars",
         "sqlite",
         "impala",
         "oracle",
diff --git a/ibis/common/tests/test_collections.py b/ibis/common/tests/test_collections.py
--- a/ibis/common/tests/test_collections.py
+++ b/ibis/common/tests/test_collections.py
@@ -423,12 +423,9 @@ def test_frozendict():
     with pytest.raises(TypeError, match=msg):
         d["d"] = 4
 
-    with pytest.raises(TypeError):
-        d.__view__["a"] = 2
-    with pytest.raises(TypeError):
-        d.__view__ = {"a": 2}
+    assert hash(FrozenDict(a=1, b=2)) == hash(FrozenDict(b=2, a=1))
+    assert hash(FrozenDict(a=1, b=2)) != hash(d)
 
-    assert hash(d)
     assert_pickle_roundtrip(d)