Repository: ibis-project/ibis

Issue:
bug: Unhelpful error
### What happened?

I have a postgres table which has a column of type "Vector" coming from the pg_vector extension - when I try to access this table;

```
> con.table('article_paragraphs')
```

I get the following;

<details>
<summary>stacktrace</summary>


```
---------------------------------------------------------------------------
SyntaxError                               Traceback (most recent call last)
File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:1900](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:1900), in Connection._execute_context(self, dialect, constructor, statement, parameters, execution_options, *args, **kw)
   1899     if not evt_handled:
-> 1900         self.dialect.do_execute(
   1901             cursor, statement, parameters, context
   1902         )
   1904 if self._has_events or self.engine._has_events:

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/default.py:736](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/default.py:736), in DefaultDialect.do_execute(self, cursor, statement, parameters, context)
    735 def do_execute(self, cursor, statement, parameters, context=None):
--> 736     cursor.execute(statement, parameters)

SyntaxError: syntax error at or near "article_paragraphs"
LINE 1: ...PORARY VIEW "2787edf13b8047f0909bd18a0abdb7aa" AS article_pa...
                                                             ^


The above exception was the direct cause of the following exception:

ProgrammingError                          Traceback (most recent call last)
Cell In[9], line 1
----> 1 con.table('article_paragraphs')

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/ibis/backends/base/sql/alchemy/__init__.py:520](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/ibis/backends/base/sql/alchemy/__init__.py:520), in BaseAlchemyBackend.table(self, name, database, schema)
    518     if database != self.current_database:
    519         return self.database(name=database).table(name=name, schema=schema)
--> 520 sqla_table = self._get_sqla_table(name, database=database, schema=schema)
    521 return self._sqla_table_to_expr(sqla_table)

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/ibis/backends/base/sql/alchemy/__init__.py:436](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/ibis/backends/base/sql/alchemy/__init__.py:436), in BaseAlchemyBackend._get_sqla_table(self, name, schema, autoload, **_)
    434 if not nulltype_cols:
    435     return table
--> 436 return self._handle_failed_column_type_inference(table, nulltype_cols)

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/ibis/backends/base/sql/alchemy/__init__.py:447](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/ibis/backends/base/sql/alchemy/__init__.py:447), in BaseAlchemyBackend._handle_failed_column_type_inference(self, table, nulltype_cols)
    444 dialect = self.con.dialect
    445 quoted_name = dialect.identifier_preparer.quote(table.name)
--> 447 for colname, type in self._metadata(quoted_name):
    448     if colname in nulltype_cols:
    449         # replace null types discovered by sqlalchemy with non null
    450         # types
    451         table.append_column(
    452             sa.Column(
    453                 colname,
   (...)
    458             replace_existing=True,
    459         )

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/ibis/backends/postgres/__init__.py:198](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/ibis/backends/postgres/__init__.py:198), in Backend._metadata(self, query)
    188         type_info_sql = """\
    189 SELECT
    190   attname,
   (...)
    195   AND NOT attisdropped
    196 ORDER BY attnum"""
    197         with self.begin() as con:
--> 198             con.exec_driver_sql(f"CREATE TEMPORARY VIEW {name} AS {query}")
    199             type_info = con.execute(
    200                 sa.text(type_info_sql).bindparams(raw_name=raw_name)
    201             )
    202             yield from ((col, _get_type(typestr)) for col, typestr in type_info)

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:1760](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:1760), in Connection.exec_driver_sql(self, statement, parameters, execution_options)
   1710 r"""Executes a SQL statement construct and returns a
   1711 :class:`_engine.CursorResult`.
   1712 
   (...)
   1755 
   1756 """
   1758 args_10style, kwargs_10style = _distill_params_20(parameters)
-> 1760 return self._exec_driver_sql(
   1761     statement,
   1762     args_10style,
   1763     kwargs_10style,
   1764     execution_options,
   1765     future=True,
   1766 )

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:1669](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:1669), in Connection._exec_driver_sql(self, statement, multiparams, params, execution_options, future)
   1659         (
   1660             statement,
   1661             distilled_params,
   (...)
   1665             statement, distilled_parameters, execution_options
   1666         )
   1668 dialect = self.dialect
-> 1669 ret = self._execute_context(
   1670     dialect,
   1671     dialect.execution_ctx_cls._init_statement,
   1672     statement,
   1673     distilled_parameters,
   1674     execution_options,
   1675     statement,
   1676     distilled_parameters,
   1677 )
   1679 if not future:
   1680     if self._has_events or self.engine._has_events:

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:1943](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:1943), in Connection._execute_context(self, dialect, constructor, statement, parameters, execution_options, *args, **kw)
   1940             branched.close()
   1942 except BaseException as e:
-> 1943     self._handle_dbapi_exception(
   1944         e, statement, parameters, cursor, context
   1945     )
   1947 return result

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:2124](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:2124), in Connection._handle_dbapi_exception(self, e, statement, parameters, cursor, context)
   2122     util.raise_(newraise, with_traceback=exc_info[2], from_=e)
   2123 elif should_wrap:
-> 2124     util.raise_(
   2125         sqlalchemy_exception, with_traceback=exc_info[2], from_=e
   2126     )
   2127 else:
   2128     util.raise_(exc_info[1], with_traceback=exc_info[2])

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/util/compat.py:211](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/util/compat.py:211), in raise_(***failed resolving arguments***)
    208     exception.__cause__ = replace_context
    210 try:
--> 211     raise exception
    212 finally:
    213     # credit to
    214     # https://cosmicpercolator.com/2016/01/13/exception-leaks-in-python-2-and-3/
    215     # as the __traceback__ object creates a cycle
    216     del exception, replace_context, from_, with_traceback

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:1900](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/base.py:1900), in Connection._execute_context(self, dialect, constructor, statement, parameters, execution_options, *args, **kw)
   1898                 break
   1899     if not evt_handled:
-> 1900         self.dialect.do_execute(
   1901             cursor, statement, parameters, context
   1902         )
   1904 if self._has_events or self.engine._has_events:
   1905     self.dispatch.after_cursor_execute(
   1906         self,
   1907         cursor,
   (...)
   1911         context.executemany,
   1912     )

File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/default.py:736](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/sqlalchemy/engine/default.py:736), in DefaultDialect.do_execute(self, cursor, statement, parameters, context)
    735 def do_execute(self, cursor, statement, parameters, context=None):
--> 736     cursor.execute(statement, parameters)

ProgrammingError: (psycopg2.errors.SyntaxError) syntax error at or near "article_paragraphs"
LINE 1: ...PORARY VIEW "2787edf13b8047f0909bd18a0abdb7aa" AS article_pa...
                                                             ^

[SQL: CREATE TEMPORARY VIEW "2787edf13b8047f0909bd18a0abdb7aa" AS article_paragraphs]
(Background on this error at: https://sqlalche.me/e/14/f405)
```


</details>

Hidden within this is the line;

```
File [~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/ibis/backends/base/sql/alchemy/__init__.py:436](https://file+.vscode-resource.vscode-cdn.net/Users/josh-gree/chatting_with_the_guardian/scratch/~/Library/Caches/pypoetry/virtualenvs/chatting-with-the-guardian-28mMINub-py3.9/lib/python3.9/site-packages/ibis/backends/base/sql/alchemy/__init__.py:436), in BaseAlchemyBackend._get_sqla_table(self, name, schema, autoload, **_)
    434 if not nulltype_cols:
    435     return table
--> 436 return self._handle_failed_column_type_inference(table, nulltype_cols)
```

Which leads me to beleive its a problem with the extension type?

Is there a way to get ibis to ignore this column?

### What version of ibis are you using?

5.0.0

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

postgres

### 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/backends/postgres/__init__.py b/ibis/backends/postgres/__init__.py
--- a/ibis/backends/postgres/__init__.py
+++ b/ibis/backends/postgres/__init__.py
@@ -194,6 +194,8 @@ def _metadata(self, query: str) -> Iterable[tuple[str, dt.DataType]]:
   AND attnum > 0
   AND NOT attisdropped
 ORDER BY attnum"""
+        if self.inspector.has_table(query):
+            query = f"TABLE {query}"
         with self.begin() as con:
             con.exec_driver_sql(f"CREATE TEMPORARY VIEW {name} AS {query}")
             type_info = con.execute(
diff --git a/ibis/backends/postgres/datatypes.py b/ibis/backends/postgres/datatypes.py
--- a/ibis/backends/postgres/datatypes.py
+++ b/ibis/backends/postgres/datatypes.py
@@ -46,7 +46,14 @@ def _get_type(typestr: str) -> dt.DataType:
     is_array = typestr.endswith(_BRACKETS)
     if (typ := _type_mapping.get(typestr.replace(_BRACKETS, ""))) is not None:
         return dt.Array(typ) if is_array else typ
-    return _parse_numeric(typestr)
+    try:
+        return _parse_numeric(typestr)
+    except parsy.ParseError:
+        # postgres can have arbitrary types unknown to ibis, so we just
+        # consider them null since we can't know what to do with them without
+        # explicit support, return null to effectively give no public API to
+        # such columns
+        return dt.null
 
 
 _type_mapping = {


Test patch:
diff --git a/ibis/backends/postgres/tests/test_client.py b/ibis/backends/postgres/tests/test_client.py
--- a/ibis/backends/postgres/tests/test_client.py
+++ b/ibis/backends/postgres/tests/test_client.py
@@ -207,3 +207,11 @@ def test_get_schema_from_query(con, pg_type, expected_type):
     expected_schema = ibis.schema(dict(x=expected_type, y=dt.Array(expected_type)))
     result_schema = con._get_schema_using_query(f"SELECT x, y FROM {name}")
     assert result_schema == expected_schema
+
+
+@pytest.mark.parametrize(
+    ("col", "expected_type"), [("search", dt.string), ("simvec", dt.null)]
+)
+def test_unknown_column_type(con, col, expected_type):
+    awards_players = con.table("awards_players")
+    assert awards_players[col].type() == expected_type
diff --git a/ibis/backends/postgres/tests/test_string.py b/ibis/backends/postgres/tests/test_string.py
--- a/ibis/backends/postgres/tests/test_string.py
+++ b/ibis/backends/postgres/tests/test_string.py
@@ -4,7 +4,6 @@
 from pytest import param
 
 import ibis
-import ibis.expr.datatypes as dt
 
 
 @pytest.mark.parametrize(
@@ -17,9 +16,3 @@ def test_special_strings(alltypes, data, data_type):
     expr = alltypes[[alltypes.id, lit]].head(1)
     df = expr.execute()
     assert df['tmp'].iloc[0] == uuid.UUID(data)
-
-
-def test_load_tsvector_table(con):
-    awards_players = con.table("awards_players")
-    assert "search" in awards_players.columns
-    assert awards_players.schema()["search"] == dt.String(nullable=True)