details:   https://code.tryton.org/tryton/commit/6fc18060d097
branch:    default
user:      Cédric Krier <[email protected]>
date:      Wed Jul 15 09:31:21 2026 +0200
description:
        Add bulk_create, bulk_delete, bulk_save and bulk_func to ModelStorage
diffstat:

 trytond/CHANGELOG                          |   1 +
 trytond/doc/ref/models.rst                 |  39 +++++++++++++++++++
 trytond/trytond/model/modelstorage.py      |  61 ++++++++++++++++++++++++++++++
 trytond/trytond/tests/test_modelstorage.py |  45 ++++++++++++++++++++++
 4 files changed, 146 insertions(+), 0 deletions(-)

diffs (222 lines):

diff -r 6072341a32b5 -r 6fc18060d097 trytond/CHANGELOG
--- a/trytond/CHANGELOG Sat Sep 19 11:26:06 2026 +0200
+++ b/trytond/CHANGELOG Wed Jul 15 09:31:21 2026 +0200
@@ -1,3 +1,4 @@
+* Add bulk_create, bulk_delete, bulk_save and bulk_func to ModelStorage
 * Add editable state on field
 * Enforce readonly on field when checking access
 * Add the icon parameter to register_authentication_service
diff -r 6072341a32b5 -r 6fc18060d097 trytond/doc/ref/models.rst
--- a/trytond/doc/ref/models.rst        Sat Sep 19 11:26:06 2026 +0200
+++ b/trytond/doc/ref/models.rst        Wed Jul 15 09:31:21 2026 +0200
@@ -382,6 +382,10 @@
    ``vlist`` is list of dictionaries with fields names as key and created
    values as value and return the list of new instances.
 
+.. classmethod:: ModelStorage.bulk_create([batch_size[, auto]])
+
+   Returns a :class:`BulkBuffer` for the :meth:`~ModelStorage.create` method.
+
 .. classmethod:: ModelStorage.read(ids, fields_names)
 
    Return a list of dictionary for the record ids.
@@ -426,6 +430,10 @@
 
    Delete records.
 
+.. classmethod:: ModelStorage.bulk_delete([batch_size[, auto]])
+
+   Returns a :class:`BulkBuffer` for the :meth:`~ModelStorage.delete` method.
+
 .. classmethod:: ModelStorage.check_modification(mode, records[, values[, 
external]])
 
    Method called after ``records`` are created with ``values`` as a list of
@@ -586,6 +594,14 @@
    method must be overridden to add validation for the field names set and must
    raise an exception if validation fails.
 
+.. classmethod:: ModelStorage.bulk_save([batch_size[, auto]])
+
+   Returns a :class:`BulkBuffer` for the :meth:`~ModelStorage.save` method.
+
+.. classmethod:: ModelStorage.bulk_func(name[, batch_size[, auto]])
+
+   Returns a :class:`BulkBuffer` for the named method.
+
 Dual methods:
 
 .. classmethod:: ModelStorage.save(records)
@@ -884,6 +900,28 @@
    * ``begin``: optimize for constant pattern and anchored to the beginning of
      the string
 
+BulkBuffer
+==========
+
+.. class:: BulkBuffer(function[, batch_size[, auto]])
+
+   A `context manager`_ that bufferizes a list of values.
+   When the list is greater than the ``batch_size`` and ``auto`` is ``True`` or
+   the context is exited, the buffer is flushed by calling the function with
+   the values in the list by ``batch_size``.
+
+.. method:: BulkBuffer.push(values)
+
+   Push a values into the buffer.
+
+.. method:: BulkBuffer.extend(vlist)
+
+   Extend the buffer by appending the values from the iterable.
+
+.. method:: BulkBuffer.flush()
+
+   Call the function with the current valuess in the list and clear it.
+
 BrowseList
 ==========
 
@@ -1347,3 +1385,4 @@
 .. _mixin: http://en.wikipedia.org/wiki/Mixin
 .. _JSON: http://en.wikipedia.org/wiki/Json
 .. _UNION: http://en.wikipedia.org/wiki/Union_(SQL)#UNION_operator
+.. _`context manager`: 
http://docs.python.org/reference/datamodel.html#context-managers
diff -r 6072341a32b5 -r 6fc18060d097 trytond/trytond/model/modelstorage.py
--- a/trytond/trytond/model/modelstorage.py     Sat Sep 19 11:26:06 2026 +0200
+++ b/trytond/trytond/model/modelstorage.py     Wed Jul 15 09:31:21 2026 +0200
@@ -347,6 +347,10 @@
             return ids
 
     @classmethod
+    def bulk_create(cls, batch_size=None, auto=True):
+        return BulkBuffer(cls.create, batch_size=batch_size, auto=auto)
+
+    @classmethod
     def _before_read(cls, ids, fields_names):
         pool = Pool()
         ModelAccess = pool.get('ir.model.access')
@@ -559,6 +563,10 @@
             meth()
 
     @classmethod
+    def bulk_delete(cls, batch_size=None, auto=True):
+        return BulkBuffer(cls.delete, batch_size=batch_size, auto=auto)
+
+    @classmethod
     def check_modification(cls, mode, records, values=None, external=False):
         assert mode in {'create', 'write', 'delete'}
 
@@ -2322,6 +2330,59 @@
                 record._removed = None
             records = latter
 
+    @classmethod
+    def bulk_save(cls, batch_size=None, auto=True):
+        return BulkBuffer(cls.save, batch_size=batch_size, auto=auto)
+
+    @classmethod
+    def bulk_func(cls, name, batch_size=None, auto=True):
+        return BulkBuffer(getattr(cls, name), batch_size=batch_size, auto=auto)
+
+
+class BulkBuffer:
+    __slots__ = ('_func', '_batch_size', '_auto', '_pending')
+
+    def __init__(self, function, batch_size=None, auto=True):
+        if batch_size is None:
+            batch_size = record_cache_size(Transaction())
+        if batch_size <= 0:
+            raise ValueError("batch_size must be greater than 0")
+        self._func = function
+        self._batch_size = batch_size
+        self._auto = auto
+        self._pending = []
+
+    def push(self, values):
+        self._pending.append(values)
+        if self._auto and len(self._pending) >= self._batch_size:
+            self.flush()
+
+    def extend(self, vlist):
+        if self._auto:
+            for value in vlist:
+                self.push(value)
+        else:
+            self._pending.extend(vlist)
+
+    def flush(self):
+        while self._pending:
+            batch = self._pending[:self._batch_size]
+            del self._pending[:self._batch_size]
+            self._func(batch)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        if type is None:
+            self.flush()
+
+    def __len__(self):
+        return len(self._pending)
+
+    def __bool__(self):
+        return bool(self._pending)
+
 
 class BrowseList(list):
     __slots__ = (
diff -r 6072341a32b5 -r 6fc18060d097 trytond/trytond/tests/test_modelstorage.py
--- a/trytond/trytond/tests/test_modelstorage.py        Sat Sep 19 11:26:06 
2026 +0200
+++ b/trytond/trytond/tests/test_modelstorage.py        Wed Jul 15 09:31:21 
2026 +0200
@@ -108,6 +108,51 @@
             self.assertEqual(record.context.get('foo'), 'bar')
 
     @with_transaction()
+    def test_bulk_create(self):
+        "Test bulk create"
+        pool = Pool()
+        ModelStorage = pool.get('test.modelstorage')
+
+        with ModelStorage.bulk_create(batch_size=2) as create:
+            for i in range(3):
+                create.push({'name': str(i)})
+
+        records = ModelStorage.search([])
+        self.assertEqual(len(records), 3)
+
+    @with_transaction()
+    def test_bulk_delete(self):
+        "Test bulk delete"
+        pool = Pool()
+        ModelStorage = pool.get('test.modelstorage')
+
+        records = ModelStorage.create([{'name': str(i)} for i in range(1, 4)])
+
+        with ModelStorage.bulk_delete(batch_size=2) as delete:
+            delete.extend(records)
+
+        records = ModelStorage.search([])
+        self.assertEqual(len(records), 0)
+
+    @with_transaction()
+    def test_bulk_save(self):
+        "Test bulk save"
+        pool = Pool()
+        ModelStorage = pool.get('test.modelstorage')
+
+        record = ModelStorage(name="foo")
+        record.save()
+
+        with ModelStorage.bulk_save(batch_size=2) as save:
+            record.name = "bar"
+            save.push(record)
+            for i in range(3):
+                save.push(ModelStorage(name=str(i)))
+
+        records = ModelStorage.search([])
+        self.assertEqual(len(records), 4)
+
+    @with_transaction()
     def test_save_mixed_context(self):
         'Test save with mixed context '
         pool = Pool()

Reply via email to