18.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:06:50 +02:00
parent d72e748793
commit 0a7ae8db93
337 changed files with 399651 additions and 232598 deletions

View file

@ -58,11 +58,8 @@ take a recursive form: a domain is not a valid second-level operand.
Unaccent - Accent-insensitive search
OpenERP will use the SQL function 'unaccent' when available for the
'ilike' and 'not ilike' operators, and enabled in the configuration.
Normally the 'unaccent' function is obtained from `the PostgreSQL
'unaccent' contrib module
<http://developer.postgresql.org/pgdocs/postgres/unaccent.html>`_.
Odoo will use the SQL function 'unaccent' when available for the
'ilike', 'not ilike' and '=ilike' operators, and enabled in the configuration.
.. todo: The following explanation should be moved in some external
installation guide
@ -117,19 +114,16 @@ import collections
import collections.abc
import json
import logging
import pytz
import reprlib
import traceback
import warnings
from datetime import date, datetime, time
import psycopg2.sql
import odoo.modules
from odoo.models import BaseModel, check_property_field_value_name
from odoo.tools import (
pycompat, pattern_to_translated_trigram_pattern, value_to_translated_trigram_pattern,
Query, SQL,
)
from odoo.models import check_property_field_value_name, READ_GROUP_NUMBER_GRANULARITY
from odoo.tools import Query, SQL, get_lang
from odoo.tools.sql import pattern_to_translated_trigram_pattern, value_to_translated_trigram_pattern
# Domain operators.
NOT_OPERATOR = '!'
@ -142,8 +136,6 @@ DOMAIN_OPERATORS = (NOT_OPERATOR, OR_OPERATOR, AND_OPERATOR)
# for consistency. This list doesn't contain '<>' as it is simplified to '!='
# by the normalize_operator() function (so later part of the code deals with
# only one representation).
# Internals (i.e. not available to the user) 'inselect' and 'not inselect'
# operators are also used. In this case its right operand has the form (subselect, params).
TERM_OPERATORS = ('=', '!=', '<=', '<', '>', '>=', '=?', '=like', '=ilike',
'like', 'not like', 'ilike', 'not ilike', 'in', 'not in',
'child_of', 'parent_of', 'any', 'not any')
@ -175,6 +167,9 @@ TERM_OPERATORS_NEGATION = {
'any': 'not any',
'not any': 'any',
}
WILDCARD_OPERATORS = ('like', 'ilike', 'not like', 'not ilike')
ANY_IN = {'any': 'in', 'not any': 'not in'}
TRUE_LEAF = (1, '=', 1)
@ -292,16 +287,14 @@ def combine(operator, unit, zero, domains):
"""
result = []
count = 0
if domains == [unit]:
return unit
for domain in domains:
domain = normalize_domain(domain)
if domain == unit:
continue
if domain == zero:
return zero
if domain:
result += normalize_domain(domain)
count += 1
result += domain
count += 1
result = [operator] * (count - 1) + result
return result or unit
@ -322,7 +315,7 @@ def distribute_not(domain):
Because we don't use SQL semantic for processing a 'left not in right'
query (i.e. our 'not in' is not simply translated to a SQL 'not in'),
it means that a '! left in right' can not be simply processed
by __leaf_to_sql by first emitting code for 'left in right' then wrapping
by model._condition_to_sql by first emitting code for 'left in right' then wrapping
the result with 'not (...)', as it would result in a 'not in' at the SQL
level.
@ -347,7 +340,9 @@ def distribute_not(domain):
if is_leaf(token):
if negate:
left, operator, right = token
if operator in TERM_OPERATORS_NEGATION:
if operator in TERM_OPERATORS_NEGATION and (isinstance(left, int) or "." not in left):
# rewrite using the negated operator, except for relationship traversal
# because not ('a.b', '=', x) should become ('a', 'not any', ('b', '=', x))
if token in (TRUE_LEAF, FALSE_LEAF):
result.append(FALSE_LEAF if token == TRUE_LEAF else TRUE_LEAF)
else:
@ -436,6 +431,13 @@ def _tree_from_domain(domain):
def _tree_not(tree):
""" Negate a tree node. """
if tree[0] == '=?':
# already update operator '=?' here, so that '!' is distributed correctly
assert len(tree) == 3
if tree[2]:
tree = ('=', tree[1], tree[2])
else:
return ('?', False)
if tree[0] == '?':
return ('?', not tree[1])
if tree[0] == '!':
@ -720,7 +722,7 @@ def is_operator(element):
return isinstance(element, str) and element in DOMAIN_OPERATORS
def is_leaf(element, internal=False):
def is_leaf(element):
""" Test whether an object is a valid domain term:
- is a list or tuple
@ -728,14 +730,10 @@ def is_leaf(element, internal=False):
- second element if a valid op
:param tuple element: a leaf in form (left, operator, right)
:param bool internal: allow or not the 'inselect' internal operator
in the term. This should be always left to False.
Note: OLD TODO change the share wizard to use this function.
"""
INTERNAL_OPS = TERM_OPERATORS + ('<>',)
if internal:
INTERNAL_OPS += ('inselect', 'not inselect')
return (isinstance(element, tuple) or isinstance(element, list)) \
and len(element) == 3 \
and element[1] in INTERNAL_OPS \
@ -747,8 +745,8 @@ def is_boolean(element):
return element == TRUE_LEAF or element == FALSE_LEAF
def check_leaf(element, internal=False):
if not is_operator(element) and not is_leaf(element, internal):
def check_leaf(element):
if not is_operator(element) and not is_leaf(element):
raise ValueError("Invalid leaf %s" % str(element))
@ -756,17 +754,12 @@ def check_leaf(element, internal=False):
# SQL utils
# --------------------------------------------------
def _unaccent_wrapper(x):
if isinstance(x, SQL):
return SQL("unaccent(%s)", x)
if isinstance(x, psycopg2.sql.Composable):
return psycopg2.sql.SQL('unaccent({})').format(x)
return 'unaccent({})'.format(x)
def get_unaccent_wrapper(cr):
if odoo.registry(cr.dbname).has_unaccent:
return _unaccent_wrapper
return lambda x: x
warnings.warn(
"Since 18.0, deprecated method, use env.registry.unaccent instead",
DeprecationWarning, 2,
)
return odoo.modules.registry.Registry(cr.dbname).unaccent
class expression(object):
@ -790,7 +783,7 @@ class expression(object):
:attr result: the result of the parsing, as a pair (query, params)
:attr query: Query object holding the final result
"""
self._unaccent_wrapper = get_unaccent_wrapper(model._cr)
self._unaccent = model.pool.unaccent
self._has_trigram = model.pool.has_trigram
self.root_model = model
self.root_alias = alias or model._table
@ -799,16 +792,11 @@ class expression(object):
self.expression = domain_combine_anies(domain, model)
# this object handles all the joins
self.query = Query(model.env.cr, model._table, model._table_query) if query is None else query
self.query = Query(model.env, model._table, model._table_sql) if query is None else query
# parse the domain expression
self.parse()
def _unaccent(self, field):
if getattr(field, 'unaccent', False):
return self._unaccent_wrapper
return lambda x: x
# ----------------------------------------
# Parsing
# ----------------------------------------
@ -886,7 +874,7 @@ class expression(object):
return list({
rid
for name in names
for rid in comodel._name_search(name, [], 'ilike')
for rid in comodel._search([('display_name', 'ilike', name)])
})
return list(value)
@ -956,10 +944,10 @@ class expression(object):
""" Pop a leaf to process. """
return stack.pop()
def push(leaf, model, alias, internal=False):
def push(leaf, model, alias):
""" Push a leaf to be processed right after. """
leaf = normalize_leaf(leaf)
check_leaf(leaf, internal)
check_leaf(leaf)
stack.append((leaf, model, alias))
def pop_result():
@ -997,34 +985,44 @@ class expression(object):
push_result(SQL("(%s OR %s)", pop_result(), pop_result()))
continue
if is_boolean(leaf):
push_result(self.__leaf_to_sql(leaf, model, alias))
if leaf == TRUE_LEAF:
push_result(SQL("TRUE"))
continue
if leaf == FALSE_LEAF:
push_result(SQL("FALSE"))
continue
# Get working variables
left, operator, right = leaf
path = left.split('.', 1)
field = model._fields.get(path[0])
comodel = model.env.get(getattr(field, 'comodel_name', None))
field = model._fields[path[0]]
if field.type == 'many2one':
comodel = model.env[field.comodel_name].with_context(active_test=False)
elif field.type in ('one2many', 'many2many'):
comodel = model.env[field.comodel_name].with_context(**field.context)
# ----------------------------------------
# FIELD NOT FOUND
# -> from inherits'd fields -> work on the related model, and add
# a join condition
# -> ('id', 'child_of', '..') -> use a 'to_ids'
# -> but is one on the _log_access special fields, add directly to
# result
# TODO: make these fields explicitly available in self.columns instead!
# -> else: crash
# ----------------------------------------
if (
field.company_dependent
and field.index == 'btree_not_null'
and not isinstance(right, (SQL, Query))
and not (field.type in ('datetime', 'date') and len(path) > 1) # READ_GROUP_NUMBER_GRANULARITY is not supported
and model.env['ir.default']._evaluate_condition_with_fallback(model._name, leaf) is False
):
push('&', model, alias)
sql_col_is_not_null = SQL('%s.%s IS NOT NULL', SQL.identifier(alias), SQL.identifier(field.name))
push_result(sql_col_is_not_null)
if field.inherited:
parent_model = model.env[field.related_field.model_name]
parent_fname = model._inherits[parent_model._name]
parent_alias = self.query.left_join(
alias, parent_fname, parent_model._table, 'id', parent_fname,
)
# LEFT JOIN parent_model._table AS parent_alias ON alias.parent_fname = parent_alias.id
parent_alias = self.query.make_alias(alias, parent_fname)
self.query.add_join('LEFT JOIN', parent_alias, parent_model._table, SQL(
"%s = %s",
model._field_to_sql(alias, parent_fname, self.query),
SQL.identifier(parent_alias, 'id'),
))
push(leaf, parent_model, parent_alias)
elif left == 'id' and operator in HIERARCHY_FUNCS:
@ -1063,12 +1061,6 @@ class expression(object):
))
else:
if operator in ('like', 'ilike', 'not like', 'not ilike'):
right = f'%{pycompat.to_text(right)}%'
unaccent = self._unaccent(field)
else:
unaccent = lambda x: x
sql_field = model._field_to_sql(alias, field.name, self.query)
if operator in ('in', 'not in'):
@ -1078,10 +1070,15 @@ class expression(object):
sql_right = SQL("%s", json.dumps(right))
push_result(SQL(
"(%s (%s) %s (%s))",
sql_not, unaccent(sql_left), sql_operator, unaccent(sql_right),
sql_not, sql_left, sql_operator, sql_right,
))
elif isinstance(right, str):
if operator in ('ilike', 'not ilike'):
right = f'%{right}%'
unaccent = self._unaccent
else:
unaccent = lambda x: x # noqa: E731
sql_left = SQL("%s ->> %s", sql_field, property_name) # JSONified value
sql_operator = SQL_OPERATORS[operator]
sql_right = SQL("%s", right)
@ -1096,8 +1093,20 @@ class expression(object):
sql_right = SQL("%s", json.dumps(right))
push_result(SQL(
"((%s) %s (%s))",
unaccent(sql_left), sql_operator, unaccent(sql_right),
sql_left, sql_operator, sql_right,
))
elif field.type in ('datetime', 'date') and len(path) == 2:
if path[1] not in READ_GROUP_NUMBER_GRANULARITY:
raise ValueError(f'Error when processing the field {field!r}, the granularity {path[1]} is not supported. Only {", ".join(READ_GROUP_NUMBER_GRANULARITY.keys())} are supported')
sql_field = model._field_to_sql(alias, field.name, self.query)
if model._context.get('tz') in pytz.all_timezones_set and field.type == 'datetime':
sql_field = SQL("timezone(%s, timezone('UTC', %s))", model._context['tz'], sql_field)
if path[1] == 'day_of_week':
first_week_day = int(get_lang(model.env, model._context.get('tz')).week_start)
sql = SQL("mod(7 - %s + date_part(%s, %s)::int, 7) %s %s", first_week_day, READ_GROUP_NUMBER_GRANULARITY[path[1]], sql_field, SQL_OPERATORS[operator], right)
else:
sql = SQL('date_part(%s, %s) %s %s', READ_GROUP_NUMBER_GRANULARITY[path[1]], sql_field, SQL_OPERATORS[operator], right)
push_result(sql)
# ----------------------------------------
# PATH SPOTTED
@ -1114,9 +1123,12 @@ class expression(object):
elif operator in ('any', 'not any') and field.store and field.type == 'many2one' and field.auto_join:
# res_partner.state_id = res_partner__state_id.id
coalias = self.query.left_join(
alias, field.name, comodel._table, 'id', field.name,
)
coalias = self.query.make_alias(alias, field.name)
self.query.add_join('LEFT JOIN', coalias, comodel._table, SQL(
"%s = %s",
model._field_to_sql(alias, field.name, self.query),
SQL.identifier(coalias, 'id'),
))
if operator == 'not any':
right = ['|', ('id', '=', False), '!', *right]
@ -1127,7 +1139,7 @@ class expression(object):
elif operator in ('any', 'not any') and field.store and field.type == 'one2many' and field.auto_join:
# use a subquery bypassing access rules and business logic
domain = right + field.get_domain_list(model)
query = comodel.with_context(**field.context)._where_calc(domain)
query = comodel._where_calc(domain)
sql = query.subselect(
comodel._field_to_sql(comodel._table, field.inverse_name, query),
)
@ -1136,8 +1148,8 @@ class expression(object):
elif operator in ('any', 'not any') and field.store and field.auto_join:
raise NotImplementedError('auto_join attribute not supported on field %s' % field)
elif operator in ('any', 'not any') and field.store and field.type == 'many2one':
right_ids = comodel.with_context(active_test=False)._search(right)
elif operator in ('any', 'not any') and field.type == 'many2one':
right_ids = comodel._search(right)
if operator == 'any':
push((left, 'in', right_ids), model, alias)
else:
@ -1145,8 +1157,10 @@ class expression(object):
push(dom_leaf, model, alias)
# Making search easier when there is a left operand as one2many or many2many
elif operator in ('any', 'not any') and field.store and field.type in ('many2many', 'one2many'):
right_ids = comodel.with_context(**field.context)._search(right)
elif operator in ('any', 'not any') and field.type in ('many2many', 'one2many'):
domain = field.get_domain_list(model)
domain = AND([domain, right])
right_ids = comodel._search(domain)
push((left, ANY_IN[operator], right_ids), model, alias)
elif not field.store:
@ -1164,10 +1178,9 @@ class expression(object):
right = comodel._search([(path[1], operator, right)])
operator = 'in'
domain = field.determine_domain(model, operator, right)
model._flush_search(domain)
for elem in domain_combine_anies(domain, model):
push(elem, model, alias, internal=True)
push(elem, model, alias)
# -------------------------------------------------
# RELATIONAL FIELDS
@ -1179,7 +1192,7 @@ class expression(object):
if field.comodel_name != model._name:
dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=field.comodel_name)
else:
dom = HIERARCHY_FUNCS[operator]('id', ids2, model, parent=left)
dom = HIERARCHY_FUNCS[operator]('id', ids2, comodel, parent=left)
for dom_leaf in dom:
push(dom_leaf, model, alias)
@ -1194,7 +1207,7 @@ class expression(object):
if isinstance(right, str):
op2 = (TERM_OPERATORS_NEGATION[operator]
if operator in NEGATIVE_TERM_OPERATORS else operator)
ids2 = comodel._name_search(right, domain or [], op2)
ids2 = comodel._search(AND([domain or [], [('display_name', op2, right)]]))
elif isinstance(right, collections.abc.Iterable):
ids2 = right
else:
@ -1213,6 +1226,9 @@ class expression(object):
sql_inverse = comodel._field_to_sql(ids2.table, inverse_field.name, ids2)
if not inverse_field.required:
ids2.add_where(SQL("%s IS NOT NULL", sql_inverse))
if (inverse_field.company_dependent and inverse_field.index == 'btree_not_null'
and not inverse_field.get_company_dependent_fallback(comodel)):
ids2.add_where(SQL('%s IS NOT NULL', SQL.identifier(ids2.table, inverse_field.name)))
push_result(SQL(
"(%s %s %s)",
SQL.identifier(alias, 'id'),
@ -1232,7 +1248,7 @@ class expression(object):
# rewrite condition to match records with/without lines
sub_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
comodel_domain = [(inverse_field.name, '!=', False)]
query = comodel.with_context(active_test=False)._where_calc(comodel_domain)
query = comodel._where_calc(comodel_domain)
sql_inverse = comodel._field_to_sql(query.table, inverse_field.name, query)
sql = query.subselect(sql_inverse)
push(('id', sub_op, sql), model, alias)
@ -1255,21 +1271,16 @@ class expression(object):
ids2 = to_ids(right, comodel, leaf)
domain = HIERARCHY_FUNCS[operator]('id', ids2, comodel)
ids2 = comodel._search(domain)
# rewrite condition in terms of ids2
if comodel == model:
push(('id', 'in', ids2), model, alias)
else:
rel_alias = self.query.make_alias(alias, field.name)
push_result(SQL(
"EXISTS (SELECT 1 FROM %s AS %s WHERE %s = %s AND %s IN %s)",
SQL.identifier(rel_table),
SQL.identifier(rel_alias),
SQL.identifier(rel_alias, rel_id1),
SQL.identifier(alias, 'id'),
SQL.identifier(rel_alias, rel_id2),
tuple(ids2) or (None,),
))
rel_alias = self.query.make_alias(alias, field.name)
push_result(SQL(
"EXISTS (SELECT 1 FROM %s AS %s WHERE %s = %s AND %s IN %s)",
SQL.identifier(rel_table),
SQL.identifier(rel_alias),
SQL.identifier(rel_alias, rel_id1),
SQL.identifier(alias, 'id'),
SQL.identifier(rel_alias, rel_id2),
tuple(ids2) or (None,),
))
elif right is not False:
# determine ids2 in comodel
@ -1277,7 +1288,7 @@ class expression(object):
domain = field.get_domain_list(model)
op2 = (TERM_OPERATORS_NEGATION[operator]
if operator in NEGATIVE_TERM_OPERATORS else operator)
ids2 = comodel._name_search(right, domain or [], op2)
ids2 = comodel._search(AND([domain or [], [('display_name', op2, right)]]))
elif isinstance(right, collections.abc.Iterable):
ids2 = right
else:
@ -1329,7 +1340,7 @@ class expression(object):
if field.comodel_name != model._name:
dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=field.comodel_name)
else:
dom = HIERARCHY_FUNCS[operator]('id', ids2, model, parent=left)
dom = HIERARCHY_FUNCS[operator]('id', ids2, comodel, parent=left)
for dom_leaf in dom:
push(dom_leaf, model, alias)
@ -1348,16 +1359,17 @@ class expression(object):
operator = dict_op[operator]
elif isinstance(right, list) and operator in ('!=', '='): # for domain (FIELD,'=',['value1','value2'])
operator = dict_op[operator]
res_ids = comodel.with_context(active_test=False)._name_search(right, [], operator)
if operator in NEGATIVE_TERM_OPERATORS:
for dom_leaf in ('|', (left, 'in', res_ids), (left, '=', False)):
res_ids = comodel._search([('display_name', TERM_OPERATORS_NEGATION[operator], right)])
for dom_leaf in ('|', (left, 'not in', res_ids), (left, '=', False)):
push(dom_leaf, model, alias)
else:
res_ids = comodel._search([('display_name', operator, right)])
push((left, 'in', res_ids), model, alias)
else:
# right == [] or right == False and all other cases are handled by __leaf_to_sql()
push_result(self.__leaf_to_sql(leaf, model, alias))
# right == [] or right == False and all other cases are handled by _condition_to_sql()
push_result(model._condition_to_sql(alias, left, operator, right, self.query))
# -------------------------------------------------
# BINARY FIELDS STORED IN ATTACHMENT
@ -1399,80 +1411,38 @@ class expression(object):
right = datetime.combine(right, time.min)
push((left, operator, right), model, alias)
else:
push_result(self.__leaf_to_sql(leaf, model, alias))
push_result(model._condition_to_sql(alias, left, operator, right, self.query))
elif field.translate and isinstance(right, str) and left == field.name:
model_raw_trans = model.with_context(prefetch_langs=True)
sql_field = model_raw_trans._field_to_sql(alias, field.name, self.query)
elif field.translate and (isinstance(right, str) or right is False) and left == field.name and \
self._has_trigram and field.index == 'trigram' and operator in ('=', 'like', 'ilike', '=like', '=ilike'):
right = right or ''
sql_operator = SQL_OPERATORS[operator]
sql_exprs = []
need_wildcard = operator in WILDCARD_OPERATORS
if need_wildcard and not right:
push_result(SQL("FALSE") if operator in NEGATIVE_TERM_OPERATORS else SQL("TRUE"))
continue
push_result(model._condition_to_sql(alias, left, operator, right, self.query))
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']
right = field.convert_to_column(right, model, validate=False)
if (need_wildcard and not right) or (right and operator in NEGATIVE_TERM_OPERATORS):
sql_exprs.append(SQL("%s IS NULL OR", sql_field))
if self._has_trigram and field.index == 'trigram' and operator in ('=', 'like', 'ilike', '=like', '=ilike'):
# a prefilter using trigram index to speed up '=', 'like', 'ilike'
# '!=', '<=', '<', '>', '>=', 'in', 'not in', 'not like', 'not ilike' cannot use this trick
if operator == '=':
_right = value_to_translated_trigram_pattern(right)
else:
_right = pattern_to_translated_trigram_pattern(right)
if _right != '%':
_unaccent = self._unaccent(field)
_left = SQL("jsonb_path_query_array(%s, '$.*')::text", sql_field)
_sql_operator = SQL('LIKE') if operator == '=' else sql_operator
sql_exprs.append(SQL(
"%s %s %s AND",
_unaccent(_left),
_sql_operator,
_unaccent(SQL("%s", _right))
))
unaccent = self._unaccent(field) if operator.endswith('ilike') else lambda x: x
sql_left = model._field_to_sql(alias, field.name, self.query)
if need_wildcard:
right = f'%{right}%'
sql_exprs.append(SQL(
"%s %s %s",
unaccent(sql_left),
sql_operator,
unaccent(SQL("%s", right)),
))
push_result(SQL("(%s)", SQL(" ").join(sql_exprs)))
elif field.translate and operator in ['in', 'not in'] and isinstance(right, (list, tuple)) and left == field.name:
model_raw_trans = model.with_context(prefetch_langs=True)
sql_field = model_raw_trans._field_to_sql(alias, field.name, self.query)
sql_operator = SQL_OPERATORS[operator]
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]
langs = field.get_translation_fallback_langs(model.env)
sql_left_langs = [SQL("%s->>%s", sql_field, lang) for lang in langs]
if len(sql_left_langs) == 1:
sql_left = sql_left_langs[0]
else:
sql_left = SQL('COALESCE(%s)', SQL(', ').join(sql_left_langs))
sql = SQL("%s %s %s", sql_left, sql_operator, tuple(params))
# a prefilter using trigram index to speed up '=', 'like', 'ilike'
# '!=', '<=', '<', '>', '>=', 'in', 'not in', 'not like', 'not ilike' cannot use this trick
if operator == '=':
_right = value_to_translated_trigram_pattern(right)
else:
# The case for (left, 'in', []) or (left, 'not in', []).
sql = SQL("FALSE") if operator == 'in' else SQL("TRUE")
if (operator == 'in' and check_null) or (operator == 'not in' and not check_null):
sql = SQL("(%s OR %s IS NULL)", sql, sql_field)
elif operator == 'not in' and check_null:
sql = SQL("(%s AND %s IS NOT NULL)", sql, sql_field) # needed only for TRUE.
push_result(sql)
_right = pattern_to_translated_trigram_pattern(right)
if _right != '%':
# combine both generated SQL expressions (above and below) with an AND
push('&', model, alias)
sql_column = SQL('%s.%s', SQL.identifier(alias), SQL.identifier(field.name))
indexed_value = self._unaccent(SQL("jsonb_path_query_array(%s, '$.*')::text", sql_column))
_sql_operator = SQL('LIKE') if operator == '=' else sql_operator
push_result(SQL("%s %s %s", indexed_value, _sql_operator, self._unaccent(SQL("%s", _right))))
else:
push_result(self.__leaf_to_sql(leaf, model, alias))
push_result(model._condition_to_sql(alias, left, operator, right, self.query))
# ----------------------------------------
# END OF PARSING FULL DOMAIN
@ -1481,120 +1451,3 @@ class expression(object):
[self.result] = result_stack
self.query.add_where(self.result)
def __leaf_to_sql(self, leaf: tuple, model: BaseModel, alias: str) -> SQL:
left, operator, right = leaf
# final sanity checks - should never fail
assert operator in (TERM_OPERATORS + ('inselect', 'not inselect')), \
"Invalid operator %r in domain term %r" % (operator, leaf)
assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in model._fields, \
"Invalid field %r in domain term %r" % (left, leaf)
assert not isinstance(right, BaseModel), \
"Invalid value %r in domain term %r" % (right, leaf)
if leaf == TRUE_LEAF:
return SQL("TRUE")
if leaf == FALSE_LEAF:
return SQL("FALSE")
field = model._fields[left]
sql_field = model._field_to_sql(alias, left, self.query)
if operator == 'inselect':
subquery, subparams = right
return SQL("(%s IN (%s))", sql_field, SQL(subquery, *subparams))
if operator == 'not inselect':
subquery, subparams = right
return SQL("(%s NOT IN (%s))", sql_field, SQL(subquery, *subparams))
if operator == '=?':
if right is False or right is None:
# '=?' is a short-circuit that makes the term TRUE if right is None or False
return SQL("TRUE")
else:
# '=?' behaves like '=' in other cases
return self.__leaf_to_sql((left, '=', right), model, alias)
sql_operator = SQL_OPERATORS[operator]
if operator in ('in', 'not in'):
# Two cases: right is a boolean or a list. The boolean case is an
# abuse and handled for backward compatibility.
if isinstance(right, bool):
_logger.warning("The domain term '%s' should use the '=' or '!=' operator." % (leaf,))
if (operator == 'in' and right) or (operator == 'not in' and not right):
return SQL("(%s IS NOT NULL)", sql_field)
else:
return SQL("(%s IS NULL)", sql_field)
elif isinstance(right, SQL):
return SQL("(%s %s %s)", sql_field, sql_operator, right)
elif isinstance(right, Query):
return SQL("(%s %s %s)", sql_field, sql_operator, right.subselect())
elif isinstance(right, (list, tuple)):
if field.type == "boolean":
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 is not False and it is not None]
check_null = len(params) < len(right)
if params:
if left != 'id':
params = [field.convert_to_column(p, model, validate=False) for p in params]
sql = SQL("(%s %s %s)", sql_field, sql_operator, tuple(params))
else:
# The case for (left, 'in', []) or (left, 'not in', []).
sql = SQL("FALSE") if operator == 'in' else SQL("TRUE")
if (operator == 'in' and check_null) or (operator == 'not in' and not check_null):
sql = SQL("(%s OR %s IS NULL)", sql, sql_field)
elif operator == 'not in' and check_null:
sql = SQL("(%s AND %s IS NOT NULL)", sql, sql_field) # needed only for TRUE
return sql
else: # Must not happen
raise ValueError(f"Invalid domain term {leaf!r}")
if field.type == 'boolean' and operator in ('=', '!=') and isinstance(right, bool):
value = (not right) if operator in NEGATIVE_TERM_OPERATORS else right
if value:
return SQL("(%s = TRUE)", sql_field)
else:
return SQL("(%s IS NULL OR %s = FALSE)", sql_field, sql_field)
if operator == '=' and (right is False or right is None):
return SQL("%s IS NULL", sql_field)
if operator == '!=' and (right is False or right is None):
return SQL("%s IS NOT NULL", sql_field)
# general case
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
if isinstance(right, SQL):
sql_right = right
elif need_wildcard:
sql_right = SQL("%s", f"%{pycompat.to_text(right)}%")
else:
sql_right = SQL("%s", field.convert_to_column(right, model, validate=False))
sql_left = sql_field
if operator.endswith('like'):
sql_left = SQL("%s::text", sql_field)
if operator.endswith('ilike'):
unaccent = self._unaccent(field)
sql_left = unaccent(sql_left)
sql_right = unaccent(sql_right)
sql = SQL("(%s %s %s)", sql_left, sql_operator, sql_right)
if (need_wildcard and not right) or (right and operator in NEGATIVE_TERM_OPERATORS):
sql = SQL("(%s OR %s IS NULL)", sql, sql_field)
return sql