diff --git a/CHANGES.txt b/CHANGES.txt index 2ffe8765..4fbb10b9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -13,6 +13,8 @@ Unreleased certificate details are immanent, like no longer accepting the long deprecated ``commonName`` attribute. Instead, going forward, only the ``subjectAltName`` attribute will be used. +- SQLAlchemy: Improve DDL compiler to ignore foreign key and uniqueness + constraints .. _urllib3 v2.0 migration guide: https://urllib3.readthedocs.io/en/latest/v2-migration-guide.html .. _urllib3 v2.0 roadmap: https://urllib3.readthedocs.io/en/stable/v2-roadmap.html diff --git a/src/crate/client/sqlalchemy/compiler.py b/src/crate/client/sqlalchemy/compiler.py index 3ae7a7cb..cdc07728 100644 --- a/src/crate/client/sqlalchemy/compiler.py +++ b/src/crate/client/sqlalchemy/compiler.py @@ -20,6 +20,7 @@ # software solely pursuant to the terms of the relevant commercial agreement. import string +import warnings from collections import defaultdict import sqlalchemy as sa @@ -178,6 +179,22 @@ def post_create_table(self, table): ', '.join(sorted(table_opts))) return special_options + def visit_foreign_key_constraint(self, constraint, **kw): + """ + CrateDB does not support foreign key constraints. + """ + warnings.warn("CrateDB does not support foreign key constraints, " + "they will be omitted when generating DDL statements.") + return None + + def visit_unique_constraint(self, constraint, **kw): + """ + CrateDB does not support unique key constraints. + """ + warnings.warn("CrateDB does not support unique constraints, " + "they will be omitted when generating DDL statements.") + return None + class CrateTypeCompiler(compiler.GenericTypeCompiler): diff --git a/src/crate/client/sqlalchemy/tests/__init__.py b/src/crate/client/sqlalchemy/tests/__init__.py index 606c545c..d6d37493 100644 --- a/src/crate/client/sqlalchemy/tests/__init__.py +++ b/src/crate/client/sqlalchemy/tests/__init__.py @@ -14,7 +14,7 @@ from .connection_test import SqlAlchemyConnectionTest from .dict_test import SqlAlchemyDictTypeTest from .datetime_test import SqlAlchemyDateAndDateTimeTest -from .compiler_test import SqlAlchemyCompilerTest +from .compiler_test import SqlAlchemyCompilerTest, SqlAlchemyDDLCompilerTest from .update_test import SqlAlchemyUpdateTest from .match_test import SqlAlchemyMatchTest from .bulk_test import SqlAlchemyBulkTest @@ -36,6 +36,7 @@ def test_suite_unit(): tests.addTest(makeSuite(SqlAlchemyDictTypeTest)) tests.addTest(makeSuite(SqlAlchemyDateAndDateTimeTest)) tests.addTest(makeSuite(SqlAlchemyCompilerTest)) + tests.addTest(makeSuite(SqlAlchemyDDLCompilerTest)) tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": None})) tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": (4, 0, 12)})) tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": (4, 1, 10)})) diff --git a/src/crate/client/sqlalchemy/tests/compiler_test.py b/src/crate/client/sqlalchemy/tests/compiler_test.py index 5d5cc89e..44cb16ce 100644 --- a/src/crate/client/sqlalchemy/tests/compiler_test.py +++ b/src/crate/client/sqlalchemy/tests/compiler_test.py @@ -18,18 +18,30 @@ # However, if you have executed another commercial license agreement # with Crate these terms will supersede the license and you may use the # software solely pursuant to the terms of the relevant commercial agreement. +import warnings from textwrap import dedent -from unittest import mock, skipIf +from unittest import mock, skipIf, TestCase +from unittest.mock import MagicMock, patch +from crate.client.cursor import Cursor from crate.client.sqlalchemy.compiler import crate_before_execute import sqlalchemy as sa from sqlalchemy.sql import text, Update +from crate.testing.util import ExtraAssertions + +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base + from crate.client.sqlalchemy.sa_version import SA_VERSION, SA_1_4, SA_2_0 from crate.client.sqlalchemy.types import ObjectType from crate.client.test_util import ParametrizedTestCase +from crate.testing.settings import crate_host + class SqlAlchemyCompilerTest(ParametrizedTestCase): @@ -244,3 +256,151 @@ def test_insert_manyvalues(self): mock.call(mock.ANY, 'INSERT INTO mytable (name) VALUES (?), (?)', ('foo_2', 'foo_3'), None), mock.call(mock.ANY, 'INSERT INTO mytable (name) VALUES (?)', ('foo_4', ), None), ]) + + +FakeCursor = MagicMock(name='FakeCursor', spec=Cursor) + + +class CompilerTestCase(TestCase): + """ + A base class for providing mocking infrastructure to validate the DDL compiler. + """ + + def setUp(self): + self.engine = sa.create_engine(f"crate://{crate_host}") + self.metadata = sa.MetaData(schema="testdrive") + self.session = sa.orm.Session(bind=self.engine) + self.setup_mock() + + def setup_mock(self): + """ + Set up a fake cursor, in order to intercept query execution. + """ + + self.fake_cursor = MagicMock(name="fake_cursor") + FakeCursor.return_value = self.fake_cursor + + self.executed_statement = None + self.fake_cursor.execute = self.execute_wrapper + + def execute_wrapper(self, query, *args, **kwargs): + """ + Receive the SQL query expression, and store it. + """ + self.executed_statement = query + return self.fake_cursor + + +@patch('crate.client.connection.Cursor', FakeCursor) +class SqlAlchemyDDLCompilerTest(CompilerTestCase, ExtraAssertions): + """ + Verify a few scenarios regarding the DDL compiler. + """ + + def test_ddl_with_foreign_keys(self): + """ + Verify the CrateDB dialect properly ignores foreign key constraints. + """ + + Base = declarative_base(metadata=self.metadata) + + class RootStore(Base): + """The main store.""" + + __tablename__ = "root" + + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String) + + items = sa.orm.relationship( + "ItemStore", + back_populates="root", + passive_deletes=True, + ) + + class ItemStore(Base): + """The auxiliary store.""" + + __tablename__ = "item" + + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String) + root_id = sa.Column( + sa.Integer, + sa.ForeignKey( + f"{RootStore.__tablename__}.id", + ondelete="CASCADE", + ), + ) + root = sa.orm.relationship(RootStore, back_populates="items") + + with warnings.catch_warnings(record=True) as w: + + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + + # Verify SQL DDL statement. + self.metadata.create_all(self.engine, tables=[RootStore.__table__], checkfirst=False) + self.assertEqual(self.executed_statement, dedent(""" + CREATE TABLE testdrive.root ( + \tid INT NOT NULL, + \tname STRING, + \tPRIMARY KEY (id) + ) + + """)) # noqa: W291, W293 + + # Verify SQL DDL statement. + self.metadata.create_all(self.engine, tables=[ItemStore.__table__], checkfirst=False) + self.assertEqual(self.executed_statement, dedent(""" + CREATE TABLE testdrive.item ( + \tid INT NOT NULL, + \tname STRING, + \troot_id INT, + \tPRIMARY KEY (id) + ) + + """)) # noqa: W291, W293 + + # Verify if corresponding warning is emitted. + self.assertEqual(len(w), 1) + self.assertIsSubclass(w[-1].category, UserWarning) + self.assertIn("CrateDB does not support foreign key constraints, " + "they will be omitted when generating DDL statements.", str(w[-1].message)) + + def test_ddl_with_unique_key(self): + """ + Verify the CrateDB dialect properly ignores unique key constraints. + """ + + Base = declarative_base(metadata=self.metadata) + + class FooBar(Base): + """The entity.""" + + __tablename__ = "foobar" + + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String, unique=True) + + with warnings.catch_warnings(record=True) as w: + + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + + # Verify SQL DDL statement. + self.metadata.create_all(self.engine, tables=[FooBar.__table__], checkfirst=False) + self.assertEqual(self.executed_statement, dedent(""" + CREATE TABLE testdrive.foobar ( + \tid INT NOT NULL, + \tname STRING, + \tPRIMARY KEY (id) + ) + + """)) # noqa: W291, W293 + + # Verify if corresponding warning is emitted. + self.assertEqual(len(w), 1) + self.assertIsSubclass(w[-1].category, UserWarning) + self.assertIn("CrateDB does not support unique constraints, " + "they will be omitted when generating DDL statements.", str(w[-1].message))