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-12-03 10:04:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-peewee (Old) and /work/SRC/openSUSE:Factory/.python-peewee.new.1835 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-peewee" Sat Dec 3 10:04:00 2022 rev:20 rq:1039750 version:3.15.4 Changes: -------- --- /work/SRC/openSUSE:Factory/python-peewee/python-peewee.changes 2022-11-01 13:42:27.827862749 +0100 +++ /work/SRC/openSUSE:Factory/.python-peewee.new.1835/python-peewee.changes 2022-12-03 10:04:14.619442717 +0100 @@ -1,0 +2,24 @@ +Fri Dec 2 21:43:51 UTC 2022 - Yogalakshmi Arunachalam <yarunacha...@suse.com> + +- Update to 3.15.4 + Raise an exception in ReconnectMixin if connection is lost while inside a transaction (if the transaction was interrupted presumably some changes were lost and explicit intervention is needed). + Add db.Model property to reduce boilerplate. + Add support for running prefetch() queries with joins instead of subqueries (this helps overcome a MySQL limitation about applying LIMITs to a subquery). + Add SQL AVG to whitelist to avoid coercing by default. + Allow arbitrary keywords in metaclass constructor, #2627 + Add a pyproject.toml to silence warnings from newer pips when wheel package is not available. + This release has a small helper for reducing boilerplate in some cases by exposing a base model class as an attribute of the database instance. + # old: + db = SqliteDatabase('...') + class BaseModel(Model): + class Meta: + database = db + class MyModel(BaseModel): + pass + + # new: + db = SqliteDatabase('...') + class MyModel(db.Model): + pass + +------------------------------------------------------------------- Old: ---- peewee-3.15.3.tar.gz New: ---- peewee-3.15.4.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-peewee.spec ++++++ --- /var/tmp/diff_new_pack.2sQj2x/_old 2022-12-03 10:04:15.015444918 +0100 +++ /var/tmp/diff_new_pack.2sQj2x/_new 2022-12-03 10:04:15.019444940 +0100 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-peewee -Version: 3.15.3 +Version: 3.15.4 Release: 0 Summary: An expressive ORM that supports multiple SQL backends License: BSD-3-Clause ++++++ peewee-3.15.3.tar.gz -> peewee-3.15.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/.github/workflows/tests.yaml new/peewee-3.15.4/.github/workflows/tests.yaml --- old/peewee-3.15.3/.github/workflows/tests.yaml 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/.github/workflows/tests.yaml 2022-11-11 15:32:56.000000000 +0100 @@ -34,6 +34,8 @@ include: - python-version: 3.6 peewee-backend: cockroachdb + - python-version: "3.10" + peewee-backend: cockroachdb steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 @@ -54,9 +56,9 @@ - name: crdb if: ${{ matrix.peewee-backend == 'cockroachdb' }} run: | - wget -qO- https://binaries.cockroachdb.com/cockroach-v20.1.1.linux-amd64.tgz | tar xz - ./cockroach-v20.1.1.linux-amd64/cockroach start-single-node --insecure --background - ./cockroach-v20.1.1.linux-amd64/cockroach sql --insecure -e 'create database peewee_test;' + wget -qO- https://binaries.cockroachdb.com/cockroach-v22.1.7.linux-amd64.tgz | tar xz + ./cockroach-v22.1.7.linux-amd64/cockroach start-single-node --insecure --background + ./cockroach-v22.1.7.linux-amd64/cockroach sql --insecure -e 'create database peewee_test;' - name: runtests ${{ matrix.peewee-backend }} - ${{ matrix.python-version }} env: PEEWEE_TEST_BACKEND: ${{ matrix.peewee-backend }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/CHANGELOG.md new/peewee-3.15.4/CHANGELOG.md --- old/peewee-3.15.3/CHANGELOG.md 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/CHANGELOG.md 2022-11-11 15:32:56.000000000 +0100 @@ -7,7 +7,43 @@ ## master -[View commits](https://github.com/coleifer/peewee/compare/3.15.3...master) +[View commits](https://github.com/coleifer/peewee/compare/3.15.4...master) + +## 3.15.4 + +* Raise an exception in `ReconnectMixin` if connection is lost while inside a + transaction (if the transaction was interrupted presumably some changes were + lost and explicit intervention is needed). +* Add `db.Model` property to reduce boilerplate. +* Add support for running `prefetch()` queries with joins instead of subqueries + (this helps overcome a MySQL limitation about applying LIMITs to a subquery). +* Add SQL `AVG` to whitelist to avoid coercing by default. +* Allow arbitrary keywords in metaclass constructor, #2627 +* Add a `pyproject.toml` to silence warnings from newer pips when `wheel` + package is not available. + +This release has a small helper for reducing boilerplate in some cases by +exposing a base model class as an attribute of the database instance. + +```python +# old: +db = SqliteDatabase('...') + +class BaseModel(Model): + class Meta: + database = db + +class MyModel(BaseModel): + pass + +# new: +db = SqliteDatabase('...') + +class MyModel(db.Model): + pass +``` + +[View commits](https://github.com/coleifer/peewee/compare/3.15.3...3.15.4) ## 3.15.3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/README.rst new/peewee-3.15.4/README.rst --- old/peewee-3.15.3/README.rst 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/README.rst 2022-11-11 15:32:56.000000000 +0100 @@ -104,7 +104,10 @@ .group_by(User) .order_by(tweet_ct.desc())) - # Do an atomic update + # Do an atomic update (for illustrative purposes only, imagine a simple + # table for tracking a "count" associated with each URL). We don't want to + # naively get the save in two separate steps since this is prone to race + # conditions. Counter.update(count=Counter.count + 1).where(Counter.url == request.url) Check out the `example twitter app <http://docs.peewee-orm.com/en/latest/peewee/example.html>`_. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/bench.py new/peewee-3.15.4/bench.py --- old/peewee-3.15.3/bench.py 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/bench.py 2022-11-11 15:32:56.000000000 +0100 @@ -117,6 +117,14 @@ for i in c.items: pass +@timed +def select_prefetch_join(i): + query = prefetch(Collection.select(), Item, + prefetch_type=PREFETCH_TYPE.JOIN) + for c in query: + for i in c.items: + pass + if __name__ == '__main__': db.create_tables([Register, Collection, Item]) @@ -138,4 +146,5 @@ select_related_dicts() select_related_dbapi_raw() select_prefetch() + select_prefetch_join() db.drop_tables([Register, Collection, Item]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/docs/peewee/api.rst new/peewee-3.15.4/docs/peewee/api.rst --- old/peewee-3.15.3/docs/peewee/api.rst 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/docs/peewee/api.rst 2022-11-11 15:32:56.000000000 +0100 @@ -4784,14 +4784,20 @@ Use Django-style filters to express a WHERE clause. - .. py:method:: prefetch(*subqueries) + .. py:method:: prefetch(*subqueries[, prefetch_type=PREFETCH_TYPE.WHERE]) :param subqueries: A list of :py:class:`Model` classes or select queries to prefetch. + :param prefetch_type: Query type to use for the subqueries. :returns: a list of models with selected relations prefetched. Execute the query, prefetching the given additional resources. + Prefetch type may be one of: + + * ``PREFETCH_TYPE.WHERE`` + * ``PREFETCH_TYPE.JOIN`` + See also :py:func:`prefetch` standalone function. Example: @@ -4812,15 +4818,23 @@ mapped correctly. -.. py:function:: prefetch(sq, *subqueries) +.. py:function:: prefetch(sq, *subqueries[, prefetch_type=PREFETCH_TYPE.WHERE]) :param sq: Query to use as starting-point. :param subqueries: One or more models or :py:class:`ModelSelect` queries to eagerly fetch. + :param prefetch_type: Query type to use for the subqueries. :returns: a list of models with selected relations prefetched. Eagerly fetch related objects, allowing efficient querying of multiple - tables when a 1-to-many relationship exists. + tables when a 1-to-many relationship exists. The prefetch type changes how + the subqueries are constructed which may be desirable dependending on the + database engine in use. + + Prefetch type may be one of: + + * ``PREFETCH_TYPE.WHERE`` + * ``PREFETCH_TYPE.JOIN`` For example, it is simple to query a many-to-1 relationship efficiently:: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/docs/peewee/playhouse.rst new/peewee-3.15.4/docs/peewee/playhouse.rst --- old/peewee-3.15.3/docs/peewee/playhouse.rst 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/docs/peewee/playhouse.rst 2022-11-11 15:32:56.000000000 +0100 @@ -2997,6 +2997,26 @@ migrator.add_column('comment_tbl', 'comment', comment_field), ) +.. note:: + Peewee follows the Django convention of, by default, appending ``_id`` to + the column name for a given :py:class:`ForeignKeyField`. When adding a + foreign-key, you will want to ensure you give it the proper column name. For + example, if I want to add a ``user`` foreign-key to a ``Tweet`` model: + + .. code-block:: python + + # Our desired model will look like this: + class Tweet(BaseModel): + user = ForeignKeyField(User) # I want to add this field. + # ... other fields ... + + # Migration code: + user = ForeignKeyField(User, field=User.id, null=True) + migrate( + # Note that the column name given is "user_id". + migrator.add_column(Tweet._meta.table_name, 'user_id', user), + ) + Renaming a field: .. code-block:: python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/docs/peewee/relationships.rst new/peewee-3.15.4/docs/peewee/relationships.rst --- old/peewee-3.15.3/docs/peewee/relationships.rst 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/docs/peewee/relationships.rst 2022-11-11 15:32:56.000000000 +0100 @@ -1005,3 +1005,5 @@ * Foreign keys must exist between the models being prefetched. * `LIMIT` works as you'd expect on the outer-most query, but may be difficult to implement correctly if trying to limit the size of the sub-selects. + * The parameter `prefetch_type` may be used when `LIMIT` is not supported + with the default query construction (e.g. with MySQL). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/peewee.py new/peewee-3.15.4/peewee.py --- old/peewee-3.15.3/peewee.py 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/peewee.py 2022-11-11 15:32:56.000000000 +0100 @@ -70,7 +70,7 @@ mysql = None -__version__ = '3.15.3' +__version__ = '3.15.4' __all__ = [ 'AnyField', 'AsIs', @@ -129,6 +129,7 @@ 'PostgresqlDatabase', 'PrimaryKeyField', # XXX: Deprecated, change to AutoField. 'prefetch', + 'PREFETCH_TYPE', 'ProgrammingError', 'Proxy', 'QualifiedNames', @@ -346,6 +347,11 @@ CONSTRUCTOR=4, MODEL=5) +# Query type to use with prefetch +PREFETCH_TYPE = attrdict( + WHERE=1, + JOIN=2) + SCOPE_NORMAL = 1 SCOPE_SOURCE = 2 SCOPE_VALUES = 4 @@ -458,6 +464,8 @@ """ Proxy implementation specifically for proxying `Database` objects. """ + __slots__ = ('obj', '_callbacks', '_Model') + def connection_context(self): return ConnectionContext(self) def atomic(self, *args, **kwargs): @@ -468,6 +476,12 @@ return _transaction(self, *args, **kwargs) def savepoint(self): return _savepoint(self) + @property + def Model(self): + if not hasattr(self, '_Model'): + class Meta: database = self + self._Model = type('BaseModel', (Model,), {'Meta': Meta}) + return self._Model class ModelDescriptor(object): pass @@ -1563,13 +1577,15 @@ class Function(ColumnBase): + no_coerce_functions = set(('sum', 'count', 'avg', 'cast', 'array_agg')) + def __init__(self, name, arguments, coerce=True, python_value=None): self.name = name self.arguments = arguments self._filter = None self._order_by = None self._python_value = python_value - if name and name.lower() in ('sum', 'count', 'cast', 'array_agg'): + if name and name.lower() in self.no_coerce_functions: self._coerce = False else: self._coerce = coerce @@ -3441,6 +3457,13 @@ def get_noop_select(self, ctx): return ctx.sql(Select().columns(SQL('0')).where(SQL('0'))) + @property + def Model(self): + if not hasattr(self, '_Model'): + class Meta: database = self + self._Model = type('BaseModel', (Model,), {'Meta': Meta}) + return self._Model + def __pragma__(name): def __get__(self): @@ -6266,9 +6289,10 @@ 'only_save_dirty', 'legacy_table_names', 'table_settings', 'strict_tables']) - def __new__(cls, name, bases, attrs): + def __new__(cls, name, bases, attrs, **kwargs): if name == MODEL_BASE or bases[0].__name__ == MODEL_BASE: - return super(ModelBase, cls).__new__(cls, name, bases, attrs) + return super(ModelBase, cls).__new__(cls, name, bases, attrs, + **kwargs) meta_options = {} meta = attrs.pop('Meta', None) @@ -6308,7 +6332,7 @@ Schema = meta_options.get('schema_manager_class', SchemaManager) # Construct the new class. - cls = super(ModelBase, cls).__new__(cls, name, bases, attrs) + cls = super(ModelBase, cls).__new__(cls, name, bases, attrs, **kwargs) cls.__data__ = cls.__rel__ = None cls._meta = Meta(cls, **meta_options) @@ -7038,8 +7062,8 @@ self.execute() return iter(self._cursor_wrapper) - def prefetch(self, *subqueries): - return prefetch(self, *subqueries) + def prefetch(self, *subqueries, **kwargs): + return prefetch(self, *subqueries, **kwargs) def get(self, database=None): clone = self.paginate(1, 1) @@ -7864,7 +7888,7 @@ id_map[key].append(instance) -def prefetch_add_subquery(sq, subqueries): +def prefetch_add_subquery(sq, subqueries, prefetch_type): fixed_queries = [PrefetchQuery(sq)] for i, subquery in enumerate(subqueries): if isinstance(subquery, tuple): @@ -7900,28 +7924,55 @@ dest = (target_model,) if target_model else None if fks: - expr = reduce(operator.or_, [ - (fk << last_query.select(pk)) - for (fk, pk) in zip(fks, pks)]) - subquery = subquery.where(expr) + if prefetch_type == PREFETCH_TYPE.WHERE: + expr = reduce(operator.or_, [ + (fk << last_query.select(pk)) + for (fk, pk) in zip(fks, pks)]) + subquery = subquery.where(expr) + elif prefetch_type == PREFETCH_TYPE.JOIN: + expr = [] + select_pks = [] + for fk, pk in zip(fks, pks): + expr.append(getattr(last_query.c, pk.column_name) == fk) + select_pks.append(pk) + subquery = subquery.distinct().join( + last_query.select(*select_pks), + on=reduce(operator.or_, expr)) fixed_queries.append(PrefetchQuery(subquery, fks, False, dest)) elif backrefs: - expressions = [] + expr = [] + fields = [] for backref in backrefs: rel_field = getattr(subquery_model, backref.rel_field.name) fk_field = getattr(last_obj, backref.name) - expressions.append(rel_field << last_query.select(fk_field)) - subquery = subquery.where(reduce(operator.or_, expressions)) + fields.append((rel_field, fk_field)) + + if prefetch_type == PREFETCH_TYPE.WHERE: + for rel_field, fk_field in fields: + expr.append(rel_field << last_query.select(fk_field)) + subquery = subquery.where(reduce(operator.or_, expr)) + elif prefetch_type == PREFETCH_TYPE.JOIN: + select_fks = [] + for rel_field, fk_field in fields: + select_fks.append(fk_field) + target = getattr(last_query.c, fk_field.column_name) + expr.append(rel_field == target) + subquery = subquery.distinct().join( + last_query.select(*select_fks), + on=reduce(operator.or_, expr)) fixed_queries.append(PrefetchQuery(subquery, backrefs, True, dest)) return fixed_queries -def prefetch(sq, *subqueries): +def prefetch(sq, *subqueries, **kwargs): if not subqueries: return sq + prefetch_type = kwargs.pop('prefetch_type', PREFETCH_TYPE.WHERE) + if kwargs: + raise ValueError('Unrecognized arguments: %s' % kwargs) - fixed_queries = prefetch_add_subquery(sq, subqueries) + fixed_queries = prefetch_add_subquery(sq, subqueries, prefetch_type) deps = {} rel_map = {} for pq in reversed(fixed_queries): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/playhouse/shortcuts.py new/peewee-3.15.4/playhouse/shortcuts.py --- old/peewee-3.15.3/playhouse/shortcuts.py 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/playhouse/shortcuts.py 2022-11-11 15:32:56.000000000 +0100 @@ -249,6 +249,11 @@ try: return super(ReconnectMixin, self).execute_sql(sql, params, commit) except Exception as exc: + # If we are in a transaction, do not reconnect silently as + # any changes could be lost. + if self.in_transaction(): + raise exc + exc_class = type(exc) if exc_class not in self._reconnect_errors: raise exc diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/pyproject.toml new/peewee-3.15.4/pyproject.toml --- old/peewee-3.15.3/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 +++ new/peewee-3.15.4/pyproject.toml 2022-11-11 15:32:56.000000000 +0100 @@ -0,0 +1,2 @@ +[build-system] +requires = ["setuptools", "wheel"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/tests/db_tests.py new/peewee-3.15.4/tests/db_tests.py --- old/peewee-3.15.3/tests/db_tests.py 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/tests/db_tests.py 2022-11-11 15:32:56.000000000 +0100 @@ -921,3 +921,24 @@ if exc is None: raise Exception('expected integrity error not raised') self.assertTrue(exc.orig.__module__ != 'peewee') + + +class TestModelPropertyHelper(BaseTestCase): + def test_model_property(self): + database = get_in_memory_db() + class M1(database.Model): pass + class M2(database.Model): pass + class CM1(M1): pass + for M in (M1, M2, CM1): + self.assertTrue(M._meta.database is database) + + def test_model_property_on_proxy(self): + db = DatabaseProxy() + class M1(db.Model): pass + class M2(db.Model): pass + class CM1(M1): pass + + test_db = get_in_memory_db() + db.initialize(test_db) + for M in (M1, M2, CM1): + self.assertEqual(M._meta.database.database, ':memory:') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/tests/manytomany.py new/peewee-3.15.4/tests/manytomany.py --- old/peewee-3.15.3/tests/manytomany.py 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/tests/manytomany.py 2022-11-11 15:32:56.000000000 +0100 @@ -277,33 +277,37 @@ def test_prefetch_notes(self): self._set_data() - with self.assertQueryCount(3): - gargie, huey, mickey, zaizee = prefetch( - User.select().order_by(User.username), - NoteUserThrough, - Note) + for pt in PREFETCH_TYPE.values(): + with self.assertQueryCount(3): + gargie, huey, mickey, zaizee = prefetch( + User.select().order_by(User.username), + NoteUserThrough, + Note, + prefetch_type=pt) - with self.assertQueryCount(0): - self.assertNotes(gargie.notes, [1, 2]) - with self.assertQueryCount(0): - self.assertNotes(zaizee.notes, [4, 5]) + with self.assertQueryCount(0): + self.assertNotes(gargie.notes, [1, 2]) + with self.assertQueryCount(0): + self.assertNotes(zaizee.notes, [4, 5]) with self.assertQueryCount(2): self.assertNotes(User.create(username='x').notes, []) def test_prefetch_users(self): self._set_data() - with self.assertQueryCount(3): - n1, n2, n3, n4, n5 = prefetch( - Note.select().order_by(Note.text), - NoteUserThrough, - User) + for pt in PREFETCH_TYPE.values(): + with self.assertQueryCount(3): + n1, n2, n3, n4, n5 = prefetch( + Note.select().order_by(Note.text), + NoteUserThrough, + User, + prefetch_type=pt) - with self.assertQueryCount(0): - self.assertUsers(n1.users, ['gargie']) - with self.assertQueryCount(0): - self.assertUsers(n2.users, ['gargie', 'huey']) - with self.assertQueryCount(0): - self.assertUsers(n5.users, ['zaizee']) + with self.assertQueryCount(0): + self.assertUsers(n1.users, ['gargie']) + with self.assertQueryCount(0): + self.assertUsers(n2.users, ['gargie', 'huey']) + with self.assertQueryCount(0): + self.assertUsers(n5.users, ['zaizee']) with self.assertQueryCount(2): self.assertUsers(Note.create(text='x').users, []) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/tests/models.py new/peewee-3.15.4/tests/models.py --- old/peewee-3.15.3/tests/models.py 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/tests/models.py 2022-11-11 15:32:56.000000000 +0100 @@ -1865,8 +1865,8 @@ query = Sample.select(counter) self.assertEqual(query.scalar(), [0, 1, 2]) - @requires_models(Category) - def test_no_coerce_count(self): + @requires_models(Category, Sample) + def test_no_coerce_count_avg(self): for i in range(10): Category.create(name=str(i)) @@ -1878,6 +1878,11 @@ query = Category.select(fn.COUNT(Category.name).coerce(True)) self.assertEqual(query.scalar(), '10') + # Ensure avg over an integer field is returned as a float. + Sample.insert_many([(1, 0), (2, 0)]).execute() + query = Sample.select(fn.AVG(Sample.counter).alias('a')) + self.assertEqual(query.get().a, 1.5) + class TestJoinModelAlias(ModelTestCase): data = ( @@ -2482,6 +2487,20 @@ .returning(User.username.alias('new_username'))) self.assertEqual([x.new_username for x in query.execute()], ['sipp']) + # Minimal test with insert_many. + query = User.insert_many([('u7',), ('u8',)]) + self.assertEqual([r for r, in query.execute()], [7, 8]) + + # Test with insert / on conflict. + query = (User + .insert_many([(7, 'u7',), (9, 'u9',)], + [User.id, User.username]) + .on_conflict(conflict_target=[User.id], + update={User.username: User.username + 'x'}) + .returning(User)) + self.assertEqual([(x.id, x.username) for x in query], + [(7, 'u7x'), (9, 'u9')]) + def test_simple_returning_insert_update_delete(self): res = User.insert(username='charlie').returning(User).execute() self.assertEqual([u.username for u in res], ['charlie']) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/peewee-3.15.3/tests/prefetch_tests.py new/peewee-3.15.4/tests/prefetch_tests.py --- old/peewee-3.15.3/tests/prefetch_tests.py 2022-09-22 04:55:53.000000000 +0200 +++ new/peewee-3.15.4/tests/prefetch_tests.py 2022-11-11 15:32:56.000000000 +0100 @@ -100,164 +100,175 @@ return accum def test_prefetch_simple(self): - with self.assertQueryCount(3): - people = Person.select().order_by(Person.name) - query = people.prefetch(Note, NoteItem) - accum = self.accumulate_results(query, sort_items=True) - - self.assertEqual(accum, [ - ('huey', [ - ('hiss', ['hiss-1', 'hiss-2']), - ('meow', ['meow-1', 'meow-2', 'meow-3']), - ('purr', [])]), - ('mickey', [ - ('bark', ['bark-1', 'bark-2']), - ('woof', [])]), - ('zaizee', []), - ]) + for pt in PREFETCH_TYPE.values(): + with self.assertQueryCount(3): + people = Person.select().order_by(Person.name) + query = people.prefetch(Note, NoteItem, prefetch_type=pt) + accum = self.accumulate_results(query, sort_items=True) - def test_prefetch_filter(self): - with self.assertQueryCount(3): - people = Person.select().order_by(Person.name) - notes = (Note - .select() - .where(Note.content.not_in(('hiss', 'meow', 'woof'))) - .order_by(Note.content.desc())) - items = NoteItem.select().where(~NoteItem.content.endswith('-2')) - query = prefetch(people, notes, items) - self.assertEqual(self.accumulate_results(query), [ - ('huey', [('purr', [])]), - ('mickey', [('bark', ['bark-1'])]), + self.assertEqual(accum, [ + ('huey', [ + ('hiss', ['hiss-1', 'hiss-2']), + ('meow', ['meow-1', 'meow-2', 'meow-3']), + ('purr', [])]), + ('mickey', [ + ('bark', ['bark-1', 'bark-2']), + ('woof', [])]), ('zaizee', []), ]) + def test_prefetch_filter(self): + for pt in PREFETCH_TYPE.values(): + with self.assertQueryCount(3): + people = Person.select().order_by(Person.name) + notes = (Note + .select() + .where(Note.content.not_in(('hiss', 'meow', 'woof'))) + .order_by(Note.content.desc())) + items = NoteItem.select().where( + ~NoteItem.content.endswith('-2')) + query = prefetch(people, notes, items, prefetch_type=pt) + self.assertEqual(self.accumulate_results(query), [ + ('huey', [('purr', [])]), + ('mickey', [('bark', ['bark-1'])]), + ('zaizee', []), + ]) + def test_prefetch_reverse(self): - with self.assertQueryCount(2): + for pt in PREFETCH_TYPE.values(): + with self.assertQueryCount(2): + people = Person.select().order_by(Person.name) + notes = Note.select().order_by(Note.content) + query = prefetch(notes, people, prefetch_type=pt) + accum = [(note.content, note.person.name) for note in query] + self.assertEqual(accum, [ + ('bark', 'mickey'), + ('hiss', 'huey'), + ('meow', 'huey'), + ('purr', 'huey'), + ('woof', 'mickey')]) + + def test_prefetch_reverse_with_parent_join(self): + for pt in PREFETCH_TYPE.values(): + with self.assertQueryCount(2): + notes = (Note + .select(Note, Person) + .join(Person) + .order_by(Note.content)) + items = NoteItem.select().order_by(NoteItem.content.desc()) + query = prefetch(notes, items, prefetch_type=pt) + accum = [(note.person.name, + note.content, + [item.content for item in note.items]) + for note in query] + self.assertEqual(accum, [ + ('mickey', 'bark', ['bark-2', 'bark-1']), + ('huey', 'hiss', ['hiss-2', 'hiss-1']), + ('huey', 'meow', ['meow-3', 'meow-2', 'meow-1']), + ('huey', 'purr', []), + ('mickey', 'woof', []), + ]) + + def test_prefetch_multi_depth(self): + for pt in PREFETCH_TYPE.values(): people = Person.select().order_by(Person.name) notes = Note.select().order_by(Note.content) - query = prefetch(notes, people) - accum = [(note.content, note.person.name) for note in query] - self.assertEqual(accum, [ - ('bark', 'mickey'), - ('hiss', 'huey'), - ('meow', 'huey'), - ('purr', 'huey'), - ('woof', 'mickey')]) + items = NoteItem.select().order_by(NoteItem.content) + flags = Flag.select().order_by(Flag.id) + + LikePerson = Person.alias('lp') + likes = (Like + .select(Like, LikePerson.name) + .join(LikePerson, on=(Like.person == LikePerson.id))) + + # Five queries: + # - person (outermost query) + # - notes for people + # - items for notes + # - flags for notes + # - likes for notes (includes join to person) + with self.assertQueryCount(5): + query = prefetch(people, notes, items, flags, likes, + prefetch_type=pt) + accum = [] + for person in query: + notes = [] + for note in person.notes: + items = [item.content for item in note.items] + likes = [like.person.name for like in note.likes] + flags = [flag.is_spam for flag in note.flags] + notes.append((note.content, items, likes, flags)) + accum.append((person.name, notes)) - def test_prefetch_reverse_with_parent_join(self): - with self.assertQueryCount(2): - notes = (Note - .select(Note, Person) - .join(Person) - .order_by(Note.content)) - items = NoteItem.select().order_by(NoteItem.content.desc()) - query = prefetch(notes, items) - accum = [(note.person.name, - note.content, - [item.content for item in note.items]) for note in query] self.assertEqual(accum, [ - ('mickey', 'bark', ['bark-2', 'bark-1']), - ('huey', 'hiss', ['hiss-2', 'hiss-1']), - ('huey', 'meow', ['meow-3', 'meow-2', 'meow-1']), - ('huey', 'purr', []), - ('mickey', 'woof', []), + ('huey', [ + ('hiss', ['hiss-1', 'hiss-2'], [], []), + ('meow', ['meow-1', 'meow-2', 'meow-3'], ['mickey'], []), + ('purr', [], [], [True])]), + ('mickey', [ + ('bark', ['bark-1', 'bark-2'], [], []), + ('woof', [], ['huey'], [True])]), + (u'zaizee', []), ]) - def test_prefetch_multi_depth(self): - people = Person.select().order_by(Person.name) - notes = Note.select().order_by(Note.content) - items = NoteItem.select().order_by(NoteItem.content) - flags = Flag.select().order_by(Flag.id) - - LikePerson = Person.alias('lp') - likes = (Like - .select(Like, LikePerson.name) - .join(LikePerson, on=(Like.person == LikePerson.id))) - - # Five queries: - # - person (outermost query) - # - notes for people - # - items for notes - # - flags for notes - # - likes for notes (includes join to person) - with self.assertQueryCount(5): - query = prefetch(people, notes, items, flags, likes) - accum = [] - for person in query: - notes = [] - for note in person.notes: - items = [item.content for item in note.items] - likes = [like.person.name for like in note.likes] - flags = [flag.is_spam for flag in note.flags] - notes.append((note.content, items, likes, flags)) - accum.append((person.name, notes)) - - self.assertEqual(accum, [ - ('huey', [ - ('hiss', ['hiss-1', 'hiss-2'], [], []), - ('meow', ['meow-1', 'meow-2', 'meow-3'], ['mickey'], []), - ('purr', [], [], [True])]), - ('mickey', [ - ('bark', ['bark-1', 'bark-2'], [], []), - ('woof', [], ['huey'], [True])]), - (u'zaizee', []), - ]) - def test_prefetch_multi_depth_no_join(self): - LikePerson = Person.alias() - people = Person.select().order_by(Person.name) - notes = Note.select().order_by(Note.content) - items = NoteItem.select().order_by(NoteItem.content) - flags = Flag.select().order_by(Flag.id) - - with self.assertQueryCount(6): - query = prefetch(people, notes, items, flags, Like, LikePerson) - accum = [] - for person in query: - notes = [] - for note in person.notes: - items = [item.content for item in note.items] - likes = [like.person.name for like in note.likes] - flags = [flag.is_spam for flag in note.flags] - notes.append((note.content, items, likes, flags)) - accum.append((person.name, notes)) - - self.assertEqual(accum, [ - ('huey', [ - ('hiss', ['hiss-1', 'hiss-2'], [], []), - ('meow', ['meow-1', 'meow-2', 'meow-3'], ['mickey'], []), - ('purr', [], [], [True])]), - ('mickey', [ - ('bark', ['bark-1', 'bark-2'], [], []), - ('woof', [], ['huey'], [True])]), - (u'zaizee', []), - ]) + for pt in PREFETCH_TYPE.values(): + LikePerson = Person.alias() + people = Person.select().order_by(Person.name) + notes = Note.select().order_by(Note.content) + items = NoteItem.select().order_by(NoteItem.content) + flags = Flag.select().order_by(Flag.id) - def test_prefetch_with_group_by(self): - people = (Person - .select(Person, fn.COUNT(Note.id).alias('note_count')) - .join(Note, JOIN.LEFT_OUTER) - .group_by(Person) - .order_by(Person.name)) - notes = Note.select().order_by(Note.content) - items = NoteItem.select().order_by(NoteItem.content) - with self.assertQueryCount(3): - query = prefetch(people, notes, items) - self.assertEqual(self.accumulate_results(query), [ + with self.assertQueryCount(6): + query = prefetch(people, notes, items, flags, Like, LikePerson, + prefetch_type=pt) + accum = [] + for person in query: + notes = [] + for note in person.notes: + items = [item.content for item in note.items] + likes = [like.person.name for like in note.likes] + flags = [flag.is_spam for flag in note.flags] + notes.append((note.content, items, likes, flags)) + accum.append((person.name, notes)) + + self.assertEqual(accum, [ ('huey', [ - ('hiss', ['hiss-1', 'hiss-2']), - ('meow', ['meow-1', 'meow-2', 'meow-3']), - ('purr', [])]), + ('hiss', ['hiss-1', 'hiss-2'], [], []), + ('meow', ['meow-1', 'meow-2', 'meow-3'], ['mickey'], []), + ('purr', [], [], [True])]), ('mickey', [ - ('bark', ['bark-1', 'bark-2']), - ('woof', [])]), - ('zaizee', []), + ('bark', ['bark-1', 'bark-2'], [], []), + ('woof', [], ['huey'], [True])]), + (u'zaizee', []), ]) - huey, mickey, zaizee = query - self.assertEqual(huey.note_count, 3) - self.assertEqual(mickey.note_count, 2) - self.assertEqual(zaizee.note_count, 0) + def test_prefetch_with_group_by(self): + for pt in PREFETCH_TYPE.values(): + people = (Person + .select(Person, fn.COUNT(Note.id).alias('note_count')) + .join(Note, JOIN.LEFT_OUTER) + .group_by(Person) + .order_by(Person.name)) + notes = Note.select().order_by(Note.content) + items = NoteItem.select().order_by(NoteItem.content) + with self.assertQueryCount(3): + query = prefetch(people, notes, items, prefetch_type=pt) + self.assertEqual(self.accumulate_results(query), [ + ('huey', [ + ('hiss', ['hiss-1', 'hiss-2']), + ('meow', ['meow-1', 'meow-2', 'meow-3']), + ('purr', [])]), + ('mickey', [ + ('bark', ['bark-1', 'bark-2']), + ('woof', [])]), + ('zaizee', []), + ]) + + huey, mickey, zaizee = query + self.assertEqual(huey.note_count, 3) + self.assertEqual(mickey.note_count, 2) + self.assertEqual(zaizee.note_count, 0) @requires_models(Category) def test_prefetch_self_join(self): @@ -270,22 +281,24 @@ for i in range(2): cc('%s-%s' % (p.name, i + 1), p) - Child = Category.alias('child') - with self.assertQueryCount(2): - query = prefetch(Category.select().order_by(Category.id), Child) - names_and_children = [ - (cat.name, [child.name for child in cat.children]) - for cat in query] - - self.assertEqual(names_and_children, [ - ('root', ['p1', 'p2']), - ('p1', ['p1-1', 'p1-2']), - ('p2', ['p2-1', 'p2-2']), - ('p1-1', []), - ('p1-2', []), - ('p2-1', []), - ('p2-2', []), - ]) + for pt in PREFETCH_TYPE.values(): + Child = Category.alias('child') + with self.assertQueryCount(2): + query = prefetch(Category.select().order_by(Category.id), + Child, prefetch_type=pt) + names_and_children = [ + (cat.name, [child.name for child in cat.children]) + for cat in query] + + self.assertEqual(names_and_children, [ + ('root', ['p1', 'p2']), + ('p1', ['p1-1', 'p1-2']), + ('p2', ['p2-1', 'p2-2']), + ('p1-1', []), + ('p1-2', []), + ('p2-1', []), + ('p2-2', []), + ]) @requires_models(Category) def test_prefetch_adjacency_list(self): @@ -313,21 +326,23 @@ for child_tree in children: stack.insert(0, (node, child_tree)) - C = Category.alias('c') - G = Category.alias('g') - GG = Category.alias('gg') - GGG = Category.alias('ggg') - query = Category.select().where(Category.name == 'root') - with self.assertQueryCount(5): - pf = prefetch(query, C, (G, C), (GG, G), (GGG, GG)) - def gather(c): - children = sorted([gather(ch) for ch in c.children]) - return (c.name, tuple(children)) - nodes = list(pf) - self.assertEqual(len(nodes), 1) - pf_tree = gather(nodes[0]) + for pt in PREFETCH_TYPE.values(): + C = Category.alias('c') + G = Category.alias('g') + GG = Category.alias('gg') + GGG = Category.alias('ggg') + query = Category.select().where(Category.name == 'root') + with self.assertQueryCount(5): + pf = prefetch(query, C, (G, C), (GG, G), (GGG, GG), + prefetch_type=pt) + def gather(c): + children = sorted([gather(ch) for ch in c.children]) + return (c.name, tuple(children)) + nodes = list(pf) + self.assertEqual(len(nodes), 1) + pf_tree = gather(nodes[0]) - self.assertEqual(tree, pf_tree) + self.assertEqual(tree, pf_tree) def test_prefetch_specific_model(self): # Person -> Note @@ -336,29 +351,31 @@ person=Person.get(Person.name == 'zaizee')) NoteAlias = Note.alias('na') - with self.assertQueryCount(3): - people = Person.select().order_by(Person.name) - notes = Note.select().order_by(Note.content) - likes = (Like - .select(Like, NoteAlias.content) - .join(NoteAlias, on=(Like.note == NoteAlias.id)) - .order_by(NoteAlias.content)) - query = prefetch(people, notes, (likes, Person)) - accum = [] - for person in query: - likes = [] - notes = [] - for note in person.notes: - notes.append(note.content) - for like in person.likes: - likes.append(like.note.content) - accum.append((person.name, notes, likes)) - - self.assertEqual(accum, [ - ('huey', ['hiss', 'meow', 'purr'], ['woof']), - ('mickey', ['bark', 'woof'], ['meow']), - ('zaizee', [], ['woof']), - ]) + for pt in PREFETCH_TYPE.values(): + with self.assertQueryCount(3): + people = Person.select().order_by(Person.name) + notes = Note.select().order_by(Note.content) + likes = (Like + .select(Like, NoteAlias.content) + .join(NoteAlias, on=(Like.note == NoteAlias.id)) + .order_by(NoteAlias.content)) + query = prefetch(people, notes, (likes, Person), + prefetch_type=pt) + accum = [] + for person in query: + likes = [] + notes = [] + for note in person.notes: + notes.append(note.content) + for like in person.likes: + likes.append(like.note.content) + accum.append((person.name, notes, likes)) + + self.assertEqual(accum, [ + ('huey', ['hiss', 'meow', 'purr'], ['woof']), + ('mickey', ['bark', 'woof'], ['meow']), + ('zaizee', [], ['woof']), + ]) @requires_models(Relationship) def test_multiple_foreign_keys(self): @@ -373,43 +390,44 @@ r4 = RC(z, c) def assertRelationships(attr, values): + self.assertEqual(len(attr),len(values)) for relationship, value in zip(attr, values): self.assertEqual(relationship.__data__, value) - with self.assertQueryCount(2): - people = Person.select().order_by(Person.name) - relationships = Relationship.select().order_by(Relationship.id) - - query = prefetch(people, relationships) - cp, hp, zp = list(query) - - assertRelationships(cp.relationships, [ - {'id': r1.id, 'from_person': c.id, 'to_person': h.id}, - {'id': r2.id, 'from_person': c.id, 'to_person': z.id}]) - assertRelationships(cp.related_to, [ - {'id': r3.id, 'from_person': h.id, 'to_person': c.id}, - {'id': r4.id, 'from_person': z.id, 'to_person': c.id}]) - - assertRelationships(hp.relationships, [ - {'id': r3.id, 'from_person': h.id, 'to_person': c.id}]) - assertRelationships(hp.related_to, [ - {'id': r1.id, 'from_person': c.id, 'to_person': h.id}]) - - assertRelationships(zp.relationships, [ - {'id': r4.id, 'from_person': z.id, 'to_person': c.id}]) - assertRelationships(zp.related_to, [ - {'id': r2.id, 'from_person': c.id, 'to_person': z.id}]) - - with self.assertQueryCount(2): - query = prefetch(relationships, people) - accum = [] - for row in query: - accum.append((row.from_person.name, row.to_person.name)) - self.assertEqual(accum, [ - ('charlie', 'huey'), - ('charlie', 'zaizee'), - ('huey', 'charlie'), - ('zaizee', 'charlie')]) + for pt in PREFETCH_TYPE.values(): + with self.assertQueryCount(2): + people = Person.select().order_by(Person.name) + relationships = Relationship.select().order_by(Relationship.id) + query = prefetch(people, relationships, prefetch_type=pt) + cp, hp, zp = list(query) + + assertRelationships(cp.relationships, [ + {'id': r1.id, 'from_person': c.id, 'to_person': h.id}, + {'id': r2.id, 'from_person': c.id, 'to_person': z.id}]) + assertRelationships(cp.related_to, [ + {'id': r3.id, 'from_person': h.id, 'to_person': c.id}, + {'id': r4.id, 'from_person': z.id, 'to_person': c.id}]) + + assertRelationships(hp.relationships, [ + {'id': r3.id, 'from_person': h.id, 'to_person': c.id}]) + assertRelationships(hp.related_to, [ + {'id': r1.id, 'from_person': c.id, 'to_person': h.id}]) + + assertRelationships(zp.relationships, [ + {'id': r4.id, 'from_person': z.id, 'to_person': c.id}]) + assertRelationships(zp.related_to, [ + {'id': r2.id, 'from_person': c.id, 'to_person': z.id}]) + + with self.assertQueryCount(2): + query = prefetch(relationships, people, prefetch_type=pt) + accum = [] + for row in query: + accum.append((row.from_person.name, row.to_person.name)) + self.assertEqual(accum, [ + ('charlie', 'huey'), + ('charlie', 'zaizee'), + ('huey', 'charlie'), + ('zaizee', 'charlie')]) m = Person.create(name='mickey') RC(h, m) @@ -418,34 +436,35 @@ self.assertEqual([r.to_person.name for r in p.relationships], ns) # Use prefetch to go Person -> Relationship <- Person (PA). - with self.assertQueryCount(3): - people = (Person - .select() - .where(Person.name != 'mickey') - .order_by(Person.name)) - relationships = Relationship.select().order_by(Relationship.id) - PA = Person.alias() - query = prefetch(people, relationships, PA) - cp, hp, zp = list(query) - assertNames(cp, ['huey', 'zaizee']) - assertNames(hp, ['charlie', 'mickey']) - assertNames(zp, ['charlie']) - - # User prefetch to go Person -> Relationship+Person (PA). - with self.assertQueryCount(2): - people = (Person - .select() - .where(Person.name != 'mickey') - .order_by(Person.name)) - rels = (Relationship - .select(Relationship, PA) - .join(PA, on=(Relationship.to_person == PA.id)) - .order_by(Relationship.id)) - query = prefetch(people, rels) - cp, hp, zp = list(query) - assertNames(cp, ['huey', 'zaizee']) - assertNames(hp, ['charlie', 'mickey']) - assertNames(zp, ['charlie']) + for pt in PREFETCH_TYPE.values(): + with self.assertQueryCount(3): + people = (Person + .select() + .where(Person.name != 'mickey') + .order_by(Person.name)) + relationships = Relationship.select().order_by(Relationship.id) + PA = Person.alias() + query = prefetch(people, relationships, PA, prefetch_type=pt) + cp, hp, zp = list(query) + assertNames(cp, ['huey', 'zaizee']) + assertNames(hp, ['charlie', 'mickey']) + assertNames(zp, ['charlie']) + + # User prefetch to go Person -> Relationship+Person (PA). + with self.assertQueryCount(2): + people = (Person + .select() + .where(Person.name != 'mickey') + .order_by(Person.name)) + rels = (Relationship + .select(Relationship, PA) + .join(PA, on=(Relationship.to_person == PA.id)) + .order_by(Relationship.id)) + query = prefetch(people, rels, prefetch_type=pt) + cp, hp, zp = list(query) + assertNames(cp, ['huey', 'zaizee']) + assertNames(hp, ['charlie', 'mickey']) + assertNames(zp, ['charlie']) def test_prefetch_through_manytomany(self): Like.create(note=Note.get(Note.content == 'meow'), @@ -453,23 +472,24 @@ Like.create(note=Note.get(Note.content == 'woof'), person=Person.get(Person.name == 'zaizee')) - with self.assertQueryCount(3): - people = Person.select().order_by(Person.name) - notes = Note.select().order_by(Note.content) - likes = Like.select().order_by(Like.id) - query = prefetch(people, likes, notes) - accum = [] - for person in query: - liked_notes = [] - for like in person.likes: - liked_notes.append(like.note.content) - accum.append((person.name, liked_notes)) - - self.assertEqual(accum, [ - ('huey', ['woof']), - ('mickey', ['meow']), - ('zaizee', ['meow', 'woof']), - ]) + for pt in PREFETCH_TYPE.values(): + with self.assertQueryCount(3): + people = Person.select().order_by(Person.name) + notes = Note.select().order_by(Note.content) + likes = Like.select().order_by(Like.id) + query = prefetch(people, likes, notes, prefetch_type=pt) + accum = [] + for person in query: + liked_notes = [] + for like in person.likes: + liked_notes.append(like.note.content) + accum.append((person.name, liked_notes)) + + self.assertEqual(accum, [ + ('huey', ['woof']), + ('mickey', ['meow']), + ('zaizee', ['meow', 'woof']), + ]) @requires_models(Package, PackageItem) def test_prefetch_non_pk_fk(self): @@ -484,22 +504,24 @@ for item in items: PackageItem.create(package=barcode, name=item) - packages = Package.select().order_by(Package.barcode) - items = PackageItem.select().order_by(PackageItem.name) - - with self.assertQueryCount(2): - query = prefetch(packages, items) - for package, (barcode, items) in zip(query, data): - self.assertEqual(package.barcode, barcode) - self.assertEqual([item.name for item in package.items], - list(items)) + for pt in PREFETCH_TYPE.values(): + packages = Package.select().order_by(Package.barcode) + items = PackageItem.select().order_by(PackageItem.name) + + with self.assertQueryCount(2): + query = prefetch(packages, items, prefetch_type=pt) + for package, (barcode, items) in zip(query, data): + self.assertEqual(package.barcode, barcode) + self.assertEqual([item.name for item in package.items], + list(items)) def test_prefetch_mark_dirty_regression(self): - people = Person.select().order_by(Person.name) - query = people.prefetch(Note, NoteItem) - for person in query: - self.assertEqual(person.dirty_fields, []) - for note in person.notes: - self.assertEqual(note.dirty_fields, []) - for item in note.items: - self.assertEqual(item.dirty_fields, []) + for pt in PREFETCH_TYPE.values(): + people = Person.select().order_by(Person.name) + query = people.prefetch(Note, NoteItem, prefetch_type=pt) + for person in query: + self.assertEqual(person.dirty_fields, []) + for note in person.notes: + self.assertEqual(note.dirty_fields, []) + for item in note.items: + self.assertEqual(item.dirty_fields, [])