Repository: trinodb/trino-python-client

Issue:
Exception 'TableClause' object has no attribute 'dialect_options' in Apache superset
### Expected behavior

When I create a chart in Apache Superset and click on 'Update Chart', I want Superset (or in other ways trino-python-client) to run the charts query and load the data.

### Actual behavior

Superset shows an 'DB engine Error': 'TableClause' object has no attribute 'dialect_options' (see Image)
![grafik](https://user-images.githubusercontent.com/2621219/191671949-6e042018-f78d-4068-bcd0-17af2f1a37e7.png)


### Steps To Reproduce

1. Install and initialize apache superset version 2.0.0 and trino-python-client with sqlalchemy version 0.316.0 (with SQLAlchemy version 1.3.24, SQLAlchemy-Utils version 0.37.8)
2. Create a database connecting to an trino instance
3. Create a dataset from a table in the trino database
4. Create a new chart with the newly created dataset
5. Configure the columns for the chart and run 'Update chart'
6. superset shows the 'DB engine Error'

### Log output

```
2022-09-22 06:09:29,514:ERROR:superset.views.base:'TableClause' object has no attribute 'dialect_options'
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1516, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1502, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
  File "/usr/local/lib/python3.8/site-packages/flask_appbuilder/security/decorators.py", line 89, in wraps
    return f(self, *args, **kwargs)
  File "/app/superset/views/base_api.py", line 113, in wraps
    raise ex
  File "/app/superset/views/base_api.py", line 110, in wraps
    duration, response = time_function(f, self, *args, **kwargs)
  File "/app/superset/utils/core.py", line 1507, in time_function
    response = func(*args, **kwargs)
  File "/app/superset/utils/log.py", line 245, in wrapper
    value = f(*args, **kwargs)
  File "/app/superset/charts/data/api.py", line 244, in data
    return self._get_data_response(
  File "/app/superset/charts/data/api.py", line 390, in _get_data_response
    result = command.run(force_cached=force_cached)
  File "/app/superset/charts/data/commands/get_data_command.py", line 43, in run
    payload = self._query_context.get_payload(
  File "/app/superset/common/query_context.py", line 96, in get_payload
    return self._processor.get_payload(cache_query_context, force_cached)
  File "/app/superset/common/query_context_processor.py", line 386, in get_payload
    query_results = [
  File "/app/superset/common/query_context_processor.py", line 387, in <listcomp>
    get_query_results(
  File "/app/superset/common/query_actions.py", line 200, in get_query_results
    return result_func(query_context, query_obj, force_cached)
  File "/app/superset/common/query_actions.py", line 104, in _get_full
    payload = query_context.get_df_payload(query_obj, force_cached=force_cached)
  File "/app/superset/common/query_context.py", line 115, in get_df_payload
    return self._processor.get_df_payload(query_obj, force_cached)
  File "/app/superset/common/query_context_processor.py", line 126, in get_df_payload
    query_result = self.get_query_result(query_obj)
  File "/app/superset/common/query_context_processor.py", line 186, in get_query_result
    result = query_context.datasource.query(query_object.to_dict())
  File "/app/superset/connectors/sqla/models.py", line 1864, in query
    query_str_ext = self.get_query_str_extended(query_obj)
  File "/app/superset/connectors/sqla/models.py", line 980, in get_query_str_extended
    sql = self.database.compile_sqla_query(sqlaq.sqla_query)
  File "/app/superset/models/core.py", line 465, in compile_sqla_query
    sql = str(qry.compile(engine, compile_kwargs={"literal_binds": True}))
  File "<string>", line 1, in <lambda>
  File "/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/elements.py", line 481, in compile
    return self._compiler(dialect, bind=bind, **kw)
  File "/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/elements.py", line 487, in _compiler
    return dialect.statement_compiler(dialect, self, **kw)
  File "/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/compiler.py", line 592, in __init__
    Compiled.__init__(self, dialect, statement, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/compiler.py", line 322, in __init__
    self.string = self.process(self.statement, **compile_kwargs)
  File "/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/compiler.py", line 352, in process
    return obj._compiler_dispatch(self, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/visitors.py", line 96, in _compiler_dispatch
    return meth(self, **kw)
  File "/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/compiler.py", line 2201, in visit_select
    text = self._compose_select_body(
  File "/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/compiler.py", line 2292, in _compose_select_body
    [
  File "/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/compiler.py", line 2293, in <listcomp>
    f._compiler_dispatch(self, asfrom=True, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/visitors.py", line 96, in _compiler_dispatch
    return meth(self, **kw)
  File "/usr/local/lib/python3.8/site-packages/trino/sqlalchemy/compiler.py", line 121, in visit_table
    return self.add_catalog(sql, table)
  File "/usr/local/lib/python3.8/site-packages/trino/sqlalchemy/compiler.py", line 132, in add_catalog
    'trino' not in table.dialect_options
AttributeError: 'TableClause' object has no attribute 'dialect_options'
127.0.0.1 - - [22/Sep/2022:06:09:29 +0000] "POST /api/v1/chart/data?force HTTP/1.1" 500 254 "http://localhost:8088/superset/explore/?form_data_key=YCzzHrgwWaboPAhlbo10Uffjd24xZBsR83cYDpG545rUpujMDubUWFAi1Kq7qcoo&datasource_id=1&datasource_type=table" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"
127.0.0.1 - - [22/Sep/2022:06:09:29 +0000] "GET /static/assets/26de9c1087b5f1f837a0.woff2 HTTP/1.1" 200 0 "http://localhost:8088/static/assets/theme.4639b060fd9b6a4b89fb.entry.css" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"
127.0.0.1 - - [22/Sep/2022:06:09:30 +0000] "GET /api/v1/chart/0 HTTP/1.1" 404 24 "http://localhost:8088/superset/explore/?form_data_key=YCzzHrgwWaboPAhlbo10Uffjd24xZBsR83cYDpG545rUpujMDubUWFAi1Kq7qcoo&datasource_id=1&datasource_type=table" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"
127.0.0.1 - - [22/Sep/2022:06:09:30 +0000] "PUT /api/v1/explore/form_data/YCzzHrgwWaboPAhlbo10Uffjd24xZBsR83cYDpG545rUpujMDubUWFAi1Kq7qcoo?tab_id=1 HTTP/1.1" 200 75 "http://localhost:8088/superset/explore/?form_data_key=YCzzHrgwWaboPAhlbo10Uffjd24xZBsR83cYDpG545rUpujMDubUWFAi1Kq7qcoo&datasource_id=1&datasource_type=table" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"
127.0.0.1 - - [22/Sep/2022:06:09:30 +0000] "POST /superset/log/?explode=events HTTP/1.1" 200 20 "http://localhost:8088/superset/explore/?form_data_key=YCzzHrgwWaboPAhlbo10Uffjd24xZBsR83cYDpG545rUpujMDubUWFAi1Kq7qcoo&datasource_id=1&datasource_type=table" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"
```

### Operating System

Debian GNU/Linux 11 (bullseye)

### Trino Python client version

0.316.0

### Trino Server version

395

### Python version

Python 3.8.12

### Are you willing to submit PR?

- [ ] Yes I am willing to submit a PR!

Discussion:
mdesmet: @hashhar: PTAL. It should fix an exception being thrown in Superset.
mdesmet: @ebyhr : PTAL, test has been added.

Patch:
diff --git a/trino/sqlalchemy/compiler.py b/trino/sqlalchemy/compiler.py
index 99b272f..0078e68 100644
--- a/trino/sqlalchemy/compiler.py
+++ b/trino/sqlalchemy/compiler.py
@@ -10,17 +10,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 from sqlalchemy.sql import compiler
-try:
-    from sqlalchemy.sql.expression import (
-        Alias,
-        CTE,
-        Subquery,
-    )
-except ImportError:
-    # For SQLAlchemy versions < 1.4, the CTE and Subquery classes did not explicitly exist
-    from sqlalchemy.sql.expression import Alias
-    CTE = type(None)
-    Subquery = type(None)
+from sqlalchemy.sql.base import DialectKWArgs
+
 
 # https://trino.io/docs/current/language/reserved.html
 RESERVED_WORDS = {
@@ -122,10 +113,7 @@ class TrinoSQLCompiler(compiler.SQLCompiler):
 
     @staticmethod
     def add_catalog(sql, table):
-        if table is None:
-            return sql
-
-        if isinstance(table, (Alias, CTE, Subquery)):
+        if table is None or not isinstance(table, DialectKWArgs):
             return sql
 
         if (


Test patch:
diff --git a/tests/unit/sqlalchemy/test_compiler.py b/tests/unit/sqlalchemy/test_compiler.py
index 00bd686..9e4aad4 100644
--- a/tests/unit/sqlalchemy/test_compiler.py
+++ b/tests/unit/sqlalchemy/test_compiler.py
@@ -20,11 +20,12 @@ from sqlalchemy import (
     Table,
 )
 from sqlalchemy.schema import CreateTable
+from sqlalchemy.sql import column, table
 
 from trino.sqlalchemy.dialect import TrinoDialect
 
 metadata = MetaData()
-table = Table(
+table_without_catalog = Table(
     'table',
     metadata,
     Column('id', Integer),
@@ -45,26 +46,26 @@ def dialect():
 
 
 def test_limit_offset(dialect):
-    statement = select(table).limit(10).offset(0)
+    statement = select(table_without_catalog).limit(10).offset(0)
     query = statement.compile(dialect=dialect)
     assert str(query) == 'SELECT "table".id, "table".name \nFROM "table"\nOFFSET :param_1\nLIMIT :param_2'
 
 
 def test_limit(dialect):
-    statement = select(table).limit(10)
+    statement = select(table_without_catalog).limit(10)
     query = statement.compile(dialect=dialect)
     assert str(query) == 'SELECT "table".id, "table".name \nFROM "table"\nLIMIT :param_1'
 
 
 def test_offset(dialect):
-    statement = select(table).offset(0)
+    statement = select(table_without_catalog).offset(0)
     query = statement.compile(dialect=dialect)
     assert str(query) == 'SELECT "table".id, "table".name \nFROM "table"\nOFFSET :param_1'
 
 
 def test_cte_insert_order(dialect):
-    cte = select(table).cte('cte')
-    statement = insert(table).from_select(table.columns, cte)
+    cte = select(table_without_catalog).cte('cte')
+    statement = insert(table_without_catalog).from_select(table_without_catalog.columns, cte)
     query = statement.compile(dialect=dialect)
     assert str(query) == \
         'INSERT INTO "table" (id, name) WITH cte AS \n'\
@@ -89,3 +90,9 @@ def test_catalogs_create_table(dialect):
         '\tid INTEGER\n'\
         ')\n'\
         '\n'
+
+
+def test_table_clause(dialect):
+    statement = select(table("user", column("id"), column("name"), column("description")))
+    query = statement.compile(dialect=dialect)
+    assert str(query) == 'SELECT user.id, user.name, user.description \nFROM user'