From b63c28503c6928f9136f9f6cbb9cebbf8a3a450b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 16 Jun 2025 13:48:00 +0200 Subject: [PATCH] Use recursive CTE for child/parent-of clause when the number of ids is too large The path or MPTT queries are faster than the recursive CTE only when the number of OR-ed clauses does not require more index scan than the CTE. This number can be approximate to the depth of the tree. Log10 is used to estimate the depth of the tree if it has an average of 10 children per node. Closes #14078 --- trytond/trytond/model/fields/many2one.py | 11 +- trytond/trytond/tests/test_field_many2one.py | 108 ++++++++++++------- 2 files changed, 77 insertions(+), 42 deletions(-) diff --git a/trytond/trytond/model/fields/many2one.py b/trytond/trytond/model/fields/many2one.py index c881d5cc01..ab18133fd8 100644 --- a/trytond/trytond/model/fields/many2one.py +++ b/trytond/trytond/model/fields/many2one.py @@ -1,5 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. + +import math import warnings from sql import As, Column, Expression, Literal, Null, Query, With @@ -255,10 +257,15 @@ class Many2One(Field): if operator.startswith('not'): return ~expression return expression - elif self.left and self.right: + elif (self.left and self.right + and len(ids) <= math.floor(math.log10( + Target.estimated_count() or 1))): return self.convert_domain_mptt( (name, operator, ids), tables) - elif self.path: + elif (self.path + and (operator.endswith('parent_of') + or len(ids) <= math.floor(math.log10( + Target.estimated_count() or 1)))): return self.convert_domain_path( (name, operator, ids), tables) else: diff --git a/trytond/trytond/tests/test_field_many2one.py b/trytond/trytond/tests/test_field_many2one.py index 8c9c2480d3..ec3da5a5c7 100644 --- a/trytond/trytond/tests/test_field_many2one.py +++ b/trytond/trytond/tests/test_field_many2one.py @@ -1,6 +1,8 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +from unittest.mock import patch + from sql import Join from trytond.model.exceptions import DomainValidationError @@ -348,9 +350,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'child_of', [self.root1.id]), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'child_of', [self.root1.id]), + ]) self.assertListEqual( sorted(result), @@ -364,9 +368,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'not child_of', [self.root1.id]), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'not child_of', [self.root1.id]), + ]) self.assertListEqual( sorted(result), @@ -378,9 +384,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'child_of', [self.second1.id]), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'child_of', [self.second1.id]), + ]) self.assertListEqual(result, [self.second1]) @@ -406,10 +414,12 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'child_of', - [self.first1.id, self.second1.id]), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'child_of', + [self.first1.id, self.second1.id]), + ]) self.assertListEqual( sorted(result), @@ -421,9 +431,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'child_of', []), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'child_of', []), + ]) self.assertListEqual(result, []) @@ -433,9 +445,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'not child_of', []), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'not child_of', []), + ]) self.assertListEqual( sorted(result), @@ -449,9 +463,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'child_of', [None]), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'child_of', [None]), + ]) self.assertListEqual(result, []) @@ -461,9 +477,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'parent_of', [self.root1.id]), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'parent_of', [self.root1.id]), + ]) self.assertListEqual(result, [self.root1]) @@ -473,9 +491,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'not parent_of', [self.root1.id]), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'not parent_of', [self.root1.id]), + ]) self.assertListEqual( sorted(result), @@ -489,9 +509,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'parent_of', [self.second4.id]), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'parent_of', [self.second4.id]), + ]) self.assertListEqual( sorted(result), @@ -503,9 +525,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'not parent_of', [self.second4.id]), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'not parent_of', [self.second4.id]), + ]) self.assertListEqual( sorted(result), @@ -519,9 +543,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'parent_of', []), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'parent_of', []), + ]) self.assertListEqual(result, []) @@ -531,9 +557,11 @@ class FieldMany2OneTreeTestCase(TestCase): Many2One = Pool().get(self.model_name) self.create_tree(Many2One) - result = Many2One.search([ - ('many2one', 'not parent_of', []), - ]) + with patch.object(Many2One, 'estimated_count') as ec: + ec.return_value = 100 + result = Many2One.search([ + ('many2one', 'not parent_of', []), + ]) self.assertListEqual( sorted(result),