From d462db158ec4ad3d91a047a822d34004c9a0deae Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 11 Aug 2011 11:31:36 +0200 Subject: [PATCH] [REF] expression: comments, minor changes. bzr revid: vmt@openerp.com-20110811093136-8whjybyvf3kf3t4y --- openerp/osv/expression.py | 132 +++++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 23 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 0b613797f7b..ca1360288ff 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -20,10 +20,65 @@ # ############################################################################## +""" Domain expression processing + +The main duty of this module is to compile a domain expression into a SQL +query. A lot of things should be documented here, but as a first step in the +right direction, some tests in test_osv_expression.yml might give you some +additional information. + +For legacy reasons, a domain uses an inconsistent two-levels abstract syntax +(domains are regular Python data structures). At the first level, a domain +is an expression made of terms (sometimes called leaves) and (domain) operators +used in prefix notation. The available operators at this level are '!', '&', +and '|'. '!' is a unary 'not', '&' is a binary 'and', and '|' is a binary 'or'. +For instance, here is a possible domain. ( stands for an arbitrary term, +more on this later.) + + ['&', '!', , '|', , ] + +It is equivalent to this pseudo code using infix notation: + + (not ) and ( or ) + +The second level of syntax deals with the term representation. A term is +a triple of the form (left, operator, right). That is, a term uses an infix +notation, and the available operators, and possible left and right operands +differ with those of the previous level. Here is a possible term: + + ('company_id.name', '=', 'OpenERP') + +The left and right operand don't have the same possible values. The left +operand is field name (related to the model for which the domain applies). +Actually, the field name can use the dot-notation to traverse relationships. +The right operand is a Python value whose type should match the used operator +and field type. In the above example, a string is used because the name field +of a company has type string, and because we use the '=' operator. When +appropriate, a 'in' operator can be used, and thus the right operand should be +a list. + +Note: the non-uniform syntax could have been more uniform, but this would hide +an important limitation of the domain syntax. Say that the term representation +was ['=', 'company_id.name', 'OpenERP']. Used in a complete domain, this would +look like: + + ['!', ['=', 'company_id.name', 'OpenERP']] + +and you would be tempted to believe something like this would be possible: + + ['!', ['=', 'company_id.name', ['&', ..., ...]]] + +That is, a domain could be a valid operand. But this is not the case. A domain +is really limited to a two-level nature, and can not takes a recursive form: a +domain is not a valid second-level operand. + +""" + import logging from openerp.tools import flatten, reverse_enumerate import fields +import openerp.modules #.apidoc title: Domain Expressions @@ -31,8 +86,19 @@ NOT_OPERATOR = '!' OR_OPERATOR = '|' AND_OPERATOR = '&' -# This doesn't contain <> as it is simpified to != by normalize_operator(). +# List of available term operators. It is also possible to use the '<>' +# operator, which is strictly the same as '!='; the later should be prefered +# for consistency. This list doesn't contain '<>' as it is simpified to '!=' +# by the normalize_operator() function (so later part of the code deals with +# only one representation). +# An internal (i.e. not available to the user) 'inselect' operator is also +# used. In this case its right operand has the form (subselect, params). OPS = ('=', '!=', '<=', '<', '>', '>=', '=?', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of') + +# A subset of the above operators, with a 'negative' semantic. When the +# expressions 'in NEGATIVE_OPS' or 'not in NEGATIVE_OPS' are used in the code +# below, this doesn't necessarily mean that any of those NEGATIVE_OPS is +# legal in the processed term. NEGATIVE_OPS = ('!=', 'not like', 'not ilike', 'not in') TRUE_LEAF = (1, '=', 1) @@ -105,10 +171,17 @@ def OR(domains): return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains) def is_operator(element): + """ Test whether an object is a valid domain operator. """ return isinstance(element, (str, unicode)) and element in [AND_OPERATOR, OR_OPERATOR, NOT_OPERATOR] # TODO change the share wizard to use this function. def is_leaf(element, internal=False): + """ Test whether an object is a valid domain term. + + :param internal: allow or not the 'inselect' internal operator in the term. + This normally should be always left to False. + + """ INTERNAL_OPS = OPS + ('inselect',) return (isinstance(element, tuple) or isinstance(element, list)) \ and len(element) == 3 \ @@ -116,6 +189,9 @@ def is_leaf(element, internal=False): or (internal and element[1] in INTERNAL_OPS + ('<>',))) def normalize_leaf(left, operator, right): + """ Change a term's operator to some canonical form, simplifying later + processing. + """ original = operator operator = operator.lower() if operator == '<>': @@ -130,6 +206,17 @@ def normalize_leaf(left, operator, right): def distribute_not(domain): """ Distribute the '!' operator on a normalized 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 + the result with 'not (...)', as it would result in a 'not in' at the SQL + level. + + This function is thus responsible for pushing the '!' operator inside the + terms. + """ def negate(leaf): left, operator, right = leaf @@ -199,6 +286,7 @@ class expression(object): """ def __init__(self, cr, uid, exp, table, context): + self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent self.__field_tables = {} # used to store the table to use for the sql generation. key = index of the leaf self.__all_tables = set() self.__joins = [] @@ -214,6 +302,8 @@ class expression(object): def parse(self, cr, uid, exp, table, context): """ transform the leafs of the expression """ self.__exp = exp + self.__main_table = table + self.__all_tables.add(table) def child_of_domain(left, right, table, parent=None, prefix=''): ids = right @@ -237,19 +327,16 @@ class expression(object): return [(left, 'in', rg(ids, table, parent or table._parent_name))] # TODO rename this function as it is not strictly for 'child_of', but also for 'in'... - def child_of_right_to_ids(value, operator, field_obj): + def child_of_right_to_ids(value, field_obj): """ Normalize a single id, or a string, or a list of ids to a list of ids. """ if isinstance(value, basestring): - return [x[0] for x in field_obj.name_search(cr, uid, value, [], operator, context=context, limit=None)] + return [x[0] for x in field_obj.name_search(cr, uid, value, [], 'ilike', context=context, limit=None)] elif isinstance(value, (int, long)): return [value] else: return list(value) - self.__main_table = table - self.__all_tables.add(table) - i = -1 while i + 1