[FIX] core: fix search domain containing empty False None

For non-translated field, when search(['name', 'in', list_right])
The behavior of False and the behavior of None in list_right should be the same.

Translated fields need customized process for 'in' operator

X-original-commit: a9a3c666f08c5612432dba40a69c7cfdc54ba96e
Part-of: odoo/odoo#103031
This commit is contained in:
Chong Wang (cwg)
2022-10-11 14:10:16 +02:00
parent 105e0b9ef2
commit 25e4a53753
5 changed files with 109 additions and 11 deletions
+3 -3
View File
@@ -1171,7 +1171,7 @@ class TestQueries(TransactionCase):
with self.assertQueries(['''
SELECT "res_partner_title".id
FROM "res_partner_title"
WHERE COALESCE("res_partner_title"."name"->>'fr_FR', "res_partner_title"."name"->>'en_US') like %s
WHERE (COALESCE("res_partner_title"."name"->>'fr_FR', "res_partner_title"."name"->>'en_US') like %s)
ORDER BY COALESCE("res_partner_title"."name"->>'fr_FR', "res_partner_title"."name"->>'en_US')
''']):
Model.search([('name', 'like', 'foo')])
@@ -1221,7 +1221,7 @@ class TestQueries(TransactionCase):
SELECT "ir_model".id
FROM "ir_model"
WHERE (
"ir_model"."name"->>'en_US' ILIKE %s
("ir_model"."name"->>'en_US' ILIKE %s)
OR ("ir_model"."model"::text ILIKE %s)
)
ORDER BY "ir_model"."model"
@@ -1234,7 +1234,7 @@ class TestQueries(TransactionCase):
SELECT "ir_model".id
FROM "ir_model"
WHERE (
"ir_model"."name"->>'en_US' NOT ILIKE %s
("ir_model"."name" is NULL OR "ir_model"."name"->>'en_US' not ilike %s)
AND (("ir_model"."model"::text NOT ILIKE %s) OR "ir_model"."model" IS NULL)
)
ORDER BY "ir_model"."model"
@@ -1737,3 +1737,9 @@ class IndexedTranslation(models.Model):
_description = 'A model to indexed translated fields'
name = fields.Text('Name trigram', translate=True, index='trigram')
class EmptyChar(models.Model):
_name = 'test_new_api.empty_char'
_description = 'A model to test emtpy char'
name = fields.Char('Name')
@@ -100,3 +100,4 @@ access_test_new_api_related_translation_1,access_test_new_api_related_translatio
access_test_new_api_related_translation_2,access_test_new_api_related_translation_2,model_test_new_api_related_translation_2,,1,1,1,1
access_test_new_api_related_translation_3,access_test_new_api_related_translation_3,model_test_new_api_related_translation_3,,1,1,1,1
access_test_new_api_indexed_translation,access_test_new_api_indexed_translation,model_test_new_api_indexed_translation,,1,1,1,1
access_test_new_api_empty_char,access_test_new_api_empty_char,model_test_new_api_empty_char,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
100 access_test_new_api_related_translation_2 access_test_new_api_related_translation_2 model_test_new_api_related_translation_2 1 1 1 1
101 access_test_new_api_related_translation_3 access_test_new_api_related_translation_3 model_test_new_api_related_translation_3 1 1 1 1
102 access_test_new_api_indexed_translation access_test_new_api_indexed_translation model_test_new_api_indexed_translation 1 1 1 1
103 access_test_new_api_empty_char access_test_new_api_empty_char model_test_new_api_empty_char 1 1 1 1
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from itertools import combinations
from odoo.tests import common
@@ -39,3 +41,69 @@ class test_domain(common.TransactionCase):
self.assertEqual(eq_1+eq_2, all_bool, 'True + False != all')
self.assertEqual(neq_1+neq_2, all_bool, 'not True + not False != all')
def test_empty_char(self):
EmptyChar = self.env['test_new_api.empty_char']
EmptyChar.create([
{'name': 'name'},
{'name': ''},
{'name': False},
])
self.assertListEqual(EmptyChar.search([('name', '=', 'name')]).mapped('name'), ['name'])
self.assertListEqual(EmptyChar.search([('name', '!=', 'name')]).mapped('name'), ['', False])
self.assertListEqual(EmptyChar.search([('name', 'ilike', 'name')]).mapped('name'), ['name'])
self.assertListEqual(EmptyChar.search([('name', 'not ilike', 'name')]).mapped('name'), ['', False])
self.assertListEqual(EmptyChar.search([('name', '=', '')]).mapped('name'), [''])
self.assertListEqual(EmptyChar.search([('name', '!=', '')]).mapped('name'), ['name'])
self.assertListEqual(EmptyChar.search([('name', 'ilike', '')]).mapped('name'), ['name', '', False])
self.assertListEqual(EmptyChar.search([('name', 'not ilike', '')]).mapped('name'), [False])
self.assertListEqual(EmptyChar.search([('name', '=', False)]).mapped('name'), [False])
self.assertListEqual(EmptyChar.search([('name', '!=', False)]).mapped('name'), ['name', ''])
self.assertListEqual(EmptyChar.search([('name', 'ilike', False)]).mapped('name'), ['name', '', False])
self.assertListEqual(EmptyChar.search([('name', 'not ilike', False)]).mapped('name'), [False])
values = ['name', '', False]
for length in range(len(values) + 1):
for subset in combinations(values, length):
sublist = list(subset)
self.assertListEqual(EmptyChar.search([('name', 'in', sublist)]).mapped('name'), sublist)
sublist_remained = [v for v in values if v not in subset]
self.assertListEqual(EmptyChar.search([('name', 'not in', sublist)]).mapped('name'), sublist_remained)
def test_empty_translation(self):
records_en = self.env['test_new_api.indexed_translation'].with_context(lang='en_US').create([
{'name': 'English'},
{'name': 'English'},
{'name': 'English'},
])
self.env['res.lang']._activate_lang('fr_FR')
records_fr = records_en.with_context(lang='fr_FR')
records_fr[0].name = 'name'
records_fr[1].name = ''
records_fr[2].name = False
self.assertListEqual(records_fr.search([('name', '=', 'name')]).mapped('name'), ['name'])
self.assertListEqual(records_fr.search([('name', '!=', 'name')]).mapped('name'), ['', False])
self.assertListEqual(records_fr.search([('name', 'ilike', 'name')]).mapped('name'), ['name'])
self.assertListEqual(records_fr.search([('name', 'not ilike', 'name')]).mapped('name'), ['', False])
self.assertListEqual(records_fr.search([('name', '=', '')]).mapped('name'), [''])
self.assertListEqual(records_fr.search([('name', '!=', '')]).mapped('name'), ['name'])
self.assertListEqual(records_fr.search([('name', 'ilike', '')]).mapped('name'), ['name', '', False])
self.assertListEqual(records_fr.search([('name', 'not ilike', '')]).mapped('name'), [False])
self.assertListEqual(records_fr.search([('name', '=', False)]).mapped('name'), [False])
self.assertListEqual(records_fr.search([('name', '!=', False)]).mapped('name'), ['name', ''])
self.assertListEqual(records_fr.search([('name', 'ilike', False)]).mapped('name'), ['name', '', False])
self.assertListEqual(records_fr.search([('name', 'not ilike', False)]).mapped('name'), [False])
values = ['name', '', False]
for length in range(len(values) + 1):
for subset in combinations(values, length):
sublist = list(subset)
self.assertListEqual(records_fr.search([('name', 'in', sublist)]).mapped('name'), sublist)
sublist_remained = [v for v in values if v not in subset]
self.assertListEqual(records_fr.search([('name', 'not in', sublist)]).mapped('name'), sublist_remained)
+31 -8
View File
@@ -960,11 +960,18 @@ class expression(object):
expr, params = self.__leaf_to_sql(leaf, model, alias)
push_result(expr, params)
elif field.translate and right:
elif field.translate and isinstance(right, str):
sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
expr = ''
params = []
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
if not need_wildcard:
right = field.convert_to_column(right, model, validate=False).adapted['en_US']
if (need_wildcard and not right) or (right and sql_operator in NEGATIVE_TERM_OPERATORS):
expr += f'"{alias}"."{left}" is NULL OR '
if self._has_trigram and field.index == 'trigram' and sql_operator in ('=', 'like', 'ilike'):
# a prefilter using trigram index to speed up '=', 'like', 'ilike'
# '!=', '<=', '<', '>', '>=', 'in', 'not in', 'not like', 'not ilike' cannot use this trick
@@ -977,7 +984,7 @@ class expression(object):
_unaccent = self._unaccent(field)
_left = _unaccent(f'''jsonb_path_query_array("{alias}"."{left}", '$.*')::text''')
_sql_operator = 'like' if sql_operator == '=' else sql_operator
expr = f"{_left} {_sql_operator} {_unaccent('%s')} AND "
expr += f"{_left} {_sql_operator} {_unaccent('%s')} AND "
params.append(_right)
unaccent = self._unaccent(field) if sql_operator.endswith('like') else lambda x: x
@@ -987,16 +994,32 @@ class expression(object):
else:
left = unaccent(f'''COALESCE("{alias}"."{left}"->>'{lang}', "{alias}"."{left}"->>'en_US')''')
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
if need_wildcard:
right = '%%%s%%' % right
if sql_operator in ('in', 'not in'):
right = tuple(right)
right = f'%{right}%'
expr += f"{left} {sql_operator} {unaccent('%s')}"
params.append(right)
push_result(expr, params)
push_result(f'({expr})', params)
elif field.translate and operator in ['in', 'not in'] and isinstance(right, (list, tuple)):
params = [it for it in right if it is not False and it is not None]
check_null = len(params) < len(right)
if params:
params = [field.convert_to_column(p, model, validate=False).adapted['en_US'] for p in params]
lang = model.env.lang or 'en_US'
if lang == 'en_US':
query = f'''("{alias}"."{left}"->>'en_US' {operator} %s)'''
else:
query = f'''(COALESCE("{alias}"."{left}"->>'{lang}', "{alias}"."{left}"->>'en_US') {operator} %s)'''
params = [tuple(params)]
else:
# The case for (left, 'in', []) or (left, 'not in', []).
query = 'FALSE' if operator == 'in' else 'TRUE'
if (operator == 'in' and check_null) or (operator == 'not in' and not check_null):
query = '(%s OR %s."%s" IS NULL)' % (query, alias, left)
elif operator == 'not in' and check_null:
query = '(%s AND %s."%s" IS NOT NULL)' % (query, alias, left) # needed only for TRUE.
push_result(query, params)
else:
expr, params = self.__leaf_to_sql(leaf, model, alias)
push_result(expr, params)
@@ -1058,7 +1081,7 @@ class expression(object):
params = [it for it in (True, False) if it in right]
check_null = False in right
else:
params = [it for it in right if it != False]
params = [it for it in right if it is not False and it is not None]
check_null = len(params) < len(right)
if params:
if left != 'id':