-
Notifications
You must be signed in to change notification settings - Fork 110
Add support for table comments #308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
cfdf3f3
ba5ad75
439fe6a
57b6802
bd68ea8
7c94ce3
c09ae2b
8f8fef0
78a5ad0
2aa5805
f65324f
7b6fefb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
import re | ||
from typing import Any, List, Optional, Dict, Union, Collection, Iterable, Tuple | ||
from typing import Any, List, Optional, Dict, Union | ||
|
||
import databricks.sqlalchemy._ddl as dialect_ddl_impl | ||
import databricks.sqlalchemy._types as dialect_type_impl | ||
|
@@ -17,13 +16,13 @@ | |
import sqlalchemy | ||
from sqlalchemy import DDL, event | ||
from sqlalchemy.engine import Connection, Engine, default, reflection | ||
from sqlalchemy.engine.reflection import ObjectKind | ||
from sqlalchemy.engine.interfaces import ( | ||
ReflectedForeignKeyConstraint, | ||
ReflectedPrimaryKeyConstraint, | ||
ReflectedColumn, | ||
TableKey, | ||
ReflectedTableComment, | ||
) | ||
from sqlalchemy.engine.reflection import ReflectionDefaults | ||
from sqlalchemy.exc import DatabaseError, SQLAlchemyError | ||
|
||
try: | ||
|
@@ -285,7 +284,7 @@ def get_table_names(self, connection: Connection, schema=None, **kwargs): | |
views_result = self.get_view_names(connection=connection, schema=schema) | ||
|
||
# In Databricks, SHOW TABLES FROM <schema> returns both tables and views. | ||
# Potential optimisation: rewrite this to instead query informtation_schema | ||
# Potential optimisation: rewrite this to instead query information_schema | ||
tables_minus_views = [ | ||
row.tableName for row in tables_result if row.tableName not in views_result | ||
] | ||
|
@@ -328,7 +327,7 @@ def get_materialized_view_names( | |
def get_temp_view_names( | ||
self, connection: Connection, schema: Optional[str] = None, **kw: Any | ||
) -> List[str]: | ||
"""A wrapper around get_view_names taht fetches only the names of temporary views""" | ||
"""A wrapper around get_view_names that fetches only the names of temporary views""" | ||
return self.get_view_names(connection, schema, only_temp=True) | ||
|
||
def do_rollback(self, dbapi_connection): | ||
|
@@ -375,6 +374,30 @@ def get_schema_names(self, connection, **kw): | |
schema_list = [row[0] for row in result] | ||
return schema_list | ||
|
||
@reflection.cache | ||
def get_table_comment( | ||
self, | ||
connection: Connection, | ||
table_name: str, | ||
schema: Optional[str] = None, | ||
**kw: Any, | ||
) -> ReflectedTableComment: | ||
result = self._describe_table_extended( | ||
connection=connection, | ||
table_name=table_name, | ||
schema_name=schema, | ||
) | ||
# Type ignore is because mypy knows that self._describe_table_extended *can* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can actually catch this using |
||
# return None (even though it never will since expect_result defaults to True) | ||
comment_row: Dict[str, str] = next( | ||
filter(lambda r: r["col_name"] == "Comment", result), None | ||
) # type: ignore | ||
return ( | ||
{"text": comment_row["data_type"]} | ||
if comment_row | ||
else ReflectionDefaults.table_comment() | ||
) | ||
|
||
|
||
@event.listens_for(Engine, "do_connect") | ||
def receive_do_connect(dialect, conn_rec, cargs, cparams): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,9 @@ | |
|
||
import datetime | ||
|
||
from sqlalchemy import Integer, String, schema, inspect | ||
from sqlalchemy.testing import util | ||
from sqlalchemy.testing.config import requirements | ||
from sqlalchemy.testing.suite.test_types import ( | ||
_LiteralRoundTripFixture, | ||
fixtures, | ||
|
@@ -63,8 +66,39 @@ class DateTimeTZTestCustom(_DateFixture, fixtures.TablesTest): | |
|
||
@testing.requires.datetime_implicit_bound | ||
def test_select_direct(self, connection): | ||
|
||
# We need to pass the TIMESTAMP type to the literal function | ||
# so that the value is processed correctly. | ||
result = connection.scalar(select(literal(self.data, TIMESTAMP))) | ||
eq_(result, self.data) | ||
|
||
|
||
class TableDDLTestCustom(fixtures.TestBase): | ||
"""This test confirms that a table comment can be dropped. | ||
The difference with TableDDLTest is that the comment value is '' and not None after | ||
being dropped. | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW this is surprising. From the docs we could expect the comment to be set to NULL and not ''. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I adapted for this in #328 by simply coercing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice 👍 |
||
|
||
__backend__ = True | ||
|
||
def _simple_fixture(self, schema=None): | ||
return Table( | ||
"test_table", | ||
self.metadata, | ||
Column("id", Integer, primary_key=True, autoincrement=False), | ||
Column("data", String(50)), | ||
schema=schema, | ||
) | ||
|
||
@requirements.comment_reflection | ||
@util.provide_metadata | ||
def test_drop_table_comment(self, connection): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since I merged #328 do we still need to override this test? |
||
table = self._simple_fixture() | ||
table.create(connection, checkfirst=False) | ||
table.comment = "a comment" | ||
connection.execute(schema.SetTableComment(table)) | ||
connection.execute(schema.DropTableComment(table)) | ||
eq_(inspect(connection).get_table_comment("test_table"), {"text": ""}) | ||
|
||
|
||
class FutureTableDDLTestCustom(fixtures.FutureEngineMixin, TableDDLTestCustom): | ||
pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,9 +13,7 @@ | |
ComponentReflectionTest, | ||
ComponentReflectionTestExtra, | ||
CTETest, | ||
FutureTableDDLTest, | ||
InsertBehaviorTest, | ||
TableDDLTest, | ||
) | ||
from sqlalchemy.testing.suite import ( | ||
ArrayTest, | ||
|
@@ -251,36 +249,7 @@ class FutureWeCanSetDefaultSchemaWEventsTest(FutureWeCanSetDefaultSchemaWEventsT | |
pass | ||
|
||
|
||
class FutureTableDDLTest(FutureTableDDLTest): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perfect! |
||
@pytest.mark.skip(reason=render_future_feature(FutureFeature.TBL_COMMENTS)) | ||
def test_add_table_comment(self): | ||
"""We could use requirements.comment_reflection here to disable this but prefer a more meaningful skip message""" | ||
pass | ||
|
||
@pytest.mark.skip(reason=render_future_feature(FutureFeature.TBL_COMMENTS)) | ||
def test_drop_table_comment(self): | ||
"""We could use requirements.comment_reflection here to disable this but prefer a more meaningful skip message""" | ||
pass | ||
|
||
|
||
class TableDDLTest(TableDDLTest): | ||
@pytest.mark.skip(reason=render_future_feature(FutureFeature.TBL_COMMENTS)) | ||
def test_add_table_comment(self, connection): | ||
"""We could use requirements.comment_reflection here to disable this but prefer a more meaningful skip message""" | ||
pass | ||
|
||
@pytest.mark.skip(reason=render_future_feature(FutureFeature.TBL_COMMENTS)) | ||
def test_drop_table_comment(self, connection): | ||
"""We could use requirements.comment_reflection here to disable this but prefer a more meaningful skip message""" | ||
pass | ||
|
||
|
||
class ComponentReflectionTest(ComponentReflectionTest): | ||
@pytest.mark.skip(reason=render_future_feature(FutureFeature.TBL_COMMENTS)) | ||
def test_get_multi_table_comment(self): | ||
"""There are 84 permutations of this test that are skipped.""" | ||
pass | ||
|
||
@pytest.mark.skip(reason=render_future_feature(FutureFeature.TBL_OPTS, True)) | ||
def test_multi_get_table_options_tables(self): | ||
"""It's not clear what the expected ouput from this method would even _be_. Requires research.""" | ||
|
@@ -302,14 +271,6 @@ def test_get_multi_pk_constraint(self): | |
def test_get_multi_check_constraints(self): | ||
pass | ||
|
||
@pytest.mark.skip(reason=render_future_feature(FutureFeature.TBL_COMMENTS)) | ||
def test_get_comments(self): | ||
pass | ||
|
||
@pytest.mark.skip(reason=render_future_feature(FutureFeature.TBL_COMMENTS)) | ||
def test_get_comments_with_schema(self): | ||
pass | ||
|
||
@pytest.mark.skip(reason=render_future_feature(FutureFeature.TBL_COMMENTS)) | ||
def test_comments_unicode(self): | ||
pass | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,6 +55,7 @@ class SkipReason(Enum): | |
TIMEZONE_OPT = "timezone-optional TIMESTAMP fields" | ||
TRANSACTIONS = "transactions" | ||
UNIQUE = "UNIQUE constraints" | ||
DROP_TBL = "drop table comment" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since |
||
|
||
|
||
def render_skip_reason(rsn: SkipReason, setup_error=False, extra=False) -> str: | ||
|
@@ -222,6 +223,13 @@ def test_uuid_returning(self): | |
|
||
|
||
class FutureTableDDLTest(FutureTableDDLTest): | ||
@pytest.mark.skip(reason=render_skip_reason(SkipReason.DROP_TBL)) | ||
def test_drop_table_comment(self, connection): | ||
"""The DropTableComment statement is supported but it sets the comment to '' | ||
instead of None so this test can't pass. | ||
""" | ||
pass | ||
|
||
@pytest.mark.skip(render_skip_reason(SkipReason.INDEXES)) | ||
def test_create_index_if_not_exists(self): | ||
"""We could use requirements.index_reflection and requirements.index_ddl_if_exists | ||
|
@@ -238,6 +246,13 @@ def test_drop_index_if_exists(self): | |
|
||
|
||
class TableDDLTest(TableDDLTest): | ||
@pytest.mark.skip(reason=render_skip_reason(SkipReason.DROP_TBL)) | ||
def test_drop_table_comment(self, connection): | ||
"""The DropTableComment statement is supported but it sets the comment to '' | ||
instead of None so this test can't pass. | ||
""" | ||
pass | ||
|
||
@pytest.mark.skip(reason=render_skip_reason(SkipReason.INDEXES)) | ||
def test_create_index_if_not_exists(self, connection): | ||
"""We could use requirements.index_reflection and requirements.index_ddl_if_exists | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch 🙏