Use the EXISTS operator when using the 'where' operator in Many2One

Closes #11935
This commit is contained in:
Nicolas Évrard
2025-09-14 12:38:50 +02:00
parent 9223e17240
commit ace7be31be
3 changed files with 127 additions and 79 deletions
+1
View File
@@ -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
+8 -4
View File
@@ -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:
+118 -75
View File
@@ -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):