From b973f371c88b91ec1c351c5c5cbffe4d4c2c552b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Mon, 8 Apr 2024 13:13:14 +0200 Subject: [PATCH] Add a contextual read limit for related fields --- trytond/CHANGELOG | 1 + trytond/doc/ref/models.rst | 3 ++ trytond/trytond/model/modelsql.py | 6 ++- trytond/trytond/tests/modelsql.py | 24 ++++++++++ trytond/trytond/tests/test_modelsql.py | 66 ++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 1 deletion(-) diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 8717868df1..99f7072c8c 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Add a contextual read limit for related fields * Add xml_id field on ModelStorage * Add send_message methods * Store model on ir.rule.group diff --git a/trytond/doc/ref/models.rst b/trytond/doc/ref/models.rst index 712fa438a9..e0155d2906 100644 --- a/trytond/doc/ref/models.rst +++ b/trytond/doc/ref/models.rst @@ -324,6 +324,9 @@ Class methods: Their values will be returned under the referencing field suffixed by a ``.``. The number of *dots* in the name is not limited. + The number of records read using *dots* can be limited with the + ``related_read_limit`` key in the context. + The remaining records will only contain the ``id`` field. ``fields_names`` can also contain ``:string`` for :class:`~fields.Selection` or :class:`~fields.MultiSelection` fields. diff --git a/trytond/trytond/model/modelsql.py b/trytond/trytond/model/modelsql.py index 6940014d15..9796c1ff50 100644 --- a/trytond/trytond/model/modelsql.py +++ b/trytond/trytond/model/modelsql.py @@ -1213,7 +1213,11 @@ class ModelSQL(ModelStorage): value = row[name] if value is not None: add(value) - return Target.read(target_ids, fields) + related_read_limit = transaction.context.get('related_read_limit') + rows = Target.read(target_ids[:related_read_limit], fields) + if related_read_limit is not None: + rows += [{'id': i} for i in target_ids[related_read_limit:]] + return rows def add_related(field, rows, targets): name = field.name diff --git a/trytond/trytond/tests/modelsql.py b/trytond/trytond/tests/modelsql.py index 2a171f4904..3e430a3705 100644 --- a/trytond/trytond/tests/modelsql.py +++ b/trytond/trytond/tests/modelsql.py @@ -35,6 +35,28 @@ class ModelSQLReadContextID(ModelSQL): }) +class ModelSQLReadLimit(ModelSQL): + "ModelSQL to test related limit" + __name__ = 'test.modelsql.read.limit' + name = fields.Char("Name") + targets = fields.One2Many( + 'test.modelsql.read.limit.target', 'parent', "Targets") + sum_targets = fields.Function( + fields.Integer("Sum of Targets"), 'on_change_with_sum_targets') + + @fields.depends('targets') + def on_change_with_sum_targets(self, name=None): + return sum(t.integer for t in self.targets) + + +class ModelSQLReadLimitTarget(ModelSQL): + "ModelSQL to test related limit" + __name__ = 'test.modelsql.read.limit.target' + name = fields.Char("Name") + integer = fields.Integer("Integer") + parent = fields.Many2One('test.modelsql.read.limit', "Parent") + + class ModelSQLRequiredField(ModelSQL): 'model with a required field' __name__ = 'test.modelsql' @@ -257,6 +279,8 @@ def register(module): ModelSQLRead, ModelSQLReadTarget, ModelSQLReadContextID, + ModelSQLReadLimit, + ModelSQLReadLimitTarget, ModelSQLRequiredField, ModelSQLTimestamp, ModelSQLCreate, diff --git a/trytond/trytond/tests/test_modelsql.py b/trytond/trytond/tests/test_modelsql.py index 21f1ae36dd..c710b36cb4 100644 --- a/trytond/trytond/tests/test_modelsql.py +++ b/trytond/trytond/tests/test_modelsql.py @@ -308,6 +308,72 @@ class ModelSQLTestCase(unittest.TestCase): }], }]) + @with_transaction() + def test_read_limit(self): + "Test that we limit the number or related records read" + pool = Pool() + Model = pool.get('test.modelsql.read') + + model, = Model.create([{ + 'name': 'Record', + 'targets': [ + ('create', [{'name': str(i)} for i in range(10)]), + ] + }]) + + values, = Model.read([model.id], ['targets.name']) + self.assertTrue(all('id' in t and 'name' in t + for t in values['targets.'])) + + with Transaction().set_context(related_read_limit=4): + values, = Model.read([model.id], ['targets.name']) + self.assertTrue(all('id' in t and 'name' in t + for t in values['targets.'][:4])) + self.assertTrue(all('id' in t and len(t) == 1 + for t in values['targets.'][4:])) + + @with_transaction() + def test_read_limit_only_limit_related(self): + "Test that we don't limit reading records with related_read_limit" + pool = Pool() + Model = pool.get('test.modelsql.read') + + records = Model.create([ + { + 'name': f'Record {i}', + 'targets': [ + ('create', [{'name': str(i)} for i in range(10)]), + ] + } for i in range(10)]) + with Transaction().set_context(related_read_limit=4): + values = Model.read( + [r.id for r in records], ['name', 'targets.name']) + self.assertTrue(all('id' in v and 'name' in v for v in values)) + self.assertTrue(all(len(v['targets.']) == 10 for v in values)) + + @with_transaction() + def test_read_limit_caching(self): + "Test that limiting the related records read plays nice with caching" + pool = Pool() + Model = pool.get('test.modelsql.read.limit') + + records = Model.create([ + { + 'name': f'Record {i}', + 'targets': [ + ('create', [{'name': str(j), 'integer': j} + for j in range(1, i + 1)]), + ], + } for i in range(10)]) + + with Transaction().set_context(read_limit=4): + values = Model.read( + [r.id for r in records], + ['name', 'targets.name', 'sum_targets']) + computed = {r['name']: r['sum_targets'] for r in values} + expected = {f'Record {i}': i * (i + 1) // 2 for i in range(10)} + self.assertEqual(computed, expected) + @unittest.skipIf(backend.name == 'sqlite', 'SQLite not concerned because tryton don\'t set "NOT NULL"' 'constraint: "ALTER TABLE" don\'t support NOT NULL constraint'