changeset 428bf1c84a3d in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset&node=428bf1c84a3d
description:
        Include order columns in UNION-ed search queries

        issue10790
        review365991002
diffstat:

 trytond/model/modelsql.py      |   59 ++++++++++++--
 trytond/tests/modelsql.py      |   17 ++++
 trytond/tests/test_modelsql.py |  167 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 233 insertions(+), 10 deletions(-)

diffs (324 lines):

diff -r 9ca2feb6cc84 -r 428bf1c84a3d trytond/model/modelsql.py
--- a/trytond/model/modelsql.py Mon Oct 04 18:52:01 2021 +0200
+++ b/trytond/model/modelsql.py Wed Oct 06 17:20:40 2021 +0200
@@ -1282,7 +1282,7 @@
             raise AccessError(msg)
 
     @classmethod
-    def __search_query(cls, domain, count, query):
+    def __search_query(cls, domain, count, query, order):
         pool = Pool()
         Rule = pool.get('ir.rule')
 
@@ -1296,6 +1296,34 @@
                     local_domains.insert(0, 'OR')
                     joined_domains.append(local_domains)
 
+        def get_local_columns(order_exprs):
+            local_columns = []
+            for order_expr in order_exprs:
+                if (isinstance(order_expr, Column)
+                        and isinstance(order_expr._from, Table)
+                        and order_expr._from._name == cls._table):
+                    local_columns.append(order_expr._name)
+                else:
+                    raise NotImplementedError
+            return local_columns
+
+        # The UNION optimization needs the columns used to order the query
+        extra_columns = set()
+        if order and joined_domains:
+            tables = {
+                None: (cls.__table__(), None),
+                }
+            for oexpr, otype in order:
+                fname = oexpr.partition('.')[0]
+                field = cls._fields[fname]
+                field_orders = field.convert_order(oexpr, tables, cls)
+                try:
+                    order_columns = get_local_columns(field_orders)
+                    extra_columns.update(order_columns)
+                except NotImplementedError:
+                    joined_domains = None
+                    break
+
         # In case the search uses subqueries it's more efficient to use a UNION
         # of queries than using clauses with some JOIN because databases can
         # used indexes
@@ -1309,8 +1337,9 @@
                     expression &= domain_exp
                 main_table, _ = tables[None]
                 table = convert_from(None, tables)
-                columns = cls.__searched_columns(
-                    main_table, not count and not query)
+                columns = cls.__searched_columns(main_table,
+                    eager=not count and not query,
+                    extra_columns=extra_columns)
                 union_tables.append(table.select(
                         *columns, where=expression))
             expression = None
@@ -1327,20 +1356,30 @@
         return tables, expression
 
     @classmethod
-    def __searched_columns(cls, table, eager=False, history=False):
+    def __searched_columns(
+            cls, table, *, eager=False, history=False, extra_columns=None):
+        if extra_columns is None:
+            extra_columns = []
+        else:
+            extra_columns = sorted(extra_columns - {'id', '__id', '_datetime'})
         columns = [table.id.as_('id')]
         if (cls._history and Transaction().context.get('_datetime')
                 and (eager or history)):
             columns.append(
                 Coalesce(table.write_date, table.create_date).as_('_datetime'))
             columns.append(Column(table, '__id').as_('__id'))
+        for column_name in extra_columns:
+            field = cls._fields[column_name]
+            sql_column = field.sql_column(table).as_(column_name)
+            columns.append(sql_column)
         if eager:
             columns += [f.sql_column(table).as_(n)
-                for n, f in cls._fields.items()
+                for n, f in sorted(cls._fields.items())
                 if not hasattr(f, 'get')
-                and n != 'id'
-                and not getattr(f, 'translate', False)
-                and f.loading == 'eager']
+                    and n not in extra_columns
+                    and n != 'id'
+                    and not getattr(f, 'translate', False)
+                    and f.loading == 'eager']
             if not callable(cls.table_query):
                 sql_type = fields.Char('timestamp').sql_type().base
                 columns += [Extract('EPOCH',
@@ -1389,7 +1428,7 @@
         super(ModelSQL, cls).search(
             domain, offset=offset, limit=limit, order=order, count=count)
 
-        tables, expression = cls.__search_query(domain, count, query)
+        tables, expression = cls.__search_query(domain, count, query, order)
 
         main_table, _ = tables[None]
         if count:
@@ -1401,7 +1440,7 @@
         order_by = cls.__search_order(order, tables)
         # compute it here because __search_order might modify tables
         table = convert_from(None, tables)
-        columns = cls.__searched_columns(main_table, not query)
+        columns = cls.__searched_columns(main_table, eager=not query)
         select = table.select(
             *columns, where=expression, limit=limit, offset=offset,
             order_by=order_by)
diff -r 9ca2feb6cc84 -r 428bf1c84a3d trytond/tests/modelsql.py
--- a/trytond/tests/modelsql.py Mon Oct 04 18:52:01 2021 +0200
+++ b/trytond/tests/modelsql.py Wed Oct 06 17:20:40 2021 +0200
@@ -83,6 +83,22 @@
     name = fields.Char("Name")
 
 
+class ModelSQLSearchOR2Union(ModelSQL):
+    "ModelSQL Search OR to UNION optimization"
+    __name__ = 'test.modelsql.search.or2union'
+    name = fields.Char("Name")
+    target = fields.Many2One('test.modelsql.read.target', "Target")
+    targets = fields.One2Many('test.modelsql.read.target', 'parent', "Targets")
+    reference = fields.Reference(
+        "Reference", [(None, ""), ('test.modelsql.read.target', "Target")])
+    integer = fields.Integer("Integer")
+
+    @classmethod
+    def order_integer(cls, tables):
+        table, _ = tables[None]
+        return [table.integer + 1]
+
+
 class ModelSQLForeignKey(DeactivableMixin, ModelSQL):
     "ModelSQL Foreign Key"
     __name__ = 'test.modelsql.fk'
@@ -178,6 +194,7 @@
         ModelSQLOne2Many,
         ModelSQLOne2ManyTarget,
         ModelSQLSearch,
+        ModelSQLSearchOR2Union,
         ModelSQLForeignKey,
         ModelSQLForeignKeyTarget,
         NullOrder,
diff -r 9ca2feb6cc84 -r 428bf1c84a3d trytond/tests/test_modelsql.py
--- a/trytond/tests/test_modelsql.py    Mon Oct 04 18:52:01 2021 +0200
+++ b/trytond/tests/test_modelsql.py    Wed Oct 06 17:20:40 2021 +0200
@@ -678,6 +678,173 @@
             result_without_split)
 
     @with_transaction()
+    def test_search_or_to_union_order_eager_field(self):
+        """
+        Searching for 'OR'-ed domain mixed with ordering on an eager field
+        """
+        pool = Pool()
+        Model = pool.get('test.modelsql.search.or2union')
+        Target = pool.get('test.modelsql.read.target')
+
+        target_a, target_b, target_c = Target.create([
+                {'name': 'A'}, {'name': 'B'}, {'name': 'C'},
+                ])
+        model_a, model_b, model_c = Model.create([{
+                    'name': 'A',
+                    'target': target_a,
+                    }, {
+                    'name': 'B',
+                    'target': target_b,
+                    }, {
+                    'name': 'C',
+                    'target': target_c,
+                    'targets': [('create', [{
+                                    'name': 'C.A',
+                                    }]),
+                        ],
+                    }])
+
+        domain = ['OR',
+            ('name', 'ilike', '%A%'),
+            ('targets.name', 'ilike', '%A'),
+            ]
+        self.assertEqual(
+            Model.search(domain, order=[('name', 'ASC')]),
+            [model_a, model_c])
+        self.assertEqual(
+            Model.search(domain, order=[('name', 'DESC')]),
+            [model_c, model_a])
+        self.assertIn(
+            'UNION',
+            str(Model.search(domain, order=[('name', 'ASC')], query=True)))
+
+    @with_transaction()
+    def test_search_or_to_union_order_lazy_field(self):
+        """
+        Searching for 'OR'-ed domain mixed with ordering on a lazy field
+        """
+        pool = Pool()
+        Model = pool.get('test.modelsql.search.or2union')
+        Target = pool.get('test.modelsql.read.target')
+
+        target_a, target_b, target_c = Target.create([
+                {'name': 'A'}, {'name': 'B'}, {'name': 'C'},
+                ])
+        model_a, model_b, model_c = Model.create([{
+                    'name': 'A',
+                    'reference': str(target_a),
+                    }, {
+                    'name': 'B',
+                    'reference': str(target_b),
+                    }, {
+                    'name': 'C',
+                    'reference': str(target_c),
+                    'targets': [('create', [{
+                                    'name': 'C.A',
+                                    }]),
+                        ],
+                    }])
+
+        domain = ['OR',
+            ('name', 'ilike', '%A%'),
+            ('targets.name', 'ilike', '%A'),
+            ]
+        self.assertEqual(
+            Model.search(domain, order=[('reference', 'ASC')]),
+            [model_a, model_c])
+        self.assertEqual(
+            Model.search(domain, order=[('reference', 'DESC')]),
+            [model_c, model_a])
+        self.assertIn(
+            'UNION', str(Model.search(
+                    domain, order=[('reference', 'ASC')], query=True)))
+
+    @with_transaction()
+    def test_search_or_to_union_order_dotted_notation(self):
+        """
+        Searching for 'OR'-ed domain mixed with ordering on dotted field
+        """
+        pool = Pool()
+        Model = pool.get('test.modelsql.search.or2union')
+        Target = pool.get('test.modelsql.read.target')
+
+        target_a, target_b, target_c = Target.create([
+                {'name': 'A'}, {'name': 'B'}, {'name': 'C'},
+                ])
+        model_a, model_b, model_c = Model.create([{
+                    'name': 'A',
+                    'target': target_a,
+                    }, {
+                    'name': 'B',
+                    'target': target_b,
+                    }, {
+                    'name': 'C',
+                    'target': target_c,
+                    'targets': [('create', [{
+                                    'name': 'C.A',
+                                    }]),
+                        ],
+                    }])
+
+        domain = ['OR',
+            ('name', 'ilike', '%A%'),
+            ('targets.name', 'ilike', '%A'),
+            ]
+        self.assertEqual(
+            Model.search(domain, order=[('target.name', 'ASC')]),
+            [model_a, model_c])
+        self.assertEqual(
+            Model.search(domain, order=[('target.name', 'DESC')]),
+            [model_c, model_a])
+        self.assertNotIn(
+            'UNION', str(Model.search(
+                    domain, order=[('target.name', 'ASC')], query=True)))
+
+    @with_transaction()
+    def test_search_or_to_union_order_function(self):
+        """
+        Searching for 'OR'-ed domain mixed with ordering on a function
+        """
+        pool = Pool()
+        Model = pool.get('test.modelsql.search.or2union')
+        Target = pool.get('test.modelsql.read.target')
+
+        target_a, target_b, target_c = Target.create([
+                {'name': 'A'}, {'name': 'B'}, {'name': 'C'},
+                ])
+        model_a, model_b, model_c = Model.create([{
+                    'name': 'A',
+                    'target': target_a,
+                    'integer': 0,
+                    }, {
+                    'name': 'B',
+                    'target': target_b,
+                    'integer': 1,
+                    }, {
+                    'name': 'C',
+                    'target': target_c,
+                    'integer': 2,
+                    'targets': [('create', [{
+                                    'name': 'C.A',
+                                    }]),
+                        ],
+                    }])
+
+        domain = ['OR',
+            ('name', 'ilike', '%A%'),
+            ('targets.name', 'ilike', '%A'),
+            ]
+        self.assertEqual(
+            Model.search(domain, order=[('integer', 'ASC')]),
+            [model_a, model_c])
+        self.assertEqual(
+            Model.search(domain, order=[('integer', 'DESC')]),
+            [model_c, model_a])
+        self.assertNotIn(
+            'UNION', str(Model.search(
+                    domain, order=[('integer', 'ASC')], query=True)))
+
+    @with_transaction()
     def test_search_or_to_union_no_local_clauses(self):
         """
         Test searching for 'OR'-ed domain without local clauses

Reply via email to