diff --git a/src/sqlalchemy_cratedb/compat/core14.py b/src/sqlalchemy_cratedb/compat/core14.py index 15377f43..f7bd0288 100644 --- a/src/sqlalchemy_cratedb/compat/core14.py +++ b/src/sqlalchemy_cratedb/compat/core14.py @@ -199,9 +199,15 @@ def _get_crud_params(compiler, stmt, compile_state, **kw): if compile_state._has_multi_parameters: spd = compile_state._multi_parameters[0] stmt_parameter_tuples = list(spd.items()) - elif compile_state._ordered_values: + elif (hasattr(compile_state, "_ordered_values") and + getattr(compile_state, "_ordered_values", None) is not None): spd = compile_state._dict_parameters - stmt_parameter_tuples = compile_state._ordered_values + try: + stmt_parameter_tuples = compile_state._ordered_values + except AttributeError: + # Fallback for newer SQLAlchemy versions where _ordered_values might not be accessible + spd = compile_state._dict_parameters + stmt_parameter_tuples = list(spd.items()) if spd else None elif compile_state._dict_parameters: spd = compile_state._dict_parameters stmt_parameter_tuples = list(spd.items()) diff --git a/src/sqlalchemy_cratedb/compat/core20.py b/src/sqlalchemy_cratedb/compat/core20.py index 08a81998..b25e5285 100644 --- a/src/sqlalchemy_cratedb/compat/core20.py +++ b/src/sqlalchemy_cratedb/compat/core20.py @@ -275,9 +275,15 @@ def _get_crud_params( assert mp is not None spd = mp[0] stmt_parameter_tuples = list(spd.items()) - elif compile_state._ordered_values: + elif (hasattr(compile_state, "_ordered_values") and + getattr(compile_state, "_ordered_values", None) is not None): spd = compile_state._dict_parameters - stmt_parameter_tuples = compile_state._ordered_values + try: + stmt_parameter_tuples = compile_state._ordered_values + except AttributeError: + # Fallback for newer SQLAlchemy versions where _ordered_values might not be accessible + spd = compile_state._dict_parameters + stmt_parameter_tuples = list(spd.items()) if spd else None elif compile_state._dict_parameters: spd = compile_state._dict_parameters stmt_parameter_tuples = list(spd.items()) diff --git a/src/sqlalchemy_cratedb/dialect.py b/src/sqlalchemy_cratedb/dialect.py index 90102a78..b94e573d 100644 --- a/src/sqlalchemy_cratedb/dialect.py +++ b/src/sqlalchemy_cratedb/dialect.py @@ -20,7 +20,7 @@ # software solely pursuant to the terms of the relevant commercial agreement. import logging -from datetime import date, datetime +from datetime import date, datetime, timezone from sqlalchemy import types as sqltypes from sqlalchemy.engine import default, reflection @@ -96,7 +96,8 @@ def process(value): if not value: return None try: - return datetime.utcfromtimestamp(value / 1e3).date() + # Always return timezone-naive dates for backward compatibility + return datetime.fromtimestamp(value / 1e3, timezone.utc).replace(tzinfo=None).date() except TypeError: pass @@ -132,7 +133,12 @@ def process(value): if not value: return None try: - return datetime.utcfromtimestamp(value / 1e3) + # Check if timezone information is requested + if getattr(coltype, 'timezone', False): + return datetime.fromtimestamp(value / 1e3, timezone.utc) + else: + # For timezone-naive columns, remove timezone info for backward compatibility + return datetime.fromtimestamp(value / 1e3, timezone.utc).replace(tzinfo=None) except TypeError: pass diff --git a/src/sqlalchemy_cratedb/sa_version.py b/src/sqlalchemy_cratedb/sa_version.py index 22f31e51..790345fb 100644 --- a/src/sqlalchemy_cratedb/sa_version.py +++ b/src/sqlalchemy_cratedb/sa_version.py @@ -26,3 +26,4 @@ SA_1_4 = Version("1.4.0b1") SA_2_0 = Version("2.0.0") +SA_2_1 = Version("2.1.0") diff --git a/src/sqlalchemy_cratedb/support/polyfill.py b/src/sqlalchemy_cratedb/support/polyfill.py index 22dad7ce..13c040f5 100644 --- a/src/sqlalchemy_cratedb/support/polyfill.py +++ b/src/sqlalchemy_cratedb/support/polyfill.py @@ -56,11 +56,10 @@ def check_uniqueness(mapper, connection, target): stmt = stmt.filter( getattr(sa_entity, attribute_name) == getattr(target, attribute_name) ) - stmt = stmt.compile(bind=connection.engine) results = connection.execute(stmt) if results.rowcount > 0: raise IntegrityError( - statement=stmt, + statement=str(stmt), params=[], orig=Exception( f"DuplicateKeyException in table '{target.__tablename__}' " diff --git a/tests/dialect_test.py b/tests/dialect_test.py index 74ae11af..3bd5a5b0 100644 --- a/tests/dialect_test.py +++ b/tests/dialect_test.py @@ -19,7 +19,7 @@ # 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 datetime import datetime, timezone from unittest import TestCase, skipIf from unittest.mock import MagicMock, patch @@ -66,7 +66,7 @@ class Character(self.base): name = sa.Column(sa.String, primary_key=True) age = sa.Column(sa.Integer, primary_key=True) obj = sa.Column(ObjectType) - ts = sa.Column(sa.DateTime, onupdate=datetime.utcnow) + ts = sa.Column(sa.DateTime, onupdate=lambda: datetime.now(timezone.utc)) self.session = Session(bind=self.engine) diff --git a/tests/insert_from_select_test.py b/tests/insert_from_select_test.py index 7d57e81a..7004153e 100644 --- a/tests/insert_from_select_test.py +++ b/tests/insert_from_select_test.py @@ -18,7 +18,7 @@ # 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 datetime import datetime +from datetime import datetime, timezone from unittest import TestCase, skipIf from unittest.mock import MagicMock, patch @@ -54,7 +54,7 @@ class Character(Base): name = sa.Column(sa.String, primary_key=True) age = sa.Column(sa.Integer) - ts = sa.Column(sa.DateTime, onupdate=datetime.utcnow) + ts = sa.Column(sa.DateTime, onupdate=lambda: datetime.now(timezone.utc)) status = sa.Column(sa.String) class CharacterArchive(Base): @@ -62,7 +62,7 @@ class CharacterArchive(Base): name = sa.Column(sa.String, primary_key=True) age = sa.Column(sa.Integer) - ts = sa.Column(sa.DateTime, onupdate=datetime.utcnow) + ts = sa.Column(sa.DateTime, onupdate=lambda: datetime.now(timezone.utc)) status = sa.Column(sa.String) self.character = Character diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index 58ee499e..10e82fca 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -14,7 +14,7 @@ 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]')) + assert ex.match(re.escape('InvalidColumnNameException["_id" conflicts with system column pattern]')) assert ex.match( - 'io.crate.exceptions.InvalidColumnNameException: "_id" conflicts with system column' + 'io.crate.exceptions.InvalidColumnNameException: "_id" conflicts with system column pattern' ) diff --git a/tests/test_support_polyfill.py b/tests/test_support_polyfill.py index 181456ae..29e44cd4 100644 --- a/tests/test_support_polyfill.py +++ b/tests/test_support_polyfill.py @@ -1,4 +1,5 @@ import datetime as dt +from datetime import timezone import pytest import sqlalchemy as sa @@ -58,7 +59,7 @@ class FooBar(Base): ) # Compare outcome. - assert result["date"].year == dt.datetime.now().year + assert result["date"].year == dt.datetime.now(timezone.utc).year assert result["number"] >= 1718846016235 assert result["string"] >= "1718846016235" diff --git a/tests/update_test.py b/tests/update_test.py index 107979b3..9aa94baa 100644 --- a/tests/update_test.py +++ b/tests/update_test.py @@ -18,7 +18,7 @@ # 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 datetime import datetime +from datetime import datetime, timezone from unittest import TestCase, skipIf from unittest.mock import MagicMock, patch @@ -53,7 +53,7 @@ class Character(self.base): name = sa.Column(sa.String, primary_key=True) age = sa.Column(sa.Integer) obj = sa.Column(ObjectType) - ts = sa.Column(sa.DateTime, onupdate=datetime.utcnow) + ts = sa.Column(sa.DateTime, onupdate=lambda: datetime.now(timezone.utc)) self.character = Character self.session = Session(bind=self.engine) @@ -63,7 +63,7 @@ def test_onupdate_is_triggered(self): char = self.character(name="Arthur") self.session.add(char) self.session.commit() - now = datetime.utcnow() + now = datetime.now(timezone.utc) fake_cursor.fetchall.return_value = [("Arthur", None)] fake_cursor.description = ( @@ -80,9 +80,12 @@ def test_onupdate_is_triggered(self): args = args[1] self.assertEqual(expected_stmt, stmt) self.assertEqual(40, args[0]) - dt = datetime.strptime(args[1], "%Y-%m-%dT%H:%M:%S.%f") + dt = datetime.fromisoformat(args[1].replace('+0000', '+00:00')) self.assertIsInstance(dt, datetime) - self.assertGreater(dt, now) + # Make now timezone-naive for comparison since dt is timezone-aware + now_naive = now.replace(tzinfo=None) + dt_naive = dt.replace(tzinfo=None) + self.assertGreater(dt_naive, now_naive) self.assertEqual("Arthur", args[2]) @patch("crate.client.connection.Cursor", FakeCursor) @@ -91,7 +94,7 @@ def test_bulk_update(self): Checks whether bulk updates work correctly on native types and Crate types. """ - before_update_time = datetime.utcnow() + before_update_time = datetime.now(timezone.utc) self.session.query(self.character).update( { @@ -110,6 +113,9 @@ def test_bulk_update(self): self.assertEqual(expected_stmt, stmt) self.assertEqual("Julia", args[0]) self.assertEqual({"favorite_book": "Romeo & Juliet"}, args[1]) - dt = datetime.strptime(args[2], "%Y-%m-%dT%H:%M:%S.%f") + dt = datetime.fromisoformat(args[2].replace('+0000', '+00:00')) self.assertIsInstance(dt, datetime) - self.assertGreater(dt, before_update_time) + # Make before_update_time timezone-naive for comparison since dt is timezone-aware + before_update_time_naive = before_update_time.replace(tzinfo=None) + dt_naive = dt.replace(tzinfo=None) + self.assertGreater(dt_naive, before_update_time_naive)