Repository: ibis-project/ibis

Issue:
bug: sqlite BLOB datatype not handled
### What happened?

I'm converting some old databases that had binary output as columns, and I am unable to work with these tables in ibis. I created this failing test to show the problem: 

```
from __future__ import annotations

import sqlite3

import pandas as pd
import pytest
from packaging.version import parse as vparse

import ibis
import ibis.expr.datatypes as dt

@pytest.fixture(scope="session")
def db(tmp_path_factory):
    path = str(tmp_path_factory.mktemp("databases") / "formats.db")
    con = sqlite3.connect(path)
    con.execute('CREATE TABLE blobs (data BLOB)')
    blob_data = b'\x00\x01\x02\x03'
    con.execute('INSERT INTO blobs (data) VALUES (?)', (blob_data,))
    con.close()
    return path

def test_insert_blob(db):
    conn = ibis.sqlite.connect(db)
    t = conn.table("blobs")
    assert(t["data"].type() == dt.blob)
```

Fails with: 

E           TypeError: Unable to convert type: BLOB()

### What version of ibis are you using?

master branch

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

.sqlite

### Relevant log output

```sh
db = '/private/var/folders/0b/z9kzmm317mb6r9nyt5q1889r0000gn/T/****/pytest-13/databases0/formats.db'

    def test_insert_blob(db):
        conn = ibis.sqlite.connect(db)
>       t = conn.table("blobs")

ibis/backends/sqlite/tests/test_blob_types.py:24: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
ibis/backends/base/sql/alchemy/__init__.py:629: in table
    schema = self._schema_from_sqla_table(
ibis/backends/base/sql/alchemy/__init__.py:555: in _schema_from_sqla_table
    dtype = cls.compiler.translator_class.get_ibis_type(
ibis/backends/base/sql/alchemy/translator.py:81: in get_ibis_type
    return cls.type_mapper.to_ibis(sqla_type, nullable=nullable)
ibis/backends/sqlite/datatypes.py:32: in to_ibis
    return super().to_ibis(typ, nullable=nullable)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

cls = <class 'ibis.backends.sqlite.datatypes.SqliteType'>, typ = BLOB(), nullable = True

    @classmethod
    def to_ibis(cls, typ: sat.TypeEngine, nullable: bool = True) -> dt.DataType:
        """Convert a SQLAlchemy type to an Ibis type.
    
        Parameters
        ----------
        typ
            SQLAlchemy type to convert.
        nullable : bool, optional
            Whether the returned type should be nullable.
    
        Returns
        -------
        Ibis type.
        """
        if dtype := _from_sqlalchemy_types.get(type(typ)):
            return dtype(nullable=nullable)
        elif isinstance(typ, sat.Float):
            if (float_typ := _FLOAT_PREC_TO_TYPE.get(typ.precision)) is not None:
                return float_typ(nullable=nullable)
            return dt.Decimal(typ.precision, typ.scale, nullable=nullable)
        elif isinstance(typ, sat.Numeric):
            return dt.Decimal(typ.precision, typ.scale, nullable=nullable)
        elif isinstance(typ, ArrayType):
            return dt.Array(cls.to_ibis(typ.value_type), nullable=nullable)
        elif isinstance(typ, sat.ARRAY):
            ndim = typ.dimensions
            if ndim is not None and ndim != 1:
                raise NotImplementedError("Nested array types not yet supported")
            return dt.Array(cls.to_ibis(typ.item_type), nullable=nullable)
        elif isinstance(typ, StructType):
            fields = {k: cls.to_ibis(v) for k, v in typ.fields.items()}
            return dt.Struct(fields, nullable=nullable)
        elif isinstance(typ, MapType):
            return dt.Map(
                cls.to_ibis(typ.key_type),
                cls.to_ibis(typ.value_type),
                nullable=nullable,
            )
        elif isinstance(typ, sa.DateTime):
            timezone = "UTC" if typ.timezone else None
            return dt.Timestamp(timezone, nullable=nullable)
        elif isinstance(typ, sat.String):
            return dt.String(nullable=nullable)
        elif geospatial_supported and isinstance(typ, ga.types._GISType):
            name = typ.geometry_type.upper()
            try:
                return _GEOSPATIAL_TYPES[name](geotype=typ.name, nullable=nullable)
            except KeyError:
                raise ValueError(f"Unrecognized geometry type: {name}")
        else:
>           raise TypeError(f"Unable to convert type: {typ!r}")
E           TypeError: Unable to convert type: BLOB()

ibis/backends/base/sql/alchemy/datatypes.py:323: TypeError
```


### Code of Conduct

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


Discussion:

Resolving patch:
diff --git a/ibis/backends/base/sql/alchemy/datatypes.py b/ibis/backends/base/sql/alchemy/datatypes.py
--- a/ibis/backends/base/sql/alchemy/datatypes.py
+++ b/ibis/backends/base/sql/alchemy/datatypes.py
@@ -142,6 +142,7 @@ class Unknown(sa.Text):
     sat.BOOLEAN: dt.Boolean,
     sat.Boolean: dt.Boolean,
     sat.BINARY: dt.Binary,
+    sat.BLOB: dt.Binary,
     sat.LargeBinary: dt.Binary,
     sat.DATE: dt.Date,
     sat.Date: dt.Date,


Test patch:
diff --git a/ibis/backends/sqlite/tests/test_types.py b/ibis/backends/sqlite/tests/test_types.py
--- a/ibis/backends/sqlite/tests/test_types.py
+++ b/ibis/backends/sqlite/tests/test_types.py
@@ -39,6 +39,7 @@ def db(tmp_path_factory):
     con.execute("CREATE TABLE timestamps (ts TIMESTAMP)")
     con.execute("CREATE TABLE timestamps_tz (ts TIMESTAMP)")
     con.execute("CREATE TABLE weird (str_col STRING, date_col ITSADATE)")
+    con.execute("CREATE TABLE blobs (data BLOB)")
     with con:
         con.executemany("INSERT INTO timestamps VALUES (?)", [(t,) for t in TIMESTAMPS])
         con.executemany(
@@ -54,6 +55,7 @@ def db(tmp_path_factory):
                 ("d", "2022-01-04"),
             ],
         )
+        con.execute("INSERT INTO blobs (data) VALUES (?)", (b"\x00\x01\x02\x03",))
     con.close()
     return path
 
@@ -90,3 +92,9 @@ def test_type_map(db):
         }
     )
     assert res.equals(sol)
+
+
+def test_read_blob(db):
+    con = ibis.sqlite.connect(db)
+    t = con.table("blobs")
+    assert t["data"].type() == dt.binary