From 0975a73894e2c5e96d5ecd5f28858bdbf0db3b92 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 12:19:14 +0200 Subject: [PATCH 01/24] Remove CrateDB service from CI workflows and update test matrix for CrateDB versions --- .github/workflows/nightly.yml | 3 --- .github/workflows/tests.yml | 15 +++++++++++---- tests/docker-compose.yml | 13 ------------- 3 files changed, 11 insertions(+), 20 deletions(-) delete mode 100644 tests/docker-compose.yml diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6491ed12..99a77203 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -42,9 +42,6 @@ jobs: - name: Acquire sources uses: actions/checkout@v7 - - name: Run CrateDB - run: docker compose -f tests/docker-compose.yml up -d - - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v7 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1d93ae70..c2a65199 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,7 @@ jobs: name: " Python: ${{ matrix.python-version }} (prerelease: ${{ matrix.pip-allow-prerelease }}) SQLAlchemy: ${{ matrix.sqla-version }} + CrateDB: ${{ matrix.cratedb-version }} " runs-on: ${{ matrix.os }} strategy: @@ -34,8 +35,17 @@ jobs: - python-version: '3.14' sqla-version: '<1.4' - # Another CI test matrix slot to test against prerelease versions of Python packages. include: + - os: 'ubuntu-22.04' + python-version: '3.14' + cratedb-version: '5.10.16' + sqla-version: '<2.2' + pip-allow-prerelease: 'false' + - os: 'ubuntu-22.04' + python-version: '3.14' + cratedb-version: 'latest' + sqla-version: '<2.2' + pip-allow-prerelease: 'false' - os: 'ubuntu-latest' python-version: '3.14' cratedb-version: 'nightly' @@ -55,9 +65,6 @@ jobs: - name: Acquire sources uses: actions/checkout@v7 - - name: Run CrateDB - run: docker compose -f tests/docker-compose.yml up -d - - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v7 with: diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml deleted file mode 100644 index 6beae44f..00000000 --- a/tests/docker-compose.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Docker Compose file for testing the SQLAlchemy dialect for CrateDB. ---- -# docker compose -f tests/docker-compose.yml up -services: - cratedb: - image: crate/crate:${CRATEDB_VERSION} - environment: - CRATE_HEAP_SIZE: 2g - volumes: - - ./assets:/assets - ports: - - "4200:4200" - - "5432:5432" From 40b1aa82e68289543aa9ff9199902c0b20be91b1 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:16:26 +0200 Subject: [PATCH 02/24] Remove CrateDB version variable and delete unused integration and settings files --- bootstrap.sh | 1 - tests/integration.py | 217 ------------------------------------------- tests/settings.py | 27 ------ 3 files changed, 245 deletions(-) delete mode 100644 tests/integration.py delete mode 100644 tests/settings.py diff --git a/bootstrap.sh b/bootstrap.sh index 079f229c..684c1367 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -17,7 +17,6 @@ # set -x # Default variables. -CRATEDB_VERSION=${CRATEDB_VERSION:-5.5.1} SQLALCHEMY_VERSION=${SQLALCHEMY_VERSION:-<2.1} diff --git a/tests/integration.py b/tests/integration.py deleted file mode 100644 index 26f5b7bb..00000000 --- a/tests/integration.py +++ /dev/null @@ -1,217 +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. - -from __future__ import absolute_import - -import doctest -import logging -import os -import sys -import unittest -from pprint import pprint - -from crate.client import connect - -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_2_0, SA_VERSION -from tests.settings import crate_host - -log = logging.getLogger() -ch = logging.StreamHandler() -ch.setLevel(logging.ERROR) -log.addHandler(ch) - - -def cprint(s): - if isinstance(s, bytes): - s = s.decode("utf-8") - print(s) # noqa: T201 - - -def docs_path(*parts): - return os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), *parts)) - - -def provision_database(): - drop_tables() - - with connect(crate_host) as conn: - cursor = conn.cursor() - - with open(docs_path("tests/assets/locations.sql")) as s: - stmt = s.read() - cursor.execute(stmt) - stmt = "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'locations'" - cursor.execute(stmt) - assert cursor.fetchall()[0][0] == 1 - - # `/assets` is located within the Docker container used for running CrateDB. - # docker compose -f tests/docker-compose.yml up - data_path = "/assets/locations.jsonl" - - # load testing data into crate - cursor.execute("COPY locations FROM ?", (data_path,)) - # refresh location table so imported data is visible immediately - cursor.execute("REFRESH TABLE locations") - # create blob table - cursor.execute( - "CREATE BLOB TABLE myfiles CLUSTERED INTO 1 SHARDS " + "WITH (number_of_replicas=0)" - ) - - # create users - cursor.execute("CREATE USER me WITH (password = 'my_secret_pw')") - cursor.execute("CREATE USER trusted_me") - - cursor.close() - - ddl_statements = [ - """ - CREATE TABLE characters ( - id STRING PRIMARY KEY, - name STRING, - quote STRING, - details OBJECT, - more_details ARRAY(OBJECT), - INDEX name_ft USING FULLTEXT(name) WITH (analyzer = 'english'), - INDEX quote_ft USING FULLTEXT(quote) WITH (analyzer = 'english') - )""", - """ - CREATE VIEW characters_view - AS SELECT * FROM characters - """, - """ - CREATE TABLE cities ( - name STRING PRIMARY KEY, - coordinate GEO_POINT, - area GEO_SHAPE - )""", - """ - CREATE TABLE search ( - name STRING PRIMARY KEY, - text STRING, - embedding FLOAT_VECTOR(3) - )""", - ] - _execute_statements(ddl_statements) - - -def drop_tables(): - """ - Drop all tables, views, and users created by the test suite. - """ - ddl_statements = [ - "DROP TABLE IF EXISTS archived_tasks", - "DROP TABLE IF EXISTS characters", - "DROP TABLE IF EXISTS cities", - "DROP TABLE IF EXISTS foobar", - "DROP TABLE IF EXISTS locations", - "DROP BLOB TABLE IF EXISTS myfiles", - "DROP TABLE IF EXISTS search", - 'DROP TABLE IF EXISTS "test-testdrive"', - "DROP TABLE IF EXISTS todos", - 'DROP TABLE IF EXISTS "user"', - "DROP VIEW IF EXISTS characters_view", - "DROP USER IF EXISTS me", - "DROP USER IF EXISTS trusted_me", - ] - _execute_statements(ddl_statements) - - -def _execute_statements(statements, on_error="raise"): - with connect(crate_host) as conn: - cursor = conn.cursor() - for stmt in statements: - _execute_statement(cursor, stmt, on_error=on_error) - cursor.close() - - -def _execute_statement(cursor, stmt, on_error="raise"): - if on_error not in ["ignore", "raise"]: - raise ValueError(f"Invalid value for `on_error` argument: {on_error}") - try: - cursor.execute(stmt) - except Exception: # pragma: no cover - # FIXME: Why does this croak on statements like ``DROP TABLE cities``? - # Note: When needing to debug the test environment, you may want to - # enable this logger statement. - # log.exception("Executing SQL statement failed") # noqa: ERA001 - if on_error == "ignore": - pass - elif on_error == "raise": - raise - - -def setUp(test): - provision_database() - - test.globs["crate_host"] = crate_host - test.globs["pprint"] = pprint - test.globs["print"] = cprint - - -def tearDown(test): - pass - - -def create_test_suite(): - suite = unittest.TestSuite() - flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS - - sqlalchemy_integration_tests = [ - "docs/getting-started.rst", - "docs/crud.rst", - "docs/working-with-types.rst", - "docs/advanced-querying.rst", - ] - - # Don't run DataFrame integration tests on SQLAlchemy 1.4 and earlier. - skip_dataframe = SA_VERSION < SA_2_0 - if not skip_dataframe: - sqlalchemy_integration_tests += [ - "docs/dataframe.rst", - ] - - # Don't run reflection integration tests on SQLAlchemy 1.3 and earlier and Python 3.10 and 3.11. - skip_reflection = SA_VERSION < SA_1_4 and (3, 10) <= sys.version_info < (3, 12) - if not skip_reflection: - sqlalchemy_integration_tests += [ - "docs/inspection-reflection.rst", - ] - - s = doctest.DocFileSuite( - *sqlalchemy_integration_tests, - module_relative=False, - setUp=setUp, - tearDown=tearDown, - optionflags=flags, - encoding="utf-8", - ) - suite.addTest(s) - - return suite - - -def load_tests(loader, tests, pattern): - """ - Provide test suite to test discovery. - - https://docs.python.org/3/library/unittest.html#load-tests-protocol - """ - return create_test_suite() diff --git a/tests/settings.py b/tests/settings.py deleted file mode 100644 index 103a42ba..00000000 --- a/tests/settings.py +++ /dev/null @@ -1,27 +0,0 @@ -# vi: set encoding=utf-8 -# -*- 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. -from __future__ import absolute_import - -crate_port = 4200 -localhost = "127.0.0.1" -crate_host = "{host}:{port}".format(host=localhost, port=crate_port) -crate_uri = "http://%s" % crate_host From 7a15cf6f4369abaa07d684fa89193fb68916337b Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:18:27 +0200 Subject: [PATCH 03/24] Add network marker --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c90623df..1514c507 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -210,7 +210,7 @@ ini_options.addopts = """ -rfEXs -p pytester --strict-markers --verbosity=3 --cov --cov-report=term-missing --cov-report=xml """ -ini_options.markers = [] +ini_options.markers = ["network: Test connects to the network",] ini_options.xfail_strict = true ini_options.log_format = "%(asctime)-15s [%(name)-36s] %(levelname)-8s: %(message)s" ini_options.log_level = "DEBUG" @@ -260,7 +260,6 @@ tasks.release = [ ] tasks.test = [ { cmd = "pytest" }, - { cmd = "python -m unittest -vvv tests/integration.py" }, ] [tool.versioningit] From e752d3d4601c3809a6259109c869b991bd6958c1 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:19:15 +0200 Subject: [PATCH 04/24] Remove unused tests and update connection test for network marking --- tests/__init__.py | 7 ------- tests/compiler_test.py | 3 +-- tests/connection_test.py | 1 + 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 1d1914e2..9ee77e23 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -27,7 +27,6 @@ from .function_test import SqlAlchemyFunctionTest from .insert_from_select_test import SqlAlchemyInsertFromSelectTest from .match_test import SqlAlchemyMatchTest -from .query_caching_test import SqlAlchemyQueryCompilationCaching from .update_test import SqlAlchemyUpdateTest from .warnings_test import SqlAlchemyWarningsTest @@ -67,9 +66,3 @@ def test_suite_unit(): tests.addTest(makeSuite(SqlAlchemyArrayTypeTest)) tests.addTest(makeSuite(SqlAlchemyWarningsTest)) return tests - - -def test_suite_integration(): - tests = TestSuite() - tests.addTest(makeSuite(SqlAlchemyQueryCompilationCaching)) - return tests diff --git a/tests/compiler_test.py b/tests/compiler_test.py index cfcf360d..03c53c36 100644 --- a/tests/compiler_test.py +++ b/tests/compiler_test.py @@ -29,7 +29,6 @@ from sqlalchemy.sql import Update, text from sqlalchemy_cratedb.compiler import crate_before_execute -from tests.settings import crate_host from tests.util import ExtraAssertions try: @@ -362,7 +361,7 @@ class CompilerTestCase(TestCase): """ def setUp(self): - self.engine = sa.create_engine(f"crate://{crate_host}") + self.engine = sa.create_engine("crate://") self.metadata = sa.MetaData(schema="testdrive") self.session = sa.orm.Session(bind=self.engine) self.setup_mock() diff --git a/tests/connection_test.py b/tests/connection_test.py index c557a132..4827235d 100644 --- a/tests/connection_test.py +++ b/tests/connection_test.py @@ -199,6 +199,7 @@ def test_connection_multiple_server_https(self): CRATE_VERSION < CRATE_2_3, reason="Early connect failure arrived in crate 2.3.0", ) +@pytest.mark.network def test_connection_failure_is_raised_early(): """ Connecting to an unreachable cluster fails immediately. From 915cdd399bc378fa41850ee0269b1e4152f571aa Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:34:31 +0200 Subject: [PATCH 05/24] Remove network marker from pytest configuration and connection test --- pyproject.toml | 2 +- tests/connection_test.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1514c507..872bde03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -210,7 +210,7 @@ ini_options.addopts = """ -rfEXs -p pytester --strict-markers --verbosity=3 --cov --cov-report=term-missing --cov-report=xml """ -ini_options.markers = ["network: Test connects to the network",] +ini_options.markers = [] ini_options.xfail_strict = true ini_options.log_format = "%(asctime)-15s [%(name)-36s] %(levelname)-8s: %(message)s" ini_options.log_level = "DEBUG" diff --git a/tests/connection_test.py b/tests/connection_test.py index 4827235d..c557a132 100644 --- a/tests/connection_test.py +++ b/tests/connection_test.py @@ -199,7 +199,6 @@ def test_connection_multiple_server_https(self): CRATE_VERSION < CRATE_2_3, reason="Early connect failure arrived in crate 2.3.0", ) -@pytest.mark.network def test_connection_failure_is_raised_early(): """ Connecting to an unreachable cluster fails immediately. From 98814c48f96abb99a4f7dd52f6f999a3b1eff5f5 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:36:49 +0200 Subject: [PATCH 06/24] Refactor test fixtures to include CrateDB version and asset path configuration --- tests/conftest.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 88b10d9d..62590a1d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,10 @@ # Copyright (c) 2021-2023, Crate.io Inc. # Distributed under the terms of the AGPLv3 license, see LICENSE. +import os +from pathlib import Path + import pytest +from crate.client.http import Client from cratedb_toolkit.testing.testcontainers.cratedb import CrateDBTestAdapter # Use different schemas for storing the subsystem database tables, and the @@ -9,13 +13,45 @@ TESTDRIVE_EXT_SCHEMA = "testdrive-ext" TESTDRIVE_DATA_SCHEMA = "testdrive-data" +CRATEDB_VERSION = os.environ.get("CRATEDB_VERSION", "nightly") +ASSETS_PATH = Path(__file__).parent / "assets" +OFFLINE_SERVER_VERSION = "6.5.0" +DEFAULT_SERVERS = {"http://127.0.0.1:4200", "http://localhost:4200"} + @pytest.fixture(scope="session") def cratedb_service(): """ Provide a CrateDB service instance to the test suite. """ - db = CrateDBTestAdapter() - db.start() + db = CrateDBTestAdapter(crate_version=CRATEDB_VERSION) + db.start(volumes=[(str(ASSETS_PATH), "/assets", "ro")]) yield db db.stop() + + +@pytest.fixture(scope="session") +def crate_host(cratedb_service): + """ + Provide the `host:port` address of CrateDB's HTTP interface. + """ + container = cratedb_service.cratedb + return "{host}:{port}".format( + host=container.get_container_host_ip(), + port=container.get_exposed_port(container.port_to_expose), + ) + + +@pytest.fixture(autouse=True) +def offline_default_server(monkeypatch): + """ + Answer the server version probe for the default address `localhost:4200`. + """ + probe = Client.server_infos + + def server_infos(self, server): + if server in DEFAULT_SERVERS: + return server, "crate", OFFLINE_SERVER_VERSION + return probe(self, server) + + monkeypatch.setattr(Client, "server_infos", server_infos) From b743a1c83e0a3b1dba5484d54f7496f7baba6de5 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:39:10 +0200 Subject: [PATCH 07/24] Fix error trace assertion for CrateDB 5.x compatibility --- tests/test_error_handling.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index 58ee499e..dbd53ebd 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -14,7 +14,8 @@ def test_statement_with_error_trace(cratedb_service): connection.execute(sa.text("CREATE TABLE foo AS SELECT 1 AS _id")) # Make sure both variants match, to validate it's actually an error trace. - assert ex.match(re.escape('InvalidColumnNameException["_id" conflicts with system column]')) + # CrateDB 5.x appends the word "pattern" to the message. + assert ex.match(re.escape('InvalidColumnNameException["_id" conflicts with system column')) assert ex.match( 'io.crate.exceptions.InvalidColumnNameException: "_id" conflicts with system column' ) From a854359697f373ad315896f268f305f558486006 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:40:35 +0200 Subject: [PATCH 08/24] Add minimum server version check for UUID type in tests --- tests/test_type_compilation.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_type_compilation.py b/tests/test_type_compilation.py index 851c08c7..5ee39824 100644 --- a/tests/test_type_compilation.py +++ b/tests/test_type_compilation.py @@ -81,6 +81,10 @@ "VARBINARY": "CrateDB has no binary data type", } +MINIMUM_SERVER_VERSION = { + "UUID": (6, 2), +} + def _cases(): for name in sorted(CORE_TYPES): @@ -94,6 +98,13 @@ def _cases(): def test_core_type_is_creatable(cratedb_service, name): """CrateDB accepts the DDL the dialect generates for this type.""" engine = cratedb_service.database.engine + minimum = MINIMUM_SERVER_VERSION.get(name) + + if minimum: + with engine.connect() as connection: + if connection.dialect.server_version_info < minimum: + pytest.skip(f"CrateDB < {'.'.join(map(str, minimum))} has no {name} type") + table = sa.Table( "type_compilation", sa.MetaData(), From 2568651d71076941343a433dd9c963bbf98dc35c Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:41:04 +0200 Subject: [PATCH 09/24] Add pytest fixture to provide CrateDB engine for query caching tests --- tests/query_caching_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/query_caching_test.py b/tests/query_caching_test.py index 1362785f..509afdcd 100644 --- a/tests/query_caching_test.py +++ b/tests/query_caching_test.py @@ -23,6 +23,7 @@ from unittest import TestCase, skipIf +import pytest import sqlalchemy as sa from sqlalchemy.orm import Session from sqlalchemy.sql.operators import eq @@ -35,12 +36,13 @@ except ImportError: from sqlalchemy.ext.declarative import declarative_base -from tests.settings import crate_host - class SqlAlchemyQueryCompilationCaching(TestCase): + @pytest.fixture(autouse=True) + def provide_engine(self, cratedb_service): + self.engine = cratedb_service.database.engine + def setUp(self): - self.engine = sa.create_engine(f"crate://{crate_host}") self.metadata = sa.MetaData(schema="testdrive") self.session = Session(bind=self.engine) self.Character = self.setup_entity() From 1bb7921b766397e97fc9c05eb0ec7bde114cd9bf Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:44:09 +0200 Subject: [PATCH 10/24] Update connection url in documentation --- docs/getting-started.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 2899bcc6..b3838d22 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -99,13 +99,13 @@ Timeout Options In order to configure TCP timeout options, use the ``timeout`` parameter within ``connect_args``, - >>> timeout_engine = sa.create_engine('crate://localhost/', connect_args={'timeout': 42.42}) + >>> timeout_engine = sa.create_engine(f"crate://{crate_host}/", connect_args={'timeout': 42.42}) >>> timeout_engine.raw_connection().driver_connection.client._pool_kw["timeout"] 42.42 or use the ``timeout`` URL parameter within the database connection URL. - >>> timeout_engine = sa.create_engine('crate://localhost/?timeout=42.42') + >>> timeout_engine = sa.create_engine(f"crate://{crate_host}/?timeout=42.42") >>> timeout_engine.raw_connection().driver_connection.client._pool_kw["timeout"] 42.42 @@ -115,13 +115,13 @@ Pool Size In order to configure the database connection pool size, use the ``pool_size`` parameter within ``connect_args``, - >>> timeout_engine = sa.create_engine('crate://localhost/', connect_args={'pool_size': 20}) + >>> timeout_engine = sa.create_engine(f"crate://{crate_host}/", connect_args={'pool_size': 20}) >>> timeout_engine.raw_connection().driver_connection.client._pool_kw["maxsize"] 20 or use the ``pool_size`` URL parameter within the database connection URL. - >>> timeout_engine = sa.create_engine('crate://localhost/?pool_size=20') + >>> timeout_engine = sa.create_engine(f"crate://{crate_host}/?pool_size=20") >>> timeout_engine.raw_connection().driver_connection.client._pool_kw["maxsize"] 20 From 153b5fd5f1aee12ba02bb3791f15214a7431346d Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:52:46 +0200 Subject: [PATCH 11/24] Update CrateDB version to 'latest' in CI --- .github/workflows/tests.yml | 8 +++++--- tests/conftest.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2a65199..28e2f1a9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: matrix: os: ['ubuntu-22.04'] python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] - cratedb-version: ['nightly'] + cratedb-version: ['latest'] sqla-version: ['<1.4', '<1.5', '<2.1', '<2.2'] pip-allow-prerelease: ['false'] @@ -43,17 +43,19 @@ jobs: pip-allow-prerelease: 'false' - os: 'ubuntu-22.04' python-version: '3.14' - cratedb-version: 'latest' + cratedb-version: 'nightly' sqla-version: '<2.2' pip-allow-prerelease: 'false' - os: 'ubuntu-latest' python-version: '3.14' - cratedb-version: 'nightly' + cratedb-version: 'latest' sqla-version: 'latest' pip-allow-prerelease: 'true' fail-fast: false + continue-on-error: ${{ matrix.cratedb-version == 'nightly' }} + env: CRATEDB_VERSION: ${{ matrix.cratedb-version }} SQLALCHEMY_VERSION: ${{ matrix.sqla-version }} diff --git a/tests/conftest.py b/tests/conftest.py index 62590a1d..7c67a73a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ TESTDRIVE_EXT_SCHEMA = "testdrive-ext" TESTDRIVE_DATA_SCHEMA = "testdrive-data" -CRATEDB_VERSION = os.environ.get("CRATEDB_VERSION", "nightly") +CRATEDB_VERSION = os.environ.get("CRATEDB_VERSION", "latest") ASSETS_PATH = Path(__file__).parent / "assets" OFFLINE_SERVER_VERSION = "6.5.0" DEFAULT_SERVERS = {"http://127.0.0.1:4200", "http://localhost:4200"} From 7b7420701767083f06f1dcc76d8d5980aa2c71d0 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Tue, 13 Oct 2026 17:53:36 +0200 Subject: [PATCH 12/24] Update Docs --- DEVELOP.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/DEVELOP.md b/DEVELOP.md index 8073ae2b..a009549a 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -16,17 +16,21 @@ further commands. Verify code by running all linters and software tests: - export CRATEDB_VERSION=latest - docker compose -f tests/docker-compose.yml up poe check +The test suite starts a CrateDB instance using Docker. Select the CrateDB +version using the tag of the `crate/crate` image, the default is `latest`: + + export CRATEDB_VERSION=6.4.6 + poe test + Run specific tests: pytest -k SqlAlchemyCompilerTest pytest -k test_score # Integration tests, written as doctests. - python -m unittest -vvv tests/integration.py + pytest tests/test_documentation.py Format code: From fab8c0de5b483fa4f5ac192c509129857c41c4aa Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 10:05:49 +0200 Subject: [PATCH 13/24] refactor integration.py file to test_documentation.py --- tests/test_documentation.py | 208 ++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 tests/test_documentation.py diff --git a/tests/test_documentation.py b/tests/test_documentation.py new file mode 100644 index 00000000..e5524c71 --- /dev/null +++ b/tests/test_documentation.py @@ -0,0 +1,208 @@ +# -*- 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. + +from __future__ import absolute_import + +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 + +log = logging.getLogger() +ch = logging.StreamHandler() +ch.setLevel(logging.ERROR) +log.addHandler(ch) + + +def cprint(s): + if isinstance(s, bytes): + s = s.decode("utf-8") + print(s) # noqa: T201 + + +def docs_path(*parts): + return os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), *parts)) + + +def provision_database(crate_host): + drop_tables(crate_host) + + with connect(crate_host) as conn: + cursor = conn.cursor() + + with open(docs_path("tests/assets/locations.sql")) as s: + stmt = s.read() + cursor.execute(stmt) + stmt = "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'locations'" + cursor.execute(stmt) + assert cursor.fetchall()[0][0] == 1 + + # `/assets` is mounted into the CrateDB container, see `tests/conftest.py`. + data_path = "/assets/locations.jsonl" + + cursor.execute("COPY locations FROM ?", (data_path,)) + cursor.execute("REFRESH TABLE locations") + cursor.execute( + "CREATE BLOB TABLE myfiles CLUSTERED INTO 1 SHARDS " + "WITH (number_of_replicas=0)" + ) + cursor.execute("CREATE USER me WITH (password = 'my_secret_pw')") + cursor.execute("CREATE USER trusted_me") + + cursor.close() + + ddl_statements = [ + """ + CREATE TABLE characters ( + id STRING PRIMARY KEY, + name STRING, + quote STRING, + details OBJECT, + more_details ARRAY(OBJECT), + INDEX name_ft USING FULLTEXT(name) WITH (analyzer = 'english'), + INDEX quote_ft USING FULLTEXT(quote) WITH (analyzer = 'english') + )""", + """ + CREATE VIEW characters_view + AS SELECT * FROM characters + """, + """ + CREATE TABLE cities ( + name STRING PRIMARY KEY, + coordinate GEO_POINT, + area GEO_SHAPE + )""", + """ + CREATE TABLE search ( + name STRING PRIMARY KEY, + text STRING, + embedding FLOAT_VECTOR(3) + )""", + ] + _execute_statements(crate_host, ddl_statements) + + +def drop_tables(crate_host): + """ + Drop all tables, views, and users created by the test suite. + """ + ddl_statements = [ + "DROP TABLE IF EXISTS archived_tasks", + "DROP TABLE IF EXISTS characters", + "DROP TABLE IF EXISTS cities", + "DROP TABLE IF EXISTS foobar", + "DROP TABLE IF EXISTS locations", + "DROP BLOB TABLE IF EXISTS myfiles", + "DROP TABLE IF EXISTS search", + 'DROP TABLE IF EXISTS "test-testdrive"', + "DROP TABLE IF EXISTS todos", + 'DROP TABLE IF EXISTS "user"', + "DROP VIEW IF EXISTS characters_view", + "DROP USER IF EXISTS me", + "DROP USER IF EXISTS trusted_me", + ] + _execute_statements(crate_host, ddl_statements) + + +def _execute_statements(crate_host, statements, on_error="raise"): + with connect(crate_host) as conn: + cursor = conn.cursor() + for stmt in statements: + _execute_statement(cursor, stmt, on_error=on_error) + cursor.close() + + +def _execute_statement(cursor, stmt, on_error="raise"): + if on_error not in ["ignore", "raise"]: + raise ValueError(f"Invalid value for `on_error` argument: {on_error}") + try: + cursor.execute(stmt) + except Exception: # pragma: no cover + # FIXME: Why does this croak on statements like ``DROP TABLE cities``? + # Note: When needing to debug the test environment, you may want to + # enable this logger statement. + # log.exception("Executing SQL statement failed") # noqa: ERA001 + if on_error == "ignore": + pass + elif on_error == "raise": + raise + + +def setUp(test): + provision_database(test.globs["crate_host"]) + + test.globs["pprint"] = pprint + test.globs["print"] = cprint + + +def tearDown(test): + # The CrateDB instance is shared with the rest of the test suite. + drop_tables(test.globs["crate_host"]) + + +DOCTEST_FILES = [ + "docs/getting-started.rst", + "docs/crud.rst", + "docs/working-with-types.rst", + "docs/advanced-querying.rst", + pytest.param( + "docs/dataframe.rst", + marks=pytest.mark.skipif( + SA_VERSION < SA_2_0, + 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", + ), + ), +] + + +@pytest.mark.parametrize("filename", DOCTEST_FILES) +def test_documentation(crate_host, filename): + """ + Run the doctests of a documentation page against the session's CrateDB instance. + """ + suite = doctest.DocFileSuite( + docs_path(filename), + module_relative=False, + setUp=setUp, + tearDown=tearDown, + globs={"crate_host": crate_host}, + optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS, + encoding="utf-8", + ) + result = unittest.TestResult() + suite.run(result) + problems = result.failures + result.errors + if problems: + pytest.fail("\n".join(traceback for _, traceback in problems), pytrace=False) From c14751fae7bb00ef0ef85f7e434cdae8d2d2dc1b Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 10:16:23 +0200 Subject: [PATCH 14/24] Update CLAUDE.md to reflect changes in testing commands and environment variables --- CLAUDE.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d980e998..cc073461 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,6 @@ source bootstrap.sh # Creates .venv with Python 3.11, installs all deps in edi ``` Environment variables that influence bootstrap: -- `CRATEDB_VERSION` (default: `5.5.1`) — CrateDB Docker image version - `SQLALCHEMY_VERSION` (default: `<2.2`) — SQLAlchemy version constraint - `PIP_ALLOW_PRERELEASE=true` — allow pre-release packages @@ -22,7 +21,7 @@ Environment variables that influence bootstrap: ```bash poe format # Auto-format code (ruff + black) poe lint # Run linters (ruff, validate-pyproject) -poe test # Run pytest + integration tests +poe test # Run pytest, including the doctests poe check # lint + test combined # Run specific tests @@ -31,10 +30,11 @@ pytest -k SqlAlchemyCompilerTest pytest -k test_score # Run integration/doctests -python -m unittest -vvv tests/integration.py +pytest tests/test_documentation.py ``` Tests require a live CrateDB instance via Docker (managed automatically by `cratedb_toolkit.testing.testcontainers`). +Select the CrateDB version with `CRATEDB_VERSION`, a `crate/crate` image tag (default: `latest`), e.g. `CRATEDB_VERSION=5.10.16 pytest`. ## Architecture @@ -61,11 +61,13 @@ Tests require a live CrateDB instance via Docker (managed automatically by `crat ### Testing Tests in `tests/` follow two patterns: -- `*_test.py` files: unit/integration tests using pytest with a live CrateDB instance -- `tests/integration.py`: doctests for documentation examples, run with `unittest` - -The `conftest.py` provides a session-scoped `cratedb_service` fixture that starts CrateDB via Docker containers. +- `*_test.py` and `test_*.py` files: pytest tests. Integration tests use the `cratedb_service` fixture; unit tests mock the driver's cursor and run offline +- `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/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 - Line length: 100 characters (ruff + black) From 26be46d42ab446507b42439b16ac4c454c5b3608 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 10:30:08 +0200 Subject: [PATCH 15/24] Update SQLAlchemy version constraints in workflows and dependencies --- .github/workflows/codeql.yml | 2 +- .github/workflows/tests.yml | 11 +---------- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 12 deletions(-) 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/pyproject.toml b/pyproject.toml index 872bde03..4c81e7d1 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 = [ From d38d465b529da883342d3d29ea496bd8cf679fdc Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 10:36:58 +0200 Subject: [PATCH 16/24] Remove compatibility support for SQLAlchemy 1.3 and related deprecation warnings --- src/sqlalchemy_cratedb/__init__.py | 26 +-- src/sqlalchemy_cratedb/compat/api13.py | 152 -------------- src/sqlalchemy_cratedb/compat/core10.py | 253 ------------------------ src/sqlalchemy_cratedb/compiler.py | 27 +-- src/sqlalchemy_cratedb/dialect.py | 8 +- src/sqlalchemy_cratedb/type/vector.py | 6 +- 6 files changed, 14 insertions(+), 458 deletions(-) delete mode 100644 src/sqlalchemy_cratedb/compat/api13.py delete mode 100644 src/sqlalchemy_cratedb/compat/core10.py diff --git a/src/sqlalchemy_cratedb/__init__.py b/src/sqlalchemy_cratedb/__init__.py index 8144023e..ebf803b9 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_VERSION # noqa: F401 # Re-exported for compatibility. from .support import insert_bulk from .type.array import ObjectArray from .type.geo import Geopoint, Geoshape @@ -31,29 +30,6 @@ from .type.unresolved import UnresolvedType from .type.vector import FloatVector, knn_match -if SA_VERSION < SA_1_4: - import textwrap - import warnings - - # SQLAlchemy 1.3 is effectively EOL. - SA13_DEPRECATION_WARNING = textwrap.dedent( - """ - WARNING: SQLAlchemy 1.3 is effectively 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: - - - 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() - - try: from importlib.metadata import PackageNotFoundError, version except (ImportError, ModuleNotFoundError): # pragma:nocover 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. """ From f69f858a2253cb972a1d6bd49a070cd8f0e95b29 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 10:41:45 +0200 Subject: [PATCH 17/24] Remove skiptest decorators for v1.3 --- tests/__init__.py | 15 +------------- tests/array_test.py | 34 ++++++-------------------------- tests/bulk_test.py | 10 ++-------- tests/connection_test.py | 2 -- tests/create_table_test.py | 11 +++-------- tests/datetime_test.py | 17 ++-------------- tests/dialect_test.py | 12 +++-------- tests/dict_test.py | 5 ++--- tests/insert_from_select_test.py | 13 ++---------- tests/query_caching_test.py | 25 ++--------------------- tests/reflection_test.py | 8 +------- tests/test_support_polyfill.py | 18 +---------------- tests/update_test.py | 14 +++---------- tests/vector_test.py | 18 ++--------------- tests/warnings_test.py | 26 +----------------------- 15 files changed, 31 insertions(+), 197 deletions(-) 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/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 7d7a2f5e..ee40520e 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 @@ -284,7 +272,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/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/query_caching_test.py b/tests/query_caching_test.py index 509afdcd..628b8190 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 834ffd82..5e765bb8 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.database.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.database.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.database.engine with engine.begin() as connection: diff --git a/tests/test_support_polyfill.py b/tests/test_support_polyfill.py index 56bc89cd..bcec7a0b 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 ffc6b74f..c587b1bf 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..e350eebb 100644 --- a/tests/warnings_test.py +++ b/tests/warnings_test.py @@ -1,9 +1,7 @@ # -*- coding: utf-8; -*- -import sys import warnings -from unittest import TestCase, skipIf +from unittest import TestCase -from sqlalchemy_cratedb.sa_version import SA_1_4, SA_VERSION from tests.util import ExtraAssertions @@ -14,28 +12,6 @@ class SqlAlchemyWarningsTest(TestCase, ExtraAssertions): 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", - ) - def test_sa13_deprecation_warning(self): - """ - Verify that a `DeprecationWarning` is issued when running SQLAlchemy 1.3. - """ - 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)) - def test_craty_object_deprecation_warning(self): """ Verify that a `DeprecationWarning` is issued when accessing the deprecated From ac6f3823efaf1c269a1f11edd6125e3005513d15 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 10:42:41 +0200 Subject: [PATCH 18/24] Remove compatibility checks and imports in tests --- tests/compiler_test.py | 32 ++++++++------------------------ tests/function_test.py | 6 +----- tests/match_test.py | 8 +------- tests/numeric_test.py | 7 +------ tests/test_documentation.py | 12 ++---------- 5 files changed, 13 insertions(+), 52 deletions(-) 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/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/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 cc577a82..b792b5bb 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/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", ] From 4784d248c8f82aad3a8999b21de6dd9c155a0bfa Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 10:43:28 +0200 Subject: [PATCH 19/24] Update docs for removing support and compability checks --- docs/advanced-querying.rst | 5 +---- docs/crud.rst | 5 +---- docs/getting-started.rst | 5 +---- docs/index.rst | 2 +- docs/overview.rst | 2 +- docs/working-with-types.rst | 5 +---- 6 files changed, 6 insertions(+), 18 deletions(-) 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` From ee79bd40e39f548a39fda851962cfc723aa71017 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 11:19:49 +0200 Subject: [PATCH 20/24] Add deprecation warning for SQLAlchemy 1.4 and update tests --- src/sqlalchemy_cratedb/__init__.py | 19 ++++++++++++++++++- tests/warnings_test.py | 22 +++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/sqlalchemy_cratedb/__init__.py b/src/sqlalchemy_cratedb/__init__.py index ebf803b9..0c39a502 100644 --- a/src/sqlalchemy_cratedb/__init__.py +++ b/src/sqlalchemy_cratedb/__init__.py @@ -21,7 +21,7 @@ from .dialect import dialect from .predicate import match -from .sa_version import SA_1_4, SA_VERSION # noqa: F401 # Re-exported for compatibility. +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 @@ -30,6 +30,23 @@ from .type.unresolved import UnresolvedType from .type.vector import FloatVector, knn_match +if SA_VERSION < SA_2_0: + import textwrap + import warnings + + SA14_DEPRECATION_WARNING = textwrap.dedent( + """ + WARNING: SQLAlchemy 1.4 is EOL. + + 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/20/changelog/migration_20.html + """.lstrip("\n") + ) + warnings.warn(message=SA14_DEPRECATION_WARNING, category=DeprecationWarning, stacklevel=2) + try: from importlib.metadata import PackageNotFoundError, version except (ImportError, ModuleNotFoundError): # pragma:nocover diff --git a/tests/warnings_test.py b/tests/warnings_test.py index e350eebb..3ffe3841 100644 --- a/tests/warnings_test.py +++ b/tests/warnings_test.py @@ -1,7 +1,9 @@ # -*- coding: utf-8; -*- +import sys import warnings -from unittest import TestCase +from unittest import TestCase, skipIf +from sqlalchemy_cratedb.sa_version import SA_2_0, SA_VERSION from tests.util import ExtraAssertions @@ -12,6 +14,24 @@ class SqlAlchemyWarningsTest(TestCase, ExtraAssertions): https://docs.python.org/3/library/warnings.html#testing-warnings """ + @skipIf( + SA_VERSION >= SA_2_0, + "There is no deprecation warning for SQLAlchemy 1.4 on higher versions", + ) + def test_sa14_deprecation_warning(self): + """ + Verify that a `DeprecationWarning` is issued when running SQLAlchemy 1.4. + """ + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + del sys.modules["sqlalchemy_cratedb"] + import sqlalchemy_cratedb # noqa: F401 + + self.assertEqual(len(w), 1) + self.assertIsSubclass(w[-1].category, DeprecationWarning) + self.assertIn("SQLAlchemy 1.4 is EOL.", str(w[-1].message)) + def test_craty_object_deprecation_warning(self): """ Verify that a `DeprecationWarning` is issued when accessing the deprecated From 594023b161402b70f3cc71caf3e0582efc06b195 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 11:21:40 +0200 Subject: [PATCH 21/24] Update changelog and documentation for SQLAlchemy 1.4 support; drop SQLAlchemy 1.3 --- CHANGES.md | 5 +++++ CLAUDE.md | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7fac4a1a..a2729b6b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ # 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 `DeprecationWarning`. A future release will + require SQLAlchemy 2.0 - Types: Fixed `UUID` columns rejecting every insert, since values were bound as 32 hex digits while CrateDB's `UUID` type takes the dashed form. The portable `Uuid` type keeps its `CHAR(32)` storage. Storing `UUID` requires diff --git a/CLAUDE.md b/CLAUDE.md index cc073461..60ab5461 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`, a `crate/crate` image tag (de - **`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` From 89d1247fa9b07304dc0d19053b33951c9cf09b6a Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 11:25:11 +0200 Subject: [PATCH 22/24] Fix warning category from DeprecationWarning to FutureWarning --- CHANGES.md | 2 +- src/sqlalchemy_cratedb/__init__.py | 2 +- tests/warnings_test.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a2729b6b..ef12c1d5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ - 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 `DeprecationWarning`. A future release will + with SQLAlchemy 1.4 emits a `FutureWarning`. A future release will require SQLAlchemy 2.0 - Types: Fixed `UUID` columns rejecting every insert, since values were bound as 32 hex digits while CrateDB's `UUID` type takes the dashed form. The diff --git a/src/sqlalchemy_cratedb/__init__.py b/src/sqlalchemy_cratedb/__init__.py index 0c39a502..0eecb721 100644 --- a/src/sqlalchemy_cratedb/__init__.py +++ b/src/sqlalchemy_cratedb/__init__.py @@ -45,7 +45,7 @@ - https://docs.sqlalchemy.org/en/20/changelog/migration_20.html """.lstrip("\n") ) - warnings.warn(message=SA14_DEPRECATION_WARNING, category=DeprecationWarning, stacklevel=2) + warnings.warn(message=SA14_DEPRECATION_WARNING, category=FutureWarning, stacklevel=2) try: from importlib.metadata import PackageNotFoundError, version diff --git a/tests/warnings_test.py b/tests/warnings_test.py index 3ffe3841..78faf9c7 100644 --- a/tests/warnings_test.py +++ b/tests/warnings_test.py @@ -20,7 +20,7 @@ class SqlAlchemyWarningsTest(TestCase, ExtraAssertions): ) def test_sa14_deprecation_warning(self): """ - Verify that a `DeprecationWarning` is issued when running SQLAlchemy 1.4. + Verify that a `FutureWarning` is issued when running SQLAlchemy 1.4. """ with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") @@ -29,7 +29,7 @@ def test_sa14_deprecation_warning(self): import sqlalchemy_cratedb # noqa: F401 self.assertEqual(len(w), 1) - self.assertIsSubclass(w[-1].category, DeprecationWarning) + self.assertIsSubclass(w[-1].category, FutureWarning) self.assertIn("SQLAlchemy 1.4 is EOL.", str(w[-1].message)) def test_craty_object_deprecation_warning(self): From 24e6e00d3647073d23c09c0b9c732e67f0662324 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Wed, 14 Oct 2026 11:36:55 +0200 Subject: [PATCH 23/24] Update SQLALCHEMY_VERSION to <2.2 and enhance warning test description --- bootstrap.sh | 2 +- tests/warnings_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/tests/warnings_test.py b/tests/warnings_test.py index 78faf9c7..fa0ecfba 100644 --- a/tests/warnings_test.py +++ b/tests/warnings_test.py @@ -9,7 +9,7 @@ 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 """ From d5e9fbe89f72812d84fb30529adbe2480bf43124 Mon Sep 17 00:00:00 2001 From: Bilal Tonga Date: Mon, 19 Oct 2026 12:06:04 +0200 Subject: [PATCH 24/24] Update changelog to reflect breaking changes and support drop for SQLAlchemy 1.3 --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b89388b2..1f67c79e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,13 @@ # Changelog -## 2026/10/19 0.44.0 +## 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. - Types: Fixed `DateTime` results failing when the driver's `time_zone`