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 2022-04-17 23:50:41
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-peewee (Old)
 and      /work/SRC/openSUSE:Factory/.python-peewee.new.1941 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-peewee"

Sun Apr 17 23:50:41 2022 rev:17 rq:970474 version:3.14.10

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-peewee/python-peewee.changes      
2021-11-13 22:49:14.389282814 +0100
+++ /work/SRC/openSUSE:Factory/.python-peewee.new.1941/python-peewee.changes    
2022-04-17 23:52:11.106499863 +0200
@@ -1,0 +2,25 @@
+Sat Apr 16 15:24:55 UTC 2022 - Matej Cepl <[email protected]>
+
+- Update to 3.14.10:
+  - Add shortcut for conditional insert using sub-select
+  - Add convenience left_outer_join() method to query.
+  - Add selected_columns property to Select queries.
+  - Add name property to Alias instances.
+  - Fix regression in tests introduced by change to DataSet in
+    3.14.9.
+  - Allow calling table_exists() with a model-class, refs
+  - Improve is_connection_usable() method of MySQLDatabase class.
+  - Better support for VIEWs with playhouse.dataset.DataSet and
+    sqlite-web.
+  - Support INSERT / ON CONFLICT in playhosue.kv for newer
+    Sqlite.
+  - Add ArrayField.contained_by() method, a corollary to
+    contains() and the contains_any() methods.
+  - Support cyclical foreign-key relationships in
+    reflection/introspection, and also for sqlite-web.
+  - Add magic methods for FTS5 field to optimize, rebuild and
+    integrity check the full-text index.
+  - Add fallbacks in setup.py in the event distutils is not
+    available.
+
+-------------------------------------------------------------------

Old:
----
  peewee-3.14.8.tar.gz

New:
----
  peewee-3.14.10.tar.gz

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

Other differences:
------------------
++++++ python-peewee.spec ++++++
--- /var/tmp/diff_new_pack.g74Jyp/_old  2022-04-17 23:52:11.494500394 +0200
+++ /var/tmp/diff_new_pack.g74Jyp/_new  2022-04-17 23:52:11.498500399 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-peewee
 #
-# Copyright (c) 2021 SUSE LLC
+# Copyright (c) 2022 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-peewee
-Version:        3.14.8
+Version:        3.14.10
 Release:        0
 Summary:        An expressive ORM that supports multiple SQL backends
 License:        BSD-3-Clause

++++++ peewee-3.14.8.tar.gz -> peewee-3.14.10.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/.readthedocs.yaml 
new/peewee-3.14.10/.readthedocs.yaml
--- old/peewee-3.14.8/.readthedocs.yaml 1970-01-01 01:00:00.000000000 +0100
+++ new/peewee-3.14.10/.readthedocs.yaml        2022-03-07 18:18:46.000000000 
+0100
@@ -0,0 +1,4 @@
+version: 2
+python:
+  install:
+  - requirements: docs/requirements.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/CHANGELOG.md 
new/peewee-3.14.10/CHANGELOG.md
--- old/peewee-3.14.8/CHANGELOG.md      2021-10-28 16:29:02.000000000 +0200
+++ new/peewee-3.14.10/CHANGELOG.md     2022-03-07 18:18:46.000000000 +0100
@@ -7,7 +7,38 @@
 
 ## master
 
-[View commits](https://github.com/coleifer/peewee/compare/3.14.8...master)
+* Add `shortcuts.insert_where()` helper for generating conditional INSERT with
+  a bit less boilerplate.
+* Fix bug in `test_utils.count_queres()` which could erroneously include pool
+  events such as connect/disconnect, etc.
+
+[View commits](https://github.com/coleifer/peewee/compare/3.14.10...master)
+
+## 3.14.10
+
+* Add shortcut for conditional insert using sub-select, see #2528
+* Add convenience `left_outer_join()` method to query.
+* Add `selected_columns` property to Select queries.
+* Add `name` property to Alias instances.
+* Fix regression in tests introduced by change to DataSet in 3.14.9.
+
+[View commits](https://github.com/coleifer/peewee/compare/3.14.9...3.14.10)
+
+## 3.14.9
+
+* Allow calling `table_exists()` with a model-class, refs
+* Improve `is_connection_usable()` method of `MySQLDatabase` class.
+* Better support for VIEWs with `playhouse.dataset.DataSet` and sqlite-web.
+* Support INSERT / ON CONFLICT in `playhosue.kv` for newer Sqlite.
+* Add `ArrayField.contained_by()` method, a corollary to `contains()` and
+  the `contains_any()` methods.
+* Support cyclical foreign-key relationships in reflection/introspection, and
+  also for sqlite-web.
+* Add magic methods for FTS5 field to optimize, rebuild and integrity check the
+  full-text index.
+* Add fallbacks in `setup.py` in the event distutils is not available.
+
+[View commits](https://github.com/coleifer/peewee/compare/3.14.8...3.14.9)
 
 ## 3.14.8
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/docs/peewee/crdb.rst 
new/peewee-3.14.10/docs/peewee/crdb.rst
--- old/peewee-3.14.8/docs/peewee/crdb.rst      2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/docs/peewee/crdb.rst     2022-03-07 18:18:46.000000000 
+0100
@@ -21,6 +21,40 @@
 
 .. note:: CockroachDB requires the ``psycopg2`` (postgres) Python driver.
 
+.. note::
+    CockroachDB installation and getting-started guide can be
+    found here: 
https://www.cockroachlabs.com/docs/stable/install-cockroachdb.html
+
+
+.. _crdb_ssl:
+
+SSL Configuration
+^^^^^^^^^^^^^^^^^
+
+SSL certificates are strongly recommended when running a Cockroach cluster.
+Psycopg2 supports SSL out-of-the-box, but you may need to specify some
+additional options when initializing your database:
+
+.. code-block:: python
+
+    db = CockroachDatabase(
+        'my_app',
+        user='root',
+        host='10.1.0.8',
+        sslmode='verify-full',  # Verify the cert common-name.
+        sslrootcert='/path/to/root.crt')
+
+
+    # Or, alternatively, specified as part of a connection-string:
+    db = CockroachDatabase('postgresql://root:secret@host:26257/dbname'
+                           '?sslmode=verify-full&sslrootcert=/path/to/root.crt'
+                           '&options=--cluster=my-cluster-xyz')
+
+More details about client verification can be found on the `libpq docs 
<https://www.postgresql.org/docs/9.1/libpq-ssl.html>`_.
+
+Cockroach Extension APIs
+^^^^^^^^^^^^^^^^^^^^^^^^
+
 The ``playhouse.cockroachdb`` extension module provides the following classes
 and helpers:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/docs/peewee/database.rst 
new/peewee-3.14.10/docs/peewee/database.rst
--- old/peewee-3.14.8/docs/peewee/database.rst  2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/docs/peewee/database.rst 2022-03-07 18:18:46.000000000 
+0100
@@ -206,6 +206,10 @@
 
 .. note:: CockroachDB requires the ``psycopg2`` (postgres) Python driver.
 
+.. note::
+    CockroachDB installation and getting-started guide can be
+    found here: 
https://www.cockroachlabs.com/docs/stable/install-cockroachdb.html
+
 CRDB provides client-side transaction retries, which are available using a
 special :py:meth:`CockroachDatabase.run_transaction` helper-method. This method
 accepts a callable, which is responsible for executing any transactional
@@ -234,6 +238,7 @@
 For more information, see:
 
 * :ref:`CRDB extension documentation <crdb>`
+* :ref:`SSL configuration with CockroachDB <crdb_ssl>`
 * :ref:`Arrays <pgarrays>` (postgres-specific, but applies to CRDB)
 * :ref:`JSON <pgjson>` (postgres-specific, but applies to CRDB)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/docs/peewee/playhouse.rst 
new/peewee-3.14.10/docs/peewee/playhouse.rst
--- old/peewee-3.14.8/docs/peewee/playhouse.rst 2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/docs/peewee/playhouse.rst        2022-03-07 
18:18:46.000000000 +0100
@@ -568,14 +568,16 @@
 Sqlcipher backend
 -----------------
 
-* Although this extention's code is short, it has not been properly
-  peer-reviewed yet and may have introduced vulnerabilities.
+.. note::
+    Although this extention's code is short, it has not been properly
+    peer-reviewed yet and may have introduced vulnerabilities.
 
-Also note that this code relies on pysqlcipher_ and sqlcipher_, and
-the code there might have vulnerabilities as well, but since these
+Also note that this code relies on sqlcipher3_ (python bindings) and 
sqlcipher_,
+and the code there might have vulnerabilities as well, but since these
 are widely used crypto modules, we can expect "short zero days" there.
 
-..  _pysqlcipher: https://pypi.python.org/pypi/pysqlcipher3
+..  _sqlcipher3: https://pypi.python.org/pypi/sqlcipher3
+..  _pysqlcipher3: https://pypi.python.org/pypi/pysqlcipher3
 ..  _sqlcipher: http://sqlcipher.net
 
 sqlcipher_ext API notes
@@ -584,7 +586,7 @@
 .. py:class:: SqlCipherDatabase(database, passphrase, **kwargs)
 
     Subclass of :py:class:`SqliteDatabase` that stores the database
-    encrypted. Instead of the standard ``sqlite3`` backend, it uses 
pysqlcipher_:
+    encrypted. Instead of the standard ``sqlite3`` backend, it uses 
sqlcipher3_:
     a python wrapper for sqlcipher_, which -- in turn -- is an encrypted 
wrapper
     around ``sqlite3``, so the API is *identical* to 
:py:class:`SqliteDatabase`'s,
     except for object construction parameters:
@@ -2044,7 +2046,7 @@
 
 Hybrid attributes encapsulate functionality that operates at both the Python
 *and* SQL levels. The idea for hybrid attributes comes from a feature of the
-`same name in SQLAlchemy 
<http://docs.sqlalchemy.org/en/improve_toc/orm/extensions/hybrid.html>`_.
+`same name in SQLAlchemy 
<https://docs.sqlalchemy.org/en/14/orm/extensions/hybrid.html>`_.
 Consider the following example:
 
 .. code-block:: python
@@ -3644,6 +3646,10 @@
 
     DATABASE = 'postgresql://postgres:password@localhost:5432/my_database'
 
+    # If we want to exclude particular views from the automatic connection
+    # management, we list them this way:
+    FLASKDB_EXCLUDED_ROUTES = ('logout',)
+
     app = Flask(__name__)
     app.config.from_object(__name__)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/docs/peewee/querying.rst 
new/peewee-3.14.10/docs/peewee/querying.rst
--- old/peewee-3.14.8/docs/peewee/querying.rst  2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/docs/peewee/querying.rst 2022-03-07 18:18:46.000000000 
+0100
@@ -1979,4 +1979,4 @@
 Foreign Keys and Joins
 ----------------------
 
-This section have been moved into its own document: :ref:`relationships`.
+This section has been moved into its own document: :ref:`relationships`.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/docs/requirements.txt 
new/peewee-3.14.10/docs/requirements.txt
--- old/peewee-3.14.8/docs/requirements.txt     1970-01-01 01:00:00.000000000 
+0100
+++ new/peewee-3.14.10/docs/requirements.txt    2022-03-07 18:18:46.000000000 
+0100
@@ -0,0 +1 @@
+docutils<0.18
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/examples/analytics/app.py 
new/peewee-3.14.10/examples/analytics/app.py
--- old/peewee-3.14.8/examples/analytics/app.py 2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/examples/analytics/app.py        2022-03-07 
18:18:46.000000000 +0100
@@ -1,10 +1,9 @@
 """
 Example "Analytics" app. To start using this on your site, do the following:
 
-* Create a postgresql database with HStore support:
+* Create a postgresql database:
 
     createdb analytics
-    psql analytics -c "create extension hstore;"
 
 * Create an account for each domain you intend to collect analytics for, e.g.
 
@@ -29,7 +28,7 @@
 
 from flask import Flask, Response, abort, g, request
 from peewee import *
-from playhouse.postgres_ext import HStoreField, PostgresqlExtDatabase
+from playhouse.postgres_ext import BinaryJSONField, PostgresqlExtDatabase
 
 # Analytics settings.
 # 1px gif.
@@ -49,10 +48,7 @@
 app = Flask(__name__)
 app.config.from_object(__name__)
 
-database = PostgresqlExtDatabase(
-    DATABASE_NAME,
-    register_hstore=True,
-    user='postgres')
+database = PostgresqlExtDatabase(DATABASE_NAME, user='postgres')
 
 
 class BaseModel(Model):
@@ -76,8 +72,8 @@
     title = TextField(default='')
     ip = CharField(default='')
     referrer = TextField(default='')
-    headers = HStoreField()
-    params = HStoreField()
+    headers = BinaryJSONField()
+    params = BinaryJSONField()
 
     @classmethod
     def create_from_request(cls, account, request):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/examples/anomaly_detection.py 
new/peewee-3.14.10/examples/anomaly_detection.py
--- old/peewee-3.14.8/examples/anomaly_detection.py     1970-01-01 
01:00:00.000000000 +0100
+++ new/peewee-3.14.10/examples/anomaly_detection.py    2022-03-07 
18:18:46.000000000 +0100
@@ -0,0 +1,64 @@
+import math
+from peewee import *
+
+
+db = SqliteDatabase(':memory:')
+
+class Reg(Model):
+    key = TextField()
+    value = IntegerField()
+
+    class Meta:
+        database = db
+
+
+db.create_tables([Reg])
+
+# Create a user-defined aggregate function suitable for computing the standard
+# deviation of a series.
[email protected]('stddev')
+class StdDev(object):
+    def __init__(self):
+        self.n = 0
+        self.values = []
+
+    def step(self, value):
+        self.n += 1
+        self.values.append(value)
+
+    def finalize(self):
+        if self.n < 2:
+            return 0
+        mean = sum(self.values) / self.n
+        sqsum = sum((i - mean) ** 2 for i in self.values)
+        return math.sqrt(sqsum / (self.n - 1))
+
+
+values = [2, 3, 5, 2, 3, 12, 5, 3, 4, 1, 2, 1, -9, 3, 3, 5]
+
+Reg.create_table()
+Reg.insert_many([{'key': 'k%02d' % i, 'value': v}
+                 for i, v in enumerate(values)]).execute()
+
+# We'll calculate the mean and the standard deviation of the series in a common
+# table expression, which will then be used by our query to find rows whose
+# zscore exceeds a certain threshold.
+cte = (Reg
+       .select(fn.avg(Reg.value), fn.stddev(Reg.value))
+       .cte('stats', columns=('series_mean', 'series_stddev')))
+
+# The zscore is defined as the (value - mean) / stddev.
+zscore = (Reg.value - cte.c.series_mean) / cte.c.series_stddev
+
+# Find rows which fall outside of 2 standard deviations.
+threshold = 2
+query = (Reg
+         .select(Reg.key, Reg.value, zscore.alias('zscore'))
+         .from_(Reg, cte)
+         .where((zscore >= threshold) | (zscore <= -threshold))
+         .with_cte(cte))
+
+for row in query:
+    print(row.key, row.value, round(row.zscore, 2))
+
+db.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/examples/sqlite_fts_compression.py 
new/peewee-3.14.10/examples/sqlite_fts_compression.py
--- old/peewee-3.14.8/examples/sqlite_fts_compression.py        1970-01-01 
01:00:00.000000000 +0100
+++ new/peewee-3.14.10/examples/sqlite_fts_compression.py       2022-03-07 
18:18:46.000000000 +0100
@@ -0,0 +1,64 @@
+#
+# Small example demonstrating the use of zlib compression with the Sqlite
+# full-text search extension.
+#
+import zlib
+
+from peewee import *
+from playhouse.sqlite_ext import *
+
+
+db = SqliteExtDatabase(':memory:')
+
+class SearchIndex(FTSModel):
+    content = SearchField()
+
+    class Meta:
+        database = db
+
+
[email protected]('zlib_compress')
+def _zlib_compress(data):
+    if data is not None:
+        if isinstance(data, str):
+            data = data.encode('utf8')
+        return zlib.compress(data, 9)
+
[email protected]('zlib_decompress')
+def _zlib_decompress(data):
+    if data is not None:
+        return zlib.decompress(data)
+
+
+SearchIndex.create_table(
+    tokenize='porter',
+    compress='zlib_compress',
+    uncompress='zlib_decompress')
+
+phrases = [
+    'A faith is a necessity to a man. Woe to him who believes in nothing.',
+    ('All who call on God in true faith, earnestly from the heart, will '
+     'certainly be heard, and will receive what they have asked and desired.'),
+    ('Be faithful in small things because it is in them that your strength '
+     'lies.'),
+    ('Faith consists in believing when it is beyond the power of reason to '
+     'believe.'),
+    ('Faith has to do with things that are not seen and hope with things that '
+     'are not at hand.')]
+
+for phrase in phrases:
+    SearchIndex.create(content=phrase)
+
+# Use the simple ranking algorithm.
+query = SearchIndex.search('faith things', with_score=True)
+for row in query:
+    print(round(row.score, 2), row.content.decode('utf8'))
+
+print('---')
+
+# Use the Okapi-BM25 ranking algorithm.
+query = SearchIndex.search_bm25('believe', with_score=True)
+for row in query:
+    print(round(row.score, 2), row.content.decode('utf8'))
+
+db.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/peewee.py new/peewee-3.14.10/peewee.py
--- old/peewee-3.14.8/peewee.py 2021-10-28 16:29:02.000000000 +0200
+++ new/peewee-3.14.10/peewee.py        2022-03-07 18:18:46.000000000 +0100
@@ -70,7 +70,7 @@
         mysql = None
 
 
-__version__ = '3.14.8'
+__version__ = '3.14.10'
 __all__ = [
     'AnyField',
     'AsIs',
@@ -1306,6 +1306,13 @@
     def __hash__(self):
         return hash(self._alias)
 
+    @property
+    def name(self):
+        return self._alias
+    @name.setter
+    def name(self, value):
+        self._alias = value
+
     def alias(self, alias=None):
         if alias is None:
             return self.node
@@ -2314,6 +2321,13 @@
     def select_extend(self, *columns):
         self._returning = tuple(self._returning) + columns
 
+    @property
+    def selected_columns(self):
+        return self._returning
+    @selected_columns.setter
+    def selected_columns(self, value):
+        self._returning = value
+
     @Node.copy
     def from_(self, *sources):
         self._from_list = list(sources)
@@ -2325,6 +2339,9 @@
         item = self._from_list.pop()
         self._from_list.append(Join(item, dest, join_type, on))
 
+    def left_outer_join(self, dest, on=None):
+        return self.join(dest, JOIN.LEFT_OUTER, on)
+
     @Node.copy
     def group_by(self, *columns):
         grouping = []
@@ -3323,6 +3340,10 @@
                     yield obj
 
     def table_exists(self, table_name, schema=None):
+        if is_model(table_name):
+            model = table_name
+            table_name = model._meta.table_name
+            schema = model._meta.schema
         return table_name in self.get_tables(schema=schema)
 
     def get_tables(self, schema=None):
@@ -4032,6 +4053,18 @@
         warnings.warn('Unable to determine MySQL version: "%s"' % version)
         return (0, 0, 0)  # Unable to determine version!
 
+    def is_connection_usable(self):
+        if self._state.closed:
+            return False
+
+        conn = self._state.conn
+        if hasattr(conn, 'ping'):
+            try:
+                conn.ping(False)
+            except Exception:
+                return False
+        return True
+
     def default_values_insert(self, ctx):
         return ctx.literal('() VALUES ()')
 
@@ -6121,7 +6154,11 @@
         self.model._schema._database = database
         del self.table
 
-        # Apply any hooks that have been registered.
+        # Apply any hooks that have been registered. If we have an
+        # uninitialized proxy object, we will treat that as `None`.
+        if isinstance(database, Proxy) and database.obj is None:
+            database = None
+
         for hook in self._db_hooks:
             hook(database)
 
@@ -7172,6 +7209,9 @@
         item = self._from_list.pop()
         self._from_list.append(Join(item, dest, join_type, on))
 
+    def left_outer_join(self, dest, on=None, src=None, attr=None):
+        return self.join(dest, JOIN.LEFT_OUTER, on, src, attr)
+
     def join_from(self, src, dest, join_type=JOIN.INNER, on=None, attr=None):
         return self.join(dest, join_type, on, src, attr)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/playhouse/dataset.py 
new/peewee-3.14.10/playhouse/dataset.py
--- old/peewee-3.14.8/playhouse/dataset.py      2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/playhouse/dataset.py     2022-03-07 18:18:46.000000000 
+0100
@@ -27,7 +27,7 @@
 
 
 class DataSet(object):
-    def __init__(self, url, **kwargs):
+    def __init__(self, url, include_views=False, **kwargs):
         if isinstance(url, Database):
             self._url = None
             self._database = url
@@ -45,9 +45,11 @@
 
         # Introspect the database and generate models.
         self._introspector = Introspector.from_database(self._database)
+        self._include_views = include_views
         self._models = self._introspector.generate_models(
             skip_invalid=True,
             literal_column_names=True,
+            include_views=self._include_views,
             **kwargs)
         self._migrator = SchemaMigrator.from_database(self._database)
 
@@ -80,7 +82,14 @@
 
     @property
     def tables(self):
-        return self._database.get_tables()
+        tables = self._database.get_tables()
+        if self._include_views:
+            tables += self.views
+        return tables
+
+    @property
+    def views(self):
+        return [v.name for v in self._database.get_views()]
 
     def __contains__(self, table):
         return table in self.tables
@@ -107,7 +116,8 @@
         updated = self._introspector.generate_models(
             skip_invalid=True,
             table_names=dependencies,
-            literal_column_names=True)
+            literal_column_names=True,
+            include_views=self._include_views)
         self._models.update(updated)
 
     def get_table_dependencies(self, table):
@@ -135,10 +145,7 @@
         return self._database.execute_sql(sql, params, commit)
 
     def transaction(self):
-        if self._database.transaction_depth() == 0:
-            return self._database.transaction()
-        else:
-            return self._database.savepoint()
+        return self._database.atomic()
 
     def _check_arguments(self, filename, file_obj, format, format_dict):
         if filename and file_obj:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/playhouse/flask_utils.py 
new/peewee-3.14.10/playhouse/flask_utils.py
--- old/peewee-3.14.8/playhouse/flask_utils.py  2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/playhouse/flask_utils.py 2022-03-07 18:18:46.000000000 
+0100
@@ -85,11 +85,51 @@
     return default
 
 class FlaskDB(object):
-    def __init__(self, app=None, database=None, model_class=Model):
+    """
+    Convenience wrapper for configuring a Peewee database for use with a Flask
+    application. Provides a base `Model` class and registers handlers to manage
+    the database connection during the request/response cycle.
+
+    Usage::
+
+        from flask import Flask
+        from peewee import *
+        from playhouse.flask_utils import FlaskDB
+
+
+        # The database can be specified using a database URL, or you can pass a
+        # Peewee database instance directly:
+        DATABASE = 'postgresql:///my_app'
+        DATABASE = PostgresqlDatabase('my_app')
+
+        # If we do not want connection-management on any views, we can specify
+        # the view names using FLASKDB_EXCLUDED_ROUTES. The db connection will
+        # not be opened/closed automatically when these views are requested:
+        FLASKDB_EXCLUDED_ROUTES = ('logout',)
+
+        app = Flask(__name__)
+        app.config.from_object(__name__)
+
+        # Now we can configure our FlaskDB:
+        flask_db = FlaskDB(app)
+
+        # Or use the "deferred initialization" pattern:
+        flask_db = FlaskDB()
+        flask_db.init_app(app)
+
+        # The `flask_db` provides a base Model-class for easily binding models
+        # to the configured database:
+        class User(flask_db.Model):
+            email = CharField()
+
+    """
+    def __init__(self, app=None, database=None, model_class=Model,
+                 excluded_routes=None):
         self.database = None  # Reference to actual Peewee database instance.
         self.base_model_class = model_class
         self._app = app
         self._db = database  # dict, url, Database, or None (default).
+        self._excluded_routes = excluded_routes or ()
         if app is not None:
             self.init_app(app)
 
@@ -107,6 +147,9 @@
         else:
             initial_db = self._db
 
+        if 'FLASKDB_EXCLUDED_ROUTES' in app.config:
+            self._excluded_routes = app.config['FLASKDB_EXCLUDED_ROUTES']
+
         self._load_database(app, initial_db)
         self._register_handlers(app)
 
@@ -178,8 +221,12 @@
         return self._model_class
 
     def connect_db(self):
+        if self._excluded_routes and request.endpoint in self._excluded_routes:
+            return
         self.database.connect()
 
     def close_db(self, exc):
+        if self._excluded_routes and request.endpoint in self._excluded_routes:
+            return
         if not self.database.is_closed():
             self.database.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/playhouse/kv.py 
new/peewee-3.14.10/playhouse/kv.py
--- old/peewee-3.14.8/playhouse/kv.py   2021-10-28 16:29:02.000000000 +0200
+++ new/peewee-3.14.10/playhouse/kv.py  2022-03-07 18:18:46.000000000 +0100
@@ -1,6 +1,7 @@
 import operator
 
 from peewee import *
+from peewee import sqlite3
 from peewee import Expression
 from playhouse.fields import PickleField
 try:
@@ -37,7 +38,10 @@
         self._ordered = ordered
         self._database = database or SqliteExtDatabase(':memory:')
         self._table_name = table_name
-        if isinstance(self._database, PostgresqlDatabase):
+        support_on_conflict = (isinstance(self._database, PostgresqlDatabase) 
or
+                              (isinstance(self._database, SqliteDatabase) and
+                               self._database.server_version >= (3, 24)))
+        if support_on_conflict:
             self.upsert = self._postgres_upsert
             self.update = self._postgres_update
         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/playhouse/postgres_ext.py 
new/peewee-3.14.10/playhouse/postgres_ext.py
--- old/peewee-3.14.8/playhouse/postgres_ext.py 2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/playhouse/postgres_ext.py        2022-03-07 
18:18:46.000000000 +0100
@@ -42,6 +42,7 @@
 HKEY = '->'
 HUPDATE = '||'
 ACONTAINS = '@>'
+ACONTAINED_BY = '<@'
 ACONTAINS_ANY = '&&'
 TS_MATCH = '@@'
 JSONB_CONTAINS = '@>'
@@ -229,6 +230,9 @@
     def contains_any(self, *items):
         return Expression(self, ACONTAINS_ANY, ArrayValue(self, items))
 
+    def contained_by(self, *items):
+        return Expression(self, ACONTAINED_BY, ArrayValue(self, items))
+
 
 class ArrayValue(Node):
     def __init__(self, field, value):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/playhouse/reflection.py 
new/peewee-3.14.10/playhouse/reflection.py
--- old/peewee-3.14.8/playhouse/reflection.py   2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/playhouse/reflection.py  2022-03-07 18:18:46.000000000 
+0100
@@ -5,6 +5,7 @@
 from collections import namedtuple
 from inspect import isclass
 import re
+import warnings
 
 from peewee import *
 from peewee import _StringField
@@ -490,6 +491,10 @@
 
     @classmethod
     def from_database(cls, database, schema=None):
+        if isinstance(database, Proxy):
+            if database.obj is None:
+                raise ValueError('Cannot introspect an uninitialized Proxy.')
+            database = database.obj  # Reference the proxied db obj.
         if CockroachDatabase and isinstance(database, CockroachDatabase):
             metadata = CockroachDBMetadata(database)
         elif isinstance(database, PostgresqlDatabase):
@@ -680,12 +685,19 @@
                 database = self.metadata.database
                 schema = self.schema
 
+        pending = set()
+
         def _create_model(table, models):
+            pending.add(table)
             for foreign_key in database.foreign_keys[table]:
                 dest = foreign_key.dest_table
 
                 if dest not in models and dest != table:
-                    _create_model(dest, models)
+                    if dest in pending:
+                        warnings.warn('Possible reference cycle found between '
+                                      '%s and %s' % (table, dest))
+                    else:
+                        _create_model(dest, models)
 
             primary_keys = []
             columns = database.columns[table]
@@ -732,7 +744,11 @@
                         params['model'] = 'self'
                     else:
                         dest_table = column.foreign_key.dest_table
-                        params['model'] = models[dest_table]
+                        if dest_table in models:
+                            params['model'] = models[dest_table]
+                        else:
+                            FieldClass = DeferredForeignKey
+                            params['rel_model_name'] = dest_table
                     if column.to_field:
                         params['field'] = column.to_field
 
@@ -757,6 +773,9 @@
             except ValueError:
                 if not skip_invalid:
                     raise
+            finally:
+                if table in pending:
+                    pending.remove(table)
 
         # Actually generate Model classes.
         for table, model in sorted(database.model_names.items()):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/playhouse/shortcuts.py 
new/peewee-3.14.10/playhouse/shortcuts.py
--- old/peewee-3.14.8/playhouse/shortcuts.py    2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/playhouse/shortcuts.py   2022-03-07 18:18:46.000000000 
+0100
@@ -174,6 +174,40 @@
     return update_model_from_dict(model_class(), data, ignore_unknown)
 
 
+def insert_where(cls, data, where=None):
+    """
+    Helper for generating conditional INSERT queries.
+
+    For example, prevent INSERTing a new tweet if the user has tweeted within
+    the last hour::
+
+        INSERT INTO "tweet" ("user_id", "content", "timestamp")
+        SELECT 234, 'some content', now()
+        WHERE NOT EXISTS (
+            SELECT 1 FROM "tweet"
+            WHERE user_id = 234 AND timestamp > now() - interval '1 hour')
+
+    Using this helper:
+
+        cond = ~fn.EXISTS(Tweet.select().where(
+            Tweet.user == user_obj,
+            Tweet.timestamp > one_hour_ago))
+
+        iq = insert_where(Tweet, {
+            Tweet.user: user_obj,
+            Tweet.content: 'some content'}, where=cond)
+
+        res = iq.execute()
+    """
+    for field, default in cls._meta.defaults.items():
+        if field.name in data or field in data: continue
+        value = default() if callable_(default) else default
+        data[field] = value
+    fields, values = zip(*data.items())
+    sq = Select(columns=values).where(where)
+    return cls.insert_from(sq, fields)
+
+
 class ReconnectMixin(object):
     """
     Mixin class that attempts to automatically reconnect to the database under
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/playhouse/sqlite_ext.py 
new/peewee-3.14.10/playhouse/sqlite_ext.py
--- old/peewee-3.14.8/playhouse/sqlite_ext.py   2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/playhouse/sqlite_ext.py  2022-03-07 18:18:46.000000000 
+0100
@@ -468,6 +468,8 @@
              set(_alphabet.upper()) |
              set((chr(26),)))
 _invalid_ascii = set(chr(p) for p in range(128) if chr(p) not in _alphanum)
+del _alphabet
+del _alphanum
 _quote_re = re.compile(r'(?:[^\s"]|"(?:\\.|[^"])*")+')
 
 
@@ -697,6 +699,14 @@
         return cls._fts_cmd('merge', rank=npages)
 
     @classmethod
+    def optimize(cls):
+        return cls._fts_cmd('optimize')
+
+    @classmethod
+    def rebuild(cls):
+        return cls._fts_cmd('rebuild')
+
+    @classmethod
     def set_pgsz(cls, pgsz):
         return cls._fts_cmd('pgsz', rank=pgsz)
 
@@ -709,6 +719,10 @@
         return cls._fts_cmd('delete-all')
 
     @classmethod
+    def integrity_check(cls, rank=0):
+        return cls._fts_cmd('integrity-check', rank=rank)
+
+    @classmethod
     def VocabModel(cls, table_type='row', table=None):
         if table_type not in ('row', 'col', 'instance'):
             raise ValueError('table_type must be either "row", "col" or '
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/playhouse/test_utils.py 
new/peewee-3.14.10/playhouse/test_utils.py
--- old/peewee-3.14.8/playhouse/test_utils.py   2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/playhouse/test_utils.py  2022-03-07 18:18:46.000000000 
+0100
@@ -11,7 +11,9 @@
         logging.Handler.__init__(self, *args, **kwargs)
 
     def emit(self, record):
-        self.queries.append(record)
+        # Counts all entries logged to the "peewee" logger by execute_sql().
+        if record.name == 'peewee':
+            self.queries.append(record)
 
 
 class count_queries(object):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/setup.py new/peewee-3.14.10/setup.py
--- old/peewee-3.14.8/setup.py  2021-10-28 16:29:02.000000000 +0200
+++ new/peewee-3.14.10/setup.py 2022-03-07 18:18:46.000000000 +0100
@@ -2,10 +2,16 @@
 import platform
 import sys
 import warnings
-from distutils.command.build_ext import build_ext
-from distutils.errors import CCompilerError
-from distutils.errors import DistutilsExecError
-from distutils.errors import DistutilsPlatformError
+try:
+    from distutils.command.build_ext import build_ext
+    from distutils.errors import CCompilerError
+    from distutils.errors import DistutilsExecError
+    from distutils.errors import DistutilsPlatformError
+except ImportError:
+    from setuptools._distutils.command.build_ext import build_ext
+    from setuptools._distutils.errors import CCompilerError
+    from setuptools._distutils.errors import DistutilsExecError
+    from setuptools._distutils.errors import DistutilsPlatformError
 
 from setuptools import setup
 from setuptools.extension import Extension
@@ -56,8 +62,12 @@
 def _have_sqlite_extension_support():
     import shutil
     import tempfile
-    from distutils.ccompiler import new_compiler
-    from distutils.sysconfig import customize_compiler
+    try:
+        from distutils.ccompiler import new_compiler
+        from distutils.sysconfig import customize_compiler
+    except ImportError:
+        from setuptools.command.build_ext import customize_compiler
+        from setuptools.command.build_ext import new_compiler
 
     libraries = ['sqlite3']
     c_code = ('#include <sqlite3.h>\n\n'
@@ -151,6 +161,9 @@
         ],
         license='MIT License',
         platforms=['any'],
+        project_urls={
+            'Documentation': 'http://docs.peewee-orm.com',
+            'Source': 'https://github.com/coleifer/peewee'},
         scripts=['pwiz.py'],
         zip_safe=False,
         cmdclass={'build_ext': _PeeweeBuildExt},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/tests/dataset.py 
new/peewee-3.14.10/tests/dataset.py
--- old/peewee-3.14.8/tests/dataset.py  2021-10-28 16:29:02.000000000 +0200
+++ new/peewee-3.14.10/tests/dataset.py 2022-03-07 18:18:46.000000000 +0100
@@ -14,10 +14,11 @@
 from playhouse.dataset import DataSet
 from playhouse.dataset import Table
 
-from .base import db_loader
-from .base import skip_if
+from .base import IS_SQLITE_OLD
 from .base import ModelTestCase
 from .base import TestModel
+from .base import db_loader
+from .base import skip_if
 
 
 db = db_loader('sqlite')
@@ -30,6 +31,7 @@
     user = ForeignKeyField(User)
     content = TextField()
     timestamp = DateTimeField()
+    status = IntegerField(default=1)
 
 class Category(TestModel):
     name = TextField()
@@ -69,6 +71,38 @@
         users.insert(username='charlie')
         self.assertEqual(list(users), [{'id': 1, 'username': 'charlie'}])
 
+    @skip_if(IS_SQLITE_OLD)
+    def test_with_views(self):
+        self.dataset.query('CREATE VIEW notes_public AS '
+                           'SELECT content, timestamp FROM note '
+                           'WHERE status = 1 ORDER BY timestamp DESC')
+        try:
+            self.assertTrue('notes_public' in self.dataset.views)
+            self.assertFalse('notes_public' in self.dataset.tables)
+
+            users = self.dataset['user']
+            with self.dataset.transaction():
+                users.insert(username='charlie')
+                users.insert(username='huey')
+
+            notes = self.dataset['note']
+            for i, (ct, st) in enumerate([('n1', 1), ('n2', 2), ('n3', 1)]):
+                notes.insert(content=ct, status=st, user_id='charlie',
+                             timestamp=datetime.datetime(2022, 1, 1 + i))
+
+            self.assertFalse('notes_public' in self.dataset)
+
+            # Create a new dataset instance with views enabled.
+            dataset = DataSet(self.dataset._database, include_views=True)
+            self.assertTrue('notes_public' in dataset)
+            public = dataset['notes_public']
+            self.assertEqual(public.columns, ['content', 'timestamp'])
+            self.assertEqual(list(public), [
+                {'content': 'n3', 'timestamp': datetime.datetime(2022, 1, 3)},
+                {'content': 'n1', 'timestamp': datetime.datetime(2022, 1, 1)}])
+        finally:
+            self.dataset.query('DROP VIEW notes_public')
+
     def test_item_apis(self):
         dataset = DataSet('sqlite:///:memory:')
         users = dataset['users']
@@ -145,7 +179,8 @@
 
         note = self.dataset['note']
         columns = sorted(note.columns)
-        self.assertEqual(columns, ['content', 'id', 'timestamp', 'user_id'])
+        self.assertEqual(columns, ['content', 'id', 'status', 'timestamp',
+                                   'user_id'])
 
         category = self.dataset['category']
         columns = sorted(category.columns)
@@ -285,17 +320,20 @@
             note.insert(
                 content='note %s' % i,
                 timestamp=datetime.date(2014, 1, i),
+                status=i,
                 user_id='charlie')
 
         notes = sorted(note.all(), key=operator.itemgetter('id'))
         self.assertEqual(notes[0], {
             'content': 'note 1',
             'id': 1,
+            'status': 1,
             'timestamp': datetime.datetime(2014, 1, 1),
             'user_id': 'charlie'})
         self.assertEqual(notes[-1], {
             'content': 'note 3',
             'id': 3,
+            'status': 3,
             'timestamp': datetime.datetime(2014, 1, 3),
             'user_id': 'charlie'})
 
@@ -396,7 +434,8 @@
 
         note = self.dataset['note']
         note_ts = datetime.datetime(2017, 1, 2, 3, 4, 5)
-        note.insert(content='foo', timestamp=note_ts, user_id='charlie')
+        note.insert(content='foo', timestamp=note_ts, user_id='charlie',
+                    status=2)
 
         buf = StringIO()
         self.dataset.freeze(note.all(), 'json', file_obj=buf)
@@ -404,6 +443,7 @@
             'id': 1,
             'user_id': 'charlie',
             'content': 'foo',
+            'status': 2,
             'timestamp': '2017-01-02 03:04:05'}])
 
         note.delete(id=1)
@@ -415,6 +455,7 @@
             'id': 1,
             'user_id': 'charlie',
             'content': 'foo',
+            'status': 2,
             'timestamp': note_ts}])
 
     def test_table_column_creation(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/tests/db_tests.py 
new/peewee-3.14.10/tests/db_tests.py
--- old/peewee-3.14.8/tests/db_tests.py 2021-10-28 16:29:02.000000000 +0200
+++ new/peewee-3.14.10/tests/db_tests.py        2022-03-07 18:18:46.000000000 
+0100
@@ -502,6 +502,10 @@
         self.assertTrue(self.database.table_exists(User._meta.table_name))
         self.assertFalse(self.database.table_exists('nuggies'))
 
+        self.assertTrue(self.database.table_exists(User))
+        class X(TestModel): pass
+        self.assertFalse(self.database.table_exists(X))
+
     def test_get_tables(self):
         tables = self.database.get_tables()
         required = set(m._meta.table_name for m in self.requires)
@@ -730,6 +734,29 @@
         with_manual_commit()
         self.assertFalse(db.in_transaction())
 
+    def test_proxy_bind_ctx_callbacks(self):
+        db = Proxy()
+        class BaseModel(Model):
+            class Meta:
+                database = db
+
+        class Hook(BaseModel):
+            data = BlobField()  # Attaches hook to configure blob-type.
+
+        self.assertTrue(Hook.data._constructor is bytearray)
+
+        class CustomSqliteDB(SqliteDatabase):
+            sentinel = object()
+            def get_binary_type(self):
+                return self.sentinel
+
+        custom_db = CustomSqliteDB(':memory:')
+
+        with custom_db.bind_ctx([Hook]):
+            self.assertTrue(Hook.data._constructor is custom_db.sentinel)
+
+        self.assertTrue(Hook.data._constructor is bytearray)
+
 
 class Data(TestModel):
     key = TextField()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/tests/model_sql.py 
new/peewee-3.14.10/tests/model_sql.py
--- old/peewee-3.14.8/tests/model_sql.py        2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/tests/model_sql.py       2022-03-07 18:18:46.000000000 
+0100
@@ -1,6 +1,7 @@
 import datetime
 
 from peewee import *
+from peewee import Alias
 from peewee import Database
 from peewee import ModelIndex
 
@@ -59,6 +60,30 @@
         query = query2.select(Category.name)
         self.assertSQL(query, 'SELECT "t1"."name" FROM "category" AS "t1"', [])
 
+    def test_selected_columns(self):
+        query = (Person
+                 .select(
+                     Person.first,
+                     Person.last,
+                     fn.COUNT(Note.id).alias('ct'))
+                 .join(Note))
+        f_first, f_last, f_ct = query.selected_columns
+        self.assertEqual(f_first.name, 'first')
+        self.assertTrue(f_first.model is Person)
+        self.assertEqual(f_last.name, 'last')
+        self.assertTrue(f_last.model is Person)
+        self.assertTrue(isinstance(f_ct, Alias))
+        f_ct = f_ct.unwrap()
+        self.assertEqual(f_ct.name, 'COUNT')
+        f_nid, = f_ct.arguments
+        self.assertEqual(f_nid.name, 'id')
+        self.assertTrue(f_nid.model is Note)
+
+        query.selected_columns = (Person.first,)
+        f_first, = query.selected_columns
+        self.assertEqual(f_first.name, 'first')
+        self.assertTrue(f_first.model is Person)
+
     def test_where_coerce(self):
         query = Person.select(Person.last).where(Person.id == '1337')
         self.assertSQL(query, (
@@ -171,6 +196,19 @@
             'INNER JOIN "favorite" AS "t3" ON ("t3"."tweet_id" = "t1"."id")'),
             [])
 
+        query = 
Tweet.select(Tweet.id).left_outer_join(Favorite).switch(Tweet).left_outer_join(User)
+        self.assertSQL(query, (
+            'SELECT "t1"."id" FROM "tweet" AS "t1" '
+            'LEFT OUTER JOIN "favorite" AS "t2" ON ("t2"."tweet_id" = 
"t1"."id") '
+            'LEFT OUTER JOIN "users" AS "t3" ON ("t1"."user_id" = 
"t3"."id")'), [])
+
+        query = 
Tweet.select(Tweet.id).left_outer_join(User).switch(Tweet).left_outer_join(Favorite)
+        self.assertSQL(query, (
+            'SELECT "t1"."id" FROM "tweet" AS "t1" '
+            'LEFT OUTER JOIN "users" AS "t2" ON ("t1"."user_id" = "t2"."id") '
+            'LEFT OUTER JOIN "favorite" AS "t3" ON ("t3"."tweet_id" = 
"t1"."id")'),
+            [])
+
     def test_model_alias(self):
         TA = Tweet.alias()
         query = (User
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/tests/postgres.py 
new/peewee-3.14.10/tests/postgres.py
--- old/peewee-3.14.8/tests/postgres.py 2021-10-28 16:29:02.000000000 +0200
+++ new/peewee-3.14.10/tests/postgres.py        2022-03-07 18:18:46.000000000 
+0100
@@ -372,6 +372,9 @@
         assertAM(T.contains('omega', 'delta'))
         assertAM(T.contains('??????'), am5)
         assertAM(T.contains('alpha', 'delta'), am)
+        assertAM(T.contained_by('alpha', 'beta', 'delta'), am2, am3)
+        assertAM(T.contained_by('alpha', 'beta', 'gamma', 'delta'),
+                 am, am2, am3)
 
         # Check for any.
         assertAM(T.contains_any('beta'), am, am2)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/tests/reflection.py 
new/peewee-3.14.10/tests/reflection.py
--- old/peewee-3.14.8/tests/reflection.py       2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/tests/reflection.py      2022-03-07 18:18:46.000000000 
+0100
@@ -574,6 +574,37 @@
             ('n1', datetime.datetime(2018, 1, 1))])
 
 
+class TestCyclicalFK(BaseReflectionTestCase):
+    @requires_sqlite
+    def test_cyclical_fk(self):
+        # NOTE: this schema was provided by a user.
+        cursor = self.database.cursor()
+        cursor.executescript(
+            'CREATE TABLE flow_run_state (id CHAR(36) NOT NULL, '
+            'flow_run_id CHAR(36) NOT NULL, '
+           'CONSTRAINT pk_flow_run_state PRIMARY KEY (id), '
+           'CONSTRAINT fk_flow_run_state__flow_run_id__flow_run '
+            'FOREIGN KEY(flow_run_id) REFERENCES flow_run (id) '
+            'ON DELETE cascade); '
+            'CREATE TABLE flow_run (id CHAR(36) NOT NULL, '
+           'state_id CHAR(36) NOT NULL, '
+           'CONSTRAINT pk_flow_run PRIMARY KEY (id), '
+           'CONSTRAINT fk_flow_run__state_id__flow_run_state '
+            'FOREIGN KEY(state_id) REFERENCES flow_run_state (id) '
+            'ON DELETE SET NULL);')
+        M = self.introspector.generate_models()
+        FRS = M['flow_run_state']
+        FR = M['flow_run']
+        self.assertEqual(sorted(FR._meta.fields), ['id', 'state'])
+        self.assertEqual(sorted(FRS._meta.fields), ['flow_run', 'id'])
+        self.assertTrue(isinstance(FR.id, CharField))
+        self.assertTrue(isinstance(FR.state, ForeignKeyField))
+        self.assertTrue(FR.state.rel_model is FRS)
+        self.assertTrue(isinstance(FRS.id, CharField))
+        self.assertTrue(isinstance(FRS.flow_run, ForeignKeyField))
+        self.assertTrue(FRS.flow_run.rel_model is FR)
+
+
 class Event(TestModel):
     key = TextField()
     timestamp = DateTimeField(index=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/tests/regressions.py 
new/peewee-3.14.10/tests/regressions.py
--- old/peewee-3.14.8/tests/regressions.py      2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/tests/regressions.py     2022-03-07 18:18:46.000000000 
+0100
@@ -1256,11 +1256,15 @@
     id = UUIDField(primary_key=True, default=uuid.uuid4)
     key = TextField()
 
+class CharPKKV(TestModel):
+    id = CharField(primary_key=True)
+    key = TextField()
+    value = IntegerField(default=0)
 
-class TestBulkUpdateUUIDPK(ModelTestCase):
-    requires = [UUIDReg]
 
+class TestBulkUpdateNonIntegerPK(ModelTestCase):
     @skip_if(sys.version_info[0] == 2)
+    @requires_models(UUIDReg)
     def test_bulk_update_uuid_pk(self):
         r1 = UUIDReg.create(key='k1')
         r2 = UUIDReg.create(key='k2')
@@ -1272,6 +1276,22 @@
         self.assertEqual(r1_db.key, 'k1-x')
         self.assertEqual(r2_db.key, 'k2-x')
 
+    @requires_models(CharPKKV)
+    def test_bulk_update_non_integer_pk(self):
+        a, b, c = [CharPKKV.create(id=c, key='k%s' % c) for c in 'abc']
+        a.key = 'ka-x'
+        a.value = 1
+        b.value = 2
+        c.key = 'kc-x'
+        c.value = 3
+        CharPKKV.bulk_update((a, b, c), (CharPKKV.key, CharPKKV.value))
+
+        data = list(CharPKKV.select().order_by(CharPKKV.id).tuples())
+        self.assertEqual(data, [
+            ('a', 'ka-x', 1),
+            ('b', 'kb', 2),
+            ('c', 'kc-x', 3)])
+
 
 class TestSaveClearingPK(ModelTestCase):
     requires = [User, Tweet]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/tests/shortcuts.py 
new/peewee-3.14.10/tests/shortcuts.py
--- old/peewee-3.14.8/tests/shortcuts.py        2021-10-28 16:29:02.000000000 
+0200
+++ new/peewee-3.14.10/tests/shortcuts.py       2022-03-07 18:18:46.000000000 
+0100
@@ -737,3 +737,47 @@
 
         # In the main thread the original database has not been altered.
         self.assertTrue(M._meta.database is d1)
+
+
+class TIW(TestModel):
+    key = CharField()
+    value = IntegerField(default=0)
+    extra = IntegerField(default=lambda: 1)
+
+
+class TestInsertWhere(ModelTestCase):
+    requires = [User, Tweet, TIW]
+
+    def test_insert_where(self):
+        ua, ub = [User.create(username=n) for n in 'ab']
+
+        def _insert_where(user, content):
+            cond = (Tweet.select()
+                    .where(Tweet.user == user, Tweet.content == content))
+            where = ~fn.EXISTS(cond)
+            iq = insert_where(Tweet, {
+                Tweet.user: user,
+                Tweet.content: content},
+                where=where)
+            return 1 if iq.execute() else 0
+
+        self.assertEqual(_insert_where(ua, 't1'), 1)
+        self.assertEqual(_insert_where(ua, 't2'), 1)
+        self.assertEqual(_insert_where(ua, 't1'), 0)
+        self.assertEqual(_insert_where(ua, 't2'), 0)
+        self.assertEqual(_insert_where(ub, 't1'), 1)
+        self.assertEqual(_insert_where(ub, 't2'), 1)
+
+    def test_insert_where_defaults(self):
+        TIW.create(key='k1', value=1, extra=2)
+        def _insert_where(key):
+            where = ~fn.EXISTS(TIW.select().where(TIW.key == key))
+            iq = insert_where(TIW, {TIW.key: key}, where)
+            return 1 if iq.execute() else 0
+
+        self.assertEqual(_insert_where('k2'), 1)
+        self.assertEqual(_insert_where('k1'), 0)
+        self.assertEqual(_insert_where('k2'), 0)
+        tiw = TIW.get(TIW.key == 'k2')
+        self.assertEqual(tiw.value, 0)
+        self.assertEqual(tiw.extra, 1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/tests/sql.py 
new/peewee-3.14.10/tests/sql.py
--- old/peewee-3.14.8/tests/sql.py      2021-10-28 16:29:02.000000000 +0200
+++ new/peewee-3.14.10/tests/sql.py     2022-03-07 18:18:46.000000000 +0100
@@ -3,6 +3,7 @@
 
 from peewee import *
 from peewee import Expression
+from peewee import Function
 from peewee import query_to_string
 
 from .base import BaseTestCase
@@ -44,6 +45,29 @@
             'SELECT "t1"."username", "t1"."is_admin", "t1"."is_active", '
             '"t1"."id" FROM "users" AS "t1"'), [])
 
+    def test_selected_columns(self):
+        query = (User
+                 .select(User.c.id, User.c.username, fn.COUNT(Tweet.c.id))
+                 .join(Tweet, JOIN.LEFT_OUTER,
+                       on=(User.c.id == Tweet.c.user_id)))
+        # NOTE: because of operator overloads for equality we have to test by
+        # asserting the attributes of the selected cols.
+        c_id, c_username, c_ct = query.selected_columns
+        self.assertEqual(c_id.name, 'id')
+        self.assertTrue(c_id.source is User)
+        self.assertEqual(c_username.name, 'username')
+        self.assertTrue(c_username.source is User)
+        self.assertTrue(isinstance(c_ct, Function))
+        self.assertEqual(c_ct.name, 'COUNT')
+        c_tid, = c_ct.arguments
+        self.assertEqual(c_tid.name, 'id')
+        self.assertTrue(c_tid.source is Tweet)
+
+        query.selected_columns = (User.c.username,)
+        c_username, = query.selected_columns
+        self.assertEqual(c_username.name, 'username')
+        self.assertTrue(c_username.source is User)
+
     def test_select_explicit_columns(self):
         query = (Person
                  .select()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/peewee-3.14.8/tests/sqlite.py 
new/peewee-3.14.10/tests/sqlite.py
--- old/peewee-3.14.8/tests/sqlite.py   2021-10-28 16:29:02.000000000 +0200
+++ new/peewee-3.14.10/tests/sqlite.py  2022-03-07 18:18:46.000000000 +0100
@@ -1272,6 +1272,12 @@
             ['merge', 4])
         FTS5Test.merge(4)  # Runs without error.
 
+        FTS5Test.insert_many([{'title': 'k%08d' % i, 'data': 'v%08d' % i}
+                              for i in range(100)]).execute()
+
+        FTS5Test.integrity_check(rank=0)
+        FTS5Test.optimize()
+
     def test_create_table_options(self):
         class Test1(FTS5Model):
             f1 = SearchField()

Reply via email to