From be101e8a37ae9121206d5ef68eb7b0b2f2318e15 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Tue, 20 Dec 2022 02:32:27 +0100 Subject: [PATCH 01/12] Tests/Chore: Properly close socket connections to silence warnings --- .../client/sqlalchemy/tests/connection_test.py | 14 ++++++++++++++ src/crate/testing/layer.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/crate/client/sqlalchemy/tests/connection_test.py b/src/crate/client/sqlalchemy/tests/connection_test.py index b1dc5d85..8344adc1 100644 --- a/src/crate/client/sqlalchemy/tests/connection_test.py +++ b/src/crate/client/sqlalchemy/tests/connection_test.py @@ -35,6 +35,8 @@ def test_default_connection(self): conn = engine.raw_connection() self.assertEqual(">", repr(conn.connection)) + conn.close() + engine.dispose() def test_connection_server_uri_http(self): engine = sa.create_engine( @@ -42,6 +44,8 @@ def test_connection_server_uri_http(self): conn = engine.raw_connection() self.assertEqual(">", repr(conn.connection)) + conn.close() + engine.dispose() def test_connection_server_uri_https(self): engine = sa.create_engine( @@ -49,6 +53,8 @@ def test_connection_server_uri_https(self): conn = engine.raw_connection() self.assertEqual(">", repr(conn.connection)) + conn.close() + engine.dispose() def test_connection_server_uri_invalid_port(self): with self.assertRaises(ValueError) as context: @@ -63,6 +69,8 @@ def test_connection_server_uri_https_with_trusted_user(self): repr(conn.connection)) self.assertEqual(conn.connection.client.username, "foo") self.assertEqual(conn.connection.client.password, None) + conn.close() + engine.dispose() def test_connection_server_uri_https_with_credentials(self): engine = sa.create_engine( @@ -72,6 +80,8 @@ def test_connection_server_uri_https_with_credentials(self): repr(conn.connection)) self.assertEqual(conn.connection.client.username, "foo") self.assertEqual(conn.connection.client.password, "bar") + conn.close() + engine.dispose() def test_connection_multiple_server_http(self): engine = sa.create_engine( @@ -84,6 +94,8 @@ def test_connection_multiple_server_http(self): ">", repr(conn.connection)) + conn.close() + engine.dispose() def test_connection_multiple_server_https(self): engine = sa.create_engine( @@ -97,3 +109,5 @@ def test_connection_multiple_server_https(self): ">", repr(conn.connection)) + conn.close() + engine.dispose() diff --git a/src/crate/testing/layer.py b/src/crate/testing/layer.py index 3c5ed939..5fd6d8fd 100644 --- a/src/crate/testing/layer.py +++ b/src/crate/testing/layer.py @@ -321,12 +321,12 @@ def start(self): sys.stderr.write('\nCrate instance ready.\n') def stop(self): + self.conn_pool.clear() if self.process: self.process.terminate() self.process.communicate(timeout=10) self.process.stdout.close() self.process = None - self.conn_pool.clear() self.monitor.stop() self._clean() From 85d6273d991977f3512cc691a58413d0e0017096 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Tue, 20 Dec 2022 10:01:52 +0100 Subject: [PATCH 02/12] CI/Sandbox: Update to SQLAlchemy 1.4.45 and CrateDB 5.1.2 --- .github/workflows/tests.yml | 4 ++-- bootstrap.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8c3d6223..022f44ba 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,8 +21,8 @@ jobs: matrix: os: ['ubuntu-latest', 'macos-latest'] python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] - cratedb-version: ['5.1.1'] - sqla-version: ['1.3.24', '1.4.44'] + cratedb-version: ['5.1.2'] + sqla-version: ['1.3.24', '1.4.45'] # To save resources, only use the most recent Python version on macOS. exclude: - os: 'macos-latest' diff --git a/bootstrap.sh b/bootstrap.sh index 60d05f4f..613373a1 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -18,8 +18,8 @@ # Default variables. BUILDOUT_VERSION=${BUILDOUT_VERSION:-2.13.7} -CRATEDB_VERSION=${CRATEDB_VERSION:-5.1.1} -SQLALCHEMY_VERSION=${SQLALCHEMY_VERSION:-1.4.44} +CRATEDB_VERSION=${CRATEDB_VERSION:-5.1.2} +SQLALCHEMY_VERSION=${SQLALCHEMY_VERSION:-1.4.45} function print_header() { From 055030cdae56e3af6a8362af5cf01dafbfc05040 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Tue, 20 Dec 2022 02:33:01 +0100 Subject: [PATCH 03/12] SA20: Run tests with `SQLALCHEMY_WARN_20=1` --- .github/workflows/tests.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 022f44ba..c8975504 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,11 +60,14 @@ jobs: # Report about the test matrix slot. echo "Invoking tests with CrateDB ${CRATEDB_VERSION} and SQLAlchemy ${SQLALCHEMY_VERSION}" - # Invoke validation tasks. + # Run linter. flake8 src bin - coverage run bin/test -vv1 + + # Run tests. + export SQLALCHEMY_WARN_20=1 + coverage run bin/test -vvv - # Set the stage for the Codecov step. + # Set the stage for uploading the coverage report. coverage xml # https://github.com/codecov/codecov-action From 13de09fe53d37165f12704123d08ff3e739746a5 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Tue, 20 Dec 2022 02:54:30 +0100 Subject: [PATCH 04/12] SA20: Towards compatibility with SQLAlchemy 2.x Most notable changes: - Declarative becomes a first class API, so it is now located at `sqlalchemy.orm.declarative_base`. - The `Session` no longer supports "bound metadata" when it resolves the engine to be used for connectivity. This means that an Engine object **must** be passed to the constructor now. - `connection.execute` will now only accept SQL statements wrapped in `text()`. - When needing to run a driver-level SQL statement, the method `connection.exec_driver_sql` must be used. - Library-level "Autocommit" aka. "implicit commit" removed from both Core and ORM. - `select()` no longer accepts varied constructor arguments, only the "generative" style of `select()` will be supported. The list of columns / tables to select from should be passed positionally. - The `autoload` option to `sa.MetaData` has been deprecated. `autoload_with` takes place, it is already there and supported. https://docs.sqlalchemy.org/en/20/changelog/migration_20.html --- .../sqlalchemy/advanced-querying.rst | 9 +++--- docs/by-example/sqlalchemy/crud.rst | 30 ++++++++++--------- .../by-example/sqlalchemy/getting-started.rst | 3 +- .../sqlalchemy/inspection-reflection.rst | 1 - .../sqlalchemy/working-with-types.rst | 5 ++-- src/crate/client/sqlalchemy/dialect.py | 20 ++++++------- .../client/sqlalchemy/tests/array_test.py | 7 ++--- .../client/sqlalchemy/tests/bulk_test.py | 7 ++--- .../client/sqlalchemy/tests/compiler_test.py | 13 ++++---- .../sqlalchemy/tests/create_table_test.py | 30 +++++++++---------- .../client/sqlalchemy/tests/datetime_test.py | 7 ++--- .../client/sqlalchemy/tests/dialect_test.py | 19 +++++------- .../client/sqlalchemy/tests/dict_test.py | 28 ++++++++--------- .../client/sqlalchemy/tests/function_test.py | 4 +-- .../tests/insert_from_select_test.py | 12 ++++---- .../client/sqlalchemy/tests/match_test.py | 7 ++--- .../client/sqlalchemy/tests/update_test.py | 7 ++--- 17 files changed, 98 insertions(+), 111 deletions(-) diff --git a/docs/by-example/sqlalchemy/advanced-querying.rst b/docs/by-example/sqlalchemy/advanced-querying.rst index 026ef635..562839a5 100644 --- a/docs/by-example/sqlalchemy/advanced-querying.rst +++ b/docs/by-example/sqlalchemy/advanced-querying.rst @@ -21,8 +21,7 @@ Introduction Import the relevant symbols: >>> import sqlalchemy as sa - >>> from sqlalchemy.ext.declarative import declarative_base - >>> from sqlalchemy.orm import sessionmaker + >>> from sqlalchemy.orm import declarative_base, sessionmaker >>> from uuid import uuid4 Establish a connection to the database, see also :ref:`sa:engines_toplevel` @@ -237,8 +236,8 @@ Let's add a task to the ``Todo`` table: Now, let's use ``insert().from_select()`` to archive the task into the ``ArchivedTasks`` table: - >>> sel = select([Todos.id, Todos.content]).where(Todos.status == "done") - >>> ins = insert(ArchivedTasks).from_select(['id','content'], sel) + >>> sel = select(Todos.id, Todos.content).where(Todos.status == "done") + >>> ins = insert(ArchivedTasks).from_select(['id', 'content'], sel) >>> result = session.execute(ins) >>> session.commit() @@ -250,7 +249,7 @@ This will emit the following ``INSERT`` statement to the database: Now, verify that the data is present in the database: >>> _ = connection.execute(sa.text("REFRESH TABLE archived_tasks")) - >>> pprint([str(r) for r in session.execute("SELECT content FROM archived_tasks")]) + >>> pprint([str(r) for r in session.execute(sa.text("SELECT content FROM archived_tasks"))]) ["('Write Tests',)"] diff --git a/docs/by-example/sqlalchemy/crud.rst b/docs/by-example/sqlalchemy/crud.rst index a84404f3..35ba931b 100644 --- a/docs/by-example/sqlalchemy/crud.rst +++ b/docs/by-example/sqlalchemy/crud.rst @@ -27,8 +27,7 @@ Import the relevant symbols: >>> import sqlalchemy as sa >>> from datetime import datetime >>> from sqlalchemy import delete, func, text - >>> from sqlalchemy.ext.declarative import declarative_base - >>> from sqlalchemy.orm import sessionmaker + >>> from sqlalchemy.orm import declarative_base, sessionmaker >>> from crate.client.sqlalchemy.types import ObjectArray Establish a connection to the database, see also :ref:`sa:engines_toplevel` @@ -40,7 +39,7 @@ and :ref:`connect`: Define the ORM schema for the ``Location`` entity using SQLAlchemy's :ref:`sa:orm_declarative_mapping`: - >>> Base = declarative_base(bind=engine) + >>> Base = declarative_base() >>> class Location(Base): ... __tablename__ = 'locations' @@ -74,7 +73,7 @@ Insert a new location: Refresh "locations" table: - >>> _ = connection.execute("REFRESH TABLE locations") + >>> _ = connection.execute(text("REFRESH TABLE locations")) Inserted location is available: @@ -175,7 +174,7 @@ The datetime and date can be set using an update statement: Refresh "locations" table: - >>> _ = connection.execute("REFRESH TABLE locations") + >>> _ = connection.execute(text("REFRESH TABLE locations")) Boolean values get set natively: @@ -196,8 +195,9 @@ And verify that the date and datetime was persisted: Update a record using SQL: - >>> result = connection.execute("update locations set kind='Heimat' where name='Earth'") - >>> result.rowcount + >>> with engine.begin() as conn: + ... result = conn.execute(text("update locations set kind='Heimat' where name='Earth'")) + ... result.rowcount 1 Update multiple records: @@ -211,27 +211,29 @@ Update multiple records: Refresh table: - >>> _ = connection.execute("REFRESH TABLE locations") + >>> _ = connection.execute(text("REFRESH TABLE locations")) Update multiple records using SQL: - >>> result = connection.execute("update locations set flag=true where kind='Update'") - >>> result.rowcount + >>> with engine.begin() as conn: + ... result = conn.execute(text("update locations set flag=true where kind='Update'")) + ... result.rowcount 10 Update all records using SQL, and check that the number of documents affected of an update without ``where-clause`` matches the number of all documents in the table: - >>> result = connection.execute(u"update locations set kind='Überall'") - >>> result.rowcount == connection.execute("select * from locations limit 100").rowcount + >>> with engine.begin() as conn: + ... result = conn.execute(text(u"update locations set kind='Überall'")) + ... result.rowcount == conn.execute(text("select * from locations limit 100")).rowcount True >>> session.commit() Refresh "locations" table: - >>> _ = connection.execute("REFRESH TABLE locations") + >>> _ = connection.execute(text("REFRESH TABLE locations")) Objects can be used within lists, too: @@ -282,7 +284,7 @@ Deleting a record with SQLAlchemy works like this. >>> session.commit() >>> session.flush() - >>> _ = connection.execute("REFRESH TABLE locations") + >>> _ = connection.execute(text("REFRESH TABLE locations")) >>> session.query(Location).count() 23 diff --git a/docs/by-example/sqlalchemy/getting-started.rst b/docs/by-example/sqlalchemy/getting-started.rst index f3fa34cb..0f3bd26a 100644 --- a/docs/by-example/sqlalchemy/getting-started.rst +++ b/docs/by-example/sqlalchemy/getting-started.rst @@ -28,8 +28,7 @@ Introduction Import the relevant symbols: >>> import sqlalchemy as sa - >>> from sqlalchemy.ext.declarative import declarative_base - >>> from sqlalchemy.orm import sessionmaker + >>> from sqlalchemy.orm import declarative_base, sessionmaker Establish a connection to the database, see also :ref:`sa:engines_toplevel` and :ref:`connect`: diff --git a/docs/by-example/sqlalchemy/inspection-reflection.rst b/docs/by-example/sqlalchemy/inspection-reflection.rst index 1d811a17..bb291157 100644 --- a/docs/by-example/sqlalchemy/inspection-reflection.rst +++ b/docs/by-example/sqlalchemy/inspection-reflection.rst @@ -82,7 +82,6 @@ Create a SQLAlchemy table object: >>> meta = sa.MetaData() >>> table = sa.Table( ... "characters", meta, - ... autoload=True, ... autoload_with=engine) Reflect column data types from the table metadata: diff --git a/docs/by-example/sqlalchemy/working-with-types.rst b/docs/by-example/sqlalchemy/working-with-types.rst index 1016c439..8fc2f147 100644 --- a/docs/by-example/sqlalchemy/working-with-types.rst +++ b/docs/by-example/sqlalchemy/working-with-types.rst @@ -26,8 +26,7 @@ Import the relevant symbols: >>> from datetime import datetime >>> from geojson import Point, Polygon >>> from sqlalchemy import delete, func, text - >>> from sqlalchemy.ext.declarative import declarative_base - >>> from sqlalchemy.orm import sessionmaker + >>> from sqlalchemy.orm import declarative_base, sessionmaker >>> from sqlalchemy.sql import operators >>> from uuid import uuid4 >>> from crate.client.sqlalchemy.types import Object, ObjectArray @@ -156,7 +155,7 @@ Update nested dictionary Refresh and query "characters" table: - >>> _ = connection.execute("REFRESH TABLE characters") + >>> _ = connection.execute(text("REFRESH TABLE characters")) >>> session.refresh(char_nested) >>> char_nested = session.query(Character).filter_by(id='1234id').one() diff --git a/src/crate/client/sqlalchemy/dialect.py b/src/crate/client/sqlalchemy/dialect.py index 903a803c..80ab2c20 100644 --- a/src/crate/client/sqlalchemy/dialect.py +++ b/src/crate/client/sqlalchemy/dialect.py @@ -228,7 +228,7 @@ def has_table(self, connection, table_name, schema=None): @reflection.cache def get_schema_names(self, connection, **kw): - cursor = connection.execute( + cursor = connection.exec_driver_sql( "select schema_name " "from information_schema.schemata " "order by schema_name asc" @@ -237,21 +237,21 @@ def get_schema_names(self, connection, **kw): @reflection.cache def get_table_names(self, connection, schema=None, **kw): - cursor = connection.execute( + cursor = connection.exec_driver_sql( "SELECT table_name FROM information_schema.tables " "WHERE {0} = ? " "AND table_type = 'BASE TABLE' " "ORDER BY table_name ASC, {0} ASC".format(self.schema_column), - [schema or self.default_schema_name] + (schema or self.default_schema_name, ) ) return [row[0] for row in cursor.fetchall()] @reflection.cache def get_view_names(self, connection, schema=None, **kw): - cursor = connection.execute( + cursor = connection.exec_driver_sql( "SELECT table_name FROM information_schema.views " "ORDER BY table_name ASC, {0} ASC".format(self.schema_column), - [schema or self.default_schema_name] + (schema or self.default_schema_name, ) ) return [row[0] for row in cursor.fetchall()] @@ -262,11 +262,11 @@ def get_columns(self, connection, table_name, schema=None, **kw): "WHERE table_name = ? AND {0} = ? " \ "AND column_name !~ ?" \ .format(self.schema_column) - cursor = connection.execute( + cursor = connection.exec_driver_sql( query, - [table_name, + (table_name, schema or self.default_schema_name, - r"(.*)\[\'(.*)\'\]"] # regex to filter subscript + r"(.*)\[\'(.*)\'\]") # regex to filter subscript ) return [self._create_column_info(row) for row in cursor.fetchall()] @@ -301,9 +301,9 @@ def result_fun(result): rows = result.fetchone() return set(rows[0] if rows else []) - pk_result = engine.execute( + pk_result = engine.exec_driver_sql( query, - [table_name, schema or self.default_schema_name] + (table_name, schema or self.default_schema_name) ) pks = result_fun(pk_result) return {'constrained_columns': pks, diff --git a/src/crate/client/sqlalchemy/tests/array_test.py b/src/crate/client/sqlalchemy/tests/array_test.py index c65a2d9d..d3a19fcb 100644 --- a/src/crate/client/sqlalchemy/tests/array_test.py +++ b/src/crate/client/sqlalchemy/tests/array_test.py @@ -25,8 +25,7 @@ import sqlalchemy as sa from sqlalchemy.sql import operators -from sqlalchemy.orm import Session -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base, Session from crate.client.cursor import Cursor @@ -40,7 +39,7 @@ class SqlAlchemyArrayTypeTest(TestCase): def setUp(self): self.engine = sa.create_engine('crate://') - Base = declarative_base(bind=self.engine) + Base = declarative_base() self.metadata = sa.MetaData() class User(Base): @@ -51,7 +50,7 @@ class User(Base): scores = sa.Column(sa.ARRAY(sa.Integer)) self.User = User - self.session = Session() + self.session = Session(bind=self.engine) def assertSQL(self, expected_str, actual_expr): self.assertEqual(expected_str, str(actual_expr).replace('\n', '')) diff --git a/src/crate/client/sqlalchemy/tests/bulk_test.py b/src/crate/client/sqlalchemy/tests/bulk_test.py index c9d60319..ab10f3be 100644 --- a/src/crate/client/sqlalchemy/tests/bulk_test.py +++ b/src/crate/client/sqlalchemy/tests/bulk_test.py @@ -23,8 +23,7 @@ from unittest.mock import patch, MagicMock import sqlalchemy as sa -from sqlalchemy.orm import Session -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base, Session from crate.client.cursor import Cursor @@ -38,7 +37,7 @@ class SqlAlchemyBulkTest(TestCase): def setUp(self): self.engine = sa.create_engine('crate://') - Base = declarative_base(bind=self.engine) + Base = declarative_base() class Character(Base): __tablename__ = 'characters' @@ -47,7 +46,7 @@ class Character(Base): age = sa.Column(sa.Integer) self.character = Character - self.session = Session() + self.session = Session(bind=self.engine) @patch('crate.client.connection.Cursor', FakeCursor) def test_bulk_save(self): diff --git a/src/crate/client/sqlalchemy/tests/compiler_test.py b/src/crate/client/sqlalchemy/tests/compiler_test.py index c49e14b3..47317db7 100644 --- a/src/crate/client/sqlalchemy/tests/compiler_test.py +++ b/src/crate/client/sqlalchemy/tests/compiler_test.py @@ -24,7 +24,7 @@ from crate.client.sqlalchemy.compiler import crate_before_execute import sqlalchemy as sa -from sqlalchemy.sql import update, text +from sqlalchemy.sql import text, Update from crate.client.sqlalchemy.sa_version import SA_VERSION, SA_1_4 from crate.client.sqlalchemy.types import Craty @@ -40,7 +40,7 @@ def setUp(self): sa.Column('name', sa.String), sa.Column('data', Craty)) - self.update = update(self.mytable, text('where name=:name')) + self.update = Update(self.mytable).where(text('name=:name')) self.values = [{'name': 'crate'}] self.values = (self.values, ) @@ -75,9 +75,8 @@ def test_select_with_offset(self): """ Verify the `CrateCompiler.limit_clause` method, with offset. """ - self.metadata.bind = self.crate_engine selectable = self.mytable.select().offset(5) - statement = str(selectable.compile()) + statement = str(selectable.compile(bind=self.crate_engine)) if SA_VERSION >= SA_1_4: self.assertEqual(statement, "SELECT mytable.name, mytable.data \nFROM mytable\n LIMIT ALL OFFSET ?") else: @@ -87,16 +86,14 @@ def test_select_with_limit(self): """ Verify the `CrateCompiler.limit_clause` method, with limit. """ - self.metadata.bind = self.crate_engine selectable = self.mytable.select().limit(42) - statement = str(selectable.compile()) + statement = str(selectable.compile(bind=self.crate_engine)) self.assertEqual(statement, "SELECT mytable.name, mytable.data \nFROM mytable \n LIMIT ?") def test_select_with_offset_and_limit(self): """ Verify the `CrateCompiler.limit_clause` method, with offset and limit. """ - self.metadata.bind = self.crate_engine selectable = self.mytable.select().offset(5).limit(42) - statement = str(selectable.compile()) + statement = str(selectable.compile(bind=self.crate_engine)) self.assertEqual(statement, "SELECT mytable.name, mytable.data \nFROM mytable \n LIMIT ? OFFSET ?") diff --git a/src/crate/client/sqlalchemy/tests/create_table_test.py b/src/crate/client/sqlalchemy/tests/create_table_test.py index 595f2aa8..09deeccb 100644 --- a/src/crate/client/sqlalchemy/tests/create_table_test.py +++ b/src/crate/client/sqlalchemy/tests/create_table_test.py @@ -20,7 +20,7 @@ # software solely pursuant to the terms of the relevant commercial agreement. import sqlalchemy as sa -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base from crate.client.sqlalchemy.types import Object, ObjectArray, Geopoint from crate.client.cursor import Cursor @@ -39,7 +39,7 @@ class SqlAlchemyCreateTableTest(TestCase): def setUp(self): self.engine = sa.create_engine('crate://') - self.Base = declarative_base(bind=self.engine) + self.Base = declarative_base() def test_table_basic_types(self): class User(self.Base): @@ -57,7 +57,7 @@ class User(self.Base): float_col = sa.Column(sa.Float) double_col = sa.Column(sa.DECIMAL) - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) fake_cursor.execute.assert_called_with( ('\nCREATE TABLE users (\n\tstring_col STRING NOT NULL, ' '\n\tunicode_col STRING, \n\ttext_col STRING, \n\tint_col INT, ' @@ -74,7 +74,7 @@ class DummyTable(self.Base): __tablename__ = 'dummy' pk = sa.Column(sa.String, primary_key=True) obj_col = sa.Column(Object) - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) fake_cursor.execute.assert_called_with( ('\nCREATE TABLE dummy (\n\tpk STRING NOT NULL, \n\tobj_col OBJECT, ' '\n\tPRIMARY KEY (pk)\n)\n\n'), @@ -88,7 +88,7 @@ class DummyTable(self.Base): } pk = sa.Column(sa.String, primary_key=True) p = sa.Column(sa.String) - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) fake_cursor.execute.assert_called_with( ('\nCREATE TABLE t (\n\t' 'pk STRING NOT NULL, \n\t' @@ -102,7 +102,7 @@ class DummyTable(self.Base): __tablename__ = 't' ts = sa.Column(sa.BigInteger, primary_key=True) p = sa.Column(sa.BigInteger, sa.Computed("date_trunc('day', ts)")) - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) fake_cursor.execute.assert_called_with( ('\nCREATE TABLE t (\n\t' 'ts LONG NOT NULL, \n\t' @@ -117,7 +117,7 @@ class DummyTable(self.Base): ts = sa.Column(sa.BigInteger, primary_key=True) p = sa.Column(sa.BigInteger, sa.Computed("date_trunc('day', ts)", persisted=False)) with self.assertRaises(sa.exc.CompileError): - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) def test_table_partitioned_by(self): class DummyTable(self.Base): @@ -128,7 +128,7 @@ class DummyTable(self.Base): } pk = sa.Column(sa.String, primary_key=True) p = sa.Column(sa.String) - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) fake_cursor.execute.assert_called_with( ('\nCREATE TABLE t (\n\t' 'pk STRING NOT NULL, \n\t' @@ -146,7 +146,7 @@ class DummyTable(self.Base): } pk = sa.Column(sa.String, primary_key=True) - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) fake_cursor.execute.assert_called_with( ('\nCREATE TABLE t (\n\t' 'pk STRING NOT NULL, \n\t' @@ -163,7 +163,7 @@ class DummyTable(self.Base): } pk = sa.Column(sa.String, primary_key=True) p = sa.Column(sa.String, primary_key=True) - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) fake_cursor.execute.assert_called_with( ('\nCREATE TABLE t (\n\t' 'pk STRING NOT NULL, \n\t' @@ -178,7 +178,7 @@ class DummyTable(self.Base): pk = sa.Column(sa.String, primary_key=True) tags = sa.Column(ObjectArray) - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) fake_cursor.execute.assert_called_with( ('\nCREATE TABLE t (\n\t' 'pk STRING NOT NULL, \n\t' @@ -192,7 +192,7 @@ class DummyTable(self.Base): a = sa.Column(sa.Integer, nullable=True) b = sa.Column(sa.Integer, nullable=False) - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) fake_cursor.execute.assert_called_with( ('\nCREATE TABLE t (\n\t' 'pk STRING NOT NULL, \n\t' @@ -205,7 +205,7 @@ class DummyTable(self.Base): __tablename__ = 't' pk = sa.Column(sa.String, primary_key=True, nullable=True) with self.assertRaises(sa.exc.CompileError): - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) def test_column_crate_index(self): class DummyTable(self.Base): @@ -214,7 +214,7 @@ class DummyTable(self.Base): a = sa.Column(sa.Integer, crate_index=False) b = sa.Column(sa.Integer, crate_index=True) - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) fake_cursor.execute.assert_called_with( ('\nCREATE TABLE t (\n\t' 'pk STRING NOT NULL, \n\t' @@ -228,4 +228,4 @@ class DummyTable(self.Base): pk = sa.Column(sa.String, primary_key=True) a = sa.Column(Geopoint, crate_index=False) with self.assertRaises(sa.exc.CompileError): - self.Base.metadata.create_all() + self.Base.metadata.create_all(bind=self.engine) diff --git a/src/crate/client/sqlalchemy/tests/datetime_test.py b/src/crate/client/sqlalchemy/tests/datetime_test.py index 0e85e3ef..1f4d0c72 100644 --- a/src/crate/client/sqlalchemy/tests/datetime_test.py +++ b/src/crate/client/sqlalchemy/tests/datetime_test.py @@ -26,8 +26,7 @@ import sqlalchemy as sa from sqlalchemy.exc import DBAPIError -from sqlalchemy.orm import Session -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base, Session from crate.client.cursor import Cursor @@ -54,7 +53,7 @@ class SqlAlchemyDateAndDateTimeTest(TestCase): def setUp(self): self.engine = sa.create_engine('crate://') - Base = declarative_base(bind=self.engine) + Base = declarative_base() class Character(Base): __tablename__ = 'characters' @@ -66,7 +65,7 @@ class Character(Base): ('characters_name', None, None, None, None, None, None), ('characters_date', None, None, None, None, None, None) ) - self.session = Session() + self.session = Session(bind=self.engine) self.Character = Character def test_date_can_handle_datetime(self): diff --git a/src/crate/client/sqlalchemy/tests/dialect_test.py b/src/crate/client/sqlalchemy/tests/dialect_test.py index 51922c84..5c04f1ac 100644 --- a/src/crate/client/sqlalchemy/tests/dialect_test.py +++ b/src/crate/client/sqlalchemy/tests/dialect_test.py @@ -28,8 +28,7 @@ from crate.client.cursor import Cursor from crate.client.sqlalchemy.types import Object from sqlalchemy import inspect -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import Session +from sqlalchemy.orm import declarative_base, Session from sqlalchemy.testing import eq_, in_ FakeCursor = MagicMock(name='FakeCursor', spec=Cursor) @@ -48,13 +47,14 @@ def setUp(self): FakeCursor.return_value = self.fake_cursor self.engine = sa.create_engine('crate://') + self.executed_statement = None self.connection = self.engine.connect() self.fake_cursor.execute = self.execute_wrapper - self.base = declarative_base(bind=self.engine) + self.base = declarative_base() class Character(self.base): __tablename__ = 'characters' @@ -64,12 +64,10 @@ class Character(self.base): obj = sa.Column(Object) ts = sa.Column(sa.DateTime, onupdate=datetime.utcnow) - self.character = Character - self.session = Session() + self.session = Session(bind=self.engine) def test_primary_keys_2_3_0(self): - meta = self.character.metadata - insp = inspect(meta.bind) + insp = inspect(self.session.bind) self.engine.dialect.server_version_info = (2, 3, 0) self.fake_cursor.rowcount = 3 @@ -84,8 +82,7 @@ def test_primary_keys_2_3_0(self): in_("table_catalog = ?", self.executed_statement) def test_primary_keys_3_0_0(self): - meta = self.character.metadata - insp = inspect(meta.bind) + insp = inspect(self.session.bind) self.engine.dialect.server_version_info = (3, 0, 0) self.fake_cursor.rowcount = 3 @@ -106,7 +103,7 @@ def test_get_table_names(self): ) self.fake_cursor.fetchall = MagicMock(return_value=[["t1"], ["t2"]]) - insp = inspect(self.character.metadata.bind) + insp = inspect(self.session.bind) self.engine.dialect.server_version_info = (2, 0, 0) eq_(insp.get_table_names(schema="doc"), ['t1', 't2']) @@ -119,7 +116,7 @@ def test_get_view_names(self): ) self.fake_cursor.fetchall = MagicMock(return_value=[["v1"], ["v2"]]) - insp = inspect(self.character.metadata.bind) + insp = inspect(self.session.bind) self.engine.dialect.server_version_info = (2, 0, 0) eq_(insp.get_view_names(schema="doc"), ['v1', 'v2']) diff --git a/src/crate/client/sqlalchemy/tests/dict_test.py b/src/crate/client/sqlalchemy/tests/dict_test.py index 7f4464c0..5bd9ceed 100644 --- a/src/crate/client/sqlalchemy/tests/dict_test.py +++ b/src/crate/client/sqlalchemy/tests/dict_test.py @@ -25,8 +25,7 @@ import sqlalchemy as sa from sqlalchemy.sql import select -from sqlalchemy.orm import Session -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base, Session from crate.client.sqlalchemy.types import Craty, ObjectArray from crate.client.cursor import Cursor @@ -41,7 +40,8 @@ class SqlAlchemyDictTypeTest(TestCase): def setUp(self): self.engine = sa.create_engine('crate://') - metadata = sa.MetaData() + # FIXME: Deprecated with SA20. + metadata = sa.MetaData(bind=self.engine) self.mytable = sa.Table('mytable', metadata, sa.Column('name', sa.String), sa.Column('data', Craty)) @@ -53,12 +53,12 @@ def test_select_with_dict_column(self): mytable = self.mytable self.assertSQL( "SELECT mytable.data['x'] AS anon_1 FROM mytable", - select([mytable.c.data['x']], bind=self.engine) + select(mytable.c.data['x']) ) def test_select_with_dict_column_where_clause(self): mytable = self.mytable - s = select([mytable.c.data], bind=self.engine).\ + s = select(mytable.c.data).\ where(mytable.c.data['x'] == 1) self.assertSQL( "SELECT mytable.data FROM mytable WHERE mytable.data['x'] = ?", @@ -67,7 +67,7 @@ def test_select_with_dict_column_where_clause(self): def test_select_with_dict_column_nested_where(self): mytable = self.mytable - s = select([mytable.c.name], bind=self.engine) + s = select(mytable.c.name) s = s.where(mytable.c.data['x']['y'] == 1) self.assertSQL( "SELECT mytable.name FROM mytable " + @@ -77,7 +77,7 @@ def test_select_with_dict_column_nested_where(self): def test_select_with_dict_column_where_clause_gt(self): mytable = self.mytable - s = select([mytable.c.data], bind=self.engine).\ + s = select(mytable.c.data).\ where(mytable.c.data['x'] > 1) self.assertSQL( "SELECT mytable.data FROM mytable WHERE mytable.data['x'] > ?", @@ -86,7 +86,7 @@ def test_select_with_dict_column_where_clause_gt(self): def test_select_with_dict_column_where_clause_other_col(self): mytable = self.mytable - s = select([mytable.c.name], bind=self.engine) + s = select(mytable.c.name) s = s.where(mytable.c.data['x'] == mytable.c.name) self.assertSQL( "SELECT mytable.name FROM mytable " + @@ -96,7 +96,7 @@ def test_select_with_dict_column_where_clause_other_col(self): def test_update_with_dict_column(self): mytable = self.mytable - stmt = mytable.update(bind=self.engine).\ + stmt = mytable.update().\ where(mytable.c.name == 'Arthur Dent').\ values({ "data['x']": "Trillian" @@ -114,7 +114,7 @@ def set_up_character_and_cursor(self, return_value=None): ('characters_data', None, None, None, None, None, None) ) fake_cursor.rowcount = 1 - Base = declarative_base(bind=self.engine) + Base = declarative_base() class Character(Base): __tablename__ = 'characters' @@ -123,7 +123,7 @@ class Character(Base): data = sa.Column(Craty) data_list = sa.Column(ObjectArray) - session = Session() + session = Session(bind=self.engine) return session, Character def test_assign_null_to_object_array(self): @@ -266,7 +266,7 @@ def test_partial_dict_update_with_delitem_setitem(self): return_value=[('Trillian', {'x': 1})] ) - session = Session() + session = Session(bind=self.engine) char = Character(name='Trillian') char.data = {'x': 1} session.add(char) @@ -339,14 +339,14 @@ def set_up_character_and_cursor_data_list(self, return_value=None): ) fake_cursor.rowcount = 1 - Base = declarative_base(bind=self.engine) + Base = declarative_base() class Character(Base): __tablename__ = 'characters' name = sa.Column(sa.String, primary_key=True) data_list = sa.Column(ObjectArray) - session = Session() + session = Session(bind=self.engine) return session, Character def _setup_object_array_char(self): diff --git a/src/crate/client/sqlalchemy/tests/function_test.py b/src/crate/client/sqlalchemy/tests/function_test.py index 1b4a1983..95613955 100644 --- a/src/crate/client/sqlalchemy/tests/function_test.py +++ b/src/crate/client/sqlalchemy/tests/function_test.py @@ -22,13 +22,13 @@ from unittest import TestCase import sqlalchemy as sa +from sqlalchemy.orm import declarative_base from sqlalchemy.sql.sqltypes import TIMESTAMP -from sqlalchemy.ext.declarative import declarative_base class SqlAlchemyFunctionTest(TestCase): def setUp(self): - Base = declarative_base(bind=sa.create_engine("crate://")) + Base = declarative_base() class Character(Base): __tablename__ = "characters" diff --git a/src/crate/client/sqlalchemy/tests/insert_from_select_test.py b/src/crate/client/sqlalchemy/tests/insert_from_select_test.py index 0c5ba73f..af8624dd 100644 --- a/src/crate/client/sqlalchemy/tests/insert_from_select_test.py +++ b/src/crate/client/sqlalchemy/tests/insert_from_select_test.py @@ -24,8 +24,7 @@ from unittest.mock import patch, MagicMock import sqlalchemy as sa -from sqlalchemy.orm import Session -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base, Session from sqlalchemy import select, insert from crate.client.cursor import Cursor @@ -44,7 +43,7 @@ def assertSQL(self, expected_str, actual_expr): def setUp(self): self.engine = sa.create_engine('crate://') - Base = declarative_base(bind=self.engine) + Base = declarative_base() class Character(Base): __tablename__ = 'characters' @@ -64,7 +63,7 @@ class CharacterArchive(Base): self.character = Character self.character_archived = CharacterArchive - self.session = Session() + self.session = Session(bind=self.engine) @patch('crate.client.connection.Cursor', FakeCursor) def test_insert_from_select_triggered(self): @@ -72,11 +71,12 @@ def test_insert_from_select_triggered(self): self.session.add(char) self.session.commit() - sel = select([self.character.name, self.character.age]).where(self.character.status == "Archived") + sel = select(self.character.name, self.character.age).where(self.character.status == "Archived") ins = insert(self.character_archived).from_select(['name', 'age'], sel) self.session.execute(ins) self.session.commit() + # TODO: Verify if this is correct. self.assertSQL( - "INSERT INTO characters_archive (name, age) SELECT characters.name, characters.age FROM characters WHERE characters.status = ?", + "INSERT INTO characters_archive (name, age) SELECT characters.name, characters.age FROM characters WHERE characters.status = :status_1", ins ) diff --git a/src/crate/client/sqlalchemy/tests/match_test.py b/src/crate/client/sqlalchemy/tests/match_test.py index 71b79d0d..021ec584 100644 --- a/src/crate/client/sqlalchemy/tests/match_test.py +++ b/src/crate/client/sqlalchemy/tests/match_test.py @@ -24,8 +24,7 @@ from unittest.mock import MagicMock import sqlalchemy as sa -from sqlalchemy.orm import Session -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base, Session from crate.client.sqlalchemy.types import Craty from crate.client.sqlalchemy.predicates import match @@ -52,14 +51,14 @@ def assertSQL(self, expected_str, actual_expr): self.assertEqual(expected_str, str(actual_expr).replace('\n', '')) def set_up_character_and_session(self): - Base = declarative_base(bind=self.engine) + Base = declarative_base() class Character(Base): __tablename__ = 'characters' name = sa.Column(sa.String, primary_key=True) info = sa.Column(Craty) - session = Session() + session = Session(bind=self.engine) return session, Character def test_simple_match(self): diff --git a/src/crate/client/sqlalchemy/tests/update_test.py b/src/crate/client/sqlalchemy/tests/update_test.py index 394163aa..0a29d85e 100644 --- a/src/crate/client/sqlalchemy/tests/update_test.py +++ b/src/crate/client/sqlalchemy/tests/update_test.py @@ -26,8 +26,7 @@ from crate.client.sqlalchemy.types import Object import sqlalchemy as sa -from sqlalchemy.orm import Session -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base, Session from crate.client.cursor import Cursor @@ -42,7 +41,7 @@ class SqlAlchemyUpdateTest(TestCase): def setUp(self): self.engine = sa.create_engine('crate://') - self.base = declarative_base(bind=self.engine) + self.base = declarative_base() class Character(self.base): __tablename__ = 'characters' @@ -53,7 +52,7 @@ class Character(self.base): ts = sa.Column(sa.DateTime, onupdate=datetime.utcnow) self.character = Character - self.session = Session() + self.session = Session(bind=self.engine) @patch('crate.client.connection.Cursor', FakeCursor) def test_onupdate_is_triggered(self): From a553233cd7d84664ba0ea9572b3396e3166d9a28 Mon Sep 17 00:00:00 2001 From: Niklas Schmidtmer Date: Tue, 20 Dec 2022 11:56:00 +0100 Subject: [PATCH 05/12] SA20: Address deprecation warnings in SqlAlchemyDictTypeTest --- .../client/sqlalchemy/tests/dict_test.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/crate/client/sqlalchemy/tests/dict_test.py b/src/crate/client/sqlalchemy/tests/dict_test.py index 5bd9ceed..2ffa7f0e 100644 --- a/src/crate/client/sqlalchemy/tests/dict_test.py +++ b/src/crate/client/sqlalchemy/tests/dict_test.py @@ -37,14 +37,14 @@ class SqlAlchemyDictTypeTest(TestCase): - def setUp(self): self.engine = sa.create_engine('crate://') - # FIXME: Deprecated with SA20. - metadata = sa.MetaData(bind=self.engine) - self.mytable = sa.Table('mytable', metadata, + metadata = sa.MetaData() + self.mytable = sa.Table('mytable', + metadata, sa.Column('name', sa.String), - sa.Column('data', Craty)) + sa.Column('data', Craty), + autoload_with=self.engine) def assertSQL(self, expected_str, actual_expr): self.assertEqual(expected_str, str(actual_expr).replace('\n', '')) @@ -52,7 +52,7 @@ def assertSQL(self, expected_str, actual_expr): def test_select_with_dict_column(self): mytable = self.mytable self.assertSQL( - "SELECT mytable.data['x'] AS anon_1 FROM mytable", + "SELECT mytable.data[:data_1] AS anon_1 FROM mytable", select(mytable.c.data['x']) ) @@ -61,7 +61,7 @@ def test_select_with_dict_column_where_clause(self): s = select(mytable.c.data).\ where(mytable.c.data['x'] == 1) self.assertSQL( - "SELECT mytable.data FROM mytable WHERE mytable.data['x'] = ?", + "SELECT mytable.data FROM mytable WHERE mytable.data[:data_1] = :param_1", s ) @@ -71,7 +71,7 @@ def test_select_with_dict_column_nested_where(self): s = s.where(mytable.c.data['x']['y'] == 1) self.assertSQL( "SELECT mytable.name FROM mytable " + - "WHERE mytable.data['x']['y'] = ?", + "WHERE mytable.data[:data_1][:param_1] = :param_2", s ) @@ -80,7 +80,7 @@ def test_select_with_dict_column_where_clause_gt(self): s = select(mytable.c.data).\ where(mytable.c.data['x'] > 1) self.assertSQL( - "SELECT mytable.data FROM mytable WHERE mytable.data['x'] > ?", + "SELECT mytable.data FROM mytable WHERE mytable.data[:data_1] > :param_1", s ) @@ -90,7 +90,7 @@ def test_select_with_dict_column_where_clause_other_col(self): s = s.where(mytable.c.data['x'] == mytable.c.name) self.assertSQL( "SELECT mytable.name FROM mytable " + - "WHERE mytable.data['x'] = mytable.name", + "WHERE mytable.data[:data_1] = mytable.name", s ) @@ -98,11 +98,10 @@ def test_update_with_dict_column(self): mytable = self.mytable stmt = mytable.update().\ where(mytable.c.name == 'Arthur Dent').\ - values({ - "data['x']": "Trillian" - }) + values({mytable.c.data['x']: "Trillian"}) + self.assertSQL( - "UPDATE mytable SET data['x'] = ? WHERE mytable.name = ?", + "UPDATE mytable SET data[:data_1]=:param_1 WHERE mytable.name = :name_1", stmt ) From ddfee8a630f8ae4c0567f696997b9f212eac01b6 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Tue, 20 Dec 2022 23:23:36 +0100 Subject: [PATCH 06/12] SA20: Fix SqlAlchemyDictTypeTest Don't use `autoload_with` on table `mytable`, because it will never be persisted to a database, so it can not be inspected. --- src/crate/client/sqlalchemy/tests/dict_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/crate/client/sqlalchemy/tests/dict_test.py b/src/crate/client/sqlalchemy/tests/dict_test.py index 2ffa7f0e..655b55a4 100644 --- a/src/crate/client/sqlalchemy/tests/dict_test.py +++ b/src/crate/client/sqlalchemy/tests/dict_test.py @@ -43,8 +43,7 @@ def setUp(self): self.mytable = sa.Table('mytable', metadata, sa.Column('name', sa.String), - sa.Column('data', Craty), - autoload_with=self.engine) + sa.Column('data', Craty)) def assertSQL(self, expected_str, actual_expr): self.assertEqual(expected_str, str(actual_expr).replace('\n', '')) From b4ade5c01092080f86c4eaefaab2523e62bb96eb Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Wed, 21 Dec 2022 22:00:39 +0100 Subject: [PATCH 07/12] Revert "SA20: Address deprecation warnings in SqlAlchemyDictTypeTest" This reverts commit b20fabad0aa9330db4cbeabf243dff49129eb399. --- .../client/sqlalchemy/tests/dict_test.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/crate/client/sqlalchemy/tests/dict_test.py b/src/crate/client/sqlalchemy/tests/dict_test.py index 655b55a4..5bd9ceed 100644 --- a/src/crate/client/sqlalchemy/tests/dict_test.py +++ b/src/crate/client/sqlalchemy/tests/dict_test.py @@ -37,11 +37,12 @@ class SqlAlchemyDictTypeTest(TestCase): + def setUp(self): self.engine = sa.create_engine('crate://') - metadata = sa.MetaData() - self.mytable = sa.Table('mytable', - metadata, + # FIXME: Deprecated with SA20. + metadata = sa.MetaData(bind=self.engine) + self.mytable = sa.Table('mytable', metadata, sa.Column('name', sa.String), sa.Column('data', Craty)) @@ -51,7 +52,7 @@ def assertSQL(self, expected_str, actual_expr): def test_select_with_dict_column(self): mytable = self.mytable self.assertSQL( - "SELECT mytable.data[:data_1] AS anon_1 FROM mytable", + "SELECT mytable.data['x'] AS anon_1 FROM mytable", select(mytable.c.data['x']) ) @@ -60,7 +61,7 @@ def test_select_with_dict_column_where_clause(self): s = select(mytable.c.data).\ where(mytable.c.data['x'] == 1) self.assertSQL( - "SELECT mytable.data FROM mytable WHERE mytable.data[:data_1] = :param_1", + "SELECT mytable.data FROM mytable WHERE mytable.data['x'] = ?", s ) @@ -70,7 +71,7 @@ def test_select_with_dict_column_nested_where(self): s = s.where(mytable.c.data['x']['y'] == 1) self.assertSQL( "SELECT mytable.name FROM mytable " + - "WHERE mytable.data[:data_1][:param_1] = :param_2", + "WHERE mytable.data['x']['y'] = ?", s ) @@ -79,7 +80,7 @@ def test_select_with_dict_column_where_clause_gt(self): s = select(mytable.c.data).\ where(mytable.c.data['x'] > 1) self.assertSQL( - "SELECT mytable.data FROM mytable WHERE mytable.data[:data_1] > :param_1", + "SELECT mytable.data FROM mytable WHERE mytable.data['x'] > ?", s ) @@ -89,7 +90,7 @@ def test_select_with_dict_column_where_clause_other_col(self): s = s.where(mytable.c.data['x'] == mytable.c.name) self.assertSQL( "SELECT mytable.name FROM mytable " + - "WHERE mytable.data[:data_1] = mytable.name", + "WHERE mytable.data['x'] = mytable.name", s ) @@ -97,10 +98,11 @@ def test_update_with_dict_column(self): mytable = self.mytable stmt = mytable.update().\ where(mytable.c.name == 'Arthur Dent').\ - values({mytable.c.data['x']: "Trillian"}) - + values({ + "data['x']": "Trillian" + }) self.assertSQL( - "UPDATE mytable SET data[:data_1]=:param_1 WHERE mytable.name = :name_1", + "UPDATE mytable SET data['x'] = ? WHERE mytable.name = ?", stmt ) From c1dc310ff8d771c6e91d5efe9eb347a4ed572017 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Wed, 21 Dec 2022 22:22:35 +0100 Subject: [PATCH 08/12] SA20: Fix SqlAlchemyDictTypeTest "Implicit" and "Connectionless" execution, and "bound metadata" have been removed beginning with SQLAlchemy 2.0 [1]. Earlier and contemporary versions of SQLAlchemy had the ability to associate an `Engine` with a `MetaData` object. This allowed a number of so-called "connectionless" execution patterns. That is no longer possible. Instead, the association with an `Engine` object has to be concluded differently. On this very spot, in the context of the `dict_test` test cases, the most easy fix was to move it to the invocation of the `compile()` method of the `selectable` instance, which is now returned by the `sqlalchemy.sql.*` primitives: expression = selectable.compile(bind=self.engine) This is needed, because otherwise, when not associating `Engine` with `MetaData` properly, `CrateDialect` would be bypassed, and the "paramstyle" [2] of SQLAlchemy's `DefaultDialect` would be used, which is actually "named" [3], as originally reflected per b20fabad0. This is probably wrong, because the CrateDB Python driver uses the "qmark" paramstyle [4]. [1] https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#implicit-and-connectionless-execution-bound-metadata-removed [2] https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.paramstyle [3] https://github.com/sqlalchemy/sqlalchemy/blob/rel_2_0_0b4/lib/sqlalchemy/engine/default.py#L204 [4] https://github.com/crate/crate-python/blob/0.29.0/src/crate/client/__init__.py#L36 --- src/crate/client/sqlalchemy/tests/dict_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/crate/client/sqlalchemy/tests/dict_test.py b/src/crate/client/sqlalchemy/tests/dict_test.py index 5bd9ceed..dbec0eef 100644 --- a/src/crate/client/sqlalchemy/tests/dict_test.py +++ b/src/crate/client/sqlalchemy/tests/dict_test.py @@ -40,13 +40,13 @@ class SqlAlchemyDictTypeTest(TestCase): def setUp(self): self.engine = sa.create_engine('crate://') - # FIXME: Deprecated with SA20. - metadata = sa.MetaData(bind=self.engine) + metadata = sa.MetaData() self.mytable = sa.Table('mytable', metadata, sa.Column('name', sa.String), sa.Column('data', Craty)) - def assertSQL(self, expected_str, actual_expr): + def assertSQL(self, expected_str, selectable): + actual_expr = selectable.compile(bind=self.engine) self.assertEqual(expected_str, str(actual_expr).replace('\n', '')) def test_select_with_dict_column(self): From b5237cf890d5a28c49e2d7e81d74d3cd5cc02b05 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Thu, 22 Dec 2022 02:14:40 +0100 Subject: [PATCH 09/12] SA20: Fix SqlAlchemyInsertFromSelectTest By binding the insert clause to an engine, use the "qmark" paramstyle again, instead of falling back to the "named" paramstyle of the default dialect. --- src/crate/client/sqlalchemy/tests/insert_from_select_test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/crate/client/sqlalchemy/tests/insert_from_select_test.py b/src/crate/client/sqlalchemy/tests/insert_from_select_test.py index af8624dd..b4be7005 100644 --- a/src/crate/client/sqlalchemy/tests/insert_from_select_test.py +++ b/src/crate/client/sqlalchemy/tests/insert_from_select_test.py @@ -75,8 +75,7 @@ def test_insert_from_select_triggered(self): ins = insert(self.character_archived).from_select(['name', 'age'], sel) self.session.execute(ins) self.session.commit() - # TODO: Verify if this is correct. self.assertSQL( - "INSERT INTO characters_archive (name, age) SELECT characters.name, characters.age FROM characters WHERE characters.status = :status_1", - ins + "INSERT INTO characters_archive (name, age) SELECT characters.name, characters.age FROM characters WHERE characters.status = ?", + ins.compile(bind=self.engine) ) From 10eb2848024b6da2d6cd1674cf3cef3cfcbdb1b5 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Thu, 22 Dec 2022 14:26:31 +0100 Subject: [PATCH 10/12] CI: Turn off "fail-fast" on the test matrix to get the big picture --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c8975504..6641daf1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: python-version: '3.9' - os: 'macos-latest' python-version: '3.10' - fail-fast: true + fail-fast: false env: CRATEDB_VERSION: ${{ matrix.cratedb-version }} SQLALCHEMY_VERSION: ${{ matrix.sqla-version }} From 5bff9d20dff0b5bb115c395512f899397b5b6063 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Fri, 23 Dec 2022 23:48:07 +0100 Subject: [PATCH 11/12] SA20: Restore backward-compatibility with SQLAlchemy 1.3 Resolve import of `declarative_base`. For SA13, it is in `sqlalchemy.ext.declarative`, while it is in `sqlalchemy.orm` on more recent versions of SA. --- docs/by-example/sqlalchemy/advanced-querying.rst | 6 +++++- docs/by-example/sqlalchemy/crud.rst | 6 +++++- docs/by-example/sqlalchemy/getting-started.rst | 6 +++++- docs/by-example/sqlalchemy/working-with-types.rst | 6 +++++- src/crate/client/sqlalchemy/tests/array_test.py | 6 +++++- src/crate/client/sqlalchemy/tests/bulk_test.py | 6 +++++- src/crate/client/sqlalchemy/tests/create_table_test.py | 5 ++++- src/crate/client/sqlalchemy/tests/datetime_test.py | 6 +++++- src/crate/client/sqlalchemy/tests/dialect_test.py | 6 +++++- src/crate/client/sqlalchemy/tests/dict_test.py | 6 +++++- src/crate/client/sqlalchemy/tests/function_test.py | 5 ++++- .../client/sqlalchemy/tests/insert_from_select_test.py | 6 +++++- src/crate/client/sqlalchemy/tests/match_test.py | 6 +++++- src/crate/client/sqlalchemy/tests/update_test.py | 6 +++++- 14 files changed, 68 insertions(+), 14 deletions(-) diff --git a/docs/by-example/sqlalchemy/advanced-querying.rst b/docs/by-example/sqlalchemy/advanced-querying.rst index 562839a5..863373e4 100644 --- a/docs/by-example/sqlalchemy/advanced-querying.rst +++ b/docs/by-example/sqlalchemy/advanced-querying.rst @@ -21,7 +21,11 @@ Introduction Import the relevant symbols: >>> import sqlalchemy as sa - >>> from sqlalchemy.orm import declarative_base, sessionmaker + >>> from sqlalchemy.orm import sessionmaker + >>> try: + ... from sqlalchemy.orm import declarative_base + ... except ImportError: + ... from sqlalchemy.ext.declarative import declarative_base >>> from uuid import uuid4 Establish a connection to the database, see also :ref:`sa:engines_toplevel` diff --git a/docs/by-example/sqlalchemy/crud.rst b/docs/by-example/sqlalchemy/crud.rst index 35ba931b..d2840c52 100644 --- a/docs/by-example/sqlalchemy/crud.rst +++ b/docs/by-example/sqlalchemy/crud.rst @@ -27,7 +27,11 @@ Import the relevant symbols: >>> import sqlalchemy as sa >>> from datetime import datetime >>> from sqlalchemy import delete, func, text - >>> from sqlalchemy.orm import declarative_base, sessionmaker + >>> from sqlalchemy.orm import sessionmaker + >>> try: + ... from sqlalchemy.orm import declarative_base + ... except ImportError: + ... from sqlalchemy.ext.declarative import declarative_base >>> from crate.client.sqlalchemy.types import ObjectArray Establish a connection to the database, see also :ref:`sa:engines_toplevel` diff --git a/docs/by-example/sqlalchemy/getting-started.rst b/docs/by-example/sqlalchemy/getting-started.rst index 0f3bd26a..c64964dc 100644 --- a/docs/by-example/sqlalchemy/getting-started.rst +++ b/docs/by-example/sqlalchemy/getting-started.rst @@ -28,7 +28,11 @@ Introduction Import the relevant symbols: >>> import sqlalchemy as sa - >>> from sqlalchemy.orm import declarative_base, sessionmaker + >>> from sqlalchemy.orm import sessionmaker + >>> try: + ... from sqlalchemy.orm import declarative_base + ... except ImportError: + ... from sqlalchemy.ext.declarative import declarative_base Establish a connection to the database, see also :ref:`sa:engines_toplevel` and :ref:`connect`: diff --git a/docs/by-example/sqlalchemy/working-with-types.rst b/docs/by-example/sqlalchemy/working-with-types.rst index 8fc2f147..bcddf8f8 100644 --- a/docs/by-example/sqlalchemy/working-with-types.rst +++ b/docs/by-example/sqlalchemy/working-with-types.rst @@ -26,8 +26,12 @@ Import the relevant symbols: >>> from datetime import datetime >>> from geojson import Point, Polygon >>> from sqlalchemy import delete, func, text - >>> from sqlalchemy.orm import declarative_base, sessionmaker + >>> from sqlalchemy.orm import sessionmaker >>> from sqlalchemy.sql import operators + >>> try: + ... from sqlalchemy.orm import declarative_base + ... except ImportError: + ... from sqlalchemy.ext.declarative import declarative_base >>> from uuid import uuid4 >>> from crate.client.sqlalchemy.types import Object, ObjectArray >>> from crate.client.sqlalchemy.types import Geopoint, Geoshape diff --git a/src/crate/client/sqlalchemy/tests/array_test.py b/src/crate/client/sqlalchemy/tests/array_test.py index d3a19fcb..6d663327 100644 --- a/src/crate/client/sqlalchemy/tests/array_test.py +++ b/src/crate/client/sqlalchemy/tests/array_test.py @@ -25,7 +25,11 @@ import sqlalchemy as sa from sqlalchemy.sql import operators -from sqlalchemy.orm import declarative_base, Session +from sqlalchemy.orm import Session +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from crate.client.cursor import Cursor diff --git a/src/crate/client/sqlalchemy/tests/bulk_test.py b/src/crate/client/sqlalchemy/tests/bulk_test.py index ab10f3be..95bc1ddd 100644 --- a/src/crate/client/sqlalchemy/tests/bulk_test.py +++ b/src/crate/client/sqlalchemy/tests/bulk_test.py @@ -23,7 +23,11 @@ from unittest.mock import patch, MagicMock import sqlalchemy as sa -from sqlalchemy.orm import declarative_base, Session +from sqlalchemy.orm import Session +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from crate.client.cursor import Cursor diff --git a/src/crate/client/sqlalchemy/tests/create_table_test.py b/src/crate/client/sqlalchemy/tests/create_table_test.py index 09deeccb..7eca2628 100644 --- a/src/crate/client/sqlalchemy/tests/create_table_test.py +++ b/src/crate/client/sqlalchemy/tests/create_table_test.py @@ -20,7 +20,10 @@ # software solely pursuant to the terms of the relevant commercial agreement. import sqlalchemy as sa -from sqlalchemy.orm import declarative_base +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from crate.client.sqlalchemy.types import Object, ObjectArray, Geopoint from crate.client.cursor import Cursor diff --git a/src/crate/client/sqlalchemy/tests/datetime_test.py b/src/crate/client/sqlalchemy/tests/datetime_test.py index 1f4d0c72..07e98ede 100644 --- a/src/crate/client/sqlalchemy/tests/datetime_test.py +++ b/src/crate/client/sqlalchemy/tests/datetime_test.py @@ -26,7 +26,11 @@ import sqlalchemy as sa from sqlalchemy.exc import DBAPIError -from sqlalchemy.orm import declarative_base, Session +from sqlalchemy.orm import Session +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from crate.client.cursor import Cursor diff --git a/src/crate/client/sqlalchemy/tests/dialect_test.py b/src/crate/client/sqlalchemy/tests/dialect_test.py index 5c04f1ac..a6669df4 100644 --- a/src/crate/client/sqlalchemy/tests/dialect_test.py +++ b/src/crate/client/sqlalchemy/tests/dialect_test.py @@ -28,7 +28,11 @@ from crate.client.cursor import Cursor from crate.client.sqlalchemy.types import Object from sqlalchemy import inspect -from sqlalchemy.orm import declarative_base, Session +from sqlalchemy.orm import Session +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.testing import eq_, in_ FakeCursor = MagicMock(name='FakeCursor', spec=Cursor) diff --git a/src/crate/client/sqlalchemy/tests/dict_test.py b/src/crate/client/sqlalchemy/tests/dict_test.py index dbec0eef..2324591e 100644 --- a/src/crate/client/sqlalchemy/tests/dict_test.py +++ b/src/crate/client/sqlalchemy/tests/dict_test.py @@ -25,7 +25,11 @@ import sqlalchemy as sa from sqlalchemy.sql import select -from sqlalchemy.orm import declarative_base, Session +from sqlalchemy.orm import Session +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from crate.client.sqlalchemy.types import Craty, ObjectArray from crate.client.cursor import Cursor diff --git a/src/crate/client/sqlalchemy/tests/function_test.py b/src/crate/client/sqlalchemy/tests/function_test.py index 95613955..072ab43a 100644 --- a/src/crate/client/sqlalchemy/tests/function_test.py +++ b/src/crate/client/sqlalchemy/tests/function_test.py @@ -22,8 +22,11 @@ from unittest import TestCase import sqlalchemy as sa -from sqlalchemy.orm import declarative_base from sqlalchemy.sql.sqltypes import TIMESTAMP +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base class SqlAlchemyFunctionTest(TestCase): diff --git a/src/crate/client/sqlalchemy/tests/insert_from_select_test.py b/src/crate/client/sqlalchemy/tests/insert_from_select_test.py index b4be7005..692dfa55 100644 --- a/src/crate/client/sqlalchemy/tests/insert_from_select_test.py +++ b/src/crate/client/sqlalchemy/tests/insert_from_select_test.py @@ -24,8 +24,12 @@ from unittest.mock import patch, MagicMock import sqlalchemy as sa -from sqlalchemy.orm import declarative_base, Session from sqlalchemy import select, insert +from sqlalchemy.orm import Session +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from crate.client.cursor import Cursor diff --git a/src/crate/client/sqlalchemy/tests/match_test.py b/src/crate/client/sqlalchemy/tests/match_test.py index 021ec584..fdd5b7d0 100644 --- a/src/crate/client/sqlalchemy/tests/match_test.py +++ b/src/crate/client/sqlalchemy/tests/match_test.py @@ -24,7 +24,11 @@ from unittest.mock import MagicMock import sqlalchemy as sa -from sqlalchemy.orm import declarative_base, Session +from sqlalchemy.orm import Session +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from crate.client.sqlalchemy.types import Craty from crate.client.sqlalchemy.predicates import match diff --git a/src/crate/client/sqlalchemy/tests/update_test.py b/src/crate/client/sqlalchemy/tests/update_test.py index 0a29d85e..00aeef0a 100644 --- a/src/crate/client/sqlalchemy/tests/update_test.py +++ b/src/crate/client/sqlalchemy/tests/update_test.py @@ -26,7 +26,11 @@ from crate.client.sqlalchemy.types import Object import sqlalchemy as sa -from sqlalchemy.orm import declarative_base, Session +from sqlalchemy.orm import Session +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from crate.client.cursor import Cursor From 19dbf7d6593dc2e0c22c565101516e1821419ebc Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Fri, 23 Dec 2022 23:53:49 +0100 Subject: [PATCH 12/12] SA20: Restore backward-compatibility with SQLAlchemy 1.3 Note that this is on a best-effort basis. SQLAlchemy 2.0 will be mostly compatible with most SQLAlchemy 1.3 code which is not using any of the deprecated functionalities, modulo a few aspects. > It is possible to have code that is compatible with 2.0 and 1.3 at the > same time, you would just need to ensure you use the subset of > features and APIs that are common to both. This patch adds a small compatibility layer, which activates on SA13 only, and monkey-patches two spots where anomalies have been spotted. --- src/crate/client/sqlalchemy/__init__.py | 6 + .../client/sqlalchemy/compat/__init__.py | 0 src/crate/client/sqlalchemy/compat/api13.py | 133 ++++++++++++++++++ src/crate/client/sqlalchemy/tests/__init__.py | 8 ++ 4 files changed, 147 insertions(+) create mode 100644 src/crate/client/sqlalchemy/compat/__init__.py create mode 100644 src/crate/client/sqlalchemy/compat/api13.py diff --git a/src/crate/client/sqlalchemy/__init__.py b/src/crate/client/sqlalchemy/__init__.py index a0241e99..52864719 100644 --- a/src/crate/client/sqlalchemy/__init__.py +++ b/src/crate/client/sqlalchemy/__init__.py @@ -19,7 +19,13 @@ # with Crate these terms will supersede the license and you may use the # software solely pursuant to the terms of the relevant commercial agreement. +from .compat.api13 import monkeypatch_add_exec_driver_sql from .dialect import CrateDialect +from .sa_version import SA_1_4, SA_VERSION + +# SQLAlchemy 1.3 does not have the `exec_driver_sql` method. +if SA_VERSION < SA_1_4: + monkeypatch_add_exec_driver_sql() __all__ = [ CrateDialect, diff --git a/src/crate/client/sqlalchemy/compat/__init__.py b/src/crate/client/sqlalchemy/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/crate/client/sqlalchemy/compat/api13.py b/src/crate/client/sqlalchemy/compat/api13.py new file mode 100644 index 00000000..16f53393 --- /dev/null +++ b/src/crate/client/sqlalchemy/compat/api13.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8; -*- +# +# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. Crate licenses +# this file to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may +# obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# 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. + +""" +Compatibility module for running a subset of SQLAlchemy 2.0 programs on +SQLAlchemy 1.3. By using monkey-patching, it can do two things: + +1. Add the `exec_driver_sql` method to SA's `Connection` and `Engine`. +2. Amend the `sql.select` function to accept the calling semantics of + the modern variant. + +Reason: `exec_driver_sql` gets used within the CrateDB dialect already, +and the new calling semantics of `sql.select` already get used within +many of the test cases already. Please note that the patch for +`sql.select` is only applied when running the test suite. +""" + +import collections.abc as collections_abc + +from sqlalchemy import exc +from sqlalchemy.sql import Select +from sqlalchemy.sql import select as original_select +from sqlalchemy.util import immutabledict + + +# `_distill_params_20` copied from SA14's `sqlalchemy.engine.{base,util}`. +_no_tuple = () +_no_kw = immutabledict() + + +def _distill_params_20(params): + if params is None: + return _no_tuple, _no_kw + elif isinstance(params, list): + # collections_abc.MutableSequence): # avoid abc.__instancecheck__ + if params and not isinstance(params[0], (collections_abc.Mapping, tuple)): + raise exc.ArgumentError( + "List argument must consist only of tuples or dictionaries" + ) + + return (params,), _no_kw + elif isinstance( + params, + (tuple, dict, immutabledict), + # only do abc.__instancecheck__ for Mapping after we've checked + # for plain dictionaries and would otherwise raise + ) or isinstance(params, collections_abc.Mapping): + return (params,), _no_kw + else: + raise exc.ArgumentError("mapping or sequence expected for parameters") + + +def exec_driver_sql(self, statement, parameters=None, execution_options=None): + """ + Adapter for `exec_driver_sql`, which is available since SA14, for SA13. + """ + if execution_options is not None: + raise ValueError( + "SA13 backward-compatibility: " + "`exec_driver_sql` does not support `execution_options`" + ) + args_10style, kwargs_10style = _distill_params_20(parameters) + return self.execute(statement, *args_10style, **kwargs_10style) + + +def monkeypatch_add_exec_driver_sql(): + """ + Transparently add SA14's `exec_driver_sql()` method to SA13. + + AttributeError: 'Connection' object has no attribute 'exec_driver_sql' + AttributeError: 'Engine' object has no attribute 'exec_driver_sql' + """ + from sqlalchemy.engine.base import Connection, Engine + + # Add `exec_driver_sql` method to SA's `Connection` and `Engine` classes. + Connection.exec_driver_sql = exec_driver_sql + Engine.exec_driver_sql = exec_driver_sql + + +def select_sa14(*columns, **kw) -> Select: + """ + Adapt SA14/SA20's calling semantics of `sql.select()` to SA13. + + With SA20, `select()` no longer accepts varied constructor arguments, only + the "generative" style of `select()` will be supported. The list of columns + / tables to select from should be passed positionally. + + Derived from https://github.com/sqlalchemy/alembic/blob/b1fad6b6/alembic/util/sqla_compat.py#L557-L558 + + sqlalchemy.exc.ArgumentError: columns argument to select() must be a Python list or other iterable + """ + if isinstance(columns, tuple) and isinstance(columns[0], list): + if "whereclause" in kw: + raise ValueError( + "SA13 backward-compatibility: " + "`whereclause` is both in kwargs and columns tuple" + ) + columns, whereclause = columns + kw["whereclause"] = whereclause + return original_select(columns, **kw) + + +def monkeypatch_amend_select_sa14(): + """ + Make SA13's `sql.select()` transparently accept calling semantics of SA14 + and SA20, by swapping in the newer variant of `select_sa14()`. + + This supports the test suite of `crate-python`, because it already uses the + modern calling semantics. + """ + import sqlalchemy + + sqlalchemy.select = select_sa14 + sqlalchemy.sql.select = select_sa14 + sqlalchemy.sql.expression.select = select_sa14 diff --git a/src/crate/client/sqlalchemy/tests/__init__.py b/src/crate/client/sqlalchemy/tests/__init__.py index 81f1ba2a..61a2669b 100644 --- a/src/crate/client/sqlalchemy/tests/__init__.py +++ b/src/crate/client/sqlalchemy/tests/__init__.py @@ -1,5 +1,13 @@ # -*- coding: utf-8 -*- +from ..compat.api13 import monkeypatch_amend_select_sa14 +from ..sa_version import SA_1_4, SA_VERSION + +# `sql.select()` of SQLAlchemy 1.3 uses old calling semantics, +# but the test cases already need the modern ones. +if SA_VERSION < SA_1_4: + monkeypatch_amend_select_sa14() + from unittest import TestSuite, makeSuite from .connection_test import SqlAlchemyConnectionTest from .dict_test import SqlAlchemyDictTypeTest