Skip to content

Commit fafb5a8

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 fafb5a8

File tree

1 file changed

+44
-5
lines changed

1 file changed

+44
-5
lines changed

src/util/orm.py

Lines changed: 44 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,8 @@ 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
378380
:param int chunk_size: number of records to load in each iteration chunk, `200` by
379381
default
380382
:param logger: logger used to report the progress, by default
@@ -387,23 +389,60 @@ class iter_browse(object):
387389
See also :func:`~odoo.upgrade.util.orm.env`
388390
"""
389391

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

392394
def __init__(self, model, *args, **kw):
393395
assert len(args) in [1, 3] # either (cr, uid, ids) or (ids,)
394396
self._model = model
395397
self._cr_uid = args[:-1]
396-
ids = args[-1]
397-
self._size = len(ids)
398+
self._ids = args[-1]
399+
self._size = kw.pop("size", None)
400+
self._query = kw.pop("query", None)
398401
self._chunk_size = kw.pop("chunk_size", 200) # keyword-only argument
399402
self._logger = kw.pop("logger", _logger)
400403
self._strategy = kw.pop("strategy", "flush")
401404
assert self._strategy in {"flush", "commit"}
402405
if kw:
403406
raise TypeError("Unknown arguments: %s" % ", ".join(kw))
404407

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

408447
def _browse(self, ids):
409448
next(self._end(), None)

0 commit comments

Comments
 (0)