diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 621d6850..e8ba181e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: os: [ubuntu-latest, macos-latest] python-version: ['3.7', '3.8', '3.9', '3.10'] cratedb-version: ['4.8.0'] - sqla-version: ['1.3.24'] + sqla-version: ['1.3.24', '1.4.37'] fail-fast: true steps: diff --git a/CHANGES.txt b/CHANGES.txt index 7d754247..ef84b976 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -22,6 +22,43 @@ Unreleased - Added support for enabling SSL using SQLAlchemy DB URI with parameter ``?ssl=true``. +- Added support for SQLAlchemy 1.4 + +.. note:: + + For learning about the transition to SQLAlchemy 1.4, we recommend the + corresponding documentation `What’s New in SQLAlchemy 1.4?`_. + + + +Breaking changes +---------------- + +Textual column expressions +'''''''''''''''''''''''''' + +SQLAlchemy 1.4 became stricter on some details. It requires to wrap `CrateDB +system columns`_ like ``_score`` in a `SQLAlchemy literal_column`_ type. +Before, it was possible to use a query like this:: + + session.query(Character.name, '_score') + +It must now be written like:: + + session.query(Character.name, sa.literal_column('_score')) + +Otherwise, SQLAlchemy will complain like:: + + sqlalchemy.exc.ArgumentError: Textual column expression '_score' should be + explicitly declared with text('_score'), or use column('_score') for more + specificity + + +.. _CrateDB system columns: https://crate.io/docs/crate/reference/en/4.8/general/ddl/system-columns.html +.. _SQLAlchemy literal_column: https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.literal_column +.. _What’s New in SQLAlchemy 1.4?: https://docs.sqlalchemy.org/en/14/changelog/migration_14.html + + 2020/09/28 0.26.0 ================= diff --git a/DEVELOP.rst b/DEVELOP.rst index cdd64551..d3d7efd4 100644 --- a/DEVELOP.rst +++ b/DEVELOP.rst @@ -33,7 +33,12 @@ Run all tests:: Run specific tests:: - # Ignore all tests below src/crate/testing + ./bin/test -vvvv -t SqlAlchemyCompilerTest + ./bin/test -vvvv -t test_score + ./bin/test -vvvv -t sqlalchemy + +Ignore specific test directories:: + ./bin/test -vvvv --ignore_dir=testing You can run the tests against multiple Python interpreters with tox_:: diff --git a/README.rst b/README.rst index 8931a6c3..4c99e2cf 100644 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ Prerequisites ============= Recent versions of this library are validated on Python 3 (>= 3.7). -It might also work on earlier versions of Python. +It may also work on earlier versions of Python. Installation diff --git a/docs/getting-started.rst b/docs/getting-started.rst index c220c2ac..99d67e0d 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -15,9 +15,8 @@ Learn how to install and get started the :ref:`CrateDB Python client library Prerequisites ============= -Python 3.7 is recommended. - -However, :ref:`older versions of Python ` can still be used. +Recent versions of this library are validated on Python 3 (>= 3.7). +It may also work on earlier versions of Python. `Pip`_ should be installed on your system. diff --git a/docs/sqlalchemy.rst b/docs/sqlalchemy.rst index ee0be079..9ee38cf0 100644 --- a/docs/sqlalchemy.rst +++ b/docs/sqlalchemy.rst @@ -11,7 +11,7 @@ The CrateDB Python client library provides support for SQLAlchemy. A CrateDB configuration. The CrateDB Python client library is validated to work with SQLAlchemy versions -``1.3``. +``1.3`` and ``1.4``. .. NOTE:: diff --git a/lgtm.yml b/lgtm.yml new file mode 100644 index 00000000..55db2cfa --- /dev/null +++ b/lgtm.yml @@ -0,0 +1,12 @@ +queries: + + # Suppress some LGTM warnings. + + # A module is imported with the "import" and "import from" statements. + # https://lgtm.com/rules/1818040193/ + - exclude: py/import-and-import-from + + # Disable rule to compensate parameter naming in `CrateCompiler._get_crud_params`. + # Using an alternative name for the first parameter of an instance method makes code more difficult to read. + # https://lgtm.com/rules/910082/ + - exclude: py/not-named-self diff --git a/setup.py b/setup.py index 6bb237de..28eea262 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ def read(path): test=['zope.testing>=4,<5', 'zc.customdoctests>=1.0.1,<2', 'stopit>=1.1.2,<2'], - sqlalchemy=['sqlalchemy>=1.0,<1.4', 'geojson>=2.5.0'] + sqlalchemy=['sqlalchemy>=1.0,<1.5', 'geojson>=2.5.0'] ), python_requires='>=3.4', install_requires=requirements, diff --git a/src/crate/client/doctests/sqlalchemy.txt b/src/crate/client/doctests/sqlalchemy.txt index 12e187a4..d1648550 100644 --- a/src/crate/client/doctests/sqlalchemy.txt +++ b/src/crate/client/doctests/sqlalchemy.txt @@ -289,7 +289,7 @@ the other rows. The higher the score value, the more relevant the row. In most cases ``_score`` is not part of the SQLAlchemy Table definition, so it must be passed as a string:: - >>> session.query(Character.name, '_score') \ + >>> session.query(Character.name, sa.literal_column('_score')) \ ... .filter(match(Character.quote_ft, 'space')) \ ... .all() [('Tricia McMillan', ...)] @@ -298,11 +298,10 @@ To search on multiple columns you have to pass a dictionary with columns and ``boost`` attached. ``boost`` is a factor that increases the relevance of a column in respect to the other columns:: - >>> from sqlalchemy.sql import text >>> session.query(Character.name) \ ... .filter(match({Character.name_ft: 1.5, Character.quote_ft: 0.1}, ... 'Arthur')) \ - ... .order_by(sa.desc(text('_score'))) \ + ... .order_by(sa.desc(sa.literal_column('_score'))) \ ... .all() [('Arthur Dent',), ('Tricia McMillan',)] diff --git a/src/crate/client/sqlalchemy/compiler.py b/src/crate/client/sqlalchemy/compiler.py index 03347a27..d5c29e83 100644 --- a/src/crate/client/sqlalchemy/compiler.py +++ b/src/crate/client/sqlalchemy/compiler.py @@ -22,10 +22,10 @@ import string from collections import defaultdict -import sqlalchemy as sa -from sqlalchemy.sql import crud -from sqlalchemy.sql import compiler +import sqlalchemy as sa # lgtm[py/import-and-import-from] +from sqlalchemy.sql import compiler, crud, selectable # lgtm[py/import-and-import-from] from .types import MutableDict +from .sa_version import SA_VERSION, SA_1_4 def rewrite_update(clauseelement, multiparams, params): @@ -70,10 +70,25 @@ def rewrite_update(clauseelement, multiparams, params): @sa.event.listens_for(sa.engine.Engine, "before_execute", retval=True) -def crate_before_execute(conn, clauseelement, multiparams, params): +def crate_before_execute(conn, clauseelement, multiparams, params, *args, **kwargs): is_crate = type(conn.dialect).__name__ == 'CrateDialect' if is_crate and isinstance(clauseelement, sa.sql.expression.Update): - return rewrite_update(clauseelement, multiparams, params) + if SA_VERSION >= SA_1_4: + if params is None: + multiparams = ([],) + else: + multiparams = ([params],) + params = {} + + clauseelement, multiparams, params = rewrite_update(clauseelement, multiparams, params) + + if SA_VERSION >= SA_1_4: + if multiparams[0]: + params = multiparams[0][0] + else: + params = multiparams[0] + multiparams = [] + return clauseelement, multiparams, params @@ -189,6 +204,9 @@ def visit_update(self, update_stmt, **kw): Parts are taken from the SQLCompiler base class. """ + if SA_VERSION >= SA_1_4: + return self.visit_update_14(update_stmt, **kw) + if not update_stmt.parameters and \ not hasattr(update_stmt, '_crate_specific'): return super(CrateCompiler, self).visit_update(update_stmt, **kw) @@ -212,11 +230,14 @@ def visit_update(self, update_stmt, **kw): update_stmt, table_text ) + # CrateDB patch. crud_params = self._get_crud_params(update_stmt, **kw) text += table_text text += ' SET ' + + # CrateDB patch begin. include_table = extra_froms and \ self.render_table_with_column_in_update_from @@ -234,6 +255,7 @@ def visit_update(self, update_stmt, **kw): set_clauses.append(k + ' = ' + self.process(bindparam)) text += ', '.join(set_clauses) + # CrateDB patch end. if self.returning or update_stmt._returning: if not self.returning: @@ -269,7 +291,6 @@ def visit_update(self, update_stmt, **kw): def _get_crud_params(compiler, stmt, **kw): """ extract values from crud parameters - taken from SQLAlchemy's crud module (since 1.0.x) and adapted for Crate dialect""" @@ -325,3 +346,325 @@ def _get_crud_params(compiler, stmt, **kw): values, kw) return values + + def visit_update_14(self, update_stmt, **kw): + + compile_state = update_stmt._compile_state_factory( + update_stmt, self, **kw + ) + update_stmt = compile_state.statement + + toplevel = not self.stack + if toplevel: + self.isupdate = True + if not self.compile_state: + self.compile_state = compile_state + + extra_froms = compile_state._extra_froms + is_multitable = bool(extra_froms) + + if is_multitable: + # main table might be a JOIN + main_froms = set(selectable._from_objects(update_stmt.table)) + render_extra_froms = [ + f for f in extra_froms if f not in main_froms + ] + correlate_froms = main_froms.union(extra_froms) + else: + render_extra_froms = [] + correlate_froms = {update_stmt.table} + + self.stack.append( + { + "correlate_froms": correlate_froms, + "asfrom_froms": correlate_froms, + "selectable": update_stmt, + } + ) + + text = "UPDATE " + + if update_stmt._prefixes: + text += self._generate_prefixes( + update_stmt, update_stmt._prefixes, **kw + ) + + table_text = self.update_tables_clause( + update_stmt, update_stmt.table, render_extra_froms, **kw + ) + + # CrateDB patch. + crud_params = _get_crud_params_14( + self, update_stmt, compile_state, **kw + ) + + if update_stmt._hints: + dialect_hints, table_text = self._setup_crud_hints( + update_stmt, table_text + ) + else: + dialect_hints = None + + if update_stmt._independent_ctes: + for cte in update_stmt._independent_ctes: + cte._compiler_dispatch(self, **kw) + + text += table_text + + text += " SET " + + # CrateDB patch begin. + include_table = extra_froms and \ + self.render_table_with_column_in_update_from + + set_clauses = [] + + for c, expr, value in crud_params: + key = c._compiler_dispatch(self, include_table=include_table) + clause = key + ' = ' + value + set_clauses.append(clause) + + for k, v in compile_state._dict_parameters.items(): + if isinstance(k, str) and '[' in k: + bindparam = sa.sql.bindparam(k, v) + clause = k + ' = ' + self.process(bindparam) + set_clauses.append(clause) + + text += ', '.join(set_clauses) + # CrateDB patch end. + + if self.returning or update_stmt._returning: + if self.returning_precedes_values: + text += " " + self.returning_clause( + update_stmt, self.returning or update_stmt._returning + ) + + if extra_froms: + extra_from_text = self.update_from_clause( + update_stmt, + update_stmt.table, + render_extra_froms, + dialect_hints, + **kw + ) + if extra_from_text: + text += " " + extra_from_text + + if update_stmt._where_criteria: + t = self._generate_delimited_and_list( + update_stmt._where_criteria, **kw + ) + if t: + text += " WHERE " + t + + limit_clause = self.update_limit_clause(update_stmt) + if limit_clause: + text += " " + limit_clause + + if ( + self.returning or update_stmt._returning + ) and not self.returning_precedes_values: + text += " " + self.returning_clause( + update_stmt, self.returning or update_stmt._returning + ) + + if self.ctes: + nesting_level = len(self.stack) if not toplevel else None + text = self._render_cte_clause(nesting_level=nesting_level) + text + + self.stack.pop(-1) + + return text + + +def _get_crud_params_14(compiler, stmt, compile_state, **kw): + """create a set of tuples representing column/string pairs for use + in an INSERT or UPDATE statement. + + Also generates the Compiled object's postfetch, prefetch, and + returning column collections, used for default handling and ultimately + populating the CursorResult's prefetch_cols() and postfetch_cols() + collections. + + """ + from sqlalchemy.sql.crud import _key_getters_for_crud_column + from sqlalchemy.sql.crud import _create_bind_param + from sqlalchemy.sql.crud import REQUIRED + from sqlalchemy.sql.crud import _get_stmt_parameter_tuples_params + from sqlalchemy.sql.crud import _get_update_multitable_params + from sqlalchemy.sql.crud import _scan_insert_from_select_cols + from sqlalchemy.sql.crud import _scan_cols + from sqlalchemy import exc # noqa: F401 + from sqlalchemy.sql.crud import _extend_values_for_multiparams + + compiler.postfetch = [] + compiler.insert_prefetch = [] + compiler.update_prefetch = [] + compiler.returning = [] + + # getters - these are normally just column.key, + # but in the case of mysql multi-table update, the rules for + # .key must conditionally take tablename into account + ( + _column_as_key, + _getattr_col_key, + _col_bind_name, + ) = getters = _key_getters_for_crud_column(compiler, stmt, compile_state) + + compiler._key_getters_for_crud_column = getters + + # no parameters in the statement, no parameters in the + # compiled params - return binds for all columns + if compiler.column_keys is None and compile_state._no_parameters: + return [ + ( + c, + compiler.preparer.format_column(c), + _create_bind_param(compiler, c, None, required=True), + ) + for c in stmt.table.columns + ] + + if compile_state._has_multi_parameters: + spd = compile_state._multi_parameters[0] + stmt_parameter_tuples = list(spd.items()) + elif compile_state._ordered_values: + spd = compile_state._dict_parameters + stmt_parameter_tuples = compile_state._ordered_values + elif compile_state._dict_parameters: + spd = compile_state._dict_parameters + stmt_parameter_tuples = list(spd.items()) + else: + stmt_parameter_tuples = spd = None + + # if we have statement parameters - set defaults in the + # compiled params + if compiler.column_keys is None: + parameters = {} + elif stmt_parameter_tuples: + parameters = dict( + (_column_as_key(key), REQUIRED) + for key in compiler.column_keys + if key not in spd + ) + else: + parameters = dict( + (_column_as_key(key), REQUIRED) for key in compiler.column_keys + ) + + # create a list of column assignment clauses as tuples + values = [] + + if stmt_parameter_tuples is not None: + _get_stmt_parameter_tuples_params( + compiler, + compile_state, + parameters, + stmt_parameter_tuples, + _column_as_key, + values, + kw, + ) + + check_columns = {} + + # special logic that only occurs for multi-table UPDATE + # statements + if compile_state.isupdate and compile_state.is_multitable: + _get_update_multitable_params( + compiler, + stmt, + compile_state, + stmt_parameter_tuples, + check_columns, + _col_bind_name, + _getattr_col_key, + values, + kw, + ) + + if compile_state.isinsert and stmt._select_names: + _scan_insert_from_select_cols( + compiler, + stmt, + compile_state, + parameters, + _getattr_col_key, + _column_as_key, + _col_bind_name, + check_columns, + values, + kw, + ) + else: + _scan_cols( + compiler, + stmt, + compile_state, + parameters, + _getattr_col_key, + _column_as_key, + _col_bind_name, + check_columns, + values, + kw, + ) + + # CrateDB patch. + # + # This sanity check performed by SQLAlchemy currently needs to be + # deactivated in order to satisfy the rewriting logic of the CrateDB + # dialect in `rewrite_update` and `visit_update`. + # + # It can be quickly reproduced by activating this section and running the + # test cases:: + # + # ./bin/test -vvvv -t dict_test + # + # That croaks like:: + # + # sqlalchemy.exc.CompileError: Unconsumed column names: characters_name, data['nested'] + # + # TODO: Investigate why this is actually happening and eventually mitigate + # the root cause. + """ + if parameters and stmt_parameter_tuples: + check = ( + set(parameters) + .intersection(_column_as_key(k) for k, v in stmt_parameter_tuples) + .difference(check_columns) + ) + if check: + raise exc.CompileError( + "Unconsumed column names: %s" + % (", ".join("%s" % (c,) for c in check)) + ) + """ + + if compile_state._has_multi_parameters: + values = _extend_values_for_multiparams( + compiler, + stmt, + compile_state, + values, + _column_as_key, + kw, + ) + elif ( + not values + and compiler.for_executemany # noqa: W503 + and compiler.dialect.supports_default_metavalue # noqa: W503 + ): + # convert an "INSERT DEFAULT VALUES" + # into INSERT (firstcol) VALUES (DEFAULT) which can be turned + # into an in-place multi values. This supports + # insert_executemany_returning mode :) + values = [ + ( + stmt.table.columns[0], + compiler.preparer.format_column(stmt.table.columns[0]), + "DEFAULT", + ) + ] + + return values diff --git a/src/crate/client/sqlalchemy/dialect.py b/src/crate/client/sqlalchemy/dialect.py index 42eb4961..637a8f92 100644 --- a/src/crate/client/sqlalchemy/dialect.py +++ b/src/crate/client/sqlalchemy/dialect.py @@ -162,6 +162,7 @@ class CrateDialect(default.DefaultDialect): ddl_compiler = CrateDDLCompiler type_compiler = CrateTypeCompiler supports_native_boolean = True + supports_statement_cache = True colspecs = colspecs implicit_returning = True @@ -237,6 +238,15 @@ def get_table_names(self, connection, schema=None, **kw): ) return [row[0] for row in cursor.fetchall()] + @reflection.cache + def get_view_names(self, connection, schema=None, **kw): + cursor = connection.execute( + "SELECT table_name FROM information_schema.views " + "ORDER BY table_name ASC, {0} ASC".format(self.schema_column), + [schema or self.default_schema_name] + ) + return [row[0] for row in cursor.fetchall()] + @reflection.cache def get_columns(self, connection, table_name, schema=None, **kw): query = "SELECT column_name, data_type " \ diff --git a/src/crate/client/sqlalchemy/doctests/reflection.txt b/src/crate/client/sqlalchemy/doctests/reflection.txt index 489081a6..ae089e31 100644 --- a/src/crate/client/sqlalchemy/doctests/reflection.txt +++ b/src/crate/client/sqlalchemy/doctests/reflection.txt @@ -17,6 +17,11 @@ List all tables:: >>> set(['checks', 'cluster', 'jobs', 'jobs_log']).issubset(inspector.get_table_names(schema='sys')) True +List all views:: + + >>> inspector.get_view_names() + ['characters_view'] + Get default schema name:: >>> inspector.default_schema_name diff --git a/src/crate/client/sqlalchemy/predicates/__init__.py b/src/crate/client/sqlalchemy/predicates/__init__.py index 1c0c2bda..4f974f92 100644 --- a/src/crate/client/sqlalchemy/predicates/__init__.py +++ b/src/crate/client/sqlalchemy/predicates/__init__.py @@ -24,6 +24,7 @@ class Match(ColumnElement): + inherit_cache = True def __init__(self, column, term, match_type=None, options=None): super(Match, self).__init__() diff --git a/src/crate/client/sqlalchemy/sa_version.py b/src/crate/client/sqlalchemy/sa_version.py index 0973a14a..502e5228 100644 --- a/src/crate/client/sqlalchemy/sa_version.py +++ b/src/crate/client/sqlalchemy/sa_version.py @@ -23,3 +23,5 @@ from distutils.version import StrictVersion as V SA_VERSION = V(sa.__version__) + +SA_1_4 = V('1.4.0b1') diff --git a/src/crate/client/sqlalchemy/tests/compiler_test.py b/src/crate/client/sqlalchemy/tests/compiler_test.py index 67b3bca8..cd3c4ef3 100644 --- a/src/crate/client/sqlalchemy/tests/compiler_test.py +++ b/src/crate/client/sqlalchemy/tests/compiler_test.py @@ -45,14 +45,14 @@ def setUp(self): def test_sqlite_update_not_rewritten(self): clauseelement, multiparams, params = crate_before_execute( - self.sqlite_engine, self.update, self.values, None + self.sqlite_engine, self.update, self.values, {} ) assert hasattr(clauseelement, '_crate_specific') is False def test_crate_update_rewritten(self): clauseelement, multiparams, params = crate_before_execute( - self.crate_engine, self.update, self.values, None + self.crate_engine, self.update, self.values, {} ) assert hasattr(clauseelement, '_crate_specific') is True diff --git a/src/crate/client/sqlalchemy/tests/dialect_test.py b/src/crate/client/sqlalchemy/tests/dialect_test.py index d3811861..7505b0e4 100644 --- a/src/crate/client/sqlalchemy/tests/dialect_test.py +++ b/src/crate/client/sqlalchemy/tests/dialect_test.py @@ -83,7 +83,6 @@ def test_primary_keys(self): in_("information_schema.key_column_usage", self.executed_statement) def test_get_table_names(self): - self.fake_cursor.rowcount = 1 self.fake_cursor.description = ( ('foo', None, None, None, None, None, None), @@ -92,6 +91,20 @@ def test_get_table_names(self): insp = inspect(self.character.metadata.bind) self.engine.dialect.server_version_info = (2, 0, 0) - eq_(insp.get_table_names(self.connection, "doc"), + eq_(insp.get_table_names(schema="doc"), ['t1', 't2']) in_("WHERE table_schema = ? AND table_type = 'BASE TABLE' ORDER BY", self.executed_statement) + + def test_get_view_names(self): + self.fake_cursor.rowcount = 1 + self.fake_cursor.description = ( + ('foo', None, None, None, None, None, None), + ) + self.fake_cursor.fetchall = MagicMock(return_value=[["v1"], ["v2"]]) + + insp = inspect(self.character.metadata.bind) + self.engine.dialect.server_version_info = (2, 0, 0) + eq_(insp.get_view_names(schema="doc"), + ['v1', 'v2']) + eq_(self.executed_statement, "SELECT table_name FROM information_schema.views " + "ORDER BY table_name ASC, table_schema ASC") diff --git a/src/crate/client/sqlalchemy/tests/dict_test.py b/src/crate/client/sqlalchemy/tests/dict_test.py index d9a1cc3f..2efc6d81 100644 --- a/src/crate/client/sqlalchemy/tests/dict_test.py +++ b/src/crate/client/sqlalchemy/tests/dict_test.py @@ -138,7 +138,7 @@ def test_assign_null_to_object_array(self): @patch('crate.client.connection.Cursor', FakeCursor) def test_assign_to_craty_type_after_commit(self): session, Character = self.set_up_character_and_cursor( - return_value=[('Trillian', None, None)] + return_value=[('Trillian', None)] ) char = Character(name='Trillian') session.add(char) diff --git a/src/crate/client/sqlalchemy/tests/match_test.py b/src/crate/client/sqlalchemy/tests/match_test.py index e716ffcc..71b79d0d 100644 --- a/src/crate/client/sqlalchemy/tests/match_test.py +++ b/src/crate/client/sqlalchemy/tests/match_test.py @@ -109,7 +109,8 @@ def test_match_type_options(self): ) def test_score(self): - query = self.session.query(self.Character.name, '_score') \ + query = self.session.query(self.Character.name, + sa.literal_column('_score')) \ .filter(match(self.Character.name, 'Trillian')) self.assertSQL( "SELECT characters.name AS characters_name, _score " + diff --git a/src/crate/client/sqlalchemy/types.py b/src/crate/client/sqlalchemy/types.py index a343d5e3..1a3d7a06 100644 --- a/src/crate/client/sqlalchemy/types.py +++ b/src/crate/client/sqlalchemy/types.py @@ -132,6 +132,7 @@ def __eq__(self, other): class _Craty(sqltypes.UserDefinedType): + cache_ok = True class Comparator(sqltypes.TypeEngine.Comparator): @@ -165,15 +166,17 @@ class Any(expression.ColumnElement): """ __visit_name__ = 'any' + inherit_cache = True def __init__(self, left, right, operator=operators.eq): self.type = sqltypes.Boolean() - self.left = expression._literal_as_binds(left) + self.left = expression.literal(left) self.right = right self.operator = operator class _ObjectArray(sqltypes.UserDefinedType): + cache_ok = True class Comparator(sqltypes.TypeEngine.Comparator): def __getitem__(self, key): @@ -222,6 +225,7 @@ def get_col_spec(self, **kws): class Geopoint(sqltypes.UserDefinedType): + cache_ok = True class Comparator(sqltypes.TypeEngine.Comparator): @@ -247,6 +251,7 @@ def result_processor(self, dialect, coltype): class Geoshape(sqltypes.UserDefinedType): + cache_ok = True class Comparator(sqltypes.TypeEngine.Comparator): diff --git a/src/crate/client/tests.py b/src/crate/client/tests.py index 7b19a7fb..b63da6d0 100644 --- a/src/crate/client/tests.py +++ b/src/crate/client/tests.py @@ -195,7 +195,8 @@ def setUpCrateLayerAndSqlAlchemy(test): more_details array(object), INDEX name_ft using fulltext(name) with (analyzer = 'english'), INDEX quote_ft using fulltext(quote) with (analyzer = 'english') - ) """) + )""") + cursor.execute("CREATE VIEW characters_view AS SELECT * FROM characters") with connect(crate_host) as conn: cursor = conn.cursor() @@ -342,6 +343,7 @@ def tearDownWithCrateLayer(test): for stmt in ["DROP TABLE locations", "DROP BLOB TABLE myfiles", "DROP TABLE characters", + "DROP VIEW characters_view", "DROP TABLE cities", "DROP USER me", "DROP USER trusted_me", diff --git a/tox.ini b/tox.ini index 7a5ba805..fa7995bc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{py3,35,36,37,38,39}-sa_{1_0,1_1,1_2,1_3} +envlist = py{py3,35,36,37,38,39}-sa_{1_0,1_1,1_2,1_3,1_4} [testenv] usedevelop = True @@ -12,6 +12,7 @@ deps = sa_1_1: sqlalchemy>=1.1,<1.2 sa_1_2: sqlalchemy>=1.2,<1.3 sa_1_3: sqlalchemy>=1.3,<1.4 + sa_1_4: sqlalchemy>=1.4,<1.5 mock urllib3 commands =