[IMP] osv: use iteration for expression negating

The current code when applying negative operator on an expression used
recursion which in extreme case is not best friend with python.

e.g: on instance with a lot of wharehouse, some simple action could lead
to a domain with lot of elements which could easiliy go over the python
maximum recursion limit.

This commit fixes this by replacing recursion with iteration.

We have a stack of negation flags and loop on each token of the domain
as follow :

- when we iterate on a leaf, it consumes the top negation flag,

- after a '!' operator, the top token negation is inversed,

- after an '&' or '|' operator, the top negation flag is duplicated on
  the top of the stack.

closes #9433
opw-653802
This commit is contained in:
Raphael Collet 2015-11-05 09:18:48 +01:00 committed by Nicolas Lempereur
parent a23468c845
commit c28a28e69e
1 changed files with 49 additions and 44 deletions

View File

@ -165,6 +165,26 @@ TERM_OPERATORS = ('=', '!=', '<=', '<', '>', '>=', '=?', '=like', '=ilike',
# legal in the processed term.
NEGATIVE_TERM_OPERATORS = ('!=', 'not like', 'not ilike', 'not in')
# Negation of domain expressions
DOMAIN_OPERATORS_NEGATION = {
AND_OPERATOR: OR_OPERATOR,
OR_OPERATOR: AND_OPERATOR,
}
TERM_OPERATORS_NEGATION = {
'<': '>=',
'>': '<=',
'<=': '>',
'>=': '<',
'=': '!=',
'!=': '=',
'in': 'not in',
'like': 'not like',
'ilike': 'not ilike',
'not in': 'in',
'not like': 'like',
'not ilike': 'ilike',
}
TRUE_LEAF = (1, '=', 1)
FALSE_LEAF = (0, '=', 1)
@ -261,51 +281,36 @@ def distribute_not(domain):
['|',('user_id','!=',4),('partner_id','not in',[1,2])]
"""
def negate(leaf):
"""Negates and returns a single domain leaf term,
using the opposite operator if possible"""
left, operator, right = leaf
mapping = {
'<': '>=',
'>': '<=',
'<=': '>',
'>=': '<',
'=': '!=',
'!=': '=',
}
if operator in ('in', 'like', 'ilike'):
operator = 'not ' + operator
return [(left, operator, right)]
if operator in ('not in', 'not like', 'not ilike'):
operator = operator[4:]
return [(left, operator, right)]
if operator in mapping:
operator = mapping[operator]
return [(left, operator, right)]
return [NOT_OPERATOR, (left, operator, right)]
def distribute_negate(domain):
"""Negate the domain ``subtree`` rooted at domain[0],
leaving the rest of the domain intact, and return
(negated_subtree, untouched_domain_rest)
"""
if is_leaf(domain[0]):
return negate(domain[0]), domain[1:]
if domain[0] == AND_OPERATOR:
done1, todo1 = distribute_negate(domain[1:])
done2, todo2 = distribute_negate(todo1)
return [OR_OPERATOR] + done1 + done2, todo2
if domain[0] == OR_OPERATOR:
done1, todo1 = distribute_negate(domain[1:])
done2, todo2 = distribute_negate(todo1)
return [AND_OPERATOR] + done1 + done2, todo2
if not domain:
return []
if domain[0] != NOT_OPERATOR:
return [domain[0]] + distribute_not(domain[1:])
if domain[0] == NOT_OPERATOR:
done, todo = distribute_negate(domain[1:])
return done + distribute_not(todo)
# This is an iterative version of a recursive function that split domain
# into subdomains, processes them and combine the results. The "stack" below
# represents the recursive calls to be done.
result = []
stack = [False]
for token in domain:
negate = stack.pop()
# negate tells whether the subdomain starting with token must be negated
if is_leaf(token):
if negate:
left, operator, right = token
if operator in TERM_OPERATORS_NEGATION:
result.append((left, TERM_OPERATORS_NEGATION[operator], right))
else:
result.append(NOT_OPERATOR)
result.append(token)
else:
result.append(token)
elif token == NOT_OPERATOR:
stack.append(not negate)
elif token in DOMAIN_OPERATORS_NEGATION:
result.append(DOMAIN_OPERATORS_NEGATION[token] if negate else token)
stack.append(negate)
stack.append(negate)
else:
result.append(token)
return result
# --------------------------------------------------