diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e84eff94..b5ddf4d8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: language: [ python ] - sqla-version: ['<1.4', '<1.5', '<2.1'] + sqla-version: ['<1.5', '<2.1', '<2.2'] steps: - name: Acquire sources diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 28e2f1a9..a027775a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,18 +23,9 @@ jobs: os: ['ubuntu-22.04'] python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] cratedb-version: ['latest'] - sqla-version: ['<1.4', '<1.5', '<2.1', '<2.2'] + sqla-version: ['<1.5', '<2.1', '<2.2'] pip-allow-prerelease: ['false'] - exclude: - # SQLAlchemy 1.3 is not supported on Python 3.12 and higher. - - python-version: '3.12' - sqla-version: '<1.4' - - python-version: '3.13' - sqla-version: '<1.4' - - python-version: '3.14' - sqla-version: '<1.4' - include: - os: 'ubuntu-22.04' python-version: '3.14' diff --git a/CHANGES.md b/CHANGES.md index b8258815..1f67c79e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased +- BREAKING: Dropped support for SQLAlchemy 1.3, which is EOL since 2021. + The minimum supported version is SQLAlchemy 1.4 +- Deprecated support for SQLAlchemy 1.4, which is EOL. Importing the dialect + with SQLAlchemy 1.4 emits a `FutureWarning`. A future release will + require SQLAlchemy 2.0 + ## 2026/10/19 0.44.0 - Types: Fixed `sa.Time` raising `Cannot find data type: time`. `Time` columns are created as `STRING` and hold values in ISO 8601 format. diff --git a/CLAUDE.md b/CLAUDE.md index 3d9520c6..09628d02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -`sqlalchemy-cratedb` is a SQLAlchemy dialect for CrateDB, a distributed SQL database. It supports SQLAlchemy 1.3 through 2.1 (with ongoing 2.1 compatibility work on the current branch). +`sqlalchemy-cratedb` is a SQLAlchemy dialect for CrateDB, a distributed SQL database. It supports SQLAlchemy 1.4 through 2.1. ## Development Setup @@ -44,7 +44,7 @@ Select the CrateDB version with `CRATEDB_VERSION`, an image tag (default: `lates - **`compiler.py`** — SQL/DDL compilation: `CrateDDLCompiler`, `CrateTypeCompiler`, `CrateIdentifierPreparer`, and `rewrite_update()` for partial object updates - **`predicate.py`** — `match()` predicate for full-text search - **`sa_version.py`** — Version detection; exports `SA_VERSION`, `SA_1_4`, `SA_2_0`, `SA_2_1` constants -- **`compat/`** — Multi-version SQLAlchemy compatibility: `core10.py`, `core14.py`, `core20.py`, `core21.py`, `api13.py` +- **`compat/`** — Multi-version SQLAlchemy compatibility: `core14.py` (SQLAlchemy 1.4), `core20.py` (2.0 and 2.1) - **`type/`** — Custom CrateDB types: `ObjectType` (JSON objects), `ObjectArray`, `FloatVector`, `Geopoint`, `Geoshape` - **`support/`** — Integrations and polyfills: `pandas.py` (bulk insert), `polyfill.py` (refresh-after-DML, uniqueness, autoincrement timestamps), `util.py` @@ -65,7 +65,7 @@ Tests in `tests/` follow two patterns: - `tests/test_documentation.py`: runs the doctests in `docs/*.rst` against the same CrateDB instance The `conftest.py` provides: -- `cratedb_service`: session-scoped, starts one CrateDB container (`crate:$CRATEDB_VERSION`, or `crate/crate:nightly` for `nightly`) with `tests/assets` mounted at `/assets` +- `cratedb_service`: session-scoped, starts one CrateDB container (`crate/crate:$CRATEDB_VERSION`) with `tests/assets` mounted at `/assets` - `crate_host`: the container's `host:port`, used by the doctests - `offline_default_server`: autouse, answers the driver's connect-time server version probe for the default address `localhost:4200`. ## Code Style diff --git a/bootstrap.sh b/bootstrap.sh index 684c1367..4d52c57e 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -17,7 +17,7 @@ # set -x # Default variables. -SQLALCHEMY_VERSION=${SQLALCHEMY_VERSION:-<2.1} +SQLALCHEMY_VERSION=${SQLALCHEMY_VERSION:-<2.2} function print_header() { diff --git a/docs/advanced-querying.rst b/docs/advanced-querying.rst index e65871b7..a8747667 100644 --- a/docs/advanced-querying.rst +++ b/docs/advanced-querying.rst @@ -17,10 +17,7 @@ Import the relevant symbols: >>> import sqlalchemy as sa >>> from sqlalchemy.orm import sessionmaker - >>> try: - ... from sqlalchemy.orm import declarative_base - ... except ImportError: - ... from sqlalchemy.ext.declarative import declarative_base + >>> from sqlalchemy.orm import declarative_base >>> from uuid import uuid4 Establish a connection to the database, see also :ref:`sa:engines_toplevel` diff --git a/docs/crud.rst b/docs/crud.rst index 23e00c06..c12fd251 100644 --- a/docs/crud.rst +++ b/docs/crud.rst @@ -22,10 +22,7 @@ Import the relevant symbols: >>> from datetime import datetime >>> from sqlalchemy import delete, func, text >>> from sqlalchemy.orm import sessionmaker - >>> try: - ... from sqlalchemy.orm import declarative_base - ... except ImportError: - ... from sqlalchemy.ext.declarative import declarative_base + >>> from sqlalchemy.orm import declarative_base >>> from sqlalchemy_cratedb import ObjectArray Establish a connection to the database, see also :ref:`sa:engines_toplevel` diff --git a/docs/getting-started.rst b/docs/getting-started.rst index b3838d22..e4ad6248 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -23,10 +23,7 @@ Import the relevant symbols: >>> import sqlalchemy as sa >>> from sqlalchemy.orm import sessionmaker - >>> try: - ... from sqlalchemy.orm import declarative_base - ... except ImportError: - ... from sqlalchemy.ext.declarative import declarative_base + >>> from sqlalchemy.orm import declarative_base Establish a connection to the database, see also :ref:`sa:engines_toplevel` and :ref:`connect`: diff --git a/docs/index.rst b/docs/index.rst index 22f8d1da..6ca65212 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ About ***** The :ref:`CrateDB dialect ` for `SQLAlchemy`_ provides adapters -for `CrateDB`_ and SQLAlchemy. The supported versions are 1.3, 1.4, and 2.0. +for `CrateDB`_ and SQLAlchemy. The supported versions are 1.4, 2.0, and 2.1. The package is available from `PyPI`_ at `sqlalchemy-cratedb`_. The connector can be used to connect to both `CrateDB`_ and `CrateDB diff --git a/docs/overview.rst b/docs/overview.rst index 690dd9c0..6c803e98 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -17,7 +17,7 @@ The ``sqlalchemy-cratedb`` package provides the :ref:`SQLAlchemy dialect installation time, and can be used without further configuration. The CrateDB SQLAlchemy dialect is validated to work with SQLAlchemy versions -``1.3``, ``1.4``, and ``2.0``. +``1.4``, ``2.0``, and ``2.1``. .. SEEALSO:: diff --git a/docs/working-with-types.rst b/docs/working-with-types.rst index afe8df63..fd292984 100644 --- a/docs/working-with-types.rst +++ b/docs/working-with-types.rst @@ -23,10 +23,7 @@ Import the relevant symbols: >>> from sqlalchemy import delete, func, text >>> 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 sqlalchemy.orm import declarative_base >>> from uuid import uuid4 Establish a connection to the database, see also :ref:`sa:engines_toplevel` diff --git a/pyproject.toml b/pyproject.toml index c44f488c..950cdb0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dynamic = [ dependencies = [ "crate>=2.2.1,<3", "geojson>=2.5,<4", - "sqlalchemy>=1,<2.2", + "sqlalchemy>=1.4,<2.2", "verlib2<26.3", ] optional-dependencies.all = [ diff --git a/src/sqlalchemy_cratedb/__init__.py b/src/sqlalchemy_cratedb/__init__.py index 8144023e..0eecb721 100644 --- a/src/sqlalchemy_cratedb/__init__.py +++ b/src/sqlalchemy_cratedb/__init__.py @@ -19,10 +19,9 @@ # 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 dialect from .predicate import match -from .sa_version import SA_1_4, SA_VERSION +from .sa_version import SA_1_4, SA_2_0, SA_VERSION # noqa: F401 # Re-exported for compatibility. from .support import insert_bulk from .type.array import ObjectArray from .type.geo import Geopoint, Geoshape @@ -31,28 +30,22 @@ from .type.unresolved import UnresolvedType from .type.vector import FloatVector, knn_match -if SA_VERSION < SA_1_4: +if SA_VERSION < SA_2_0: import textwrap import warnings - # SQLAlchemy 1.3 is effectively EOL. - SA13_DEPRECATION_WARNING = textwrap.dedent( + SA14_DEPRECATION_WARNING = textwrap.dedent( """ - WARNING: SQLAlchemy 1.3 is effectively EOL. + WARNING: SQLAlchemy 1.4 is EOL. - SQLAlchemy 1.3 is EOL since 2023-01-27. - Future versions of the CrateDB SQLAlchemy dialect will drop support for SQLAlchemy 1.3. - It is recommended that you transition to using SQLAlchemy 1.4 or 2.0: + SQLAlchemy 1.4 is no longer maintained, its last release was 1.4.54 in September 2024. + Future versions of the CrateDB SQLAlchemy dialect will drop support for SQLAlchemy 1.4. + It is recommended that you transition to using SQLAlchemy 2.0 or later: - - https://docs.sqlalchemy.org/en/14/changelog/migration_14.html - https://docs.sqlalchemy.org/en/20/changelog/migration_20.html """.lstrip("\n") ) - warnings.warn(message=SA13_DEPRECATION_WARNING, category=DeprecationWarning, stacklevel=2) - - # SQLAlchemy 1.3 does not have the `exec_driver_sql` method, so add it. - monkeypatch_add_exec_driver_sql() - + warnings.warn(message=SA14_DEPRECATION_WARNING, category=FutureWarning, stacklevel=2) try: from importlib.metadata import PackageNotFoundError, version diff --git a/src/sqlalchemy_cratedb/compat/api13.py b/src/sqlalchemy_cratedb/compat/api13.py deleted file mode 100644 index 8e716f4a..00000000 --- a/src/sqlalchemy_cratedb/compat/api13.py +++ /dev/null @@ -1,152 +0,0 @@ -# -*- 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 - """ # noqa: E501 - 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, 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 - - -@property -def connectionfairy_driver_connection_sa14(self): - """The connection object as returned by the driver after a connect. - - .. versionadded:: 1.4.24 - - .. seealso:: - - :attr:`._ConnectionFairy.dbapi_connection` - - :attr:`._ConnectionRecord.driver_connection` - - :ref:`faq_dbapi_connection` - - """ - return self.connection - - -def monkeypatch_add_connectionfairy_driver_connection(): - import sqlalchemy.pool.base - - sqlalchemy.pool.base._ConnectionFairy.driver_connection = connectionfairy_driver_connection_sa14 diff --git a/src/sqlalchemy_cratedb/compat/core10.py b/src/sqlalchemy_cratedb/compat/core10.py deleted file mode 100644 index aae9c526..00000000 --- a/src/sqlalchemy_cratedb/compat/core10.py +++ /dev/null @@ -1,253 +0,0 @@ -# -*- 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 -# -# http://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. - -import sqlalchemy as sa -from sqlalchemy.dialects.postgresql.base import PGCompiler -from sqlalchemy.sql.crud import ( - REQUIRED, - _create_bind_param, - _extend_values_for_multiparams, - _get_multitable_params, - _get_stmt_parameters_params, - _key_getters_for_crud_column, - _scan_cols, - _scan_insert_from_select_cols, -) - -from sqlalchemy_cratedb.compiler import CrateCompiler - - -class CrateCompilerSA10(CrateCompiler): - def returning_clause(self, stmt, returning_cols): - """ - Generate RETURNING clause, PostgreSQL-compatible. - """ - return PGCompiler.returning_clause(self, stmt, returning_cols) - - def visit_update(self, update_stmt, **kw): - """ - used to compile expressions - Parts are taken from the SQLCompiler base class. - """ - - # [10] CrateDB patch. - if not update_stmt.parameters and not hasattr(update_stmt, "_crate_specific"): - return super().visit_update(update_stmt, **kw) - - self.isupdate = True - - extra_froms = update_stmt._extra_froms - - 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, extra_froms, **kw) - - dialect_hints = None - if update_stmt._hints: - dialect_hints, table_text = self._setup_crud_hints(update_stmt, table_text) - - # [10] CrateDB patch. - crud_params = _get_crud_params(self, update_stmt, **kw) - - text += table_text - - text += " SET " - - # [10] CrateDB patch begin. - include_table = extra_froms and self.render_table_with_column_in_update_from - - set_clauses = [] - - for k, v in crud_params: - clause = k._compiler_dispatch(self, include_table=include_table) + " = " + v - set_clauses.append(clause) - - for k, v in update_stmt.parameters.items(): - if isinstance(k, str) and "[" in k: - bindparam = sa.sql.bindparam(k, v) - set_clauses.append(k + " = " + self.process(bindparam)) - - text += ", ".join(set_clauses) - # [10] CrateDB patch end. - - if self.returning or update_stmt._returning: - if not self.returning: - self.returning = update_stmt._returning - if self.returning_precedes_values: - text += " " + self.returning_clause(update_stmt, self.returning) - - if extra_froms: - extra_from_text = self.update_from_clause( - update_stmt, update_stmt.table, extra_froms, dialect_hints, **kw - ) - if extra_from_text: - text += " " + extra_from_text - - if update_stmt._whereclause is not None: - t = self.process(update_stmt._whereclause) - if t: - text += " WHERE " + t - - limit_clause = self.update_limit_clause(update_stmt) - if limit_clause: - text += " " + limit_clause - - if self.returning and not self.returning_precedes_values: - text += " " + self.returning_clause(update_stmt, self.returning) - - return text - - -def _get_crud_params(compiler, stmt, **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 ResultProxy's prefetch_cols() and postfetch_cols() - collections. - - """ - - compiler.postfetch = [] - compiler.insert_prefetch = [] - compiler.update_prefetch = [] - compiler.returning = [] - - # no parameters in the statement, no parameters in the - # compiled params - return binds for all columns - if compiler.column_keys is None and stmt.parameters is None: - return [ - (c, _create_bind_param(compiler, c, None, required=True)) for c in stmt.table.columns - ] - - if stmt._has_multi_parameters: - stmt_parameters = stmt.parameters[0] - else: - stmt_parameters = stmt.parameters - - # 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, - ) = _key_getters_for_crud_column(compiler, stmt) - - # if we have statement parameters - set defaults in the - # compiled params - if compiler.column_keys is None: - parameters = {} - else: - parameters = dict( - (_column_as_key(key), REQUIRED) - for key in compiler.column_keys - if not stmt_parameters or key not in stmt_parameters - ) - - # create a list of column assignment clauses as tuples - values = [] - - if stmt_parameters is not None: - _get_stmt_parameters_params( - compiler, parameters, stmt_parameters, _column_as_key, values, kw - ) - - check_columns = {} - - # special logic that only occurs for multi-table UPDATE - # statements - if compiler.isupdate and stmt._extra_froms and stmt_parameters: - _get_multitable_params( - compiler, - stmt, - stmt_parameters, - check_columns, - _col_bind_name, - _getattr_col_key, - values, - kw, - ) - - if compiler.isinsert and stmt.select_names: - _scan_insert_from_select_cols( - compiler, - stmt, - parameters, - _getattr_col_key, - _column_as_key, - _col_bind_name, - check_columns, - values, - kw, - ) - else: - _scan_cols( - compiler, - stmt, - parameters, - _getattr_col_key, - _column_as_key, - _col_bind_name, - check_columns, - values, - kw, - ) - - # [10] 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_parameters: - check = ( - set(parameters) - .intersection(_column_as_key(k) for k in stmt_parameters) - .difference(check_columns) - ) - if check: - raise exc.CompileError( - "Unconsumed column names: %s" - % (", ".join("%s" % c for c in check)) - ) - """ - - if stmt._has_multi_parameters: - values = _extend_values_for_multiparams(compiler, stmt, values, kw) - - return values diff --git a/src/sqlalchemy_cratedb/compiler.py b/src/sqlalchemy_cratedb/compiler.py index a978434e..065bddd1 100644 --- a/src/sqlalchemy_cratedb/compiler.py +++ b/src/sqlalchemy_cratedb/compiler.py @@ -37,7 +37,6 @@ ) from sqlalchemy.types import String -from .sa_version import SA_1_4, SA_VERSION from .type.geo import Geopoint, Geoshape from .type.object import MutableDict, ObjectTypeImpl @@ -86,21 +85,19 @@ def rewrite_update(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): - if SA_VERSION >= SA_1_4: - if params is None: - multiparams = ([],) - else: - multiparams = ([params],) - params = {} + 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 = [] + if multiparams[0]: + params = multiparams[0][0] + else: + params = multiparams[0] + multiparams = [] return clauseelement, multiparams, params @@ -438,10 +435,6 @@ def _render_subscript(self, binary, **kw): type_ = _SubscriptType(int_as_key=_is_top_level_object(binary.left)) if not index.required: type_.literal_processor(self.dialect)(index.effective_value) - elif SA_VERSION < SA_1_4: - raise sa.exc.CompileError( - "A subscript taking its value on execution needs SQLAlchemy 1.4 or later" - ) if index.required or getattr(self, "cache_key", None) is not None: kw["literal_execute"] = True else: diff --git a/src/sqlalchemy_cratedb/dialect.py b/src/sqlalchemy_cratedb/dialect.py index 12464f20..1acb534e 100644 --- a/src/sqlalchemy_cratedb/dialect.py +++ b/src/sqlalchemy_cratedb/dialect.py @@ -35,7 +35,7 @@ CrateIdentifierPreparer, CrateTypeCompiler, ) -from .sa_version import SA_1_4, SA_2_0, SA_VERSION +from .sa_version import SA_2_0, SA_VERSION from .type import IP, FloatVector, Geopoint, Geoshape, ObjectArray, ObjectType, UnresolvedType from .util import SSLMode @@ -238,14 +238,10 @@ def process(value): from .compat.core20 import CrateCompilerSA20 statement_compiler = CrateCompilerSA20 -elif SA_VERSION >= SA_1_4: +else: from .compat.core14 import CrateCompilerSA14 statement_compiler = CrateCompilerSA14 -else: - from .compat.core10 import CrateCompilerSA10 - - statement_compiler = CrateCompilerSA10 class CrateDialect(default.DefaultDialect): diff --git a/src/sqlalchemy_cratedb/type/vector.py b/src/sqlalchemy_cratedb/type/vector.py index 7fc6447f..e0743c0b 100644 --- a/src/sqlalchemy_cratedb/type/vector.py +++ b/src/sqlalchemy_cratedb/type/vector.py @@ -21,10 +21,6 @@ <#>: max_inner_product <=>: cosine_distance -## Backlog -- After dropping support for SQLAlchemy 1.3, use - `class FloatVector(sa.TypeDecorator[t.Sequence[float]]):` - ## Origin This module is based on the corresponding pgvector implementation by Andrew Kane. Thank you. @@ -86,7 +82,7 @@ def to_db(value: t.Any, dim: t.Optional[int] = None) -> t.Optional[t.List]: return value -class FloatVector(sa.TypeDecorator): +class FloatVector(sa.TypeDecorator[t.Sequence[float]]): """ SQLAlchemy `FloatVector` data type for CrateDB. """ diff --git a/tests/__init__.py b/tests/__init__.py index 9ee77e23..fea7b484 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,19 +1,5 @@ # -*- coding: utf-8 -*- -from sqlalchemy_cratedb.compat.api13 import ( - monkeypatch_add_connectionfairy_driver_connection, - monkeypatch_amend_select_sa14, -) -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_VERSION - -from .util import ParametrizedTestCase - -# `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() - monkeypatch_add_connectionfairy_driver_connection() - from unittest import TestLoader, TestSuite from .array_test import SqlAlchemyArrayTypeTest @@ -28,6 +14,7 @@ from .insert_from_select_test import SqlAlchemyInsertFromSelectTest from .match_test import SqlAlchemyMatchTest from .update_test import SqlAlchemyUpdateTest +from .util import ParametrizedTestCase from .warnings_test import SqlAlchemyWarningsTest makeSuite = TestLoader().loadTestsFromTestCase diff --git a/tests/array_test.py b/tests/array_test.py index 7f5a9e54..35dd0e2d 100644 --- a/tests/array_test.py +++ b/tests/array_test.py @@ -20,21 +20,15 @@ # software solely pursuant to the terms of the relevant commercial agreement. -from unittest import TestCase, skipIf +from unittest import TestCase from unittest.mock import MagicMock, patch import sqlalchemy as sa -from sqlalchemy.orm import Session +from crate.client.cursor import Cursor +from sqlalchemy.orm import Session, declarative_base from sqlalchemy.sql import operators -from sqlalchemy_cratedb import SA_1_4, SA_VERSION, ObjectArray - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base - -from crate.client.cursor import Cursor +from sqlalchemy_cratedb import ObjectArray fake_cursor = MagicMock(name="fake_cursor") FakeCursor = MagicMock(name="FakeCursor", spec=Cursor) @@ -75,7 +69,6 @@ def assertWhere(self, expected_where, clause): "SELECT subscripts.idx AS subscripts_idx FROM subscripts WHERE " + expected_where, s ) - @skipIf(SA_VERSION < SA_1_4, "`as_generic` not available with SQLAlchemy 1.3") def test_as_generic(self): t1 = sa.Table( "t", @@ -112,10 +105,8 @@ def test_array_insert(self): def test_any(self): s = self.session.query(self.User.name).filter(self.User.friends.any("arthur")) - # SA 1.4+ uses the column name as the bind param; SA 1.3 uses a generic "param_1". - param = "friends_1" if SA_VERSION >= SA_1_4 else "param_1" self.assertSQL( - f"SELECT users.name AS users_name FROM users WHERE %({param})s = ANY (users.friends)", + "SELECT users.name AS users_name FROM users WHERE %(friends_1)s = ANY (users.friends)", s, ) @@ -123,10 +114,8 @@ def test_any_with_operator(self): s = self.session.query(self.User.name).filter( self.User.scores.any(6, operator=operators.lt) ) - # SA 1.4+ uses the column name as the bind param; SA 1.3 uses a generic "param_1". - param = "scores_1" if SA_VERSION >= SA_1_4 else "param_1" self.assertSQL( - f"SELECT users.name AS users_name FROM users WHERE %({param})s < ANY (users.scores)", + "SELECT users.name AS users_name FROM users WHERE %(scores_1)s < ANY (users.scores)", s, ) @@ -182,7 +171,6 @@ def test_index_type_coerce(self): "SELECT users.name AS users_name FROM users WHERE users.scores[2] = %(param_1)s", s ) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no `literal_execute`") def test_index_bindparam(self): s = self.session.query(self.User.name).filter(self.User.scores[sa.bindparam("pos")] == 5) self.assertSQL( @@ -191,16 +179,6 @@ def test_index_bindparam(self): s, ) - @skipIf(SA_VERSION >= SA_1_4, "SQLAlchemy 1.4+ renders the parameter on execution") - def test_index_bindparam_sa13(self): - s = self.session.query(self.User.name).filter(self.User.scores[sa.bindparam("pos")] == 5) - with self.assertRaises(sa.exc.CompileError) as cm: - str(s) - self.assertEqual( - "A subscript taking its value on execution needs SQLAlchemy 1.4 or later", - str(cm.exception), - ) - def test_index_invalid(self): s = self.session.query(self.User.name).filter(self.User.scores[1.5] == 5) with self.assertRaises(sa.exc.CompileError) as cm: diff --git a/tests/bulk_test.py b/tests/bulk_test.py index 43ea3be9..2e7f6901 100644 --- a/tests/bulk_test.py +++ b/tests/bulk_test.py @@ -24,17 +24,11 @@ from unittest.mock import MagicMock, patch import sqlalchemy as sa -from sqlalchemy.orm import Session +from crate.client.cursor import Cursor +from sqlalchemy.orm import Session, declarative_base from sqlalchemy_cratedb.sa_version import SA_2_0, SA_VERSION -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base - -from crate.client.cursor import Cursor - fake_cursor = MagicMock(name="fake_cursor") FakeCursor = MagicMock(name="FakeCursor", spec=Cursor, return_value=fake_cursor) diff --git a/tests/compiler_test.py b/tests/compiler_test.py index 03c53c36..e93594d6 100644 --- a/tests/compiler_test.py +++ b/tests/compiler_test.py @@ -26,19 +26,14 @@ import sqlalchemy as sa from crate.client.cursor import Cursor from sqlalchemy.dialects.postgresql import insert as pg_insert +from sqlalchemy.orm import declarative_base from sqlalchemy.sql import Update, text +from sqlalchemy_cratedb import ObjectType from sqlalchemy_cratedb.compiler import crate_before_execute +from sqlalchemy_cratedb.sa_version import SA_2_0, SA_VERSION from tests.util import ExtraAssertions -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy_cratedb import ObjectType -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_2_0, SA_VERSION - from .util import ParametrizedTestCase @@ -116,7 +111,7 @@ def test_select_with_not_ilike_no_escape(self): """ selectable = self.mytable.select().where(self.mytable.c.name.notilike("%foo%")) statement = str(selectable.compile(bind=self.crate_engine)) - if SA_VERSION < SA_1_4 or not self.crate_engine.dialect.has_ilike_operator(): + if not self.crate_engine.dialect.has_ilike_operator(): self.assertEqual( statement, dedent(""" @@ -145,10 +140,6 @@ def test_select_with_ilike_and_escape(self): selectable.compile(bind=self.crate_engine) self.assertEqual(str(cmex.exception), "Unsupported feature: ESCAPE is not supported") - @skipIf( - SA_VERSION < SA_1_4, - "SQLAlchemy 1.3 and earlier do not support native `NOT ILIKE` compilation", - ) def test_select_with_not_ilike_and_escape(self): """ Verify the compiler fails when using CrateDB's native `ILIKE` method together with `ESCAPE`. @@ -165,16 +156,10 @@ def test_select_with_offset(self): """ selectable = self.mytable.select().offset(5) 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 %(param_1)s", - ) - else: - self.assertEqual( - statement, - "SELECT mytable.name, mytable.data \nFROM mytable \n LIMIT ALL OFFSET %(param_1)s", - ) + self.assertEqual( + statement, + "SELECT mytable.name, mytable.data \nFROM mytable\n LIMIT ALL OFFSET %(param_1)s", + ) def test_select_with_limit(self): """ @@ -354,7 +339,6 @@ def test_insert_on_conflict_do_update(self): FakeCursor = MagicMock(name="FakeCursor", spec=Cursor) -@skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 suddenly has problems with these test cases") class CompilerTestCase(TestCase): """ A base class for providing mocking infrastructure to validate the DDL compiler. diff --git a/tests/connection_test.py b/tests/connection_test.py index c557a132..16c299f9 100644 --- a/tests/connection_test.py +++ b/tests/connection_test.py @@ -30,7 +30,6 @@ from sqlalchemy.exc import NoSuchModuleError, SQLAlchemyError from verlib2.packaging.version import Version -from sqlalchemy_cratedb import SA_1_4, SA_VERSION from tests.util import ExtraAssertions CRATE_VERSION = Version(crate_client_version) @@ -53,7 +52,6 @@ def test_connection_no_hostname_no_ssl(self): self.assertEqual(["http://127.0.0.1:4200"], servers) engine.dispose() - @pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Not supported by SQLAlchemy 1.3") def test_connection_no_hostname_with_ssl(self): engine = sa.create_engine("crate://?sslmode=require") servers = engine.raw_connection().driver_connection.client._active_servers diff --git a/tests/create_table_test.py b/tests/create_table_test.py index e0fc7b45..ce04c070 100644 --- a/tests/create_table_test.py +++ b/tests/create_table_test.py @@ -18,18 +18,13 @@ # 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 pytest -import sqlalchemy as sa - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base - from unittest import TestCase, skipIf from unittest.mock import MagicMock, patch +import pytest +import sqlalchemy as sa from crate.client.cursor import Cursor +from sqlalchemy.orm import declarative_base from sqlalchemy.testing import AssertsCompiledSQL from sqlalchemy_cratedb import Geopoint, ObjectArray, ObjectType diff --git a/tests/datetime_test.py b/tests/datetime_test.py index 580bc1b9..7de11633 100644 --- a/tests/datetime_test.py +++ b/tests/datetime_test.py @@ -22,20 +22,14 @@ from __future__ import absolute_import import datetime as dt -from unittest import TestCase, skipIf +from unittest import TestCase from unittest.mock import MagicMock, patch import pytest import sqlalchemy as sa -from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.orm import Session, declarative_base, sessionmaker from sqlalchemy_cratedb.dialect import DateTime -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_VERSION - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base try: import zoneinfo @@ -61,7 +55,6 @@ OUTPUT_DATETIME_TZ = dt.datetime(2009, 5, 13, 16, 00, 30, 123000) -@skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 suddenly has problems with these test cases") @patch("crate.client.connection.Cursor", FakeCursor) class SqlAlchemyDateAndDateTimeTest(TestCase): def setUp(self): @@ -119,7 +112,6 @@ def session(cratedb_service): return session -@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3") def test_datetime_notz(session): """ An integration test for `sa.Date` and `sa.DateTime`, not using timezones. @@ -157,7 +149,6 @@ def test_datetime_notz(session): assert result["datetime_tz"].tzinfo is None -@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3") def test_datetime_tz(session): """ An integration test for `sa.Date` and `sa.DateTime`, now using timezones. @@ -196,7 +187,6 @@ def test_datetime_tz(session): assert result["datetime_tz"].tzinfo is None -@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3") def test_datetime_date(session): """ Validate assigning a `date` object to a `datetime` column works. @@ -232,7 +222,6 @@ def test_datetime_date(session): assert result["datetime_tz"] == dt.datetime(2009, 5, 13, 0, 0, 0) -@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3") def test_date_cast_truncates(session): """ Casting to `sa.Date` drops the time of day, server-side. @@ -249,7 +238,6 @@ def hour_of(type_): assert hour_of(sa.DateTime) == INPUT_DATETIME_NOTZ.hour -@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3") def test_datetime_tz_aware_read(session, cratedb_service): """ With the driver's `time_zone` configured, `TIMESTAMP` columns read back as @@ -282,7 +270,6 @@ def test_datetime_tz_aware_read(session, cratedb_service): assert result == dt.datetime(2020, 6, 23, 10, 0, 0, tzinfo=dt.timezone.utc) -@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3") def test_time(session): """ An integration test for `sa.Time` and the SQL-standard `sa.TIME`. diff --git a/tests/dialect_test.py b/tests/dialect_test.py index 58853766..687db7b1 100644 --- a/tests/dialect_test.py +++ b/tests/dialect_test.py @@ -26,16 +26,11 @@ import sqlalchemy as sa from crate.client.cursor import Cursor from sqlalchemy import inspect -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, declarative_base +from sqlalchemy.testing import eq_, in_, is_true from sqlalchemy_cratedb import ObjectType -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_2_0, SA_VERSION - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.testing import eq_, in_, is_true +from sqlalchemy_cratedb.sa_version import SA_2_0, SA_VERSION FakeCursor = MagicMock(name="FakeCursor", spec=Cursor) @@ -127,7 +122,6 @@ def test_get_view_names(self): "ORDER BY table_name ASC, table_schema ASC", ) - @skipIf(SA_VERSION < SA_1_4, "Inspector.has_table only available on SQLAlchemy>=1.4") def test_has_table(self): self.init_mock(return_value=[["foo"], ["bar"]]) insp = inspect(self.session.bind) diff --git a/tests/dict_test.py b/tests/dict_test.py index 37367bb1..0a4121a8 100644 --- a/tests/dict_test.py +++ b/tests/dict_test.py @@ -22,7 +22,7 @@ from __future__ import absolute_import import re -from unittest import TestCase, skipIf +from unittest import TestCase from unittest.mock import MagicMock, patch import sqlalchemy as sa @@ -40,14 +40,13 @@ from crate.client.cursor import Cursor from sqlalchemy_cratedb import ObjectArray, ObjectType -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_2_1, SA_VERSION +from sqlalchemy_cratedb.sa_version import SA_2_1, SA_VERSION fake_cursor = MagicMock(name="fake_cursor") FakeCursor = MagicMock(name="FakeCursor", spec=Cursor) FakeCursor.return_value = fake_cursor -@skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 suddenly has problems with these test cases") class SqlAlchemyDictTypeTest(TestCase): def setUp(self): self.engine = sa.create_engine("crate://") diff --git a/tests/function_test.py b/tests/function_test.py index 3c62bff7..95613955 100644 --- a/tests/function_test.py +++ b/tests/function_test.py @@ -22,13 +22,9 @@ 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): def setUp(self): diff --git a/tests/insert_from_select_test.py b/tests/insert_from_select_test.py index 406321e5..111d1367 100644 --- a/tests/insert_from_select_test.py +++ b/tests/insert_from_select_test.py @@ -19,20 +19,12 @@ # 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 datetime import datetime -from unittest import TestCase, skipIf +from unittest import TestCase from unittest.mock import MagicMock, patch import sqlalchemy as sa -from sqlalchemy.orm import Session - -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_VERSION - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base - from crate.client.cursor import Cursor +from sqlalchemy.orm import Session, declarative_base fake_cursor = MagicMock(name="fake_cursor") fake_cursor.rowcount = 1 @@ -40,7 +32,6 @@ FakeCursor.return_value = fake_cursor -@skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 suddenly has problems with these test cases") class SqlAlchemyInsertFromSelectTest(TestCase): def assertSQL(self, expected_str, actual_expr): self.assertEqual(expected_str, str(actual_expr).replace("\n", "")) diff --git a/tests/match_test.py b/tests/match_test.py index eb3d2903..c8a85768 100644 --- a/tests/match_test.py +++ b/tests/match_test.py @@ -24,14 +24,8 @@ from unittest.mock import MagicMock import sqlalchemy as sa -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 sqlalchemy.orm import Session, declarative_base from sqlalchemy_cratedb import ObjectType from sqlalchemy_cratedb.predicate import match diff --git a/tests/numeric_test.py b/tests/numeric_test.py index a5f0752d..3470b832 100644 --- a/tests/numeric_test.py +++ b/tests/numeric_test.py @@ -2,12 +2,7 @@ import pytest import sqlalchemy as sa -from sqlalchemy.orm import sessionmaker - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base, sessionmaker from sqlalchemy_cratedb.dialect import CrateDialect diff --git a/tests/query_caching_test.py b/tests/query_caching_test.py index 7eabae28..9ec4b3c8 100644 --- a/tests/query_caching_test.py +++ b/tests/query_caching_test.py @@ -21,20 +21,14 @@ from __future__ import absolute_import -from unittest import TestCase, skipIf +from unittest import TestCase import pytest import sqlalchemy as sa -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, declarative_base from sqlalchemy.sql.operators import eq from sqlalchemy_cratedb import ObjectArray, ObjectType -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_VERSION - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base class SqlAlchemyQueryCompilationCaching(TestCase): @@ -120,7 +114,6 @@ def execute_twice(self, first, second, first_params=None, second_params=None): self.assertIs(connection.dialect.CACHE_HIT, result.context.cache_hit) return results - @skipIf(SA_VERSION < SA_1_4, "On SA13, the 'ResultProxy' object has no attribute 'scalar_one'") def test_object_multiple_select_legacy(self): """ The SQLAlchemy implementation of CrateDB's `OBJECT` type offers indexed @@ -144,7 +137,6 @@ def test_object_multiple_select_legacy(self): result = self.session.execute(selectable).scalar_one().data self.assertEqual({"y": 2}, result) - @skipIf(SA_VERSION < SA_1_4, "On SA13, the 'ResultProxy' object has no attribute 'scalar_one'") def test_object_multiple_select_modern(self): """ The SQLAlchemy implementation of CrateDB's `OBJECT` type offers indexed @@ -168,7 +160,6 @@ def test_object_multiple_select_modern(self): result = self.session.execute(selectable).scalar_one().data self.assertEqual({"y": 2}, result) - @skipIf(SA_VERSION < SA_1_4, "On SA13, the 'ResultProxy' object has no attribute 'scalar_one'") def test_objectarray_multiple_select(self): """ The `ObjectType` column of the entity turns off caching for these @@ -185,19 +176,16 @@ def test_objectarray_multiple_select(self): result = self.session.execute(selectable).scalar_one().data self.assertEqual({"y": 2}, result) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_array_index(self): self.setup_subscript_data() arr = self.subscripts.c.arr self.assertEqual([10, 20], self.execute_twice(sa.select(arr[1]), sa.select(arr[2]))) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_array_index_bindparam(self): self.setup_subscript_data() statement = sa.select(self.subscripts.c.arr[sa.bindparam("pos")]) self.assertEqual([10, 30], self.execute_twice(statement, statement, {"pos": 1}, {"pos": 3})) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_array_index_expression_bindparam(self): self.setup_subscript_data() t = self.subscripts @@ -207,7 +195,6 @@ def test_subscript_array_index_expression_bindparam(self): connection = connection.execution_options(compiled_cache=None) self.assertEqual(20, connection.execute(statement, {"off": 1}).scalar()) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_executemany(self): """ SQLAlchemy does not support `literal_execute` with `executemany()`. @@ -227,7 +214,6 @@ def test_subscript_executemany(self): 0, connection.execute(sa.select(sa.func.count()).select_from(t)).scalar() ) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has a different `select()` signature") def test_subscript_expression_executemany(self): self.setup_subscript_data() t = self.subscripts @@ -241,7 +227,6 @@ def test_subscript_expression_executemany(self): 0, connection.execute(sa.select(sa.func.count()).select_from(t)).scalar() ) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_array_index_where(self): self.setup_subscript_data() t = self.subscripts @@ -253,7 +238,6 @@ def test_subscript_array_index_where(self): ), ) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_array_slice(self): self.setup_subscript_data() arr = self.subscripts.c.arr @@ -261,7 +245,6 @@ def test_subscript_array_slice(self): [[10, 20], [20, 30]], self.execute_twice(sa.select(arr[1:2]), sa.select(arr[2:3])) ) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_array_update(self): self.setup_subscript_data() t = self.subscripts @@ -275,7 +258,6 @@ def test_subscript_array_update(self): connection.execute(sa.text("REFRESH TABLE testdrive.subscripts")) self.assertEqual([99, 88, 30], connection.execute(sa.select(t.c.arr)).scalar()) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_objectarray_key(self): self.setup_subscript_data() objarr = self.subscripts.c.objarr @@ -284,7 +266,6 @@ def test_subscript_objectarray_key(self): self.execute_twice(sa.select(objarr["foo"]), sa.select(objarr["bar"])), ) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_objectarray_index_key(self): self.setup_subscript_data() objarr = self.subscripts.c.objarr @@ -293,13 +274,11 @@ def test_subscript_objectarray_index_key(self): self.execute_twice(sa.select(objarr[1]["foo"]), sa.select(objarr[2]["foo"])), ) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_json_key(self): self.setup_subscript_data() js = self.subscripts.c.js self.assertEqual(["JX", "JY"], self.execute_twice(sa.select(js["x"]), sa.select(js["y"]))) - @skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 has no statement cache") def test_subscript_json_key_with_quote(self): self.setup_subscript_data() js = self.subscripts.c.js diff --git a/tests/reflection_test.py b/tests/reflection_test.py index 35c25b07..928205b5 100644 --- a/tests/reflection_test.py +++ b/tests/reflection_test.py @@ -7,7 +7,7 @@ from sqlalchemy_cratedb import IP, Geopoint, Geoshape, ObjectArray from sqlalchemy_cratedb.dialect import ARRAY_SUFFIX, TYPES_MAP, CrateDialect -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_2_0, SA_VERSION +from sqlalchemy_cratedb.sa_version import SA_2_0, SA_VERSION UNRESOLVABLE = "unresolvable_cratedb_type" @@ -140,9 +140,6 @@ def test_unresolved_type_refuses_ddl_and_names_the_column_type(data_type): assert "column 'c'" in message -@pytest.mark.skipif( - SA_VERSION < SA_1_4, reason="SQLAlchemy renders a type it has no visitor for from 1.4 onwards" -) def test_unresolved_type_prints_its_crate_type_name(): type_ = CrateDialect()._resolve_type(UNRESOLVABLE) assert UNRESOLVABLE in str(type_) @@ -169,7 +166,6 @@ def test_unresolved_type_remains_selectable(): assert "SELECT t.c" in str(table.select().compile(dialect=dialect)) -@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3") def test_reflected_system_table_renders_ddl(cratedb_service): engine = cratedb_service.engine table = sa.Table("summits", sa.MetaData(schema="sys"), autoload_with=engine) @@ -177,7 +173,6 @@ def test_reflected_system_table_renders_ddl(cratedb_service): assert "coordinates GEO_POINT" in ddl -@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3") def test_reflected_table_renders_string_lengths_and_ip(cratedb_service): engine = cratedb_service.engine with engine.begin() as connection: @@ -192,7 +187,6 @@ def test_reflected_table_renders_string_lengths_and_ip(cratedb_service): assert "address IP" in ddl -@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3") def test_ip_column_stores_ipaddress_objects(cratedb_service): engine = cratedb_service.engine with engine.begin() as connection: diff --git a/tests/test_documentation.py b/tests/test_documentation.py index e5524c71..61eacbc2 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -24,14 +24,13 @@ import doctest import logging import os -import sys import unittest from pprint import pprint import pytest from crate.client import connect -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_2_0, SA_VERSION +from sqlalchemy_cratedb.sa_version import SA_2_0, SA_VERSION log = logging.getLogger() ch = logging.StreamHandler() @@ -176,14 +175,7 @@ def tearDown(test): reason="Don't run DataFrame integration tests on SQLAlchemy 1.4 and earlier", ), ), - pytest.param( - "docs/inspection-reflection.rst", - marks=pytest.mark.skipif( - SA_VERSION < SA_1_4 and (3, 10) <= sys.version_info < (3, 12), - reason="Don't run reflection integration tests on SQLAlchemy 1.3 " - "and earlier and Python 3.10 and 3.11", - ), - ), + "docs/inspection-reflection.rst", ] diff --git a/tests/test_support_polyfill.py b/tests/test_support_polyfill.py index a5dd2902..4beb0d4d 100644 --- a/tests/test_support_polyfill.py +++ b/tests/test_support_polyfill.py @@ -4,14 +4,7 @@ import sqlalchemy as sa from sqlalchemy.event import listen from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import sessionmaker - -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_VERSION - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base, sessionmaker from sqlalchemy_cratedb.support import ( check_uniqueness_factory, @@ -20,9 +13,6 @@ ) -@pytest.mark.skipif( - SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3 and earlier" -) def test_autoincrement_timestamp(cratedb_service): """ Validate autoincrement columns using `sa.DateTime` columns. @@ -63,9 +53,6 @@ class FooBar(Base): assert result["string"] >= "1718846016235" -@pytest.mark.skipif( - SA_VERSION < SA_1_4, reason="Feature not supported on SQLAlchemy 1.3 and earlier" -) def test_check_uniqueness_factory(cratedb_service): """ Validate basic synthetic UNIQUE constraints. @@ -103,9 +90,6 @@ class FooBar(Base): assert ex.match("DuplicateKeyException in table 'foobar' on constraint 'id-name'") -@pytest.mark.skipif( - SA_VERSION < SA_1_4, reason="Feature not supported on SQLAlchemy 1.3 and earlier" -) @pytest.mark.parametrize("mode", ["engine", "session"]) def test_refresh_after_dml(cratedb_service, mode): """ diff --git a/tests/update_test.py b/tests/update_test.py index dc069ae8..aa0b292b 100644 --- a/tests/update_test.py +++ b/tests/update_test.py @@ -19,21 +19,14 @@ # 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 datetime import datetime -from unittest import TestCase, skipIf +from unittest import TestCase from unittest.mock import MagicMock, patch import sqlalchemy as sa -from sqlalchemy.orm import Session +from crate.client.cursor import Cursor +from sqlalchemy.orm import Session, declarative_base from sqlalchemy_cratedb import ObjectType -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_VERSION - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base - -from crate.client.cursor import Cursor fake_cursor = MagicMock(name="fake_cursor") fake_cursor.rowcount = 1 @@ -41,7 +34,6 @@ FakeCursor.return_value = fake_cursor -@skipIf(SA_VERSION < SA_1_4, "SQLAlchemy 1.3 suddenly has problems with these test cases") class SqlAlchemyUpdateTest(TestCase): def setUp(self): self.engine = sa.create_engine("crate://") diff --git a/tests/vector_test.py b/tests/vector_test.py index 54b43522..9c348e1b 100644 --- a/tests/vector_test.py +++ b/tests/vector_test.py @@ -27,18 +27,11 @@ import pytest import sqlalchemy as sa -from sqlalchemy.orm import Session, sessionmaker -from sqlalchemy.sql import select - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base - from crate.client.cursor import Cursor +from sqlalchemy.orm import Session, declarative_base, sessionmaker +from sqlalchemy.sql import select from sqlalchemy_cratedb import FloatVector, knn_match -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_VERSION from sqlalchemy_cratedb.type.vector import from_db, to_db fake_cursor = MagicMock(name="fake_cursor") @@ -46,13 +39,6 @@ FakeCursor.return_value = fake_cursor -if SA_VERSION < SA_1_4: - pytest.skip( - reason="The FloatVector type is not supported on SQLAlchemy 1.3 and earlier", - allow_module_level=True, - ) - - @patch("crate.client.connection.Cursor", FakeCursor) class SqlAlchemyVectorTypeTest(TestCase): """ diff --git a/tests/warnings_test.py b/tests/warnings_test.py index fec11581..fa0ecfba 100644 --- a/tests/warnings_test.py +++ b/tests/warnings_test.py @@ -3,38 +3,34 @@ import warnings from unittest import TestCase, skipIf -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_VERSION +from sqlalchemy_cratedb.sa_version import SA_2_0, SA_VERSION from tests.util import ExtraAssertions class SqlAlchemyWarningsTest(TestCase, ExtraAssertions): """ - Verify a few `DeprecationWarning` spots. + Verify a few `DeprecationWarning` and `FutureWarning` spots. https://docs.python.org/3/library/warnings.html#testing-warnings """ @skipIf( - SA_VERSION >= SA_1_4, - "There is no deprecation warning for SQLAlchemy 1.3 on higher versions", + SA_VERSION >= SA_2_0, + "There is no deprecation warning for SQLAlchemy 1.4 on higher versions", ) - def test_sa13_deprecation_warning(self): + def test_sa14_deprecation_warning(self): """ - Verify that a `DeprecationWarning` is issued when running SQLAlchemy 1.3. + Verify that a `FutureWarning` is issued when running SQLAlchemy 1.4. """ with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. warnings.simplefilter("always") - # Trigger a warning by importing the SQLAlchemy dialect module. - # Because it already has been loaded, unload it beforehand. del sys.modules["sqlalchemy_cratedb"] import sqlalchemy_cratedb # noqa: F401 - # Verify details of the SA13 EOL/deprecation warning. self.assertEqual(len(w), 1) - self.assertIsSubclass(w[-1].category, DeprecationWarning) - self.assertIn("SQLAlchemy 1.3 is effectively EOL.", str(w[-1].message)) + self.assertIsSubclass(w[-1].category, FutureWarning) + self.assertIn("SQLAlchemy 1.4 is EOL.", str(w[-1].message)) def test_craty_object_deprecation_warning(self): """