details:   https://code.tryton.org/tryton/commit/cb8de82c421e
branch:    7.0
user:      Nicolas Évrard <[email protected]>
date:      Fri Oct 31 14:28:27 2025 +0100
description:
        Use SQL queries to restore history

        Restoring a fields.Dict value failed before using this pure SQL 
solution.
        Also such a solution will be more efficient by minimizing the amount of 
python
        code run.

        Closes #14031
        (grafted from c81b7d05a51c491f1c7060506144bf50d395c075)
diffstat:

 trytond/trytond/model/modelsql.py     |  96 ++++++++++++++++++++++++----------
 trytond/trytond/tests/history.py      |   8 ++-
 trytond/trytond/tests/test_history.py |  24 ++++++++
 3 files changed, 99 insertions(+), 29 deletions(-)

diffs (202 lines):

diff -r f475a36bf255 -r cb8de82c421e trytond/trytond/model/modelsql.py
--- a/trytond/trytond/model/modelsql.py Thu Oct 09 18:50:07 2025 +0200
+++ b/trytond/trytond/model/modelsql.py Fri Oct 31 14:28:27 2025 +0100
@@ -7,10 +7,10 @@
 
 from sql import (
     Asc, Column, Desc, Expression, Literal, Null, NullsFirst, NullsLast, Table,
-    Union, With)
+    Union, Window, With)
 from sql.aggregate import Count, Max
 from sql.conditionals import Coalesce
-from sql.functions import CurrentTimestamp, Extract, Substring
+from sql.functions import CurrentTimestamp, Extract, RowNumber, Substring
 from sql.operators import And, Concat, Equal, Operator, Or
 
 from trytond import backend
@@ -692,16 +692,24 @@
                 for id_ in ids:
                     cache_cls.pop(id_, None)
 
+        history_select = With()
+        history_values = history_select.select(
+            where=Column(history_select, '__h_rank') == 1,
+            with_=[history_select])
+
         columns = []
         hcolumns = []
+        history_columns = []
         fnames = sorted(n for n, f in cls._fields.items()
             if f.sql_type())
+        id_idx = fnames.index('id')
         for fname in fnames:
             columns.append(Column(table, fname))
+            history_columns.append(Column(history_values, fname))
             if fname == 'write_uid':
-                hcolumns.append(Literal(transaction.user))
+                hcolumns.append(Literal(transaction.user).as_('write_uid'))
             elif fname == 'write_date':
-                hcolumns.append(CurrentTimestamp())
+                hcolumns.append(CurrentTimestamp().as_('write_date'))
             else:
                 hcolumns.append(Column(history, fname))
 
@@ -709,41 +717,73 @@
             return all(not v for n, v in zip(fnames, values)
                 if n not in ['id', 'write_uid', 'write_date'])
 
-        to_delete = []
-        to_update = []
-        for id_ in ids:
-            column_datetime = Coalesce(history.write_date, history.create_date)
+        to_delete = set()
+        to_update = set()
+
+        column_datetime = Coalesce(history.write_date, history.create_date)
+        h_order = (column_datetime.desc, Column(history, '__id').desc)
+        history_select.query = history.select(
+            *hcolumns,
+            RowNumber(
+                window=Window([history.id], order_by=h_order)
+                ).as_('__h_rank'))
+
+        for sub_ids in grouped_slice(ids):
+            sub_ids = list(sub_ids)
             if not _before:
                 hwhere = (column_datetime <= datetime)
             else:
                 hwhere = (column_datetime < datetime)
-            hwhere &= (history.id == id_)
-            horder = (column_datetime.desc, Column(history, '__id').desc)
-            cursor.execute(*history.select(*hcolumns,
-                    where=hwhere, order_by=horder, limit=1))
-            values = cursor.fetchone()
-            if not values or is_deleted(values):
-                to_delete.append(id_)
+
+            hwhere &= reduce_ids(history.id, sub_ids)
+            history_select.query.where = hwhere
+
+            cursor.execute(*history_values)
+            for values in cursor.fetchall():
+                if is_deleted(values):
+                    to_delete.add(values[id_idx])
+                else:
+                    to_update.add(values[id_idx])
+            to_delete |= (deleted_sub_ids := set(sub_ids) - to_update)
+
+            # we need to skip the deleted IDs that are all None history records
+            # because they could fail the UPDATE
+            if to_delete:
+                history_select.query.where &= ~history.id.in_(
+                    list(deleted_sub_ids))
+
+            # Some of the sub_ids are not updated because they are not in the
+            # table anymore, they should be undeleted from the value in the
+            # history table
+            to_undelete = set()
+            update_query = table.update(
+                columns, history_columns,
+                from_=[history_values],
+                where=history_values.id == table.id)
+            if transaction.database.has_returning():
+                update_query.returning = [table.id]
+                cursor.execute(*update_query)
+                to_undelete.update(to_update - set(r[0] for r in cursor))
             else:
-                to_update.append(id_)
-                values = list(values)
-                cursor.execute(*table.update(columns, values,
-                        where=table.id == id_))
-                rowcount = cursor.rowcount
-                if rowcount == -1 or rowcount is None:
-                    cursor.execute(*table.select(table.id,
-                            where=table.id == id_))
-                    rowcount = len(cursor.fetchall())
-                if rowcount < 1:
-                    cursor.execute(*table.insert(columns, [values]))
+                cursor.execute(*table
+                    .right_join(history_values, history_values.id == table.id)
+                    .select(history_values.id, where=table.id == Null))
+                to_undelete.update(r[0] for r in cursor)
+                cursor.execute(*update_query)
+            if to_undelete:
+                history_select.query.where = (hwhere
+                    & history.id.in_(list(to_undelete)))
+                cursor.execute(*table.insert(
+                        columns,
+                        history_values.select(*history_columns)))
 
         if to_delete:
             for sub_ids in grouped_slice(to_delete):
                 where = reduce_ids(table.id, sub_ids)
                 cursor.execute(*table.delete(where=where))
-            cls._insert_history(to_delete, True)
+            cls._insert_history(list(to_delete), True)
         if to_update:
-            cls._insert_history(to_update)
+            cls._insert_history(list(to_update))
 
     @classmethod
     def restore_history(cls, ids, datetime):
diff -r f475a36bf255 -r cb8de82c421e trytond/trytond/tests/history.py
--- a/trytond/trytond/tests/history.py  Thu Oct 09 18:50:07 2025 +0200
+++ b/trytond/trytond/tests/history.py  Fri Oct 31 14:28:27 2025 +0100
@@ -1,9 +1,14 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
-from trytond.model import ModelSQL, fields
+
+from trytond.model import DictSchemaMixin, ModelSQL, fields
 from trytond.pool import Pool
 
 
+class HistoryDictSchema(DictSchemaMixin, ModelSQL):
+    __name__ = 'test.history.dict.schema'
+
+
 class TestHistory(ModelSQL):
     'Test History'
     __name__ = 'test.history'
@@ -14,6 +19,7 @@
         'test.history.line', 'history', 'Lines at Stamp',
         datetime_field='stamp')
     stamp = fields.Timestamp('Stamp')
+    dico = fields.Dict('test.history.dict.schema', "Dict")
 
 
 class TestHistoryLine(ModelSQL):
diff -r f475a36bf255 -r cb8de82c421e trytond/trytond/tests/test_history.py
--- a/trytond/trytond/tests/test_history.py     Thu Oct 09 18:50:07 2025 +0200
+++ b/trytond/trytond/tests/test_history.py     Fri Oct 31 14:28:27 2025 +0100
@@ -189,6 +189,30 @@
             History.read([history_id], ['value'])
 
     @with_transaction()
+    def test_restore_history_dict(self):
+        "Test restoring a record with a field.Dict"
+        pool = Pool()
+        History = pool.get('test.history')
+        transaction = Transaction()
+
+        history = History(dico={'a': 1})
+        history.save()
+        history_id = history.id
+        first = history.create_date
+
+        transaction.commit()
+
+        history = History(history_id)
+        history.dico = {'b': 2}
+        history.save()
+
+        transaction.commit()
+
+        History.restore_history([history_id], first)
+        history = History(history_id)
+        self.assertEqual(history.dico, {'a': 1})
+
+    @with_transaction()
     def test_restore_history_before(self):
         'Test restore history before'
         pool = Pool()

Reply via email to