Skip to content

Commit 08d2d68

Browse files
committed
[ADD] pg.query_ids: query large numbers of ids memory-safely
This is mainly the code that has been recently added to `orm.recompute_fields`, here we're making it re-usasble.
1 parent 6fbf957 commit 08d2d68

File tree

2 files changed

+106
-0
lines changed

2 files changed

+106
-0
lines changed

src/base/tests/test_util.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
from odoo import modules
1919
from odoo.tools import mute_logger
2020

21+
try:
22+
from odoo.sql_db import db_connect
23+
except ImportError:
24+
from openerp.sql_db import db_connect
25+
2126
from odoo.addons.base.maintenance.migrations import util
2227
from odoo.addons.base.maintenance.migrations.testing import UnitTestCase, parametrize
2328
from odoo.addons.base.maintenance.migrations.util import snippets
@@ -1481,6 +1486,27 @@ def test_iter(self):
14811486
self.assertEqual(result, expected)
14821487

14831488

1489+
class TestQueryIds(UnitTestCase):
1490+
def test_straight(self):
1491+
result = list(util.query_ids(self.env.cr, "SELECT * FROM (VALUES (1), (2)) AS x(x)", itersize=2))
1492+
self.assertEqual(result, [1, 2])
1493+
1494+
def test_chunks(self):
1495+
with util.query_ids(self.env.cr, "SELECT * FROM (VALUES (1), (2)) AS x(x)") as ids:
1496+
result = list(util.chunks(ids, 100, fmt=list))
1497+
self.assertEqual(result, [[1, 2]])
1498+
1499+
def test_destructor(self):
1500+
ids = util.query_ids(self.env.cr, "SELECT id from res_users")
1501+
del ids
1502+
1503+
def test_pk_violation(self):
1504+
with db_connect(self.env.cr.dbname).cursor() as cr, mute_logger("odoo.sql_db"), self.assertRaises(
1505+
ValueError
1506+
), util.query_ids(cr, "SELECT * FROM (VALUES (1), (1)) AS x(x)") as ids:
1507+
list(ids)
1508+
1509+
14841510
class TestRecords(UnitTestCase):
14851511
def test_rename_xmlid(self):
14861512
cr = self.env.cr

src/util/pg.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1932,3 +1932,83 @@ def bulk_update_table(cr, table, columns, mapping, key_col="id"):
19321932
key_col=key_col,
19331933
)
19341934
cr.execute(query, [Json(mapping)])
1935+
1936+
1937+
class query_ids(object):
1938+
"""
1939+
Iterator over ids returned by a query.
1940+
1941+
This iterator can memory efficiently query a potentially huge number of ids.
1942+
1943+
:param str query: the query that returns the ids. It can be DML, e.g. `UPDATE table WHERE ... RETURNING id`.
1944+
:param int itersize: passed to a named_cursor, determines the number of rows fetched from PG at once.
1945+
"""
1946+
1947+
def __init__(self, cr, query, itersize=None):
1948+
self._ncr = None
1949+
self._cr = cr
1950+
self._tmp_tbl = "_upgrade_query_ids_{}".format(uuid.uuid4().hex)
1951+
cr.execute(
1952+
format_query(
1953+
cr,
1954+
"CREATE UNLOGGED TABLE {}(id) AS (WITH query AS ({}) SELECT * FROM query)",
1955+
self._tmp_tbl,
1956+
SQLStr(query),
1957+
)
1958+
)
1959+
self._len = cr.rowcount
1960+
try:
1961+
cr.execute(
1962+
format_query(
1963+
cr,
1964+
"ALTER TABLE {} ADD CONSTRAINT {} PRIMARY KEY (id)",
1965+
self._tmp_tbl,
1966+
"pk_{}_id".format(self._tmp_tbl),
1967+
)
1968+
)
1969+
except psycopg2.IntegrityError as e:
1970+
if e.pgcode == errorcodes.UNIQUE_VIOLATION:
1971+
raise ValueError("The query for ids is producing duplicate values: {}".format(query))
1972+
raise
1973+
self._ncr = named_cursor(cr, itersize)
1974+
self._ncr.execute(format_query(cr, "SELECT id FROM {} ORDER BY id", self._tmp_tbl))
1975+
self._it = iter(self._ncr)
1976+
1977+
def _close(self):
1978+
if self._ncr:
1979+
if self._ncr.closed:
1980+
return
1981+
self._ncr.close()
1982+
try:
1983+
self._cr.execute(format_query(self._cr, "DROP TABLE IF EXISTS {}", self._tmp_tbl))
1984+
except psycopg2.InternalError as e:
1985+
if e.pgcode != errorcodes.IN_FAILED_SQL_TRANSACTION:
1986+
raise
1987+
1988+
def __len__(self):
1989+
return self._len
1990+
1991+
def __iter__(self):
1992+
return self
1993+
1994+
def __next__(self):
1995+
if self._ncr.closed:
1996+
raise StopIteration
1997+
try:
1998+
return next(self._it)[0]
1999+
except StopIteration:
2000+
self._close()
2001+
raise
2002+
2003+
def next(self):
2004+
return self.__next__()
2005+
2006+
def __enter__(self):
2007+
return self
2008+
2009+
def __exit__(self, exc_type, exc_value, traceback):
2010+
self._close()
2011+
return False
2012+
2013+
def __del__(self):
2014+
self._close()

0 commit comments

Comments
 (0)