diff --git a/trytond/trytond/backend/database.py b/trytond/trytond/backend/database.py index 0b64f347965..aa7d636fc6f 100644 --- a/trytond/trytond/backend/database.py +++ b/trytond/trytond/backend/database.py @@ -73,6 +73,9 @@ def setnextid(self, connection, table, value): def currid(self, connection, table): pass + def estimated_count(self, connection, table): + raise NotImplementedError + @classmethod def lock(cls, connection, table): raise NotImplementedError diff --git a/trytond/trytond/backend/postgresql/database.py b/trytond/trytond/backend/postgresql/database.py index ad96859c6fe..ab68e3884c0 100644 --- a/trytond/trytond/backend/postgresql/database.py +++ b/trytond/trytond/backend/postgresql/database.py @@ -40,6 +40,7 @@ from psycopg2 import QueryCanceledError as DatabaseTimeoutError from psycopg2.extras import register_default_json, register_default_jsonb from sql import Cast, Flavor, For, Table +from sql.aggregate import Count from sql.conditionals import Coalesce from sql.functions import Function from sql.operators import BinaryOperator, Concat @@ -498,6 +499,17 @@ def currid(self, connection, table): cursor.execute(f"SELECT last_value FROM {sequence_name}") return cursor.fetchone()[0] + def estimated_count(self, connection, table): + cursor = connection.cursor() + if isinstance(table, Table): + cursor.execute( + 'SELECT n_live_tup FROM pg_stat_all_tables ' + 'WHERE relname = %s', + (table._name,)) + else: + cursor.execute(*table.select(Count())) + return cursor.fetchone()[0] + def lock(self, connection, table): cursor = connection.cursor() cursor.execute(SQL('LOCK {} IN EXCLUSIVE MODE NOWAIT').format( diff --git a/trytond/trytond/backend/sqlite/database.py b/trytond/trytond/backend/sqlite/database.py index 82561844b84..b03e77c9030 100644 --- a/trytond/trytond/backend/sqlite/database.py +++ b/trytond/trytond/backend/sqlite/database.py @@ -17,6 +17,7 @@ from weakref import WeakKeyDictionary from sql import Expression, Flavor, Literal, Null, Query, Table +from sql.aggregate import Count from sql.conditionals import NullIf from sql.functions import ( CharLength, CurrentTimestamp, Extract, Function, Overlay, Position, @@ -605,6 +606,11 @@ def lastid(self, cursor): # This call is not thread safe return cursor.lastrowid + def estimated_count(self, connection, table): + cursor = connection.cursor() + cursor.execute(*table.select(Count())) + return cursor.fetchone()[0] + def lock(self, connection, table): pass diff --git a/trytond/trytond/model/modelsql.py b/trytond/trytond/model/modelsql.py index 23ccfb69014..bc9fe97751e 100644 --- a/trytond/trytond/model/modelsql.py +++ b/trytond/trytond/model/modelsql.py @@ -594,6 +594,12 @@ def __raise_data_error( raise SizeValidationError( gettext('ir.msg_size_validation', **error_args)) + @classmethod + def _get_estimated_count(cls): + transaction = Transaction() + return transaction.database.estimated_count( + transaction.connection, cls.__table__()) + @classmethod def history_revisions(cls, ids): pool = Pool() diff --git a/trytond/trytond/model/modelstorage.py b/trytond/trytond/model/modelstorage.py index 95548de215a..1d3fe08b703 100644 --- a/trytond/trytond/model/modelstorage.py +++ b/trytond/trytond/model/modelstorage.py @@ -633,10 +633,14 @@ def estimated_count(cls): "Returns the estimation of the number of records." count = cls._count_cache.get(cls.__name__) if count is None: - count = cls.search([], count=True) + count = cls._get_estimated_count() cls._count_cache.set(cls.__name__, count) return count + @classmethod + def _get_estimated_count(cls): + return cls.search([], count=True) + def resources(self): pool = Pool() Attachment = pool.get('ir.attachment') diff --git a/trytond/trytond/tests/test_backend.py b/trytond/trytond/tests/test_backend.py index 3bb83760762..8791f7ecd74 100644 --- a/trytond/trytond/tests/test_backend.py +++ b/trytond/trytond/tests/test_backend.py @@ -7,6 +7,7 @@ from sql import Literal, Select, functions from sql.functions import CurrentTimestamp, DateTrunc, ToChar +from trytond.pool import Pool from trytond.tests.test_tryton import activate_module, with_transaction from trytond.transaction import Transaction @@ -171,3 +172,14 @@ def test_function_date_trunc(self): cursor.execute(*Select([DateTrunc(type_, date)])) value, = cursor.fetchone() self.assertEqual(str(value), str(result)) + + @with_transaction() + def test_estimated_count(self): + "Test estimated count queries" + pool = Pool() + database = Transaction().database + connection = Transaction().connection + + ModelSQLRead = pool.get('test.modelsql.read') + count = database.estimated_count(connection, ModelSQLRead.__table__()) + self.assertGreaterEqual(count, 0) diff --git a/trytond/trytond/tests/test_modelsql.py b/trytond/trytond/tests/test_modelsql.py index 23e5effba4d..bb651a93e2e 100644 --- a/trytond/trytond/tests/test_modelsql.py +++ b/trytond/trytond/tests/test_modelsql.py @@ -1110,9 +1110,12 @@ def test_search_limit(self): Model.create([{'name': str(i)} for i in range(10)]) - self.assertEqual(Model.search([], limit=5, count=True), 5) - self.assertEqual(Model.search([], limit=20, count=True), 10) - self.assertEqual(Model.search([], limit=None, count=True), 10) + # The stats of postgres might not be updated yet + with patch.object(Model, 'estimated_count') as ec: + ec.return_value = 10 + self.assertEqual(Model.search([], limit=5, count=True), 5) + self.assertEqual(Model.search([], limit=20, count=True), 10) + self.assertEqual(Model.search([], limit=None, count=True), 10) @with_transaction() def test_search_offset(self):