Skip to content

Commit 39cbdb8

Browse files
committed
[IMP] orm: iter_browse accept generator or query as ids
This allows the caller to be memory efficient on huge numbers of ids, allowing for even more millions of records to be browsed.
1 parent 92efe24 commit 39cbdb8

File tree

1 file changed

+45
-5
lines changed

1 file changed

+45
-5
lines changed

src/util/orm.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import logging
1313
import re
14+
import uuid
1415
from contextlib import contextmanager
1516
from functools import wraps
1617
from itertools import chain
@@ -374,7 +375,9 @@ class iter_browse(object):
374375
375376
:param model: the model to iterate
376377
:type model: :class:`odoo.model.Model`
377-
:param list(int) ids: list of IDs of the records to iterate
378+
:param iterable(int) ids: iterable of IDs of the records to iterate
379+
:param str query: alternative to ids, SQL query that can produce them.
380+
Can also be a DML statement with a RETURNING clause.
378381
:param int chunk_size: number of records to load in each iteration chunk, `200` by
379382
default
380383
:param logger: logger used to report the progress, by default
@@ -387,23 +390,60 @@ class iter_browse(object):
387390
See also :func:`~odoo.upgrade.util.orm.env`
388391
"""
389392

390-
__slots__ = ("_chunk_size", "_cr_uid", "_it", "_logger", "_model", "_patch", "_size", "_strategy")
393+
__slots__ = ("_chunk_size", "_cr_uid", "_ids", "_it", "_logger", "_model", "_patch", "_query", "_size", "_strategy")
391394

392395
def __init__(self, model, *args, **kw):
393396
assert len(args) in [1, 3] # either (cr, uid, ids) or (ids,)
394397
self._model = model
395398
self._cr_uid = args[:-1]
396-
ids = args[-1]
397-
self._size = len(ids)
399+
self._ids = args[-1]
400+
self._size = kw.pop("size", None)
401+
self._query = kw.pop("query", None)
398402
self._chunk_size = kw.pop("chunk_size", 200) # keyword-only argument
399403
self._logger = kw.pop("logger", _logger)
400404
self._strategy = kw.pop("strategy", "flush")
401405
assert self._strategy in {"flush", "commit"}
402406
if kw:
403407
raise TypeError("Unknown arguments: %s" % ", ".join(kw))
404408

409+
if not (self._ids is None) ^ (self._query is None):
410+
raise TypeError("Must be initialized using exactly one of `ids` or `query`")
411+
412+
if self._query:
413+
self._ids_query()
414+
415+
if not self._size:
416+
try:
417+
self._size = len(self._ids)
418+
except TypeError:
419+
raise ValueError("When passing ids as a generator, the size kwarg is mandatory")
405420
self._patch = None
406-
self._it = chunks(ids, self._chunk_size, fmt=self._browse)
421+
self._it = chunks(self._ids, self._chunk_size, fmt=self._browse)
422+
423+
def _ids_query(self):
424+
cr = self._model.env.cr
425+
tmp_tbl = "_upgrade_ib_{}".format(uuid.uuid4().hex)
426+
cr.execute(
427+
format_query(
428+
cr,
429+
"CREATE UNLOGGED TABLE {}(id) AS (WITH query AS ({}) SELECT * FROM query)",
430+
tmp_tbl,
431+
SQLStr(self._query),
432+
)
433+
)
434+
self._size = cr.rowcount
435+
cr.execute(
436+
format_query(cr, "ALTER TABLE {} ADD CONSTRAINT {} PRIMARY KEY (id)", tmp_tbl, "pk_{}_id".format(tmp_tbl))
437+
)
438+
439+
def get_ids():
440+
with named_cursor(cr, itersize=self._chunk_size) as ncr:
441+
ncr.execute(format_query(cr, "SELECT id FROM {} ORDER BY id", tmp_tbl))
442+
for (id_,) in ncr:
443+
yield id_
444+
cr.execute(format_query(cr, "DROP TABLE IF EXISTS {}", tmp_tbl))
445+
446+
self._ids = get_ids()
407447

408448
def _browse(self, ids):
409449
next(self._end(), None)

0 commit comments

Comments
 (0)