diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 1fc62f117e..e2d2a1f6cc 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Use the EXISTS operator when using the 'where' operator in Many2One * Redact sensitive values from the log (issue14220) * Add multiple attribute to the buttons in the tree view * Manage readonly wizard diff --git a/trytond/trytond/model/fields/many2one.py b/trytond/trytond/model/fields/many2one.py index ab18133fd8..646c5933c1 100644 --- a/trytond/trytond/model/fields/many2one.py +++ b/trytond/trytond/model/fields/many2one.py @@ -8,7 +8,7 @@ from sql import As, Column, Expression, Literal, Null, Query, With from sql.aggregate import Max from sql.conditionals import Coalesce from sql.functions import CharLength -from sql.operators import Or +from sql.operators import Exists, Or from trytond.config import config from trytond.pool import Pool @@ -225,6 +225,7 @@ class Many2One(Field): pool = Pool() Rule = pool.get('ir.rule') Target = self.get_target() + use_subquery = Target.estimated_count() < _subquery_threshold table, _ = tables[None] name, operator, value = domain[:3] @@ -278,8 +279,11 @@ class Many2One(Field): target_id, = query.columns if isinstance(target_id, As): target_id = target_id.expression - query.where &= target_id == column - expression = column.in_(query) + if use_subquery: + expression = column.in_(query) + else: + query.where &= target_id == column + expression = Exists(query) if operator.startswith('not'): return ~expression return expression @@ -299,7 +303,7 @@ class Many2One(Field): # No need to join with the target table return super().convert_domain( (self.name, operator, value), tables, Model) - elif Target.estimated_count() < _subquery_threshold: + elif use_subquery: query = Target.search(target_domain, order=[], query=True) expression = column.in_(query) else: diff --git a/trytond/trytond/tests/test_field_many2one.py b/trytond/trytond/tests/test_field_many2one.py index ec3da5a5c7..ac723cb09c 100644 --- a/trytond/trytond/tests/test_field_many2one.py +++ b/trytond/trytond/tests/test_field_many2one.py @@ -3,21 +3,16 @@ from unittest.mock import patch -from sql import Join +from sql import Join, Table +from trytond.config import config from trytond.model.exceptions import DomainValidationError from trytond.pool import Pool from trytond.tests.test_tryton import ( TestCase, activate_module, with_transaction) -class FieldMany2OneTestCase(TestCase): - "Test Field Many2One" - - @classmethod - def setUpClass(cls): - super().setUpClass() - activate_module('tests') +class CommonTestCaseMixin: @with_transaction() def test_create_id(self): @@ -104,6 +99,50 @@ class FieldMany2OneTestCase(TestCase): self.assertEqual(many2one.many2one, target) + @with_transaction() + def test_context_attribute(self): + "Test context on many2one attribute" + pool = Pool() + Many2One = pool.get('test.many2one_context') + Target = pool.get('test.many2one_context.target') + + target, = Target.create([{}]) + record, = Many2One.create([{ + 'target': target.id, + }]) + + self.assertEqual(record.target.context, 'foo') + + @with_transaction() + def test_context_read(self): + "Test context on many2one read" + pool = Pool() + Many2One = pool.get('test.many2one_context') + Target = pool.get('test.many2one_context.target') + + target, = Target.create([{}]) + record, = Many2One.create([{ + 'target': target.id, + }]) + data, = Many2One.read([record.id], ['target.context']) + + self.assertEqual(data['target.']['context'], 'foo') + + @with_transaction() + def test_context_set(self): + "Test context on many2one set" + pool = Pool() + Many2One = pool.get('test.many2one_context') + Target = pool.get('test.many2one_context.target') + + target, = Target.create([{}]) + record = Many2One(target=target.id) + + self.assertEqual(record.target.context, 'foo') + + +class SearchTestCaseMixin: + @with_transaction() def test_search_order_default(self): "Test search order by many2one default" @@ -165,45 +204,6 @@ class FieldMany2OneTestCase(TestCase): result = Many2One.search([('many2one', '=', target.id)]) self.assertListEqual(result, [record]) - @with_transaction() - def _test_search_join(self, subquery_threshold=1_000): - from trytond.model.fields import many2one - - pool = Pool() - Target = pool.get('test.many2one_target') - Many2One = pool.get('test.many2one') - target1, target2 = Target.create([ - {'value': 1}, - {'value': 2}, - ]) - many2one1, many2one2 = Many2One.create([ - {'many2one': target1}, - {'many2one': target2}, - ]) - - previous = many2one._subquery_threshold - many2one._subquery_threshold = subquery_threshold - self.addCleanup(setattr, many2one, '_subquery_threshold', previous) - - many2ones = Many2One.search([ - ('many2one.value', '=', 1), - ]) - self.assertListEqual(many2ones, [many2one1]) - - return Many2One.search([ - ('many2one.value', '=', 1), - ], query=True) - - def test_search_join(self): - "Test search by many2one join" - query = self._test_search_join(0) - self.assertIsInstance(query.from_[0], Join) - - def test_search_subquery(self): - "Test search by many2one subquery" - query = self._test_search_join() - self.assertNotIsInstance(query.from_[0], Join) - @with_transaction() def test_search_nested_null(self): "Test search by null many2one" @@ -278,45 +278,88 @@ class FieldMany2OneTestCase(TestCase): self.assertEqual(record.many2one.value, 42) @with_transaction() - def test_context_attribute(self): - "Test context on many2one attribute" + def test_search_strategy(self): + "Test search many2one with the right strategy" pool = Pool() - Many2One = pool.get('test.many2one_context') - Target = pool.get('test.many2one_context.target') + Target = pool.get('test.many2one_target') + Many2One = pool.get('test.many2one') + target1, target2 = Target.create([ + {'value': 1}, + {'value': 2}, + ]) + many2one1, many2one2 = Many2One.create([ + {'many2one': target1}, + {'many2one': target2}, + ]) - target, = Target.create([{}]) - record, = Many2One.create([{ - 'target': target.id, - }]) + many2ones = Many2One.search([ + ('many2one.value', '=', 1), + ]) + self.assertListEqual(many2ones, [many2one1]) - self.assertEqual(record.target.context, 'foo') + query = Many2One.search([ + ('many2one.value', '=', 1), + ], query=True) + + sql_statement = { + 'IN': Table, + 'EXISTS': Join, + }[self._strategy] + self.assertIsInstance(query.from_[0], sql_statement) @with_transaction() - def test_context_read(self): - "Test context on many2one read" + def test_search_where_strategy(self): + "Test search on many2one using where uses the right strategy" pool = Pool() - Many2One = pool.get('test.many2one_context') - Target = pool.get('test.many2one_context.target') + Target = pool.get('test.many2one_target') + Many2One = pool.get('test.many2one') + target1, target2 = Target.create([ + {'value': 1}, + {'value': 2}, + ]) + many2one1, many2one2 = Many2One.create([ + {'many2one': target1}, + {'many2one': target2}, + ]) - target, = Target.create([{}]) - record, = Many2One.create([{ - 'target': target.id, - }]) - data, = Many2One.read([record.id], ['target.context']) + many2ones = Many2One.search([ + ('many2one', 'where', [('value', '=', 1)]), + ]) + self.assertListEqual(many2ones, [many2one1]) + query = Many2One.search([ + ('many2one', 'where', [('value', '=', 1)]), + ], query=True) + self.assertIn(self._strategy, str(query)) - self.assertEqual(data['target.']['context'], 'foo') - @with_transaction() - def test_context_set(self): - "Test context on many2one set" - pool = Pool() - Many2One = pool.get('test.many2one_context') - Target = pool.get('test.many2one_context.target') +class FieldMany2OneTestCase( + TestCase, CommonTestCaseMixin, SearchTestCaseMixin): + "Test Field Many2One" + _strategy = 'IN' - target, = Target.create([{}]) - record = Many2One(target=target.id) + @classmethod + def setUpClass(cls): + super().setUpClass() + activate_module('tests') - self.assertEqual(record.target.context, 'foo') + +class FieldMany2OneExistsTestCase( + TestCase, CommonTestCaseMixin, SearchTestCaseMixin): + "Test Field Many2One" + _strategy = 'EXISTS' + + @classmethod + def setUpClass(cls): + super().setUpClass() + activate_module('tests') + + def setUp(self): + from trytond.model.fields import many2one + super().setUp() + previous = int( + config.get('database', 'subquery_threshold', default='1_000')) + many2one._subquery_threshold = 0 + self.addCleanup(setattr, many2one, '_subquery_threshold', previous) class FieldMany2OneTreeTestCase(TestCase):