Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-peewee for openSUSE:Factory 
checked in at 2026-04-25 23:27:54
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-peewee (Old)
 and      /work/SRC/openSUSE:Factory/.python-peewee.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-peewee"

Sat Apr 25 23:27:54 2026 rev:40 rq:1349267 version:4.0.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-peewee/python-peewee.changes      
2026-04-09 16:22:38.397318291 +0200
+++ /work/SRC/openSUSE:Factory/.python-peewee.new.11940/python-peewee.changes   
2026-04-25 23:28:11.929531308 +0200
@@ -1,0 +2,12 @@
+Sat Apr 25 19:36:45 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 4.0.5:
+  * Fix bug where `db_value()` may not get called in subclasses
+    of Postgres JSONField / BinaryJSONField, refs #3044.
+  * Fix bug where indexes for table may be defined on multiple
+    schema, #3043.
+  * Always fall-through to base exception class if exception is
+    not recognized in DB drivers. This simplifies checking
+    driver-specific subclasses of standard DB-API exceptions.
+
+-------------------------------------------------------------------

Old:
----
  peewee-4.0.4.tar.gz

New:
----
  peewee-4.0.5.tar.gz

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

Other differences:
------------------
++++++ python-peewee.spec ++++++
--- /var/tmp/diff_new_pack.a2OQ4k/_old  2026-04-25 23:28:12.565557322 +0200
+++ /var/tmp/diff_new_pack.a2OQ4k/_new  2026-04-25 23:28:12.565557322 +0200
@@ -23,7 +23,7 @@
 %endif
 %{?sle15_python_module_pythons}
 Name:           python-peewee
-Version:        4.0.4
+Version:        4.0.5
 Release:        0
 Summary:        An expressive ORM that supports multiple SQL backends
 License:        MIT
@@ -34,6 +34,7 @@
 BuildRequires:  %{python_module devel}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module pytest}
+BuildRequires:  %{python_module setuptools}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  %{pythons}
 BuildRequires:  fdupes

++++++ peewee-4.0.4.tar.gz -> peewee-4.0.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/CHANGELOG.md 
new/peewee-4.0.5/CHANGELOG.md
--- old/peewee-4.0.4/CHANGELOG.md       2026-04-02 15:50:42.000000000 +0200
+++ new/peewee-4.0.5/CHANGELOG.md       2026-04-23 23:15:53.000000000 +0200
@@ -7,7 +7,18 @@
 
 ## master
 
-[View commits](https://github.com/coleifer/peewee/compare/4.0.4...master)
+[View commits](https://github.com/coleifer/peewee/compare/4.0.5...master)
+
+## 4.0.5
+
+* Fix bug where `db_value()` may not get called in subclasses of Postgres
+  JSONField / BinaryJSONField, refs #3044.
+* Fix bug where indexes for table may be defined on multiple schema, #3043.
+* Always fall-through to base exception class if exception is not recognized in
+  DB drivers. This simplifies checking driver-specific subclasses of standard
+  DB-API exceptions.
+
+[View commits](https://github.com/coleifer/peewee/compare/4.0.4...4.0.5)
 
 ## 4.0.4
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/docs/peewee/api.rst 
new/peewee-4.0.5/docs/peewee/api.rst
--- old/peewee-4.0.4/docs/peewee/api.rst        2026-04-02 15:50:42.000000000 
+0200
+++ new/peewee-4.0.5/docs/peewee/api.rst        2026-04-23 23:15:53.000000000 
+0200
@@ -1210,6 +1210,113 @@
       Create a transaction context-manager, optionally using the specified
       isolation level (if unspecified, the server default will be used).
 
+
+.. class:: _atomic
+
+   Context-manager or decorator implementation for :meth:`Database.atomic`.
+   Uses a transaction at the outermost level and savepoints for nested calls.
+
+   See :ref:`transactions` for details on transaction management.
+
+   .. method:: __enter__()
+
+      Begin the transaction or savepoint.
+
+      :return: Either a :class:`_transaction` or :class:`_savepoint` instance,
+         depending on nesting.
+
+   .. method:: __exit__(exc_type, exc_val, exc_tb)
+
+      Commit the transaction or savepoint if exiting cleanly. If an unhandled
+      exception occurred, roll back.
+
+   .. method:: __call__(fn)
+
+      Decorate the wrapped function with a transaction or savepoint. Equivalent
+      to:
+
+      .. code-block::
+
+         @db.atomic()
+         def modify_data():
+             ...
+
+         def modify_data():
+             with db.atomic():
+                 ...
+
+
+.. class:: _transaction
+
+   The object yielded by :meth:`Database.transaction` and the outermost layer
+   of :meth:`Database.atomic`. Users do not instantiate ``_transaction`` 
directly.
+
+   A ``_transaction`` can be used as a context manager or as a decorator.
+   On clean exit, the wrapped block is committed; on an unhandled
+   exception, it is rolled back and the exception re-raised. Nested
+   ``_transaction`` blocks share the outermost transaction - inner
+   ``__enter__`` / ``__exit__`` pushes and pops a stack entry but does
+   not emit ``BEGIN`` / ``COMMIT`` at the database level. For true
+   nesting, use :meth:`Database.atomic`, which emits a savepoint for
+   inner blocks.
+
+   See :ref:`transactions` for details on transaction management.
+
+   .. method:: commit(begin=True)
+
+      :param bool begin: If ``True`` (default), automatically start a new
+          transaction after the commit completes.
+
+      Commit the current transaction. Subsequent statements inside the
+      wrapped block run in the new transaction unless ``begin=False``.
+
+   .. method:: rollback(begin=True)
+
+      :param bool begin: If ``True`` (default), automatically start a new
+          transaction after the rollback completes.
+
+      Roll back the current transaction. Subsequent statements inside
+      the wrapped block run in the new transaction unless
+      ``begin=False``.
+
+.. class:: _savepoint
+
+   The object yielded by :meth:`Database.savepoint` and inner layers of calls
+   to :meth:`Database.atomic`. Users do not instantiate ``_savepoint`` 
directly.
+
+   A ``_savepoint`` emits ``SAVEPOINT <id>`` on entry and either
+   ``RELEASE SAVEPOINT <id>`` (on clean exit) or ``ROLLBACK TO SAVEPOINT <id>``
+   (on exception). The savepoint name is generated automatically and is unique
+   per invocation. Savepoints can be nested arbitrarily, but must occur inside
+   an active transaction.
+
+   See :ref:`transactions` for details on transaction management.
+
+   .. method:: commit(begin=True)
+
+      :param bool begin: If ``True`` (default), automatically begin a
+          new savepoint after releasing.
+
+      Release the current savepoint. Subsequent statements run inside
+      the new savepoint unless ``begin=False``.
+
+   .. method:: rollback(begin=True)
+
+      :param bool begin: If ``True`` (default), automatically begin a
+          new savepoint after rollback.
+
+      Roll back to the current savepoint. Subsequent statements run
+      inside the new savepoint unless ``begin=False``.
+
+.. note::
+
+   Inside ``with db.atomic() as txn:``, the object bound to ``txn`` is
+   a :class:`_transaction` when ``atomic()`` is the outermost block, and
+   a :class:`_savepoint` when ``atomic()`` is nested inside another
+   transaction or savepoint. Both classes expose the same ``commit()`` /
+   ``rollback()`` interface, so code that calls ``txn.commit()`` or
+   ``txn.rollback()`` works identically in either case.
+
 .. _model-api:
 
 Model
@@ -2331,6 +2438,29 @@
          query = Note.select(fn.COUNT(Note.id).alias('count'))
          assert query.scalar(as_dict=True) == {'count': 123}
 
+   .. method:: scalars()
+
+      Return an iterator that yields scalar values from the first column of
+      each result row. This is the multi-row equivalent of
+      :py:meth:`~BaseQuery.scalar`.
+
+      Equivalent to:
+
+      .. code-block:: python
+
+         [row[0] for row in query.tuples()]
+
+      Example:
+
+      .. code-block:: python
+
+         query = Note.select(Note.timestamp).order_by(Note.timestamp)
+         all_timestamps = list(query.scalars())
+
+         # Can also be iterated directly:
+         for ts in Note.select(Note.timestamp).scalars():
+             print(ts)
+
    .. method:: count(clear_limit=False)
 
       :param bool clear_limit: Clear any LIMIT clause when counting.
@@ -2380,9 +2510,9 @@
          for row in query:
              print(row)  # {'username': 'Alice', 'tweet_count': 12}
 
-   .. method:: tuples(as_tuples=True)
+   .. method:: tuples(as_tuple=True)
 
-      :param bool as_tuples: Specify whether to return rows as tuples.
+      :param bool as_tuple: Specify whether to return rows as tuples.
 
       Return rows as tuples.
 
@@ -2574,7 +2704,9 @@
    .. method:: prefetch(*subqueries, prefetch_type=PREFETCH_TYPE.WHERE)
 
       :param subqueries: A list of :class:`Model` classes or select
-          queries to prefetch.
+          queries to prefetch. Each element may also be a ``(query, 
target_model)``
+          tuple: the ``target_model`` names which previously-listed model
+          should be joined through when more than one candidate exists.
       :param prefetch_type: Query type to use for the subqueries.
       :return: a list of models with selected relations prefetched.
 
@@ -2603,6 +2735,20 @@
       related models are selected, so that the related objects can be
       mapped correctly.
 
+      **Disambiguating multi-reference subqueries.** If a subquery relates to
+      more than one previously-fetched query (for example, a ``Favorite`` row
+      that has foreign keys to both ``User`` and ``Tweet``), use the
+      ``(subquery, target_model)`` tuple form to pin the relationship:
+
+      .. code-block:: python
+
+         users = User.select()
+         tweets = Tweet.select()
+         favorites = Favorite.select()
+
+         # Favorite will be attached to User (via Favorite.user), not to Tweet.
+         query = prefetch(users, tweets, (favorites, User))
+
 
 .. class:: DoesNotExist
 
@@ -3486,7 +3632,7 @@
       ['CS 101', 'CS151', 'English 101', 'English 151']
 
    To remove all relationships from a collection, you can use the
-   :meth:`~SelectQuery.clear` method. Let's say that English 101 is
+   :meth:`~ManyToManyQuery.clear` method. Let's say that English 101 is
    canceled, so we need to remove all the students from it:
 
    .. code-block:: pycon
@@ -3689,6 +3835,15 @@
 
       Execute DROP INDEX queries for the indexes defined for the model.
 
+   .. method:: drop_index(field=None, index=None, safe=True)
+
+      :param Field field: field index to drop (for single-column indexes).
+      :param ModelIndex index: index to drop (for indexes other than
+         single-column, e.g. multi-col, partial, etc).
+      :param bool safe: Specify IF EXISTS clause.
+
+      Execute DROP INDEX query for the index specified.
+
    .. method:: create_sequence(field)
 
       :param Field field: Field instance which specifies a sequence.
@@ -3751,6 +3906,30 @@
 
       Drop table for the model and associated indexes.
 
+   .. method:: create_table_as(table_name, query, safe=True, **meta)
+
+      :param str table_name: Name of the new table.
+      :param Select query: Query whose result set will populate the new
+          table.
+      :param bool safe: Specify IF NOT EXISTS clause.
+      :param meta: Additional table options forwarded to the context.
+
+      Execute ``CREATE TABLE ... AS SELECT ...`` for the given model. The
+      new table's schema and column names are derived from the SELECT
+      query's result set.
+
+   .. method:: create_sequences()
+
+      Create every sequence referenced by a field on the model. On
+      databases where :attr:`Database.sequences` is ``False`` this method
+      is a no-op. Called implicitly by :meth:`create_all`.
+
+   .. method:: drop_sequences()
+
+      Drop every sequence referenced by a field on the model. On
+      databases where :attr:`Database.sequences` is ``False`` this method
+      is a no-op. Called implicitly by :meth:`drop_all`.
+
 
 .. class:: Index(name, table, expressions, unique=False, safe=False, 
where=None, using=None, nulls_distinct=None)
 
@@ -4140,7 +4319,7 @@
          # FROM (VALUES (1, 'first'), (2, 'second')) AS v(idx, name)
 
 
-.. class:: CTE(name, query, recursive=False, columns=None)
+.. class:: CTE(name, query, recursive=False, columns=None, materialized=None)
 
    Represent a common-table-expression. For example queries, see :ref:`cte`.
 
@@ -4148,6 +4327,8 @@
    :param query: :class:`Select` query describing CTE.
    :param bool recursive: Whether the CTE is recursive.
    :param list columns: Explicit list of columns produced by CTE (optional).
+   :param bool materialized: Specify ``MATERIALIZED`` or ``NOT MATERIALIZED``
+       clause.
 
    .. method:: select_from(*columns)
 
@@ -4209,6 +4390,27 @@
       Indicate the alias that should be given to the specified column-like
       object.
 
+   .. method:: bind_to(dest)
+
+      :param dest: Model to bind this expression to when constructing the
+         model-graph. Destination must be among the JOINed tables.
+      :return: a :class:`BindTo` object encapsulating the binding.
+
+      Bind the current column/expression to a specific model.
+
+      Example:
+
+      .. code-block:: python
+
+         name = Case(User.username, [
+             ('u1', 'User One'),
+             ('u2', 'User Two')], 'Someone Else')
+         query = (Tweet
+                  .select(Tweet.content, name.alias('display').bind_to(User))
+                  .join(User))
+         for tweet in query:
+             print(tweet.content, tweet.user.display)
+
    .. method:: cast(as_type)
 
       :param str as_type: Type name to cast to.
@@ -4265,12 +4467,15 @@
       the new alias is ``None``, then the original column-like object is
       returned.
 
+.. class:: BindTo(node, dest)
+
+   Represent the binding of a given expression/value to a destination. Created
+   by :meth:`ColumnBase.bind_to`.
 
 .. class:: Negated(node)
 
    Represents a negated column-like object.
 
-
 .. class:: Value(value, converter=None, unpack=True)
 
    :param value: Python object or scalar value.
@@ -4334,7 +4539,7 @@
    Short-hand for instantiating an descending :class:`Ordering` object.
 
 
-.. class:: Expression(lhs, op, rhs, flat=True)
+.. class:: Expression(lhs, op, rhs, flat=False)
 
    :param lhs: Left-hand side.
    :param op: Operation.
@@ -4814,9 +5019,9 @@
 
       Return rows as dictionaries.
 
-   .. method:: tuples(as_tuples=True)
+   .. method:: tuples(as_tuple=True)
 
-      :param bool as_tuples: Specify whether to return rows as tuples.
+      :param bool as_tuple: Specify whether to return rows as tuples.
 
       Return rows as tuples.
 
@@ -5695,9 +5900,12 @@
 
    :param sq: Query to use as starting-point.
    :param subqueries: One or more models or :class:`ModelSelect` queries
-       to eagerly fetch.
+        to eagerly fetch. Each element may also be a ``(query, target_model)``
+        tuple: the ``target_model`` names which previously-listed model
+        should be joined through when more than one candidate exists.
    :param prefetch_type: Query type to use for the subqueries.
-   :return: a list of models with selected relations prefetched.
+   :return: a list of models with selected relations prefetched. When called
+       with no subqueries returns ``sq`` unmodified.
 
    Eagerly fetch related objects, allowing efficient querying of multiple
    tables when a 1-to-many relationship exists. The prefetch type changes how
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/docs/peewee/mysql.rst 
new/peewee-4.0.5/docs/peewee/mysql.rst
--- old/peewee-4.0.4/docs/peewee/mysql.rst      2026-04-02 15:50:42.000000000 
+0200
+++ new/peewee-4.0.5/docs/peewee/mysql.rst      2026-04-23 23:15:53.000000000 
+0200
@@ -46,8 +46,6 @@
 
 MySQL-specific helpers:
 
-.. module:: playhouse.mysql_ext:
-
 .. class:: JSONField()
 
    Extends :class:`TextField` with transparent JSON encoding/decoding.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/docs/peewee/relationships.rst 
new/peewee-4.0.5/docs/peewee/relationships.rst
--- old/peewee-4.0.4/docs/peewee/relationships.rst      2026-04-02 
15:50:42.000000000 +0200
+++ new/peewee-4.0.5/docs/peewee/relationships.rst      2026-04-23 
23:15:53.000000000 +0200
@@ -728,6 +728,8 @@
 Passing the field instance to ``on=`` tells Peewee which foreign key column to
 use for the join.
 
+.. _joining-without-fk:
+
 Joining without a foreign key
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -774,6 +776,89 @@
    Recursive queries over self-referential structures are covered in
    :ref:`cte` using recursive CTEs.
 
+.. _related-naming:
+
+Related-instance Names
+----------------------
+
+When Peewee reconstructs a model graph from a query, each related instance is
+attached to the parent object under an attribute. The attribute name follows
+a small set of conventions and can always be overridden explicitly.
+
+Default names
+^^^^^^^^^^^^^
+
+For a :class:`ForeignKeyField`:
+
+* **Forward direction**: the foreign-key field's own name. ``Tweet.user``
+  produces ``tweet.user``.
+* **Back-reference**: the ``backref`` argument to the foreign-key. If 
``backref``
+  is not given, the default is ``<lowercase_classname>_set``, e.g. 
``user.tweet_set``.
+  Pass ``backref='+'`` to suppress the back-reference entirely.
+
+For a :class:`ManyToManyField`, the default back-reference is the lowercase
+name of the declaring model with an ``s`` suffix. Declaring 
``ManyToManyField(User)``
+on a ``Note`` model produces ``user.notes``. Pass ``backref='...'`` to 
override,
+or ``backref='+'`` to suppress.
+
+For joins performed by :meth:`~ModelSelect.join`:
+
+* If the join follows a foreign key that Peewee can resolve, the forward
+  direction uses the FK field's name, and the backref direction uses the
+  destination model's ``_meta.name`` (typically the lowercase class name).
+* If the join has no resolvable foreign key (for example, joining on an
+  arbitrary expression, a :class:`Table`, or a subquery), you must supply
+  ``attr=`` to name the attachment point - see below.
+
+Overriding with ``attr=``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:meth:`~ModelSelect.join` accepts an ``attr`` keyword that overrides the
+attribute name used to attach the joined instance:
+
+.. code-block:: python
+
+   query = (Tweet
+            .select(Tweet, User)
+            .join(User, attr='author'))
+
+   for tweet in query:
+       print(tweet.author.username)  # Instead of tweet.user.
+
+``attr`` is required when joining to a :class:`Table`, a subquery, or any
+source for which Peewee cannot resolve a foreign key (see
+:ref:`joining without a foreign key <joining-without-fk>`). It is also
+required in the rare case that a join's inferred name would collide with
+the ``<fk>_id`` name of an aliased foreign key, in which case passing
+``attr`` equal to that ``<fk>_id`` name raises :exc:`ValueError`.
+
+Directing computed columns with ``bind_to``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When a :class:`SELECT` includes a computed or aliased column whose logical
+owner is one of the joined sources, use :meth:`~ColumnBase.bind_to` to tell
+Peewee which source the column belongs to. The column is then attached to
+the corresponding instance in the reconstructed graph:
+
+.. code-block:: python
+
+   name = Case(User.username, [
+       ('u1', 'User One'),
+       ('u2', 'User Two')], 'Someone Else')
+
+   query = (Tweet
+            .select(Tweet.content, name.alias('display').bind_to(User))
+            .join(User))
+
+   for tweet in query:
+       print(tweet.content, tweet.user.display)
+
+Without ``bind_to``, the computed ``display`` column would be bound to the
+``tweet`` instance. ``bind_to`` accepts a :class:`Model`, :class:`ModelAlias`,
+or any other source that is present in the query's FROM / JOIN list. If the
+target is not selected, peewee raises a :exc:`ValueError` when building the
+result set.
+
 .. _manytomany:
 
 Many-to-Many Relationships
@@ -941,6 +1026,16 @@
            print(f'{user.username}: {tweet.content} '
                  f'({len(tweet.favorites)} favorites)')
 
+When a subquery relates to more than one previously-listed query - for
+example, a ``Favorite`` that has foreign keys to both ``User`` and ``Tweet`` -
+pass a ``(query, target_model)`` tuple to disambiguate which relationship
+to follow:
+
+.. code-block:: python
+
+   # Fetch favorites via User, not via Tweet.
+   query = prefetch(users, tweets, (favorites, User))
+
 Filtering prefetched rows
 ^^^^^^^^^^^^^^^^^^^^^^^^^
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/docs/peewee/transactions.rst 
new/peewee-4.0.5/docs/peewee/transactions.rst
--- old/peewee-4.0.4/docs/peewee/transactions.rst       2026-04-02 
15:50:42.000000000 +0200
+++ new/peewee-4.0.5/docs/peewee/transactions.rst       2026-04-23 
23:15:53.000000000 +0200
@@ -63,8 +63,8 @@
 ------------------------
 
 You can commit or roll-back explicitly inside an :meth:`~Database.atomic`
-block. After calling :meth:`~Transaction.commit` or 
:meth:`~Transaction.rollback`
-a new transaction (or savepoint) begins automatically:
+block. After calling ``commit()`` or ``rollback()`` a new transaction (or
+savepoint) begins automatically:
 
 .. code-block:: python
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/peewee.py new/peewee-4.0.5/peewee.py
--- old/peewee-4.0.4/peewee.py  2026-04-02 15:50:42.000000000 +0200
+++ new/peewee-4.0.5/peewee.py  2026-04-23 23:15:53.000000000 +0200
@@ -44,21 +44,19 @@
     pass
 try:
     import psycopg2
-    from psycopg2 import errors as pg_errors
     from psycopg2 import extensions as pg_extensions
     from psycopg2.extras import register_uuid as pg_register_uuid
     from psycopg2.extras import Json as Json_pg2
     pg_register_uuid()
 except ImportError:
-    psycopg2 = pg_errors = Json_pg2 = None
+    psycopg2 = Json_pg2 = None
 try:
     import psycopg
-    from psycopg import errors as pg3_errors
     from psycopg.pq import TransactionStatus
     from psycopg.types.json import Json as Json_pg3
     from psycopg.types.json import Jsonb as Jsonb_pg3
 except ImportError:
-    psycopg = pg3_errors = Json_pg3 = Jsonb_pg3 = None
+    psycopg = Json_pg3 = Jsonb_pg3 = None
 
 mysql_passwd = False
 try:
@@ -71,7 +69,7 @@
         mysql = None
 
 
-__version__ = '4.0.4'
+__version__ = '4.0.5'
 __all__ = [
     'AnyField',
     'AsIs',
@@ -1123,13 +1121,15 @@
             raise ValueError('select_from() must specify one or more columns '
                              'from the CTE to select.')
 
-        query = (Select((self,), columns)
-                 .with_cte(self)
-                 .bind(self._query._database))
-        try:
-            query = query.objects(self._query.model)
-        except AttributeError:
-            pass
+        if hasattr(self._query, 'model'):
+            query = (self._query.model
+                     .select(*columns)
+                     .from_(self)
+                     .with_cte(self))
+        else:
+            query = (Select((self,), columns)
+                     .with_cte(self)
+                     .bind(self._query._database))
         return query
 
     def _get_hash(self):
@@ -3107,14 +3107,8 @@
     def __exit__(self, exc_type, exc_value, traceback):
         if exc_type is None:
             return
-        # psycopg shits out a million cute error types. Try to catch em all.
-        if pg_errors is not None and exc_type.__name__ not in self.exceptions \
-           and issubclass(exc_type, pg_errors.Error):
-            exc_type = exc_type.__bases__[0]
-        elif pg3_errors is not None and \
-           exc_type.__name__ not in self.exceptions \
-           and issubclass(exc_type, pg3_errors.Error):
-            exc_type = exc_type.__bases__[0]
+        if exc_type.__name__ not in self.exceptions:
+            exc_type = exc_type.__bases__[0]  # Try grabbing DB-API class.
         if exc_type.__name__ in self.exceptions:
             new_type = self.exceptions[exc_type.__name__]
             exc_args = exc_value.args
@@ -4220,11 +4214,14 @@
                     FROM generate_subscripts(idx.indkey, 1) AS k
                     ORDER BY k), ',')
             FROM pg_catalog.pg_class AS t
+            INNER JOIN pg_catalog.pg_namespace AS n ON t.relnamespace = n.oid
             INNER JOIN pg_catalog.pg_index AS idx ON t.oid = idx.indrelid
             INNER JOIN pg_catalog.pg_class AS i ON idx.indexrelid = i.oid
-            INNER JOIN pg_catalog.pg_indexes AS idxs ON
-                (idxs.tablename = t.relname AND idxs.indexname = i.relname)
-            WHERE t.relname = %s AND t.relkind = %s AND idxs.schemaname = %s
+            INNER JOIN pg_catalog.pg_indexes AS idxs ON (
+                idxs.tablename = t.relname
+                AND idxs.indexname = i.relname
+                AND idxs.schemaname = n.nspname)
+            WHERE t.relname = %s AND t.relkind = %s AND n.nspname = %s
             ORDER BY idx.indisunique DESC, i.relname;"""
         cursor = self.execute_sql(query, (table, 'r', schema or 'public'))
         return [IndexMetadata(name, sql.rstrip(' ;'), columns.split(','),
@@ -6022,7 +6019,6 @@
         return type(klass_name, (Model,), attrs)
 
     def get_through_model(self):
-        # XXX: Deprecated. Just use the "through_model" property.
         return self.through_model
 
 
@@ -6308,6 +6304,14 @@
                 .literal(statement)
                 .sql(index_name))
 
+    def drop_index(self, field=None, index=None, safe=True):
+        if field is None and index is None:
+            raise ValueError('field or index must be specified.')
+        elif field:
+            index = ModelIndex(self.model, (field,), unique=field.unique,
+                               using=field.index_type)
+        return self.database.execute(self._drop_index(index, safe=safe))
+
     def drop_indexes(self, safe=True):
         for query in self._drop_indexes(safe=safe):
             self.database.execute(query)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/playhouse/postgres_ext.py 
new/peewee-4.0.5/playhouse/postgres_ext.py
--- old/peewee-4.0.4/playhouse/postgres_ext.py  2026-04-02 15:50:42.000000000 
+0200
+++ new/peewee-4.0.5/playhouse/postgres_ext.py  2026-04-23 23:15:53.000000000 
+0200
@@ -367,7 +367,7 @@
         # untyped, so we need an explicit cast with psycopg2.
         if case and self.cast_json_case:
             return Cast(self.json_type(value), self._json_datatype)
-        return self.json_type(value)
+        return self.db_value(value)
 
     def __getitem__(self, value):
         return JsonLookup(self, [value])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/tests/fields.py 
new/peewee-4.0.5/tests/fields.py
--- old/peewee-4.0.4/tests/fields.py    2026-04-02 15:50:42.000000000 +0200
+++ new/peewee-4.0.5/tests/fields.py    2026-04-23 23:15:53.000000000 +0200
@@ -73,7 +73,7 @@
         self.assertEqual(i_db.value_null, 3)
 
 
-class TestDefaultValues(ModelTestCase):
+class TestDefaultValuesFields(ModelTestCase):
     requires = [DfltM]
 
     def test_default_values(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/tests/models.py 
new/peewee-4.0.5/tests/models.py
--- old/peewee-4.0.4/tests/models.py    2026-04-02 15:50:42.000000000 +0200
+++ new/peewee-4.0.5/tests/models.py    2026-04-23 23:15:53.000000000 +0200
@@ -27,6 +27,7 @@
 from .base import IS_MYSQL
 from .base import IS_MYSQL_ADVANCED_FEATURES
 from .base import IS_POSTGRESQL
+from .base import IS_PSYCOPG3
 from .base import IS_SQLITE
 from .base import IS_SQLITE_OLD
 from .base import IS_SQLITE_15  # Row-values.
@@ -7051,3 +7052,87 @@
         gc_idx = dep_models.index(DepGrandChild)
         c_idx = dep_models.index(DepChild)
         self.assertTrue(gc_idx < c_idx)
+
+
+class Customer(TestModel):
+    name = TextField()
+
+class Prod(TestModel):
+    name = TextField()
+    price = DecimalField(decimal_places=2)
+    class Meta:
+        table_name = 'product'
+
+class Order(TestModel):
+    customer = ForeignKeyField(Customer)
+    order_date = DateField()
+
+class OrderItem(TestModel):
+    order = ForeignKeyField(Order)
+    product = ForeignKeyField(Prod)
+    unit_price = DecimalField(decimal_places=2)
+    quantity = IntegerField(default=1)
+
+
+@skip_unless(IS_SQLITE_25 or IS_POSTGRESQL or IS_MYSQL_ADVANCED_FEATURES)
+class TestAnalyticalQueries(ModelTestCase):
+    requires = [Customer, Prod, Order, OrderItem]
+
+    def setUp(self):
+        super(TestAnalyticalQueries, self).setUp()
+        self.populate_sample_data()
+
+    def populate_sample_data(self):
+        c1 = Customer.create(name='c1')
+        c2 = Customer.create(name='c2')
+
+        p1 = Prod.create(name='p1', price=100)
+        p2 = Prod.create(name='p2', price=2)
+
+        def create_order(c, order_date, items):
+            o = Order.create(customer=c, order_date=order_date)
+            for p, q, up in items:
+                product = p1 if p == 'p1' else p2
+                OrderItem.create(order=o, product=product, quantity=q or 1,
+                                 unit_price=up or product.price)
+
+        create_order(c1, datetime.date(2026, 1, 1), [('p1', 1, None)])
+        create_order(c1, datetime.date(2026, 3, 1), [('p1', 2, 75)])
+        create_order(c1, datetime.date(2026, 4, 1), [
+            ('p1', 2, 100), ('p2', 1, 1)])
+        create_order(c1, datetime.date(2026, 4, 2), [('p1', 2, 100)])
+
+        create_order(c2, datetime.date(2026, 1, 31), [('p1', 1, None)])
+        create_order(c2, datetime.date(2026, 3, 31), [('p2', 1, None)])
+        create_order(c2, datetime.date(2026, 4, 29), [
+            ('p1', 5, 100), ('p2', 10, None)])
+        create_order(c2, datetime.date(2026, 4, 30), [('p1', 1, 100)])
+
+    @skip_if(IS_PSYCOPG3)
+    def test_monthly_revenue(self):
+        revenue = fn.SUM(OrderItem.quantity * OrderItem.unit_price)
+        monthly = (Order
+                   .select(
+                       Order.order_date.truncate('month').alias('month'),
+                       revenue.alias('revenue'))
+                   .join(OrderItem, JOIN.LEFT_OUTER)
+                   .group_by(Order.order_date.truncate('month'))
+                   .cte('monthly_revenue'))
+
+        window = Window(order_by=[monthly.c.month])
+        query = (monthly
+                 .select_from(
+                     monthly.c.month.converter(Order.order_date.python_value),
+                     monthly.c.revenue,
+                     fn.SUM(monthly.c.revenue).over(window).alias('cum_rev'),
+                     (monthly.c.revenue -
+                      fn.LAG(monthly.c.revenue).over(window)).alias('mom_chg'))
+                 .window(window)
+                 .order_by(monthly.c.month))
+
+        data = list(query.tuples())
+        self.assertEqual(data, [
+            (datetime.date(2026, 1, 1), 200, 200, None),
+            (datetime.date(2026, 3, 1), 152, 352, -48),
+            (datetime.date(2026, 4, 1), 1021, 1373, 869),
+        ])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/tests/postgres.py 
new/peewee-4.0.5/tests/postgres.py
--- old/peewee-4.0.4/tests/postgres.py  2026-04-02 15:50:42.000000000 +0200
+++ new/peewee-4.0.5/tests/postgres.py  2026-04-23 23:15:53.000000000 +0200
@@ -718,6 +718,26 @@
         self.assertRaises(ProgrammingError, fails)
 
 
+class Point(object):
+    def __init__(self, x, y):
+        self.x, self.y = x, y
+    def __eq__(self, other):
+        return (self.x, self.y) == (other.x, other.y)
+
+class CustomJsonField(BinaryJSONField):
+    def db_value(self, value):
+        if isinstance(value, Point):
+            value = {'x': value.x, 'y': value.y}
+        return super(CustomJsonField, self).db_value(value)
+    def python_value(self, value):
+        if value is not None:
+            return Point(**value)
+
+class CJM(TestModel):
+    name = TextField()
+    point = CustomJsonField()
+
+
 class TestJsonFieldRegressions(ModelTestCase):
     database = db
     requires = [JData]
@@ -743,6 +763,29 @@
         self.assertEqual(JD.d1.index_type, 'GIN')
         self.assertFalse(JD.d2.index)
 
+    @requires_models(CJM)
+    def test_json_field_subclass(self):
+        c1 = CJM.create(name='c1', point=Point(1, 2))
+        c2 = CJM.insert(name='c2', point=Point(2, 3)).execute()
+
+        c1_db = CJM.get(CJM.name == 'c1')
+        c2_db = CJM.get(CJM.name == 'c2')
+        self.assertEqual(c1_db.point, Point(1, 2))
+        self.assertEqual(c2_db.point, Point(2, 3))
+
+        c2_db = CJM.select().where(CJM.point == Point(2, 3)).get()
+        self.assertEqual(c2_db.name, 'c2')
+
+        CJM.update(point=Point(3, 4)).where(CJM.point == Point(1, 2)).execute()
+        c1, c2 = CJM.select().order_by(CJM.name)
+        self.assertEqual(c1.point, Point(3, 4))
+        self.assertEqual(c2.point, Point(2, 3))
+
+        c1.point = Point(1.2, 2.5)
+        c1.save()
+        c1_db = CJM.get(CJM.name == 'c1')
+        self.assertEqual(c1_db.point, Point(1.2, 2.5))
+
 
 class TestJSONFieldCustomDumps(ModelTestCase):
     database = db
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/tests/schema.py 
new/peewee-4.0.5/tests/schema.py
--- old/peewee-4.0.4/tests/schema.py    2026-04-02 15:50:42.000000000 +0200
+++ new/peewee-4.0.5/tests/schema.py    2026-04-23 23:15:53.000000000 +0200
@@ -15,12 +15,13 @@
 from peewee import NodeList
 
 from .base import BaseTestCase
-from .base import get_in_memory_db
 from .base import IS_CRDB
 from .base import IS_SQLITE
 from .base import ModelDatabaseTestCase
 from .base import ModelTestCase
 from .base import TestModel
+from .base import get_in_memory_db
+from .base import requires_postgresql
 from .base_models import Category
 from .base_models import Note
 from .base_models import Person
@@ -1237,3 +1238,36 @@
 
         TempModel._schema.drop_all()
         self.assertFalse(self.database.table_exists('temp_model'))
+
+@requires_postgresql
+class TestSchemaGetIndexes(ModelTestCase):
+    def setUp(self):
+        super(TestSchemaGetIndexes, self).setUp()
+        queries = [
+            'create schema s1', 'create schema s2',
+            'create table s1.t (c1 integer, c2 integer, c3 integer)',
+            'create table s2.t (c1 integer, c2 integer, c3 integer)',
+            'create index i1 on s1.t (c1, c2)',
+            'create index i1 on s2.t (c1, c2)',
+            'create index i2 on s1.t (c1)',
+        ]
+        with self.database:
+            for query in queries:
+                self.database.execute_sql(query)
+
+    def tearDown(self):
+        with self.database:
+            self.database.execute_sql('drop schema s1 cascade')
+            self.database.execute_sql('drop schema s2 cascade')
+        super(TestSchemaGetIndexes, self).setUp()
+
+    def test_schema_get_indexes(self):
+        tables = self.database.get_tables(schema='s1')
+        self.assertEqual(tables, ['t'])
+        idxs = self.database.get_indexes('t', schema='s1')
+        self.assertEqual([(i.name, i.columns) for i in idxs],
+                         [('i1', ['c1', 'c2']), ('i2', ['c1'])])
+
+        idxs = self.database.get_indexes('t', schema='s2')
+        self.assertEqual([(i.name, i.columns) for i in idxs],
+                         [('i1', ['c1', 'c2'])])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-4.0.4/tests/sql.py 
new/peewee-4.0.5/tests/sql.py
--- old/peewee-4.0.4/tests/sql.py       2026-04-02 15:50:42.000000000 +0200
+++ new/peewee-4.0.5/tests/sql.py       2026-04-23 23:15:53.000000000 +0200
@@ -2573,7 +2573,7 @@
 class TestOnConflictSqlite(BaseTestCase):
     database = SqliteDatabase(None)
 
-    def test_replace(self):
+    def test_replace_sqlite(self):
         query = Person.insert(name='huey').on_conflict('replace')
         self.assertSQL(query, (
             'INSERT OR REPLACE INTO "person" ("name") VALUES (?)'), ['huey'])
@@ -2598,7 +2598,7 @@
         super(TestOnConflictMySQL, self).setUp()
         self.database.server_version = None
 
-    def test_replace(self):
+    def test_replace_mysql(self):
         query = Person.insert(name='huey').on_conflict('replace')
         self.assertSQL(query, (
             'REPLACE INTO "person" ("name") VALUES (?)'), ['huey'])

Reply via email to