2

I am currently engaged in evaluating SQLAlchemy for a new project. When trying to reflect the tables of an existing database (postgres, oracle), I am encountering some issues.

In order to connect to an Oracle database the following code is being used:

from sqlalchemy import create_engine, MetaData, Table, inspect, select
import pandas as pd
import keyring

dbtype = 'Oracle'
dbenv = 'LOCAL'
dbname = 'MYDB'
dbsys = '%s%s' % (dbtype, dbenv)
dbusr = 'myusr'
dbpwd = keyring.get_password(dbsys, dbusr)
dbhost = 'mydbhost'
dbport = 1521

dbconstr = 'oracle+cx_oracle://%s:%s@%s:%s/%s' % (dbusr, dbpwd, dbhost, dbport, dbname)
engine = create_engine(dbconstr)

After some try-and-error regarding how to assemble the connection string (there is no tnsnames.ora present on my system), I managed to successfully establish a connection to the remote Oracle database.

When inspecting the database and trying to retrieve the names of the tables available, I received an empty list.

inspector = inspect(engine)
instbls = inspector.get_table_names()
print('instbls: %s' % instbls)

# instbls: []

What I actually was expecting is that the system tables or the public tables would have been listed.

Next I tried reflecting the database tables of the SYS schema.

md01 = MetaData(bind=engine, schema=['SYS', 'SYSTEM'], reflect=True)
# Warning: sqlalchemy_oracle.py:37: SADeprecationWarning: The MetaData.reflect flag is deprecated and will be removed in a future release.   Please use the MetaData.reflect() method.

print('type(md01): %s' % type(md01))
# type(md01): <class 'sqlalchemy.sql.schema.MetaData'>

md01tbls = md01.tables
print('md01tbls: %s' % md01tbls)
# md01tbls: immutabledict({})

So I modified the code accordingly in order to get rid of the warning

md02 = MetaData(bind=engine, schema=['SYS', 'SYSTEM']).reflect()
print('type(md02): %s' % type(md02))
# type(md02): <class 'NoneType'>

md02tbls = md02.tables
# Exception: Traceback (most recent call last):
#    ...
# File "sqlalchemy_oracle.py", line 44, in <module>
# AttributeError: 'NoneType' object has no attribute 'tables'

print('md02tbls: %s' % md02tbls)

No warning, but an exception instead. :(

After some researching The Net it appeared as if some further code modification is required.

md03 = MetaData().reflect(bind=engine, schema=['SYS', 'SYSTEM'])
# Exception: Traceback (most recent call last):
#    ...
# File "/site-packages/sqlalchemy/engine/default.py", line 637, in denormalize_name
#    name_lower = name.lower()
# AttributeError: 'list' object has no attribute 'lower'

print('type(md03): %s' % type(md03))
md03tbls = md03.tables
print('md03tbls: %s' % md03tbls)

Even less successful than the previous attempt. Converting the schema names listed to lower case letters lead to the identical result.

So maybe reflect can only handle strings and nolists?

md05a = MetaData().reflect(bind=engine, schema='sys')
print('type(md05a): %s' % type(md05a))
# type(md05a): <class 'NoneType'>

md05b = MetaData().reflect(bind=engine, schema='system')
print('type(md05b): %s' % type(md05b))
# type(md05b): <class 'NoneType'>

md05atbls = md05a.tables
# Exception: Traceback (most recent call last):
#    ...
# File "sqlalchemy_oracle.py", line 44, in <module>
# AttributeError: 'NoneType' object has no attribute 'tables'

print('md05atbls: %s' % md05atbls)
md05btbls = md05b.tables
print('md05btbls: %s' % md05btbls)

No luck there either!

Maybe it is not possible to reflect the system tables? So lets try user-specific objects.

md06 = MetaData().reflect(bind=engine, schema='MYUSR')
print('type(md06): %s' % type(md06))
# type(md06): <class 'NoneType'>

md06tbls = md06.tables
# Exception: Traceback (most recent call last):
#    ...
# File "sqlalchemy_oracle.py", line 44, in <module>
# AttributeError: 'NoneType' object has no attribute 'tables'

print('md06tbls: %s' % md06tbls)

Again the already know exception.

Moving back to the deprecated call version.

md07 = MetaData(bind=engine, schema=['MYUSR', 'MYUSR2'], reflect=True)
print('type(md07): %s' % type(md07))
# type(md07): <class 'sqlalchemy.sql.schema.MetaData'>

md07tbls = md07.tables
print('md07tbls: %s' % md07tbls)
# md07tbls: immutabledict({})

No exception, but no tables either, despite that there are several to be found in the schemas MYUSR and MYUSR2.

Splitting the list into strings when specifying the schema.

md08a = MetaData(bind=engine, schema='MYUSR', reflect=True)
# Warning: sqlalchemy_oracle.py:37: SADeprecationWarning: The MetaData.reflect flag is deprecated and will be removed in a future release.   Please use the MetaData.reflect() method.

print('type(md08a): %s' % type(md08a))
# ype(md08a): <class 'sqlalchemy.sql.schema.MetaData'>

md08b = MetaData(bind=engine, schema='MYUSR2', reflect=True)
# Warning: sqlalchemy_oracle.py:40: SADeprecationWarning: The MetaData.reflect flag is deprecated and will be removed in a future release.   Please use the MetaData.reflect() method.

print('type(md08b): %s' % type(md08b))
# type(md08b): <class 'sqlalchemy.sql.schema.MetaData'>

md08atbls = md08a.tables
print('md08atbls: %s' % md08atbls)
# md08atbls: immutabledict({'MYUSR.table01': Table(...)})

md08btbls = md08b.tables
print('md08btbls: %s' % md08btbls)
# md08btbls: immutabledict({'MYUSR2.table01': Table(...)})

So this attempt finally produced the expected result. :)

From all this it appears as if

  1. system/public tables (database objects) cannot be reflected;
  2. only user-specific tables can be reflected;
  3. only the deprecated version of reflection is working;
  4. this behaviour can be observed both with Oracle as well as PostgreSQL databases.

Is there something I overlooked? Something I have mis-perceived? Done wrong, differently from how it actually is intended?

My environment comprises of

  • Ubuntu linux 16.04LTS;
  • Python 3.8;
  • SQLAlchemy 1.3.16;
  • cx_Oracle 7.3.0;
  • psycopg2 2.8.5;
  • local Oracle 18c Instant client;
  • remote Oracle 19c database;
  • local PostgreSQL 9.5 database.
user4338
  • 173
  • 1
  • 12

2 Answers2

2

For Postgres, you can reflect tables in a database's pg_catalog schema like this:

>>> import sqlalchemy as sa
>>> engine = sa.create_engine('postgresql:///postgres', echo=False)
>>> meta = sa.MetaData()
>>> meta.reflect(engine, schema='pg_catalog')
>>> meta.tables.keys()
(long sequence of table names...)

Setting echo=True in create_engine will output all the queries executed by SQLAlchemy on the system tables when building the metadata, which may help in reverse-engineering the same result for other databases..

snakecharmerb
  • 47,570
  • 11
  • 100
  • 153
2

Supplementary to @snakecharmerb's answer, this works for me with SQLAlchemy 1.4:

connection_uri = (
    "oracle+cx_oracle://scott:tiger@192.168.0.199:1521/"
    "?service_name=xepdb1&encoding=UTF-8&nencoding=UTF-8"
)

engine = sa.create_engine(connection_uri)

insp = sa.inspect(engine)
print(insp.default_schema_name)
# scott
tables = insp.get_table_names()
print(tables)
# ['table1']
tables = insp.get_table_names(schema="sys")
print(tables)
# ['sqllog$', 'sqlobj$', 'sqlobj$data', 'svcobj$', …
Gord Thompson
  • 116,920
  • 32
  • 215
  • 418