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