From 213b6885312f3da3bf5bff995861758a1afcde76 Mon Sep 17 00:00:00 2001 From: "Victor Piryns (pivi)" <pivi@odoo.com> Date: Wed, 13 Sep 2023 14:52:04 +0200 Subject: [PATCH] [FIX] project: improve tags name_search robustness Description: The commit 05855b6bb6d8a22ceb1f8b25332a146eed912660 introduced a fast-path for the name_search on tags from the form view of tasks. But the implementation was too complex and wasn't handling correctly all possible operators (it just had a fallback on the default parent name_search implementation). The aim is to simplify the code for better robustness and maintainability long term. Fix: Convert the query to it's equivalent python code. It has a slight performance regression than the previous implementation (we are making 3 queries worse-case instead of 1), but the regression is non-significant and not critical. Affected versions: 16.0 up to master Reference: task-3503721 closes odoo/odoo#135323 Signed-off-by: Audric Onockx (auon) <auon@odoo.com> --- addons/project/models/project.py | 79 ++++++++++---------------------- 1 file changed, 24 insertions(+), 55 deletions(-) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 929a8ba0a76c..06cecffe502b 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -2758,58 +2758,27 @@ class ProjectTags(models.Model): @api.model def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None): - if self.env.context.get('project_id') and operator == 'ilike': - # `args` has the form of the default filter ['!', ['id', 'in', <ids>]] - # passed to exclude already selected tags -> exclude them in our query too - excluded_ids = list(args[1][2]) \ - if args and len(args) == 2 and args[0] == '!' and len(args[1]) == 3 and args[1][:2] == ["id", "in"] \ - else [] - # UNION ALL is lazy evaluated, if the first query has enough results, - # the second is not executed (just planned). - query = """ - WITH query_tags_in_tasks AS ( - SELECT tags.id, COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') AS name, 1 AS sequence - FROM project_tags AS tags - JOIN ( - SELECT project_tags_id - FROM project_tags_project_task_rel AS rel - JOIN project_task AS task - ON task.project_id = %(project_id)s - AND task.id = rel.project_task_id - ORDER BY task.id DESC - LIMIT 1000 -- arbitrary limit to speed up lookup on huge projects (fallback below on global scope) - ) AS tags__tasks_ids - ON tags__tasks_ids.project_tags_id = tags.id - WHERE tags.id != ALL(%(excluded_ids)s) - AND COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') ILIKE %(search_term)s - GROUP BY 1, 2, 3 -- faster than a distinct - LIMIT %(limit)s - ), query_all_tags AS ( - SELECT tags.id, COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') AS name, 2 AS sequence - FROM project_tags AS tags - WHERE tags.id != ALL(%(excluded_ids)s) - AND tags.id NOT IN (SELECT id FROM query_tags_in_tasks) - AND COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') ILIKE %(search_term)s - LIMIT %(limit)s - ) - SELECT id FROM ( - SELECT id, name, sequence - FROM query_tags_in_tasks - UNION ALL - SELECT id, name, sequence - FROM query_all_tags - LIMIT %(limit)s - ) AS tags - ORDER BY sequence, name - """ - params = { - 'project_id': self.env.context.get('project_id'), - 'excluded_ids': excluded_ids, - 'limit': limit, - 'lang': self.env.context.get('lang', 'en_US'), - 'search_term': '%' + name + '%', - } - self.env.cr.execute(query, params) - return [row[0] for row in self.env.cr.fetchall()] - else: - return super()._name_search(name, args, operator, limit, name_get_uid) + ids = [] + if not (name == '' and operator in ('like', 'ilike')): + args += [('name', operator, name)] + if self.env.context.get('project_id'): + # optimisation for large projects, we look first for tags present on the last 1000 tasks of said project. + # when not enough results are found, we complete them with a fallback on a regular search + self.env.cr.execute(""" + SELECT DISTINCT project_tasks_tags.id + FROM ( + SELECT rel.project_tags_id AS id + FROM project_tags_project_task_rel AS rel + JOIN project_task AS task + ON task.id=rel.project_task_id + AND task.project_id=%(project_id)s + ORDER BY task.id DESC + LIMIT 1000 + ) AS project_tasks_tags + """, {'project_id': self.env.context['project_id']}) + project_tasks_tags_domain = [('id', 'in', [row[0] for row in self.env.cr.fetchall()])] + # we apply the args and limit to the ids we've already found + ids += self.env['project.tags'].search(expression.AND([args, project_tasks_tags_domain]), limit=limit).ids + if len(ids) < limit: + ids += self.env['project.tags'].search(expression.AND([args, [('id', 'not in', ids)]]), limit=limit - len(ids)).ids + return ids -- GitLab