Closed mr-gabe49 closed 11 months ago
It appears SQLAlchemy can't handle reflection of tables which have no columns. I've reproduced this issue on my machine. We'll need to write a custom reflection function if we want to be able to handle this. I think this would be best, since it's a valid state for the DB that we'll want to be able to work with eventually.
@kgodey @mathemancer can I work on this?
Hey @bohemia420, @kgodey will be offline for some time, and it's very late in Hong Kong for @mathemancer. I'll assign it to you. Let us know if you have any questions. Thanks!
this is the catch in sqlalchemy reflections.py
that's breaking it:
for col_d in self.get_columns(
table_name, schema, **table.dialect_kwargs
):
found_table = True
self._reflect_column(
table,
col_d,
include_columns,
exclude_columns,
cols_by_orig_name,
)
firstly, found_table = True if len(self.get_columns) else False
would have been a better handling rather than redundantly setting found_table = True
for say, 1000+ columns in a table.
@mathemancer rather than diverting it to a custom reflections, better to "tweak" the "sqlalchemy" and replace it in our docker image's requirement.txt
?
correcting this puny found_table
issue, the UI just loads well that table with albeit 0 columns.
@kgodey In addition to my proposal above being painful syncing/maintaining cherrypicked files, it could 've had other problems:
table_found
variable may also let in FPs i.e tables that just never got created, say "Table 999999"remediating afresh as I played with SQLAlchemy reflections and what we're calling under the hood, following is the complete troubleshooting, followed by an issue/PR made to SQLAlchemy:
current version of SQLAlchemy that we use, is 1.4.18
$ docker exec -it mathesar_service_1 sh
# pip3.9 freeze | grep -i sqlalchemy
SQLAlchemy==1.4.18
which, fails to recognise the reflections of empty(no column/keys) tables:
# python3.9
>>> from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData
>>> engine = create_engine('postgresql://mathesar:mathesar@db:5432/mathesar')
>>> engine.table_names()
<stdin>:1: SADeprecationWarning: The Engine.table_names() method is deprecated and will be removed in a future release. Please refer to Inspector.get_table_names(). (deprecated since: 1.4)
[]
>>> metadata = MetaData(bind=engine)
>>> emp2 = Table('emp2', metadata, )
>>> tab1 = Table('tab1', metadata,Column('id', Integer, primary_key = True), )
>>> metadata.create_all(engine)
>>> engine.table_names()
['emp2', 'tab1']
on checking for/returning those Tables
(where exactly NoSuchTableError
i.e:
return Table(name, metadata, schema=schema, autoload_with=autoload_with, extend_existing=True)
inside db/tables/operations/select.py
gets raised):
>>> Table('tab1', MetaData(bind=engine), schema='public', autoload_with=engine, extend_existing=True)
Table('tab1', MetaData(bind=Engine(postgresql://mathesar:***@db:5432/mathesar)), Column('id', INTEGER(), table=<tab1>, primary_key=True, nullable=False, server_default=DefaultClause(<sqlalchemy.sql.elements.TextClause object at 0x7ffb2d99cd90>, for_update=False)), schema='public')
A table with atleast one column works, however:
>>> Table('emp2', MetaData(bind=engine), schema='public', autoload_with=engine, extend_existing=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 2, in __new__
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/deprecations.py", line 298, in warned
return fn(*args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 600, in __new__
metadata._remove_table(name, schema)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/langhelpers.py", line 70, in __exit__
compat.raise_(
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/compat.py", line 207, in raise_
raise exception
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 595, in __new__
table._init(name, metadata, *args, **kw)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 670, in _init
self._autoload(
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 705, in _autoload
conn_insp.reflect_table(
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/reflection.py", line 788, in reflect_table
raise exc.NoSuchTableError(table.name)
sqlalchemy.exc.NoSuchTableError: emp2
tables with zero columns i.e no keys e.g emp2
, a NoSuchTableError
Exception is thrown.
However, upgrading the version to 1.4.23
proves to be futile, too:
# pip3.9 install SQLAlchemy==1.4.23
# pip3.9 freeze | grep -i sqlalchemy
SQLAlchemy==1.4.23
and executing the same steps as above:
# python3.9
Python 3.9.7 (default, Sep 3 2021, 20:19:49)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData
>>> engine = create_engine('postgresql://mathesar:mathesar@db:5432/mathesar')
>>> engine.table_names()
<stdin>:1: SADeprecationWarning: The Engine.table_names() method is deprecated and will be removed in a future release. Please refer to Inspector.get_table_names(). (deprecated since: 1.4)
[...]
>>> metadata = MetaData(bind=engine)
>>> emp23 = Table('emp23', metadata, )
>>> tab23 = Table('tab23', metadata,Column('id', Integer, primary_key = True), )
>>> metadata.create_all(engine)
>>> engine.table_names()
['emp23', 'tab23', ...]
>>> Table('tab23', MetaData(bind=engine), schema='public', autoload_with=engine, extend_existing=True)
Table('tab23', MetaData(bind=Engine(postgresql://mathesar:***@db:5432/mathesar)), Column('id', INTEGER(), table=<tab23>, primary_key=True, nullable=False, server_default=DefaultClause(<sqlalchemy.sql.elements.TextClause object at 0x7efeaf9ba580>, for_update=False)), schema='public')
>>> Table('emp23', MetaData(bind=engine), schema='public', autoload_with=engine, extend_existing=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 2, in __new__
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/deprecations.py", line 298, in warned
return fn(*args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 601, in __new__
metadata._remove_table(name, schema)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/langhelpers.py", line 70, in __exit__
compat.raise_(
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/compat.py", line 207, in raise_
raise exception
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 596, in __new__
table._init(name, metadata, *args, **kw)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 671, in _init
self._autoload(
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 706, in _autoload
conn_insp.reflect_table(
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/reflection.py", line 788, in reflect_table
raise exc.NoSuchTableError(table.name)
sqlalchemy.exc.NoSuchTableError: emp23
>>>
p.s : Having checked against 1.4.25 also yieleded similar "...NoSuchTableError": emp25
no doubt, as stated by @mathemancer, SQLAlchemy can't handle reflections of tables with no columns, and the right fix of that, no matter removing redundant found_table
initialisation as we've been digging right, is to rather populate basis presence in self.engine.table_names
.
With that fix, things begin to work well:
>>> Table('emp_23', MetaData(bind=engine), schema='public', autoload_with=engine, extend_existing=True)
Table('emp_23', MetaData(bind=Engine(postgresql://mathesar:***@0.0.0.0:5432/mathesar)), schema='public')
>>> Table('empidontexist', MetaData(bind=engine), schema='public', autoload_with=engine, extend_existing=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 2, in __new__
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/deprecations.py", line 298, in warned
return fn(*args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 607, in __new__
metadata._remove_table(name, schema)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/langhelpers.py", line 70, in __exit__
compat.raise_(
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/compat.py", line 207, in raise_
raise exception
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 602, in __new__
table._init(name, metadata, *args, **kw)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 677, in _init
self._autoload(
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/schema.py", line 712, in _autoload
conn_insp.reflect_table(
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/reflection.py", line 774, in reflect_table
for col_d in self.get_columns(
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/reflection.py", line 497, in get_columns
col_defs = self.dialect.get_columns(
File "<string>", line 2, in get_columns
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/reflection.py", line 55, in cache
ret = fn(self, con, *args, **kw)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/base.py", line 3605, in get_columns
table_oid = self.get_table_oid(
File "<string>", line 2, in get_table_oid
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/reflection.py", line 55, in cache
ret = fn(self, con, *args, **kw)
File "/usr/local/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/base.py", line 3485, in get_table_oid
raise exc.NoSuchTableError(table_name)
sqlalchemy.exc.NoSuchTableError: empidontexist
I have raised this issue at SQLAlchemy here besides providing a PR to help the issue, here. I'll keep you posted how it is further remediated and merged.
Interestingly, this issue had already been reported to SQLAlchemy, and they've asked us to track the earlier raised issue on this note and closed our issue raised. My PR which they are likely to consider owing to agreeing at fixing found_table
in reflection.py for handling "tables with oid and no columns" should fix this issue.
Thanks for the detailed debug steps @bohemia420! I'll mark this as blocked until the upstream PR is merged.
there's actually a discussion thread on the PR https://github.com/sqlalchemy/sqlalchemy/pull/7118 accompanied by a Gerrit and still under review - somewhere it's being contemplating that the fix in this PR be a part of settings which is bound to raise NoSuchTableError
exception by default
Thanks for the update!
This is fixed upstream, so we just need to upgrade to SQLAlchemy 1.4.26 or higher. Marking as unblocked.
This issue has not been updated in 90 days and is being marked as stale.
The referenced PR seems to fix this (judging by the title).
Description
Expected behavior
To Reproduce
Environment
Full Traceback