Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-SQLAlchemy-Utils for 
openSUSE:Factory checked in at 2026-10-27 16:48:43
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-SQLAlchemy-Utils (Old)
 and      /work/SRC/openSUSE:Factory/.python-SQLAlchemy-Utils.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-SQLAlchemy-Utils"

Tue Oct 27 16:48:43 2026 rev:45 rq:1392850 version:0.43.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-SQLAlchemy-Utils/python-SQLAlchemy-Utils.changes
  2026-03-27 06:47:14.876383680 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-SQLAlchemy-Utils.new.2252680/python-SQLAlchemy-Utils.changes
     2026-10-27 16:49:18.022337492 +0100
@@ -1,0 +2,20 @@
+Mon Oct 26 22:01:23 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.43.0:
+  * Add support for SQLAlchemy 2.1.
+  * Reimplement generic_relationship as a hybrid property built
+    only on public SQLAlchemy APIs, instead of private attribute
+    internals. Assigning to a generic relationship now sets the
+    discriminator and id attributes through regular attribute
+    instrumentation, so re-assigning it on a persistent object is
+    flushed to the database.
+  * Support querying generic relationships through aliased
+    models, including composite keys and renamed columns.
+  * Drop support for sqlalchemy 1.4.
+  * Drop support for Python 3.9.
+  * Support Python 3.14.
+  * Fix ChoiceType returning the raw scalar instead of a Choice
+    for falsy codes such as 0 or the empty string. (#813) NULL
+    values continue to return None.
+
+-------------------------------------------------------------------

Old:
----
  sqlalchemy_utils-0.42.1.tar.gz

New:
----
  sqlalchemy_utils-0.43.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-SQLAlchemy-Utils.spec ++++++
--- /var/tmp/diff_new_pack.QNLCF1/_old  2026-10-27 16:49:19.327392185 +0100
+++ /var/tmp/diff_new_pack.QNLCF1/_new  2026-10-27 16:49:19.331392353 +0100
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-SQLAlchemy-Utils
-Version:        0.42.1
+Version:        0.43.0
 Release:        0
 Summary:        Various utility functions for SQLAlchemy
 License:        BSD-3-Clause
@@ -27,9 +27,8 @@
 BuildRequires:  %{python_module Babel >= 1.3}
 BuildRequires:  %{python_module Jinja2 >= 2.3}
 BuildRequires:  %{python_module Pygments >= 1.2}
-BuildRequires:  %{python_module SQLAlchemy >= 1.0}
+BuildRequires:  %{python_module SQLAlchemy >= 2.0}
 BuildRequires:  %{python_module arrow >= 0.3.4}
-BuildRequires:  %{python_module backports.zoneinfo if %python-base < 3.9}
 BuildRequires:  %{python_module colour >= 0.0.4}
 BuildRequires:  %{python_module cryptography >= 0.6}
 BuildRequires:  %{python_module docutils >= 0.10}
@@ -43,14 +42,14 @@
 BuildRequires:  %{python_module psycopg2 >= 2.5.1}
 BuildRequires:  %{python_module psycopg2cffi >= 2.8.1}
 BuildRequires:  %{python_module pyodbc}
-BuildRequires:  %{python_module pytest >= 2.7.1}
+BuildRequires:  %{python_module pytest >= 9.0}
 BuildRequires:  %{python_module python-dateutil >= 2.6}
 BuildRequires:  %{python_module pytz >= 2014.2}
-BuildRequires:  %{python_module setuptools}
+BuildRequires:  %{python_module setuptools >= 77.0.0}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
-Requires:       python-SQLAlchemy >= 1.0
+Requires:       python-SQLAlchemy >= 2.0
 Recommends:     python-Babel >= 1.3
 Recommends:     python-arrow >= 0.3.4
 Recommends:     python-colour >= 0.0.4
@@ -62,9 +61,6 @@
 Recommends:     python-phonenumbers >= 5.9.2
 Suggests:       python-pendulum >= 2.0.5
 BuildArch:      noarch
-%if 0%{?python_version_nodots} < 39
-Requires:       python-backports.zoneinfo
-%endif
 %python_subpackages
 
 %description

++++++ sqlalchemy_utils-0.42.1.tar.gz -> sqlalchemy_utils-0.43.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/CHANGES.rst 
new/sqlalchemy_utils-0.43.0/CHANGES.rst
--- old/sqlalchemy_utils-0.42.1/CHANGES.rst     2025-12-13 04:12:25.000000000 
+0100
+++ new/sqlalchemy_utils-0.43.0/CHANGES.rst     2026-10-09 17:35:51.229952300 
+0200
@@ -3,6 +3,21 @@
 
 Here you can see the full list of changes between each SQLAlchemy-Utils 
release.
 
+0.43.0 (2026-10-09)
+^^^^^^^^^^^^^^^^^^^
+
+- Add support for SQLAlchemy 2.1.
+- Reimplement ``generic_relationship`` as a hybrid property built only on 
public SQLAlchemy APIs, instead of private attribute internals.
+
+  Assigning to a generic relationship now sets the discriminator and id 
attributes through regular attribute instrumentation, so re-assigning it on a 
persistent object is flushed to the database.
+- Support querying generic relationships through aliased models, including 
composite keys and renamed columns.
+- Drop support for sqlalchemy 1.4.
+- Drop support for Python 3.9.
+- Support Python 3.14.
+- Fix ``ChoiceType`` returning the raw scalar instead of a ``Choice`` for 
falsy codes such as ``0`` or the empty string. (#813)
+
+  NULL values continue to return ``None``.
+
 0.42.1 (2025-12-12)
 ^^^^^^^^^^^^^^^^^^^
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/PKG-INFO 
new/sqlalchemy_utils-0.43.0/PKG-INFO
--- old/sqlalchemy_utils-0.42.1/PKG-INFO        2025-12-13 04:12:59.770267500 
+0100
+++ new/sqlalchemy_utils-0.43.0/PKG-INFO        2026-10-09 17:46:12.943159800 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: SQLAlchemy-Utils
-Version: 0.42.1
+Version: 0.43.0
 Summary: Various utility functions for SQLAlchemy.
 Author: Ryan Leckey, Janne Vanhala, Vesa Uimonen
 Author-email: Konsta Vesterinen <[email protected]>
@@ -10,19 +10,19 @@
 Classifier: Intended Audience :: Developers
 Classifier: Operating System :: OS Independent
 Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Requires-Python: >=3.9
+Requires-Python: >=3.10
 Description-Content-Type: text/x-rst
 License-File: LICENSE
-Requires-Dist: SQLAlchemy>=1.4
+Requires-Dist: SQLAlchemy>=2.0
 Provides-Extra: test
-Requires-Dist: pytest==7.4.4; extra == "test"
+Requires-Dist: pytest<10,>=9.0; extra == "test"
 Requires-Dist: Pygments>=1.2; extra == "test"
 Requires-Dist: Jinja2>=2.3; extra == "test"
 Requires-Dist: docutils>=0.10; extra == "test"
@@ -34,8 +34,6 @@
 Requires-Dist: pytz>=2014.2; extra == "test"
 Requires-Dist: python-dateutil>=2.6; extra == "test"
 Requires-Dist: pymysql; extra == "test"
-Requires-Dist: flake8>=2.4.0; extra == "test"
-Requires-Dist: isort>=4.2.2; extra == "test"
 Requires-Dist: pyodbc; extra == "test"
 Provides-Extra: babel
 Requires-Dist: Babel>=1.3; extra == "babel"
@@ -58,7 +56,7 @@
 Provides-Extra: encrypted
 Requires-Dist: cryptography>=0.6; extra == "encrypted"
 Provides-Extra: test-all
-Requires-Dist: pytest==7.4.4; extra == "test-all"
+Requires-Dist: pytest<10,>=9.0; extra == "test-all"
 Requires-Dist: Pygments>=1.2; extra == "test-all"
 Requires-Dist: Jinja2>=2.3; extra == "test-all"
 Requires-Dist: docutils>=0.10; extra == "test-all"
@@ -70,8 +68,6 @@
 Requires-Dist: pytz>=2014.2; extra == "test-all"
 Requires-Dist: python-dateutil>=2.6; extra == "test-all"
 Requires-Dist: pymysql; extra == "test-all"
-Requires-Dist: flake8>=2.4.0; extra == "test-all"
-Requires-Dist: isort>=4.2.2; extra == "test-all"
 Requires-Dist: pyodbc; extra == "test-all"
 Requires-Dist: Babel>=1.3; extra == "test-all"
 Requires-Dist: arrow>=0.3.4; extra == "test-all"
@@ -88,8 +84,6 @@
 SQLAlchemy-Utils
 ================
 
-|Build| |Version Status| |Downloads|
-
 Various utility functions, new data types and helpers for SQLAlchemy.
 
 
@@ -99,11 +93,3 @@
 - `Documentation <https://sqlalchemy-utils.readthedocs.io/>`_
 - `Issue Tracker <https://github.com/kvesteri/sqlalchemy-utils/issues>`_
 - `Code <https://github.com/kvesteri/sqlalchemy-utils/>`_
-
-.. |Build| image:: 
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml/badge.svg?branch=master
-   :target: 
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml
-   :alt: Tests
-.. |Version Status| image:: https://img.shields.io/pypi/v/SQLAlchemy-Utils.svg
-   :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
-.. |Downloads| image:: https://img.shields.io/pypi/dm/SQLAlchemy-Utils.svg
-   :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/README.rst 
new/sqlalchemy_utils-0.43.0/README.rst
--- old/sqlalchemy_utils-0.42.1/README.rst      2024-03-21 22:26:25.000000000 
+0100
+++ new/sqlalchemy_utils-0.43.0/README.rst      2026-08-06 17:05:53.343547800 
+0200
@@ -1,8 +1,6 @@
 SQLAlchemy-Utils
 ================
 
-|Build| |Version Status| |Downloads|
-
 Various utility functions, new data types and helpers for SQLAlchemy.
 
 
@@ -12,11 +10,3 @@
 - `Documentation <https://sqlalchemy-utils.readthedocs.io/>`_
 - `Issue Tracker <https://github.com/kvesteri/sqlalchemy-utils/issues>`_
 - `Code <https://github.com/kvesteri/sqlalchemy-utils/>`_
-
-.. |Build| image:: 
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml/badge.svg?branch=master
-   :target: 
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml
-   :alt: Tests
-.. |Version Status| image:: https://img.shields.io/pypi/v/SQLAlchemy-Utils.svg
-   :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
-.. |Downloads| image:: https://img.shields.io/pypi/dm/SQLAlchemy-Utils.svg
-   :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/SQLAlchemy_Utils.egg-info/PKG-INFO 
new/sqlalchemy_utils-0.43.0/SQLAlchemy_Utils.egg-info/PKG-INFO
--- old/sqlalchemy_utils-0.42.1/SQLAlchemy_Utils.egg-info/PKG-INFO      
2025-12-13 04:12:59.000000000 +0100
+++ new/sqlalchemy_utils-0.43.0/SQLAlchemy_Utils.egg-info/PKG-INFO      
2026-10-09 17:46:12.877671700 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: SQLAlchemy-Utils
-Version: 0.42.1
+Version: 0.43.0
 Summary: Various utility functions for SQLAlchemy.
 Author: Ryan Leckey, Janne Vanhala, Vesa Uimonen
 Author-email: Konsta Vesterinen <[email protected]>
@@ -10,19 +10,19 @@
 Classifier: Intended Audience :: Developers
 Classifier: Operating System :: OS Independent
 Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Requires-Python: >=3.9
+Requires-Python: >=3.10
 Description-Content-Type: text/x-rst
 License-File: LICENSE
-Requires-Dist: SQLAlchemy>=1.4
+Requires-Dist: SQLAlchemy>=2.0
 Provides-Extra: test
-Requires-Dist: pytest==7.4.4; extra == "test"
+Requires-Dist: pytest<10,>=9.0; extra == "test"
 Requires-Dist: Pygments>=1.2; extra == "test"
 Requires-Dist: Jinja2>=2.3; extra == "test"
 Requires-Dist: docutils>=0.10; extra == "test"
@@ -34,8 +34,6 @@
 Requires-Dist: pytz>=2014.2; extra == "test"
 Requires-Dist: python-dateutil>=2.6; extra == "test"
 Requires-Dist: pymysql; extra == "test"
-Requires-Dist: flake8>=2.4.0; extra == "test"
-Requires-Dist: isort>=4.2.2; extra == "test"
 Requires-Dist: pyodbc; extra == "test"
 Provides-Extra: babel
 Requires-Dist: Babel>=1.3; extra == "babel"
@@ -58,7 +56,7 @@
 Provides-Extra: encrypted
 Requires-Dist: cryptography>=0.6; extra == "encrypted"
 Provides-Extra: test-all
-Requires-Dist: pytest==7.4.4; extra == "test-all"
+Requires-Dist: pytest<10,>=9.0; extra == "test-all"
 Requires-Dist: Pygments>=1.2; extra == "test-all"
 Requires-Dist: Jinja2>=2.3; extra == "test-all"
 Requires-Dist: docutils>=0.10; extra == "test-all"
@@ -70,8 +68,6 @@
 Requires-Dist: pytz>=2014.2; extra == "test-all"
 Requires-Dist: python-dateutil>=2.6; extra == "test-all"
 Requires-Dist: pymysql; extra == "test-all"
-Requires-Dist: flake8>=2.4.0; extra == "test-all"
-Requires-Dist: isort>=4.2.2; extra == "test-all"
 Requires-Dist: pyodbc; extra == "test-all"
 Requires-Dist: Babel>=1.3; extra == "test-all"
 Requires-Dist: arrow>=0.3.4; extra == "test-all"
@@ -88,8 +84,6 @@
 SQLAlchemy-Utils
 ================
 
-|Build| |Version Status| |Downloads|
-
 Various utility functions, new data types and helpers for SQLAlchemy.
 
 
@@ -99,11 +93,3 @@
 - `Documentation <https://sqlalchemy-utils.readthedocs.io/>`_
 - `Issue Tracker <https://github.com/kvesteri/sqlalchemy-utils/issues>`_
 - `Code <https://github.com/kvesteri/sqlalchemy-utils/>`_
-
-.. |Build| image:: 
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml/badge.svg?branch=master
-   :target: 
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml
-   :alt: Tests
-.. |Version Status| image:: https://img.shields.io/pypi/v/SQLAlchemy-Utils.svg
-   :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
-.. |Downloads| image:: https://img.shields.io/pypi/dm/SQLAlchemy-Utils.svg
-   :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/SQLAlchemy_Utils.egg-info/requires.txt 
new/sqlalchemy_utils-0.43.0/SQLAlchemy_Utils.egg-info/requires.txt
--- old/sqlalchemy_utils-0.42.1/SQLAlchemy_Utils.egg-info/requires.txt  
2025-12-13 04:12:59.000000000 +0100
+++ new/sqlalchemy_utils-0.43.0/SQLAlchemy_Utils.egg-info/requires.txt  
2026-10-09 17:46:12.879012600 +0200
@@ -1,4 +1,4 @@
-SQLAlchemy>=1.4
+SQLAlchemy>=2.0
 
 [arrow]
 arrow>=0.3.4
@@ -25,7 +25,7 @@
 phonenumbers>=5.9.2
 
 [test]
-pytest==7.4.4
+pytest<10,>=9.0
 Pygments>=1.2
 Jinja2>=2.3
 docutils>=0.10
@@ -37,12 +37,10 @@
 pytz>=2014.2
 python-dateutil>=2.6
 pymysql
-flake8>=2.4.0
-isort>=4.2.2
 pyodbc
 
 [test_all]
-pytest==7.4.4
+pytest<10,>=9.0
 Pygments>=1.2
 Jinja2>=2.3
 docutils>=0.10
@@ -54,8 +52,6 @@
 pytz>=2014.2
 python-dateutil>=2.6
 pymysql
-flake8>=2.4.0
-isort>=4.2.2
 pyodbc
 Babel>=1.3
 arrow>=0.3.4
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/docs/installation.rst 
new/sqlalchemy_utils-0.43.0/docs/installation.rst
--- old/sqlalchemy_utils-0.42.1/docs/installation.rst   2025-08-30 
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/docs/installation.rst   2026-08-06 
17:05:53.343715700 +0200
@@ -6,12 +6,7 @@
 Supported platforms
 -------------------
 
-SQLAlchemy-Utils is currently tested against the following Python platforms:
-
-- CPython 3.9
-- CPython 3.10
-- CPython 3.11
-- CPython 3.12
+SQLAlchemy-Utils is tested against supported CPython versions.
 
 
 Installing an official release
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/pyproject.toml 
new/sqlalchemy_utils-0.43.0/pyproject.toml
--- old/sqlalchemy_utils-0.42.1/pyproject.toml  2025-12-13 04:12:35.000000000 
+0100
+++ new/sqlalchemy_utils-0.43.0/pyproject.toml  2026-10-09 17:35:51.230171000 
+0200
@@ -4,7 +4,7 @@
 
 [project]
 name = "SQLAlchemy-Utils"
-version = "0.42.1"
+version = "0.43.0"
 description = "Various utility functions for SQLAlchemy."
 readme = "README.rst"
 license = "BSD-3-Clause"
@@ -14,22 +14,22 @@
     {name = "Janne Vanhala"},
     {name = "Vesa Uimonen"}
 ]
-requires-python = ">=3.9"
+requires-python = ">=3.10"
 classifiers = [
     "Environment :: Web Environment",
     "Intended Audience :: Developers",
     "Operating System :: OS Independent",
     "Programming Language :: Python",
-    "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
     "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
     "Topic :: Software Development :: Libraries :: Python Modules",
 ]
 dependencies = [
-    "SQLAlchemy>=1.4",
+    "SQLAlchemy>=2.0",
 ]
 
 [project.urls]
@@ -37,7 +37,7 @@
 
 [project.optional-dependencies]
 test = [
-    "pytest==7.4.4",
+    "pytest>=9.0,<10",
     "Pygments>=1.2",
     "Jinja2>=2.3",
     "docutils>=0.10",
@@ -49,8 +49,6 @@
     "pytz>=2014.2",
     "python-dateutil>=2.6",
     "pymysql",
-    "flake8>=2.4.0",
-    "isort>=4.2.2",
     "pyodbc",
 ]
 babel = ["Babel>=1.3"]
@@ -64,7 +62,7 @@
 url = ["furl >= 0.4.1"]
 encrypted = ["cryptography>=0.6"]
 test_all = [
-    "pytest==7.4.4",
+    "pytest>=9.0,<10",
     "Pygments>=1.2",
     "Jinja2>=2.3",
     "docutils>=0.10",
@@ -76,8 +74,6 @@
     "pytz>=2014.2",
     "python-dateutil>=2.6",
     "pymysql",
-    "flake8>=2.4.0",
-    "isort>=4.2.2",
     "pyodbc",
     "Babel>=1.3",
     "arrow>=0.3.4",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/__init__.py 
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/__init__.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/__init__.py    2025-08-30 
20:44:39.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/__init__.py    2026-10-09 
17:35:51.230389800 +0200
@@ -101,4 +101,4 @@
     refresh_materialized_view,
 )
 
-__version__ = '0.42.0'
+__version__ = '0.43.0'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/database.py 
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/database.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/database.py  
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/database.py  
2026-08-06 17:05:53.345343600 +0200
@@ -1,7 +1,6 @@
 import itertools
 import os
 from collections.abc import Mapping, Sequence
-from copy import copy
 
 import sqlalchemy as sa
 from sqlalchemy.engine.url import make_url
@@ -416,13 +415,8 @@
     :param database: New database to set.
 
     """
-    if hasattr(url, '_replace'):
-        # Cannot use URL.set() as database may need to be set to None.
-        ret = url._replace(database=database)
-    else:  # SQLAlchemy <1.4
-        url = copy(url)
-        url.database = database
-        ret = url
+    # Cannot use URL.set() as database may need to be set to None.
+    ret = url._replace(database=database)
     assert ret.database == database, ret
     return ret
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/mock.py 
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/mock.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/mock.py      
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/mock.py      
2026-08-06 17:05:53.345567200 +0200
@@ -47,10 +47,7 @@
         def dump(*args, **kw):
             return None
 
-    try:
-        engine = sa.create_mock_engine(bind_url, executor=dump)
-    except AttributeError:  # SQLAlchemy <1.4
-        engine = sa.create_engine(bind_url, strategy='mock', executor=dump)
+    engine = sa.create_mock_engine(bind_url, executor=dump)
     return engine
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/orm.py 
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/orm.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/orm.py       
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/orm.py       
2026-08-06 17:05:53.345759400 +0200
@@ -10,10 +10,7 @@
 from sqlalchemy.orm.attributes import InstrumentedAttribute
 from sqlalchemy.orm.exc import UnmappedInstanceError
 
-try:
-    from sqlalchemy.orm.context import _ColumnEntity, _MapperEntity
-except ImportError:  # SQLAlchemy <1.4
-    from sqlalchemy.orm.query import _ColumnEntity, _MapperEntity
+from sqlalchemy.orm.context import _ColumnEntity, _MapperEntity
 
 from sqlalchemy.orm.session import object_session
 from sqlalchemy.orm.util import AliasedInsp
@@ -281,12 +278,9 @@
     if isinstance(mixed, sa.orm.attributes.InstrumentedAttribute):
         mixed = mixed.class_
     if isinstance(mixed, sa.Table):
-        if hasattr(mapperlib, '_all_registries'):
-            all_mappers = set()
-            for mapper_registry in mapperlib._all_registries():
-                all_mappers.update(mapper_registry.mappers)
-        else:  # SQLAlchemy <1.4
-            all_mappers = mapperlib._mapper_registry
+        all_mappers = set()
+        for mapper_registry in mapperlib._all_registries():
+            all_mappers.update(mapper_registry.mappers)
         mappers = [mapper for mapper in all_mappers if mixed in mapper.tables]
         if len(mappers) > 1:
             raise ValueError("Multiple mappers found for table '%s'." % 
mixed.name)
@@ -449,11 +443,10 @@
         SA Table object, SA Mapper, SA declarative class, SA declarative class
         instance or an alias of any of these objects
     """
+    if isinstance(mixed, sa.SelectBase):
+        return mixed.subquery().c
     if isinstance(mixed, sa.sql.selectable.Selectable):
-        try:
-            return mixed.selected_columns
-        except AttributeError:  # SQLAlchemy <1.4
-            return mixed.c
+        return mixed.c
     if isinstance(mixed, sa.orm.util.AliasedClass):
         return sa.inspect(mixed).mapper.columns
     if isinstance(mixed, sa.orm.Mapper):
@@ -520,10 +513,7 @@
 
 
 def _get_query_compile_state(query):
-    if hasattr(query, '_compile_state'):
-        return query._compile_state()
-    else:  # SQLAlchemy <1.4
-        return query
+    return query._compile_state()
 
 
 def get_polymorphic_mappers(mixed):
@@ -878,7 +868,4 @@
 
 
 def _get_class_registry(class_):
-    try:
-        return class_.registry._class_registry
-    except AttributeError:  # SQLAlchemy <1.4
-        return class_._decl_class_registry
+    return class_.registry._class_registry
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/sort_query.py 
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/sort_query.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/sort_query.py        
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/sort_query.py        
2026-08-06 17:05:53.345980400 +0200
@@ -40,10 +40,7 @@
     """
     order_by_func = sa.asc
 
-    try:
-        order_by_clauses = query._order_by_clauses
-    except AttributeError:  # SQLAlchemy <1.4
-        order_by_clauses = query._order_by
+    order_by_clauses = query._order_by_clauses
     if not order_by_clauses:
         column = None
     else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/generic.py 
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/generic.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/generic.py     2025-08-30 
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/generic.py     2026-10-09 
17:35:51.230671200 +0200
@@ -1,112 +1,51 @@
+import builtins
 from collections.abc import Iterable
 
 import sqlalchemy as sa
-from sqlalchemy.ext.hybrid import hybrid_property
-from sqlalchemy.orm import attributes, class_mapper, ColumnProperty
-from sqlalchemy.orm.interfaces import MapperProperty, PropComparator
-from sqlalchemy.orm.session import _state_session
-from sqlalchemy.util import set_creation_order
+from sqlalchemy.ext.hybrid import Comparator, hybrid_property
+from sqlalchemy.orm import object_session
 
 from .exceptions import ImproperlyConfigured
 from .functions import identity
-from .functions.orm import _get_class_registry
 
 
-class GenericAttributeImpl(attributes.ScalarAttributeImpl):
-    def __init__(self, *args, **kwargs):
-        """
-        The constructor of attributes.AttributeImpl changed in SQLAlchemy 
2.0.22,
-        adding a 'default_function' required positional argument before 
'dispatch'.
-        This adjustment ensures compatibility across versions by inserting 
None for
-        'default_function' in versions >= 2.0.22.
-
-        Arguments received: (class, key, dispatch)
-        Required by AttributeImpl: (class, key, default_function, dispatch)
-        Setting None as default_function here.
-        """
-        # Adjust for SQLAlchemy version change
-        sqlalchemy_version = tuple(map(int, sa.__version__.split('.')))
-        if sqlalchemy_version >= (2, 0, 22):
-            args = (*args[:2], None, *args[2:])
-
-        super().__init__(*args, **kwargs)
-
-    def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
-        if self.key in dict_:
-            return dict_[self.key]
-
-        # Retrieve the session bound to the state in order to perform
-        # a lazy query for the attribute.
-        session = _state_session(state)
-        if session is None:
-            # State is not bound to a session; we cannot proceed.
-            return None
-
-        # Find class for discriminator.
-        # TODO: Perhaps optimize with some sort of lookup?
-        discriminator = self.get_state_discriminator(state)
-        target_class = _get_class_registry(state.class_).get(discriminator)
-
-        if target_class is None:
-            # Unknown discriminator; return nothing.
-            return None
-
-        id = self.get_state_id(state)
-
-        target = session.get(target_class, id)
-
-        # Return found (or not found) target.
-        return target
-
-    def get_state_discriminator(self, state):
-        discriminator = self.parent_token.discriminator
-        if isinstance(discriminator, hybrid_property):
-            return getattr(state.obj(), discriminator.__name__)
-        else:
-            return state.attrs[discriminator.key].value
-
-    def get_state_id(self, state):
-        # Lookup row with the discriminator and id.
-        return tuple(state.attrs[id.key].value for id in self.parent_token.id)
-
-    def set(
-        self,
-        state,
-        dict_,
-        initiator,
-        passive=attributes.PASSIVE_OFF,
-        check_old=None,
-        pop=False,
-    ):
-        # Set us on the state.
-        dict_[self.key] = initiator
-
-        if initiator is None:
-            # Nullify relationship args
-            for id in self.parent_token.id:
-                dict_[id.key] = None
-            dict_[self.parent_token.discriminator.key] = None
-        else:
-            # Get the primary key of the initiator and ensure we
-            # can support this assignment.
-            class_ = type(initiator)
-            mapper = class_mapper(class_)
-
-            pk = mapper.identity_key_from_instance(initiator)[1]
-
-            # Set the identifier and the discriminator.
-            discriminator = class_.__name__
+class GenericComparator(Comparator):
+    def __init__(self, relationship, cls):
+        self.relationship = relationship
+        self.cls = cls
+
+    def __clause_element__(self):
+        raise NotImplementedError(
+            'A generic relationship can only be compared against a model '
+            'instance or with is_type().'
+        )
 
-            for index, id in enumerate(self.parent_token.id):
-                dict_[id.key] = pk[index]
-            dict_[self.parent_token.discriminator.key] = discriminator
+    def __eq__(self, other):
+        discriminator, ids = self.relationship._attributes(self.cls)
+        q = discriminator == type(other).__name__
+        for column, value in zip(ids, identity(other)):
+            q &= column == value
+        return q
+
+    def __ne__(self, other):
+        return ~(self == other)
+
+    def is_type(self, other):
+        discriminator, _ = self.relationship._attributes(self.cls)
+        class_names = [
+            mapper.class_.__name__ for mapper in 
sa.inspect(other).self_and_descendants
+        ]
+        return discriminator.in_(class_names)
 
 
-class GenericRelationshipProperty(MapperProperty):
+class GenericRelationship:
     """A generic form of the relationship property.
 
     Creates a 1 to many relationship between the parent model
-    and any other models using a discriminator (the table name).
+    and any other models using a discriminator (the class name).
+
+    Implemented as a hybrid property on top of the discriminator and id
+    attributes, so it only relies on public SQLAlchemy APIs.
 
     :param discriminator:
         Field to discriminate which model we are referring to.
@@ -114,87 +53,94 @@
         Field to point to the model we are referring to.
     """
 
-    def __init__(self, discriminator, id, doc=None):
-        super().__init__()
-        self._discriminator_col = discriminator
-        self._id_cols = id
-        self._id = None
-        self._discriminator = None
-        self.doc = doc
-
-        set_creation_order(self)
-
-    def _column_to_property(self, column):
-        if isinstance(column, hybrid_property):
-            attr_key = column.__name__
-            for key, attr in self.parent.all_orm_descriptors.items():
-                if key == attr_key:
-                    return attr
-        else:
-            for attr in self.parent.attrs.values():
-                if isinstance(attr, ColumnProperty):
-                    if attr.columns[0].name == column.name:
-                        return attr
-
-    def init(self):
-        def convert_strings(column):
-            if isinstance(column, str):
-                return self.parent.columns[column]
-            return column
-
-        self._discriminator_col = convert_strings(self._discriminator_col)
-        self._id_cols = convert_strings(self._id_cols)
-
-        if isinstance(self._id_cols, Iterable):
-            self._id_cols = list(map(convert_strings, self._id_cols))
+    def __init__(self, discriminator, id):
+        self.discriminator = discriminator
+        if isinstance(id, Iterable) and not isinstance(id, str):
+            self.ids = list(id)
         else:
-            self._id_cols = [self._id_cols]
-
-        self.discriminator = self._column_to_property(self._discriminator_col)
+            self.ids = [id]
+        # Key for caching the assigned target object on the instance dict.
+        self.cache_key = f'_generic_relationship_{builtins.id(self)}'
 
-        if self.discriminator is None:
-            raise ImproperlyConfigured('Could not find discriminator 
descriptor.')
+    def _key(self, cls, column):
+        if isinstance(column, str):
+            return column
+        if isinstance(column, hybrid_property):
+            return column.__name__
+        try:
+            return sa.inspect(cls).mapper.get_property_by_column(column).key
+        except sa.orm.exc.UnmappedColumnError:
+            raise ImproperlyConfigured(
+                f'Could not find generic relationship attribute for 
{column!r}.'
+            )
 
-        self.id = list(map(self._column_to_property, self._id_cols))
+    def _keys(self, cls):
+        return (
+            self._key(cls, self.discriminator),
+            [self._key(cls, column) for column in self.ids],
+        )
 
-    class Comparator(PropComparator):
-        def __init__(self, prop, parentmapper):
-            self.property = prop
-            self._parententity = parentmapper
-
-        def __eq__(self, other):
-            discriminator = type(other).__name__
-            q = self.property._discriminator_col == discriminator
-            other_id = identity(other)
-            for index, id in enumerate(self.property._id_cols):
-                q &= id == other_id[index]
-            return q
-
-        def __ne__(self, other):
-            return ~(self == other)
-
-        def is_type(self, other):
-            mapper = sa.inspect(other)
-            # Iterate through the weak sequence in order to get the actual
-            # mappers
-            class_names = [other.__name__]
-            class_names.extend(
-                [submapper.class_.__name__ for submapper in 
mapper._inheriting_mappers]
-            )
+    def _attributes(self, cls):
+        discriminator, ids = self._keys(cls)
+        return getattr(cls, discriminator), [getattr(cls, key) for key in ids]
+
+    def _target_class(self, cls, discriminator):
+        for mapper in sa.inspect(cls).registry.mappers:
+            if mapper.class_.__name__ == discriminator:
+                return mapper.class_
+
+    def get(self, obj):
+        discriminator_key, id_keys = self._keys(type(obj))
+        discriminator = getattr(obj, discriminator_key)
+        id = tuple(getattr(obj, key) for key in id_keys)
+
+        # Return the object assigned to this relationship as long as the
+        # discriminator and id still point to it.
+        cached = obj.__dict__.get(self.cache_key)
+        if (
+            cached is not None
+            and type(cached).__name__ == discriminator
+            and identity(cached) == id
+        ):
+            return cached
+
+        # Use the session the object is bound to in order to perform
+        # a lazy query for the target.
+        session = object_session(obj)
+        if session is None:
+            return None
 
-            return self.property._discriminator_col.in_(class_names)
+        target_class = self._target_class(type(obj), discriminator)
+        if target_class is None:
+            # Unknown discriminator; return nothing.
+            return None
 
-    def instrument_class(self, mapper):
-        attributes.register_attribute(
-            mapper.class_,
-            self.key,
-            comparator=self.Comparator(self, mapper),
-            parententity=mapper,
-            doc=self.doc,
-            impl_class=GenericAttributeImpl,
-            parent_token=self,
-        )
+        return session.get(target_class, id)
 
+    def set(self, obj, value):
+        discriminator_key, id_keys = self._keys(type(obj))
+        obj.__dict__[self.cache_key] = value
 
-def generic_relationship(*args, **kwargs):
-    return GenericRelationshipProperty(*args, **kwargs)
+        if value is None:
+            # Nullify relationship args
+            for key in id_keys:
+                setattr(obj, key, None)
+            setattr(obj, discriminator_key, None)
+        else:
+            for key, pk in zip(id_keys, identity(value)):
+                setattr(obj, key, pk)
+            setattr(obj, discriminator_key, type(value).__name__)
+
+    def comparator(self, cls):
+        return GenericComparator(self, cls)
+
+
+def generic_relationship(discriminator, id, doc=None):
+    relationship = GenericRelationship(discriminator, id)
+    prop = hybrid_property(
+        relationship.get,
+        relationship.set,
+        custom_comparator=relationship.comparator,
+    )
+    prop.__doc__ = doc
+    return prop
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/choice.py 
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/choice.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/choice.py        
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/choice.py        
2026-08-06 17:05:53.346468000 +0200
@@ -183,9 +183,9 @@
         return value
 
     def process_result_value(self, value, dialect):
-        if value:
-            return Choice(value, self.choices_dict[value])
-        return value
+        if value is None:
+            return None
+        return Choice(value, self.choices_dict[value])
 
 
 class EnumTypeImpl:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/password.py 
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/password.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/password.py      
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/password.py      
2026-08-06 17:05:53.347087600 +0200
@@ -42,8 +42,10 @@
 
     def __eq__(self, value):
         if self.hash is None or value is None:
-            # Ensure that we don't continue comparison if one of us is None.
-            return self.hash is value
+            # If either the hash or the value is None,
+            # equivalence depends on *all* values being None,
+            # including the secret.
+            return self.hash is None and self.secret is None and value is None
 
         if isinstance(value, Password):
             # Comparing 2 hashes isn't very useful; but this equality
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/uuid.py 
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/uuid.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/uuid.py  2025-08-30 
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/uuid.py  2026-08-06 
17:05:53.347343000 +0200
@@ -3,11 +3,8 @@
 from sqlalchemy import types, util
 from sqlalchemy.dialects import mssql, postgresql
 
-from ..compat import get_sqlalchemy_version
 from .scalar_coercible import ScalarCoercible
 
-sqlalchemy_version = get_sqlalchemy_version()
-
 
 class UUIDType(ScalarCoercible, types.TypeDecorator):
     """
@@ -71,16 +68,8 @@
 
         return value
 
-    # sqlalchemy >= 1.4.30 quotes UUID's automatically.
-    # It is only necessary to quote UUID's in sqlalchemy < 1.4.30.
-    if sqlalchemy_version < (1, 4, 30):
-
-        def process_literal_param(self, value, dialect):
-            return f"'{value}'" if value else value
-    else:
-
-        def process_literal_param(self, value, dialect):
-            return value
+    def process_literal_param(self, value, dialect):
+        return value
 
     def process_bind_param(self, value, dialect):
         if value is None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/tests/conftest.py 
new/sqlalchemy_utils-0.43.0/tests/conftest.py
--- old/sqlalchemy_utils-0.42.1/tests/conftest.py       2025-08-30 
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/tests/conftest.py       2026-10-09 
17:35:51.230960800 +0200
@@ -64,7 +64,7 @@
 def postgresql_dsn(
     postgresql_db_user, postgresql_db_password, postgresql_db_host, db_name
 ):
-    return 'postgresql://{}:{}@{}/{}'.format(
+    return 'postgresql+psycopg2://{}:{}@{}/{}'.format(
         postgresql_db_user, postgresql_db_password, postgresql_db_host, db_name
     )
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/tests/functions/test_database.py 
new/sqlalchemy_utils-0.43.0/tests/functions/test_database.py
--- old/sqlalchemy_utils-0.42.1/tests/functions/test_database.py        
2024-03-14 18:37:59.000000000 +0100
+++ new/sqlalchemy_utils-0.43.0/tests/functions/test_database.py        
2026-08-06 17:05:53.347864600 +0200
@@ -2,7 +2,6 @@
 import sqlalchemy as sa
 
 from sqlalchemy_utils import create_database, database_exists, drop_database
-from sqlalchemy_utils.compat import get_sqlalchemy_version
 
 pymysql = None
 try:
@@ -10,8 +9,6 @@
 except ImportError:
     pass
 
-sqlalchemy_version = get_sqlalchemy_version()
-
 
 class DatabaseTest:
     def test_create_and_drop(self, dsn):
@@ -101,7 +98,6 @@
         )
 
 
[email protected]('sqlalchemy_version < (2, 0, 0)')
 class TestDatabasePostgresPsycoPG3(DatabaseTest):
 
     @pytest.fixture
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sqlalchemy_utils-0.42.1/tests/generic_relationship/__init__.py 
new/sqlalchemy_utils-0.43.0/tests/generic_relationship/__init__.py
--- old/sqlalchemy_utils-0.42.1/tests/generic_relationship/__init__.py  
2024-03-14 18:37:59.000000000 +0100
+++ new/sqlalchemy_utils-0.43.0/tests/generic_relationship/__init__.py  
2026-10-09 17:35:51.231217400 +0200
@@ -1,3 +1,6 @@
+from sqlalchemy.orm import aliased
+
+
 class GenericRelationshipTestCase:
     def test_set_as_none(self, Event):
         event = Event()
@@ -100,3 +103,46 @@
         statement = Event.object.is_type(User)
         q = session.query(Event).filter(statement)
         assert q.first() is not None
+
+    def test_reassign_persistent_relationship(self, session, User, Event):
+        user1 = User()
+        user2 = User()
+        session.add_all([user1, user2])
+        session.commit()
+        event = Event(object=user1)
+        session.add(event)
+        session.commit()
+
+        event.object = user2
+        session.commit()
+        session.expunge(event)
+
+        event = session.query(Event).one()
+        assert event.object == user2
+
+    def test_compare_aliased_query(self, session, User, Event):
+        user = User()
+        session.add(user)
+        session.commit()
+        event = Event(object=user)
+        session.add(event)
+        session.commit()
+
+        event_alias = aliased(Event)
+        query = session.query(event_alias)
+        assert query.filter(event_alias.object == user).one() == event
+
+    def test_change_identifier(self, session, User, Event):
+        user1 = User()
+        user2 = User()
+        session.add_all([user1, user2])
+        session.commit()
+        event = Event(object=user1)
+        session.add(event)
+        session.commit()
+
+        event.object_id = user2.id
+        if hasattr(event, 'object_code'):
+            event.object_code = user2.code
+
+        assert event.object == user2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/tests/types/test_choice.py 
new/sqlalchemy_utils-0.43.0/tests/types/test_choice.py
--- old/sqlalchemy_utils-0.42.1/tests/types/test_choice.py      2025-08-30 
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/tests/types/test_choice.py      2026-08-06 
17:05:53.348252300 +0200
@@ -209,3 +209,66 @@
         session.commit()
 
         assert order_nullable.status is None
+
+
+class TestChoiceTypeFalsyCode:
+    """A list-of-tuples ``ChoiceType`` must coerce *every* non-NULL value
+    back into a :class:`Choice`, including false-y values like ``0``
+    or the empty string.
+    """
+
+    @pytest.fixture
+    def Item(self, Base):
+        class Item(Base):
+            STATUSES = [(0, 'inactive'), (1, 'active')]
+
+            __tablename__ = 'item'
+            id = sa.Column(sa.Integer, primary_key=True)
+            status = sa.Column(ChoiceType(STATUSES, impl=sa.Integer()))
+
+        return Item
+
+    @pytest.fixture
+    def init_models(self, Item):
+        pass
+
+    def test_result_value_wraps_falsy_int_code(self):
+        type_ = ChoiceType([(0, 'inactive'), (1, 'active')], impl=sa.Integer())
+        result = type_.process_result_value(0, None)
+        assert isinstance(result, Choice)
+        assert result.code == 0
+        assert result.value == 'inactive'
+
+    def test_result_value_wraps_empty_string_code(self):
+        type_ = ChoiceType([('', 'blank'), ('a', 'Letter A')])
+        result = type_.process_result_value('', None)
+        assert isinstance(result, Choice)
+        assert result.code == ''
+        assert result.value == 'blank'
+
+    def test_matches_enum_sibling_for_falsy_code(self):
+        class Status(Enum):
+            inactive = 0
+            active = 1
+
+        tuple_type = ChoiceType([(0, 'inactive'), (1, 'active')], 
impl=sa.Integer())
+        enum_type = ChoiceType(Status, impl=sa.Integer())
+
+        # Both siblings must wrap the falsy code, not leak the raw scalar.
+        assert type(tuple_type.process_result_value(0, None)) is Choice
+        assert enum_type.process_result_value(0, None) is Status.inactive
+        assert tuple_type.process_result_value(0, None) is not None
+
+    def test_falsy_code_roundtrips_through_database(self, session, Item):
+        item = Item()
+        item.status = 0
+
+        session.add(item)
+        session.commit()
+
+        item = session.query(Item).first()
+        assert isinstance(item.status, Choice)
+        assert type(item.status.code) is int  # must be an int, not a bool
+        assert item.status.code == 0
+        assert item.status.value == 'inactive'
+        assert item.status == 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/tests/types/test_json.py 
new/sqlalchemy_utils-0.43.0/tests/types/test_json.py
--- old/sqlalchemy_utils-0.42.1/tests/types/test_json.py        2025-08-30 
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/tests/types/test_json.py        2026-08-06 
17:05:53.348627000 +0200
@@ -82,10 +82,7 @@
     def test_unhashable_type(self, AAA, BBB, session):
         """Verify there are no TypeErrors with certain JSON queries.
 
-        This test will fail under these conditions:
-
-        1.  sqlalchemy versions 1.4.19 through 1.4.23 are installed.
-        2.  `JSONType.hashable` is not set to False.
+        This test will fail if `JSONType.hashable` is not set to False.
 
         For more info, see:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/tests/types/test_password.py 
new/sqlalchemy_utils-0.43.0/tests/types/test_password.py
--- old/sqlalchemy_utils-0.42.1/tests/types/test_password.py    2025-08-30 
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/tests/types/test_password.py    2026-08-06 
17:05:53.349164500 +0200
@@ -164,7 +164,7 @@
     def test_update_none(self, session, User):
         """
         Should be able to change a password from ``None`` to a valid
-        password.
+        password, and the change must be persisted to the database.
         """
 
         obj = User()
@@ -178,6 +178,36 @@
 
         session.commit()
 
+        # Re-read from the database to confirm the None -> 'b' update was
+        # actually persisted and not dropped by change detection.
+        session.expire_all()
+        obj = session.get(User, obj.id)
+
+        assert obj.password is not None
+        assert obj.password == b'b'
+        assert obj.password != 'a'
+
+    def test_pending_password_not_equal_to_none(self):
+        """
+        A pending (coerced but not-yet-hashed) password holds a real secret
+        and must not compare equal to ``None``.
+
+        If it did, SQLAlchemy's scalar change detection would treat setting a
+        value on a previously-``NULL`` column as "unchanged" and skip the
+        ``UPDATE``, silently dropping the new password.
+        """
+
+        pending = Password('b', secret=True)
+
+        assert pending.hash is None
+        assert pending.secret == 'b'
+        assert not (pending == None)  # noqa: E711
+        assert pending != None  # noqa: E711
+
+        # A genuinely empty password still equals ``None``.
+        empty = Password(None)
+        assert empty == None  # noqa: E711
+
     def test_compare_none(self, User):
         """
         Should be able to compare a password of ``None``.

Reply via email to