diff --git a/openerp/addons/base/test/test_osv_expression.yml b/openerp/addons/base/test/test_osv_expression.yml index b0c46ce7f13..97ab6c9e83c 100644 --- a/openerp/addons/base/test/test_osv_expression.yml +++ b/openerp/addons/base/test/test_osv_expression.yml @@ -177,6 +177,251 @@ res_ids = self.search(cr, uid, [('company_id.partner_id', 'not in', [])]) res_ids.sort() assert res_ids == all_ids, "Searching against empty set failed, returns %r" % res_ids +- + Test the '(not) like/in' behavior. res.partner and its parent_id column are used because + parent_id is a many2one, allowing to test the Null value, and there are actually some + null and non-null values in the demo data. +- + !python {model: res.partner }: | + partner_ids = self.search(cr, uid, []) + partner_ids.sort() + max_partner_id = max(partner_ids) + + # Grab test sample data without using a normal + # search domain, because we want to test these later, + # so we can't rely on them! + partners = self.browse(cr, uid, partner_ids) + with_parent = [] + without_parent = [] + with_website = [] + for x in partners: + if x.parent_id: + with_parent.append(x.id) + else: + without_parent.append(x.id) + if x.website: + with_website.append(x.id) + with_parent.sort() + without_parent.sort() + with_website.sort() + + # We treat null values differently than in SQL. For instance in SQL: + # SELECT id FROM res_partner WHERE parent_id NOT IN (0) + # will return only the records with non-null parent_id. + # SELECT id FROM res_partner WHERE parent_id IN (0) + # will return expectedly nothing (our ids always begin at 1). + # This means the union of those two results will give only some + # records, but not all present in database. + # + # When using domains and the ORM's search method, we think it is + # more intuitive that the union returns all the records, and that + # a domain like ('parent_id', 'not in', [0]) will return all + # the records. For instance, if you perform a search for the companies + # that don't have OpenERP has a parent company, you expect to find, + # among others, the companies that don't have parent company. + # + # ('parent_id', 'not in', [0]) must give the same result than + # ('parent_id', 'not in', []), i.e. a empty set or a set with non- + # existing values be treated similarly if we simply check that some + # existing value belongs to them. + + res_0 = self.search(cr, uid, [('parent_id', 'not like', 'probably_unexisting_name')]) # get all rows, included null parent_id + res_0.sort() + res_1 = self.search(cr, uid, [('parent_id', 'not in', [max_partner_id + 1])]) # get all rows, included null parent_id + res_1.sort() + res_2 = self.search(cr, uid, [('parent_id', 'not in', False)]) # get rows with not null parent_id, deprecated syntax + res_2.sort() + res_3 = self.search(cr, uid, [('parent_id', 'not in', [])]) # get all rows, included null parent_id + res_3.sort() + res_4 = self.search(cr, uid, [('parent_id', 'not in', [False])]) # get rows with not null parent_id + res_4.sort() + assert res_0 == partner_ids + assert res_1 == partner_ids + assert res_2 == with_parent + assert res_3 == partner_ids + assert res_4 == with_parent + # The results of these queries, when combined with queries 0..4 must + # give the whole set of ids. + res_5 = self.search(cr, uid, [('parent_id', 'like', 'probably_unexisting_name')]) + res_5.sort() + res_6 = self.search(cr, uid, [('parent_id', 'in', [max_partner_id + 1])]) + res_6.sort() + res_7 = self.search(cr, uid, [('parent_id', 'in', False)]) + res_7.sort() + res_8 = self.search(cr, uid, [('parent_id', 'in', [])]) + res_8.sort() + res_9 = self.search(cr, uid, [('parent_id', 'in', [False])]) + res_9.sort() + assert res_5 == [] + assert res_6 == [] + assert res_7 == without_parent + assert res_8 == [] + assert res_9 == without_parent + # These queries must return exactly the results than the queries 0..4, + # i.e. not ... in ... must be the same as ... not in ... . + res_10 = self.search(cr, uid, ['!', ('parent_id', 'like', 'probably_unexisting_name')]) + res_10.sort() + res_11 = self.search(cr, uid, ['!', ('parent_id', 'in', [max_partner_id + 1])]) + res_11.sort() + res_12 = self.search(cr, uid, ['!', ('parent_id', 'in', False)]) + res_12.sort() + res_13 = self.search(cr, uid, ['!', ('parent_id', 'in', [])]) + res_13.sort() + res_14 = self.search(cr, uid, ['!', ('parent_id', 'in', [False])]) + res_14.sort() + assert res_0 == res_10 + assert res_1 == res_11 + assert res_2 == res_12 + assert res_3 == res_13 + assert res_4 == res_14 + + # Testing many2one field is not enough, a regular char field is tested + # with in [] and must not return any result. + res_15 = self.search(cr, uid, [('website', 'in', [])]) + assert res_15 == [] + # not in [] must return everything. + res_16 = self.search(cr, uid, [('website', 'not in', [])]) + res_16.sort() + assert res_16 == partner_ids + + res_17 = self.search(cr, uid, [('website', 'not in', False)]) + res_17.sort() + assert res_17 == with_website +- + Property of the query (one2many not in False). +- + !python {model: res.currency }: | + ids = self.search(cr, uid, []) + referenced_companies = set([x.company_id.id for x in self.browse(cr, uid, ids)]) + companies = set(self.pool.get('res.company').search(cr, uid, [('currency_ids', 'not in', False)])) + assert referenced_companies == companies +- + Property of the query (one2many in False). +- + !python {model: res.currency }: | + ids = self.search(cr, uid, []) + referenced_companies = set([x.company_id.id for x in self.browse(cr, uid, ids)]) + unreferenced_companies = set(self.pool.get('res.company').search(cr, uid, [])).difference(referenced_companies) + companies = set(self.pool.get('res.company').search(cr, uid, [('currency_ids', 'in', False)])) + assert unreferenced_companies == companies +- + Equivalent queries. +- + !python {model: res.currency }: | + max_currency_id = max(self.search(cr, uid, [])) + res_0 = self.search(cr, uid, []) + res_1 = self.search(cr, uid, [('name', 'not like', 'probably_unexisting_name')]) + res_2 = self.search(cr, uid, [('id', 'not in', [max_currency_id + 1003])]) + res_3 = self.search(cr, uid, [('id', 'not in', [])]) + res_4 = self.search(cr, uid, [('id', 'not in', False)]) + res_0.sort() + res_1.sort() + res_2.sort() + res_3.sort() + res_4.sort() + assert res_0 == res_1 + assert res_0 == res_2 + assert res_0 == res_3 + assert res_0 == res_4 +- + Equivalent queries, integer and string. +- + !python {model: res.partner }: | + all_ids = self.search(cr, uid, []) + if len(all_ids) > 1: + one = all_ids[0] + record = self.browse(cr, uid, one) + others = all_ids[1:] + res_1 = self.search(cr, uid, [('id', '=', one)]) + # self.search(cr, uid, [('id', '!=', others)]) # not permitted + res_2 = self.search(cr, uid, [('id', 'not in', others)]) + res_3 = self.search(cr, uid, ['!', ('id', '!=', one)]) + res_4 = self.search(cr, uid, ['!', ('id', 'in', others)]) + # res_5 = self.search(cr, uid, [('id', 'in', one)]) # TODO make it permitted, just like for child_of + res_6 = self.search(cr, uid, [('id', 'in', [one])]) + res_7 = self.search(cr, uid, [('name', '=', record.name)]) + res_8 = self.search(cr, uid, [('name', 'in', [record.name])]) + # res_9 = self.search(cr, uid, [('name', 'in', record.name)]) # TODO + assert [one] == res_1 + assert [one] == res_2 + assert [one] == res_3 + assert [one] == res_4 + #assert [one] == res_5 + assert [one] == res_6 + assert [one] == res_7 +- + Need a company with a parent_id. +- + !record {model: res.company, id: ymltest_company3}: + name: Acme 3 +- + Need a company with a parent_id. +- + !record {model: res.company, id: ymltest_company4}: + name: Acme 4 + parent_id: ymltest_company3 +- + Equivalent queries, one2many. +- + !python {model: res.company }: | + # Search the company via its one2many (the one2many must point back at the company). + company = self.browse(cr, uid, ref('ymltest_company3')) + max_currency_id = max(self.pool.get('res.currency').search(cr, uid, [])) + currency_ids1 = self.pool.get('res.currency').search(cr, uid, [('name', 'not like', 'probably_unexisting_name')]) + currency_ids2 = self.pool.get('res.currency').search(cr, uid, [('id', 'not in', [max_currency_id + 1003])]) + currency_ids3 = self.pool.get('res.currency').search(cr, uid, [('id', 'not in', [])]) + assert currency_ids1 == currency_ids2 == currency_ids3, 'All 3 results should have be the same: all currencies' + default_company = self.browse(cr, uid, 1) + # one2many towards same model + res_1 = self.search(cr, uid, [('child_ids', 'in', [x.id for x in company.child_ids])]) # any company having a child of company3 as child + res_2 = self.search(cr, uid, [('child_ids', 'in', [company.child_ids[0].id])]) # any company having the first child of company3 as child + # one2many towards another model + res_3 = self.search(cr, uid, [('currency_ids', 'in', [x.id for x in default_company.currency_ids])]) # companies having a currency of main company + res_4 = self.search(cr, uid, [('currency_ids', 'in', [default_company.currency_ids[0].id])]) # companies having first currency of main company + res_5 = self.search(cr, uid, [('currency_ids', 'in', default_company.currency_ids[0].id)]) # companies having first currency of main company + # res_6 = self.search(cr, uid, [('currency_ids', 'in', [default_company.currency_ids[0].name])]) # TODO + res_7 = self.search(cr, uid, [('currency_ids', '=', default_company.currency_ids[0].name)]) + res_8 = self.search(cr, uid, [('currency_ids', 'like', default_company.currency_ids[0].name)]) + res_9 = self.search(cr, uid, [('currency_ids', 'like', 'probably_unexisting_name')]) + # self.search(cr, uid, [('currency_ids', 'unexisting_op', 'probably_unexisting_name')]) # TODO expected exception + assert res_1 == [ref('ymltest_company3')] + assert res_2 == [ref('ymltest_company3')] + assert res_3 == [1] + assert res_4 == [1] + assert res_5 == [1] + assert res_7 == [1] + assert res_8 == [1] + assert res_9 == [] + + # get the companies referenced by some currency (this is normally the main company) + res_10 = self.search(cr, uid, [('currency_ids', 'not like', 'probably_unexisting_name')]) + res_11 = self.search(cr, uid, [('currency_ids', 'not in', [max_currency_id + 1])]) + res_12 = self.search(cr, uid, [('currency_ids', 'not in', False)]) + res_13 = self.search(cr, uid, [('currency_ids', 'not in', [])]) + res_10.sort() + res_11.sort() + res_12.sort() + res_13.sort() + assert res_10 == res_11 + assert res_10 == res_12 + assert res_10 == res_13 + + # child_of x returns x and its children (direct or not). + company = self.browse(cr, uid, ref('ymltest_company3')) + expected = [ref('ymltest_company3'), ref('ymltest_company4')] + expected.sort() + res_1 = self.search(cr, uid, [('id', 'child_of', [ref('ymltest_company3')])]) + res_1.sort() + res_2 = self.search(cr, uid, [('id', 'child_of', ref('ymltest_company3'))]) + res_2.sort() + res_3 = self.search(cr, uid, [('id', 'child_of', [company.name])]) + res_3.sort() + res_4 = self.search(cr, uid, [('id', 'child_of', company.name)]) + res_4.sort() + assert res_1 == expected + assert res_2 == expected + assert res_3 == expected + assert res_4 == expected - Verify that normalize_domain() works. - @@ -187,6 +432,24 @@ domain = [('x','in',['y','z']),('a.v','=','e'),'|','|',('a','=','b'),'!',('c','>','d'),('e','!=','f'),('g','=','h')] norm_domain = ['&','&','&'] + domain assert norm_domain == expression.normalize(domain), "Non-normalized domains should be properly normalized" +- + Unaccent. Create a company with an accent in its name. +- + !record {model: res.company, id: ymltest_unaccent_company}: + name: Hélène +- + Test the unaccent-enabled 'ilike'. +- + !python {model: res.company}: | + if self.pool.has_unaccent: + ids = self.search(cr, uid, [('name','ilike','Helene')], {}) + assert ids == [ref('ymltest_unaccent_company')] + ids = self.search(cr, uid, [('name','ilike','hélène')], {}) + assert ids == [ref('ymltest_unaccent_company')] + ids = self.search(cr, uid, [('name','not ilike','Helene')], {}) + assert ref('ymltest_unaccent_company') not in ids + ids = self.search(cr, uid, [('name','not ilike','hélène')], {}) + assert ref('ymltest_unaccent_company') not in ids - Check that =like/=ilike expressions (no wildcard variants of like/ilike) are working on an untranslated field. - diff --git a/openerp/modules/db.py b/openerp/modules/db.py index 86ba0259106..6e6dd64b13c 100644 --- a/openerp/modules/db.py +++ b/openerp/modules/db.py @@ -123,4 +123,14 @@ def create_categories(cr, categories): categories = categories[1:] return p_id +def has_unaccent(cr): + """ Test if the database has an unaccent function. + + The unaccent is supposed to be provided by the PostgreSQL unaccent contrib + module but any similar function will be picked by OpenERP. + + """ + cr.execute("SELECT proname FROM pg_proc WHERE proname='unaccent'") + return len(cr.fetchall()) > 0 + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index 7769d492197..d5dacdfdd67 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -24,9 +24,12 @@ """ import threading +import logging + import openerp.sql_db import openerp.osv.orm - +import openerp.modules.db +import openerp.tools.config class Registry(object): """ Model registry for a particular database. @@ -45,6 +48,14 @@ class Registry(object): self.db_name = db_name self.db = openerp.sql_db.db_connect(db_name) + cr = self.db.cursor() + has_unaccent = openerp.modules.db.has_unaccent(cr) + if openerp.tools.config['unaccent'] and not has_unaccent: + logger = logging.getLogger('unaccent') + logger.warning("The option --unaccent was given but no unaccent() function was found in database.") + self.has_unaccent = openerp.tools.config['unaccent'] and has_unaccent + cr.close() + def do_parent_store(self, cr): for o in self._init_parent: self.get(o)._parent_store_compute(cr) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 96f5a5cf34f..bbf4569cba8 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -20,17 +20,145 @@ # ############################################################################## +""" 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. + +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[0]. + + +..todo: The following explanation should be moved in some external installation + guide + +The steps to install the module might differ on specific PostgreSQL versions. +We give here some instruction for PostgreSQL 9.x on a Ubuntu system. + +Ubuntu doesn't come yet with PostgreSQL 9.x, so an alternative package source +is used. We use Martin Pitt's PPA available at ppa:pitti/postgresql[1]. See +[2] for instructions. Basically: + + > sudo add-apt-repository ppa:pitti/postgresql + > sudo apt-get update + +Once the package list is up-to-date, you have to install PostgreSQL 9.0 and +its contrib modules. + + > sudo apt-get install postgresql-9.0 postgresql-contrib-9.0 + +When you want to enable unaccent on some database: + + > psql9 -f /usr/share/postgresql/9.0/contrib/unaccent.sql + +Here 'psql9' is an alias for the newly installed PostgreSQL 9.0 tool, together +with the correct port if necessary (for instance if PostgreSQL 8.4 is running +on 5432). (Other aliases can be used for createdb and dropdb.) + + > alias psql9='/usr/lib/postgresql/9.0/bin/psql -p 5433' + +You can check unaccent is working: + + > psql9 -c"select unaccent('hélène')" + +Finally, to instruct OpenERP to really use the unaccent function, you have to +start the server specifying the --unaccent flag. + +[0] http://developer.postgresql.org/pgdocs/postgres/unaccent.html +[1] https://launchpad.net/~pitti/+archive/postgresql +[2] https://launchpad.net/+help/soyuz/ppa-sources-list.html + +""" + +import logging + from openerp.tools import flatten, reverse_enumerate import fields +import openerp.modules #.apidoc title: Domain Expressions +# Domain operators. NOT_OPERATOR = '!' OR_OPERATOR = '|' AND_OPERATOR = '&' +DOMAIN_OPERATORS = (NOT_OPERATOR, OR_OPERATOR, AND_OPERATOR) -TRUE_DOMAIN = [(1,'=',1)] -FALSE_DOMAIN = [(0,'=',1)] +# 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). +TERM_OPERATORS = ('=', '!=', '<=', '<', '>', '>=', '=?', '=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_TERM_OPERATORS' or 'not in NEGATIVE_TERM_OPERATORS' are used in the code +# below, this doesn't necessarily mean that any of those NEGATIVE_TERM_OPERATORS is +# legal in the processed term. +NEGATIVE_TERM_OPERATORS = ('!=', 'not like', 'not ilike', 'not in') + +TRUE_LEAF = (1, '=', 1) +FALSE_LEAF = (0, '=', 1) + +TRUE_DOMAIN = [TRUE_LEAF] +FALSE_DOMAIN = [FALSE_LEAF] + +_logger = logging.getLogger('expression') def normalize(domain): """Returns a normalized version of ``domain_expr``, where all implicit '&' operators @@ -45,10 +173,10 @@ def normalize(domain): op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2} for token in domain: if expected == 0: # more than expected, like in [A, B] - result[0:0] = ['&'] # put an extra '&' in front + result[0:0] = [AND_OPERATOR] # put an extra '&' in front expected = 1 result.append(token) - if isinstance(token, (list,tuple)): # domain term + if isinstance(token, (list, tuple)): # domain term expected -= 1 else: expected += op_arity.get(token, 0) - 1 @@ -57,7 +185,8 @@ def normalize(domain): def combine(operator, unit, zero, domains): """Returns a new domain expression where all domain components from ``domains`` - have been added together using the binary operator ``operator``. + have been added together using the binary operator ``operator``. The given + domains must be normalized. :param unit: the identity element of the domains "set" with regard to the operation performed by ``operator``, i.e the domain component ``i`` which, when @@ -69,6 +198,7 @@ def combine(operator, unit, zero, domains): combined with any domain ``x`` via ``operator``, yields ``z``. E.g. [(1,'=',1)] is the typical zero for OR_OPERATOR: as soon as you see it in a domain component the resulting domain is the zero. + :param domains: a list of normalized domains. """ result = [] count = 0 @@ -84,13 +214,130 @@ def combine(operator, unit, zero, domains): return result def AND(domains): - """ AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """ + """AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """ return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains) def OR(domains): - """ OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """ + """OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """ 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, basestring) and element in DOMAIN_OPERATORS + +# 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 = TERM_OPERATORS + ('inselect',) + return (isinstance(element, tuple) or isinstance(element, list)) \ + and len(element) == 3 \ + and (((not internal) and element[1] in TERM_OPERATORS + ('<>',)) \ + 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 == '<>': + operator = '!=' + if isinstance(right, bool) and operator in ('in', 'not in'): + _logger.warning("The domain term '%s' should use the '=' or '!=' operator." % ((left, original, right),)) + operator = '=' if operator == 'in' else '!=' + if isinstance(right, (list, tuple)) and operator in ('=', '!='): + _logger.warning("The domain term '%s' should use the 'in' or 'not in' operator." % ((left, original, right),)) + operator = 'in' if operator == '=' else 'not in' + return left, operator, right + +def distribute_not(domain): + """ Distribute any '!' domain operators found inside 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 any '!' domain operators + inside the terms themselves. For example:: + + ['!','&',('user_id','=',4),('partner_id','in',[1,2])] + will be turned into: + ['|',('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) + +def select_from_where(cr, select_field, from_table, where_field, where_ids, where_operator): + # todo: merge into parent query as sub-query + res = [] + if where_ids: + if where_operator in ['<','>','>=','<=']: + cr.execute('SELECT "%s" FROM "%s" WHERE "%s" %s %%s' % \ + (select_field, from_table, where_field, where_operator), + (where_ids[0],)) # TODO shouldn't this be min/max(where_ids) ? + res = [r[0] for r in cr.fetchall()] + else: # TODO where_operator is supposed to be 'in'? It is called with child_of... + for i in range(0, len(where_ids), cr.IN_MAX): + subids = where_ids[i:i+cr.IN_MAX] + cr.execute('SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % \ + (select_field, from_table, where_field), (tuple(subids),)) + res.extend([r[0] for r in cr.fetchall()]) + return res + +def select_distinct_from_where_not_null(cr, select_field, from_table): + cr.execute('SELECT distinct("%s") FROM "%s" where "%s" is not null' % \ + (select_field, from_table, select_field)) + return [r[0] for r in cr.fetchall()] class expression(object): """ @@ -100,127 +347,103 @@ class expression(object): For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html """ - @classmethod - def _is_operator(cls, element): - return isinstance(element, (str, unicode)) and element in [AND_OPERATOR, OR_OPERATOR, NOT_OPERATOR] - - @classmethod - def _is_leaf(cls, element, internal=False): - OPS = ('=', '!=', '<>', '<=', '<', '>', '>=', '=?', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of') - INTERNAL_OPS = OPS + ('inselect',) - return (isinstance(element, tuple) or isinstance(element, list)) \ - and len(element) == 3 \ - and (((not internal) and element[1] in OPS) \ - or (internal and element[1] in INTERNAL_OPS)) - - def __execute_recursive_in(self, cr, s, f, w, ids, op, type): - # todo: merge into parent query as sub-query - res = [] - if ids: - if op in ['<','>','>=','<=']: - cr.execute('SELECT "%s"' \ - ' FROM "%s"' \ - ' WHERE "%s" %s %%s' % (s, f, w, op), (ids[0],)) - res.extend([r[0] for r in cr.fetchall()]) - else: - for i in range(0, len(ids), cr.IN_MAX): - subids = ids[i:i+cr.IN_MAX] - cr.execute('SELECT "%s"' \ - ' FROM "%s"' \ - ' WHERE "%s" IN %%s' % (s, f, w),(tuple(subids),)) - res.extend([r[0] for r in cr.fetchall()]) - else: - cr.execute('SELECT distinct("%s")' \ - ' FROM "%s" where "%s" is not null' % (s, f, s)), - res.extend([r[0] for r in cr.fetchall()]) - return res - - def __init__(self, exp): - # check if the expression is valid - if not reduce(lambda acc, val: acc and (self._is_operator(val) or self._is_leaf(val)), exp, True): - raise ValueError('Bad domain expression: %r' % (exp,)) - self.__exp = exp + 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 = [] self.__main_table = None # 'root' table. set by parse() - self.__DUMMY_LEAF = (1, '=', 1) # a dummy leaf that must not be parsed or sql generated + # assign self.__exp with the normalized, parsed domain. + self.parse(cr, uid, distribute_not(normalize(exp)), table, context) + # TODO used only for osv_memory @property def exp(self): return self.__exp[:] - def parse(self, cr, uid, table, context): - """ transform the leafs of the expression """ - if not self.__exp: - return self + def parse(self, cr, uid, exp, table, context): + """ transform the leaves of the expression """ + self.__exp = exp + self.__main_table = table + self.__all_tables.add(table) - def _rec_get(ids, table, parent=None, left='id', prefix=''): - if table._parent_store and (not table.pool._init): -# TODO: Improve where joins are implemented for many with '.', replace by: -# doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)] + def child_of_domain(left, ids, left_model, parent=None, prefix=''): + """Returns a domain implementing the child_of operator for [(left,child_of,ids)], + either as a range using the parent_left/right tree lookup fields (when available), + or as an expanded [(left,in,child_ids)]""" + if left_model._parent_store and (not left_model.pool._init): + # TODO: Improve where joins are implemented for many with '.', replace by: + # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)] doms = [] - for o in table.browse(cr, uid, ids, context=context): + for o in left_model.browse(cr, uid, ids, context=context): if doms: doms.insert(0, OR_OPERATOR) doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)] if prefix: - return [(left, 'in', table.search(cr, uid, doms, context=context))] + return [(left, 'in', left_model.search(cr, uid, doms, context=context))] return doms else: - def rg(ids, table, parent): + def recursive_children(ids, model, parent_field): if not ids: return [] - ids2 = table.search(cr, uid, [(parent, 'in', ids)], context=context) - return ids + rg(ids2, table, parent) - return [(left, 'in', rg(ids, table, parent or table._parent_name))] + ids2 = model.search(cr, uid, [(parent_field, 'in', ids)], context=context) + return ids + recursive_children(ids2, model, parent_field) + return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))] - def child_of_right_to_ids(value): - """ Normalize a single id, or a string, or a list of ids to a list of ids. - - This function is always used with _rec_get() above, so it should be - called directly from _rec_get instead of repeatedly before _rec_get. - - """ + def to_ids(value, field_obj): + """Normalize a single id or name, or a list of those, into a list of ids""" + names = [] if isinstance(value, basestring): - return [x[0] for x in field_obj.name_search(cr, uid, value, [], 'ilike', context=context, limit=None)] + names = [value] + if value and isinstance(value, (tuple, list)) and isinstance(value[0], basestring): + names = value + if names: + return flatten([[x[0] for x in field_obj.name_search(cr, uid, n, [], 'ilike', context=context, limit=None)] \ + for n in names]) elif isinstance(value, (int, long)): return [value] - else: - return list(value) - - self.__main_table = table - self.__all_tables.add(table) + return list(value) i = -1 while i + 1 1: if field._type == 'many2one': right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context) - if right == []: - self.__exp[i] = ( 'id', '=', 0 ) - else: - self.__exp[i] = (fargs[0], 'in', right) + self.__exp[i] = (fargs[0], 'in', right) # Making search easier when there is a left operand as field.o2m or field.m2m - if field._type in ['many2many','one2many']: + if field._type in ['many2many', 'one2many']: right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context) - right1 = table.search(cr, uid, [(fargs[0],'in', right)], context=context) - if right1 == []: - self.__exp[i] = ( 'id', '=', 0 ) - else: - self.__exp[i] = ('id', 'in', right1) + right1 = table.search(cr, uid, [(fargs[0], 'in', right)], context=context) + self.__exp[i] = ('id', 'in', right1) - if not isinstance(field,fields.property): + if not isinstance(field, fields.property): continue if field._properties and not field.store: @@ -249,16 +466,16 @@ class expression(object): if not field._fnct_search: # the function field doesn't provide a search function and doesn't store # values in the database, so we must ignore it : we generate a dummy leaf - self.__exp[i] = self.__DUMMY_LEAF + self.__exp[i] = TRUE_LEAF else: subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context) if not subexp: - self.__exp[i] = self.__DUMMY_LEAF + self.__exp[i] = TRUE_LEAF else: # we assume that the expression is valid # we create a dummy leaf for forcing the parsing of the resulting expression self.__exp[i] = AND_OPERATOR - self.__exp.insert(i + 1, self.__DUMMY_LEAF) + self.__exp.insert(i + 1, TRUE_LEAF) for j, se in enumerate(subexp): self.__exp.insert(i + 2 + j, se) # else, the value of the field is store in the database, so we search on it @@ -266,11 +483,11 @@ class expression(object): elif field._type == 'one2many': # Applying recursivity on field(one2many) if operator == 'child_of': - ids2 = child_of_right_to_ids(right) + ids2 = to_ids(right, field_obj) if field._obj != working_table._name: - dom = _rec_get(ids2, field_obj, left=left, prefix=field._obj) + dom = child_of_domain(left, ids2, field_obj, prefix=field._obj) else: - dom = _rec_get(ids2, working_table, parent=left) + dom = child_of_domain('id', ids2, working_table, parent=left) self.__exp = self.__exp[:i] + dom + self.__exp[i+1:] else: @@ -282,7 +499,7 @@ class expression(object): if ids2: operator = 'in' else: - if not isinstance(right,list): + if not isinstance(right, list): ids2 = [right] else: ids2 = right @@ -290,22 +507,16 @@ class expression(object): if operator in ['like','ilike','in','=']: #no result found with given search criteria call_null = False - self.__exp[i] = ('id','=',0) - else: - call_null = True - operator = 'in' # operator changed because ids are directly related to main object + self.__exp[i] = FALSE_LEAF else: - call_null = False - o2m_op = 'in' - if operator in ['not like','not ilike','not in','<>','!=']: - o2m_op = 'not in' - self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', ids2, operator, field._type)) + ids2 = select_from_where(cr, field._fields_id, field_obj._table, 'id', ids2, operator) + if ids2: + call_null = False + self.__exp[i] = ('id', 'in', ids2) if call_null: - o2m_op = 'not in' - if operator in ['not like','not ilike','not in','<>','!=']: - o2m_op = 'in' - self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', [], operator, field._type) or [0]) + o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' + self.__exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, field_obj._table)) elif field._type == 'many2many': #FIXME @@ -313,10 +524,10 @@ class expression(object): def _rec_convert(ids): if field_obj == table: return ids - return self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, ids, operator, field._type) + return select_from_where(cr, field._id1, field._rel, field._id2, ids, operator) - ids2 = child_of_right_to_ids(right) - dom = _rec_get(ids2, field_obj) + ids2 = to_ids(right, field_obj) + dom = child_of_domain('id', ids2, field_obj) ids2 = field_obj.search(cr, uid, dom, context=context) self.__exp[i] = ('id', 'in', _rec_convert(ids2)) else: @@ -335,34 +546,28 @@ class expression(object): if operator in ['like','ilike','in','=']: #no result found with given search criteria call_null_m2m = False - self.__exp[i] = ('id','=',0) + self.__exp[i] = FALSE_LEAF else: - call_null_m2m = True operator = 'in' # operator changed because ids are directly related to main object else: call_null_m2m = False - m2m_op = 'in' - if operator in ['not like','not ilike','not in','<>','!=']: - m2m_op = 'not in' + m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' + self.__exp[i] = ('id', m2m_op, select_from_where(cr, field._id1, field._rel, field._id2, res_ids, operator) or [0]) - self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, res_ids, operator, field._type) or [0]) if call_null_m2m: - m2m_op = 'not in' - if operator in ['not like','not ilike','not in','<>','!=']: - m2m_op = 'in' - self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, [], operator, field._type) or [0]) + m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' + self.__exp[i] = ('id', m2m_op, select_distinct_from_where_not_null(cr, field._id1, field._rel)) elif field._type == 'many2one': if operator == 'child_of': - ids2 = child_of_right_to_ids(right) - self.__operator = 'in' + ids2 = to_ids(right, field_obj) if field._obj != working_table._name: - dom = _rec_get(ids2, field_obj, left=left, prefix=field._obj) + dom = child_of_domain(left, ids2, field_obj, prefix=field._obj) else: - dom = _rec_get(ids2, working_table, parent=left) + dom = child_of_domain('id', ids2, working_table, parent=left) self.__exp = self.__exp[:i] + dom + self.__exp[i+1:] else: - def _get_expression(field_obj,cr, uid, left, right, operator, context=None): + def _get_expression(field_obj, cr, uid, left, right, operator, context=None): if context is None: context = {} c = context.copy() @@ -370,46 +575,35 @@ class expression(object): #Special treatment to ill-formed domains operator = ( operator in ['<','>','<=','>='] ) and 'in' or operator - dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in','<>':'not in'} - if isinstance(right,tuple): + dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in'} + if isinstance(right, tuple): right = list(right) - if (not isinstance(right,list)) and operator in ['not in','in']: + if (not isinstance(right, list)) and operator in ['not in','in']: operator = dict_op[operator] - elif isinstance(right,list) and operator in ['<>','!=','=']: #for domain (FIELD,'=',['value1','value2']) + elif isinstance(right, list) and operator in ['!=','=']: #for domain (FIELD,'=',['value1','value2']) operator = dict_op[operator] - res_ids = field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c) - if not res_ids: - return ('id','=',0) - else: - right = map(lambda x: x[0], res_ids) - return (left, 'in', right) + res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)] + if operator in NEGATIVE_TERM_OPERATORS: + res_ids.append(False) # TODO this should not be appended if False was in 'right' + return (left, 'in', res_ids) m2o_str = False if right: if isinstance(right, basestring): # and not isinstance(field, fields.related): m2o_str = True - elif isinstance(right,(list,tuple)): + elif isinstance(right, (list, tuple)): m2o_str = True for ele in right: if not isinstance(ele, basestring): m2o_str = False break + if m2o_str: + self.__exp[i] = _get_expression(field_obj, cr, uid, left, right, operator, context=context) elif right == []: - m2o_str = False - if operator in ('not in', '!=', '<>'): - # (many2one not in []) should return all records - self.__exp[i] = self.__DUMMY_LEAF - else: - self.__exp[i] = ('id','=',0) - else: - new_op = '=' - if operator in ['not like','not ilike','not in','<>','!=']: - new_op = '!=' - #Is it ok to put 'left' and not 'id' ? - self.__exp[i] = (left,new_op,False) + pass # Handled by __leaf_to_sql(). + else: # right is False + pass # Handled by __leaf_to_sql(). - if m2o_str: - self.__exp[i] = _get_expression(field_obj,cr, uid, left, right, operator, context=context) else: # other field type # add the time part to datetime field when it's not there: @@ -430,7 +624,7 @@ class expression(object): if need_wildcard: right = '%%%s%%' % right - query1 = '( SELECT res_id' \ + subselect = '( SELECT res_id' \ ' FROM ir_translation' \ ' WHERE name = %s' \ ' AND lang = %s' \ @@ -439,112 +633,143 @@ class expression(object): #Covering in,not in operators with operands (%s,%s) ,etc. if sql_operator in ['in','not in']: instr = ','.join(['%s'] * len(right)) - query1 += ' AND value ' + sql_operator + ' ' +" (" + instr + ")" \ + subselect += ' AND value ' + sql_operator + ' ' +" (" + instr + ")" \ ') UNION (' \ ' SELECT id' \ ' FROM "' + working_table._table + '"' \ ' WHERE "' + left + '" ' + sql_operator + ' ' +" (" + instr + "))" else: - query1 += ' AND value ' + sql_operator + instr + \ + subselect += ' AND value ' + sql_operator + instr + \ ') UNION (' \ ' SELECT id' \ ' FROM "' + working_table._table + '"' \ ' WHERE "' + left + '" ' + sql_operator + instr + ")" - query2 = [working_table._name + ',' + left, + params = [working_table._name + ',' + left, context.get('lang', False) or 'en_US', 'model', right, right, ] - self.__exp[i] = ('id', 'inselect', (query1, query2)) - return self + self.__exp[i] = ('id', 'inselect', (subselect, params)) def __leaf_to_sql(self, leaf, table): - if leaf == self.__DUMMY_LEAF: - return ('(1=1)', []) left, operator, right = leaf - if operator == 'inselect': - query = '(%s."%s" in (%s))' % (table._table, left, right[0]) - params = right[1] - elif operator in ['in', 'not in']: - params = right and right[:] or [] - len_before = len(params) - for i in range(len_before)[::-1]: - if params[i] == False: - del params[i] - - len_after = len(params) - check_nulls = len_after != len_before - query = '(1=0)' - - if len_after: - if left == 'id': - instr = ','.join(['%s'] * len_after) - else: - instr = ','.join([table._columns[left]._symbol_set[0]] * len_after) - query = '(%s."%s" %s (%s))' % (table._table, left, operator, instr) - else: - # the case for [field, 'in', []] or [left, 'not in', []] - if operator == 'in': - query = '(%s."%s" IS NULL)' % (table._table, left) - else: - query = '(%s."%s" IS NOT NULL)' % (table._table, left) - if check_nulls: - query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left) - else: + if leaf == TRUE_LEAF: + query = 'TRUE' params = [] - if right == False and (leaf[0] in table._columns) and table._columns[leaf[0]]._type=="boolean" and (operator == '='): - query = '(%s."%s" IS NULL or %s."%s" = false )' % (table._table, left,table._table, left) - elif (((right == False) and (type(right)==bool)) or (right is None)) and (operator == '='): - query = '%s."%s" IS NULL ' % (table._table, left) - elif right == False and (leaf[0] in table._columns) and table._columns[leaf[0]]._type=="boolean" and (operator in ['<>', '!=']): - query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table._table, left,table._table, left) - elif (((right == False) and (type(right)==bool)) or right is None) and (operator in ['<>', '!=']): - query = '%s."%s" IS NOT NULL' % (table._table, left) - elif (operator == '=?'): - op = '=' - if (right is False or right is None): - return ( 'TRUE',[]) - if left in table._columns: - format = table._columns[left]._symbol_set[0] - query = '(%s."%s" %s %s)' % (table._table, left, op, format) - params = table._columns[left]._symbol_set[1](right) - else: - query = "(%s.\"%s\" %s '%%s')" % (table._table, left, op) - params = right + elif leaf == FALSE_LEAF: + query = 'FALSE' + params = [] - else: - if left == 'id': - query = '%s.id %s %%s' % (table._table, operator) - params = right + elif operator == 'inselect': + query = '(%s."%s" in (%s))' % (table._table, left, right[0]) + params = right[1] + + elif 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': + r = 'NOT NULL' if right else 'NULL' else: - need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') - sql_operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator) - if left in table._columns: - format = need_wildcard and '%s' or table._columns[left]._symbol_set[0] - query = '(%s."%s" %s %s)' % (table._table, left, sql_operator, format) + r = 'NULL' if right else 'NOT NULL' + query = '(%s."%s" IS %s)' % (table._table, left, r) + params = [] + elif isinstance(right, (list, tuple)): + params = right[:] + check_nulls = False + for i in range(len(params))[::-1]: + if params[i] == False: + check_nulls = True + del params[i] + + if params: + if left == 'id': + instr = ','.join(['%s'] * len(params)) else: - query = "(%s.\"%s\" %s '%s')" % (table._table, left, sql_operator, right) + instr = ','.join([table._columns[left]._symbol_set[0]] * len(params)) + query = '(%s."%s" %s (%s))' % (table._table, left, operator, instr) + else: + # The case for (left, 'in', []) or (left, 'not in', []). + query = 'FALSE' if operator == 'in' else 'TRUE' - add_null = False - if need_wildcard: - if isinstance(right, str): - str_utf8 = right - elif isinstance(right, unicode): - str_utf8 = right.encode('utf-8') - else: - str_utf8 = str(right) - params = '%%%s%%' % str_utf8 - add_null = not str_utf8 - elif left in table._columns: - params = table._columns[left]._symbol_set[1](right) + if check_nulls and operator == 'in': + query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left) + elif not check_nulls and operator == 'not in': + query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left) + elif check_nulls and operator == 'not in': + query = '(%s AND %s."%s" IS NOT NULL)' % (query, table._table, left) # needed only for TRUE. + else: # Must not happen. + pass - if add_null: - query = '(%s OR \"%s\" IS NULL)' % (query, left) + elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '='): + query = '(%s."%s" IS NULL or %s."%s" = false )' % (table._table, left, table._table, left) + params = [] + + elif (right is False or right is None) and (operator == '='): + query = '%s."%s" IS NULL ' % (table._table, left) + params = [] + + elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '!='): + query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table._table, left, table._table, left) + params = [] + + elif (right is False or right is None) and (operator == '!='): + query = '%s."%s" IS NOT NULL' % (table._table, left) + params = [] + + elif (operator == '=?'): + if (right is False or right is None): + query = 'TRUE' + params = [] + elif left in table._columns: + format = table._columns[left]._symbol_set[0] + query = '(%s."%s" = %s)' % (table._table, left, format) + params = table._columns[left]._symbol_set[1](right) + else: + query = "(%s.\"%s\" = '%%s')" % (table._table, left) + params = right + + elif left == 'id': + query = '%s.id %s %%s' % (table._table, operator) + params = right + + else: + need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') + sql_operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator) + + if left in table._columns: + format = need_wildcard and '%s' or table._columns[left]._symbol_set[0] + if self.has_unaccent and sql_operator in ('ilike', 'not ilike'): + query = '(unaccent(%s."%s") %s unaccent(%s))' % (table._table, left, sql_operator, format) + else: + query = '(%s."%s" %s %s)' % (table._table, left, sql_operator, format) + else: + if self.has_unaccent and sql_operator in ('ilike', 'not ilike'): + query = "(unaccent(%s.\"%s\") %s unaccent('%s'))" % (table._table, left, sql_operator, right) + else: + query = "(%s.\"%s\" %s '%s')" % (table._table, left, sql_operator, right) + + add_null = False + if need_wildcard: + if isinstance(right, str): + str_utf8 = right + elif isinstance(right, unicode): + str_utf8 = right.encode('utf-8') + else: + str_utf8 = str(right) + params = '%%%s%%' % str_utf8 + add_null = not str_utf8 + elif left in table._columns: + params = table._columns[left]._symbol_set[1](right) + + if add_null: + query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left) if isinstance(params, basestring): params = [params] @@ -554,25 +779,26 @@ class expression(object): def to_sql(self): stack = [] params = [] + # Process the domain from right to left, using a stack, to generate a SQL expression. for i, e in reverse_enumerate(self.__exp): - if self._is_leaf(e, internal=True): + if is_leaf(e, internal=True): table = self.__field_tables.get(i, self.__main_table) q, p = self.__leaf_to_sql(e, table) params.insert(0, p) stack.append(q) + elif e == NOT_OPERATOR: + stack.append('(NOT (%s))' % (stack.pop(),)) else: - if e == NOT_OPERATOR: - stack.append('(NOT (%s))' % (stack.pop(),)) - else: - ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '} - q1 = stack.pop() - q2 = stack.pop() - stack.append('(%s %s %s)' % (q1, ops[e], q2,)) + ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '} + q1 = stack.pop() + q2 = stack.pop() + stack.append('(%s %s %s)' % (q1, ops[e], q2,)) - query = ' AND '.join(reversed(stack)) + assert len(stack) == 1 + query = stack[0] joins = ' AND '.join(self.__joins) if joins: - query = '(%s) AND (%s)' % (joins, query) + query = '(%s) AND %s' % (joins, query) return (query, flatten(params)) def get_tables(self): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 5d8ed2b27f6..5f19ddf2378 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2459,8 +2459,7 @@ class orm_memory(orm_template): args = [('active', '=', 1)] if args: import expression - e = expression.expression(args) - e.parse(cr, user, self, context) + e = expression.expression(cr, user, args, self, context) res = e.exp return res or [] @@ -2490,6 +2489,9 @@ class orm_memory(orm_template): break f = True for arg in result: + if len(arg) != 3: + # Amazing hack: orm_memory handles only simple domains. + continue if arg[1] == '=': val = eval('data[arg[0]]'+'==' +' arg[2]', locals()) elif arg[1] in ['<', '>', 'in', 'not in', '<=', '>=', '<>']: @@ -4418,8 +4420,7 @@ class orm(orm_template): if domain: import expression - e = expression.expression(domain) - e.parse(cr, user, self, context) + e = expression.expression(cr, user, domain, self, context) tables = e.get_tables() where_clause, where_params = e.to_sql() where_clause = where_clause and [where_clause] or [] diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 18f40e0011f..f7335bcf0c5 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -250,6 +250,8 @@ class configmanager(object): "osv_memory tables. This is a decimal value expressed in hours, " "and the default is 1 hour.", type="float") + group.add_option("--unaccent", dest="unaccent", my_default=False, action="store_true", + help="Use the unaccent function provided by the database when available.") parser.add_option_group(group) # Copy all optparse options (i.e. MyOption) into self.options. @@ -335,7 +337,7 @@ class configmanager(object): 'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog', 'list_db', 'xmlrpcs', 'test_file', 'test_disable', 'test_commit', 'test_report_directory', - 'osv_memory_count_limit', 'osv_memory_age_limit', + 'osv_memory_count_limit', 'osv_memory_age_limit', 'unaccent', ] for arg in keys: