Repository: ibis-project/ibis

Issue:
bug: ibis raises with Polars Array
### What happened?

```python
In [3]: import polars as pl

In [4]: import ibis

In [5]: ibis.memtable(pl.DataFrame({'a': [[1, 2], [3, 4]]}, schema={'a': pl.Array(pl.Int64, 2)}))
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[5], line 1
----> 1 ibis.memtable(pl.DataFrame({'a': [[1, 2], [3, 4]]}, schema={'a': pl.Array(pl.Int64, 2)}))

File ~/scratch/.venv/lib/python3.12/site-packages/ibis/expr/api.py:462, in memtable(data, columns, schema, name)
    457 if columns is not None and schema is not None:
    458     raise NotImplementedError(
    459         "passing `columns` and schema` is ambiguous; "
    460         "pass one or the other but not both"
    461     )
--> 462 return _memtable(data, name=name, schema=schema, columns=columns)

File ~/scratch/.venv/lib/python3.12/site-packages/ibis/common/dispatch.py:140, in lazy_singledispatch.<locals>.call(arg, *args, **kwargs)
    137 @functools.wraps(func)
    138 def call(arg, *args, **kwargs):
    139     impl = dispatcher.dispatch(type(arg))
--> 140     return impl(arg, *args, **kwargs)

File ~/scratch/.venv/lib/python3.12/site-packages/ibis/expr/api.py:557, in _memtable_from_polars_dataframe(data, name, schema, columns)
    553     assert schema is None, "if `columns` is not `None` then `schema` must be `None`"
    554     schema = sch.Schema(dict(zip(columns, sch.infer(data).values())))
    555 return ops.InMemoryTable(
    556     name=name if name is not None else util.gen_name("polars_memtable"),
--> 557     schema=sch.infer(data) if schema is None else schema,
    558     data=PolarsDataFrameProxy(data),
    559 ).to_expr()

File ~/scratch/.venv/lib/python3.12/site-packages/ibis/common/dispatch.py:140, in lazy_singledispatch.<locals>.call(arg, *args, **kwargs)
    137 @functools.wraps(func)
    138 def call(arg, *args, **kwargs):
    139     impl = dispatcher.dispatch(type(arg))
--> 140     return impl(arg, *args, **kwargs)

File ~/scratch/.venv/lib/python3.12/site-packages/ibis/expr/schema.py:373, in infer_polars_dataframe(df)
    368 @infer.register("polars.DataFrame")
    369 @infer.register("polars.LazyFrame")
    370 def infer_polars_dataframe(df):
    371     from ibis.formats.polars import PolarsSchema
--> 373     return PolarsSchema.to_ibis(df.collect_schema())

File ~/scratch/.venv/lib/python3.12/site-packages/ibis/formats/polars.py:128, in PolarsSchema.to_ibis(cls, schema)
    124 @classmethod
    125 def to_ibis(cls, schema: dict[str, pl.DataType]) -> Schema:
    126     """Convert a polars schema to a schema."""
    127     return Schema.from_tuples(
--> 128         [(name, PolarsType.to_ibis(typ)) for name, typ in schema.items()]
    129     )

File ~/scratch/.venv/lib/python3.12/site-packages/ibis/formats/polars.py:77, in PolarsType.to_ibis(cls, typ, nullable)
     72     return dt.Struct.from_tuples(
     73         [(field.name, cls.to_ibis(field.dtype)) for field in typ.fields],
     74         nullable=nullable,
     75     )
     76 else:
---> 77     return _from_polars_types[base_type](nullable=nullable)

KeyError: Array
```

### What version of ibis are you using?

9.5.0

### What backend(s) are you using, if any?

duckdb (default)

### Relevant log output

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

Discussion:

Patch:
diff --git a/ibis/formats/polars.py b/ibis/formats/polars.py
index 20cc8998d..b49ceb7e6 100644
--- a/ibis/formats/polars.py
+++ b/ibis/formats/polars.py
@@ -66,7 +66,7 @@ class PolarsType(TypeMapper):
             except AttributeError:  # pragma: no cover
                 time_unit = typ.tu  # pragma: no cover
             return dt.Interval(unit=time_unit, nullable=nullable)
-        elif base_type is pl.List:
+        elif base_type is pl.List or base_type is pl.Array:
             return dt.Array(cls.to_ibis(typ.inner), nullable=nullable)
         elif base_type is pl.Struct:
             return dt.Struct.from_tuples(


Test patch:
diff --git a/ibis/formats/tests/test_polars.py b/ibis/formats/tests/test_polars.py
index 201e42db1..d641dd096 100644
--- a/ibis/formats/tests/test_polars.py
+++ b/ibis/formats/tests/test_polars.py
@@ -171,3 +171,7 @@ def test_convert_table():
     )
     assert df.equals(sol)
     assert df.schema == sol.schema
+
+
+def test_array_type():
+    assert PolarsType.to_ibis(pl.Array(pl.Int64, 2)) == dt.Array(dt.int64)