[MERGE]: Merge with latest trunk-server
[odoo/odoo.git] / openerp / osv / expression.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 ##############################################################################
4 #
5 #    OpenERP, Open Source Management Solution
6 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 """ Domain expression processing
24
25 The main duty of this module is to compile a domain expression into a SQL
26 query. A lot of things should be documented here, but as a first step in the
27 right direction, some tests in test_osv_expression.yml might give you some
28 additional information.
29
30 For legacy reasons, a domain uses an inconsistent two-levels abstract syntax
31 (domains are regular Python data structures). At the first level, a domain
32 is an expression made of terms (sometimes called leaves) and (domain) operators
33 used in prefix notation. The available operators at this level are '!', '&',
34 and '|'. '!' is a unary 'not', '&' is a binary 'and', and '|' is a binary 'or'.
35 For instance, here is a possible domain. (<term> stands for an arbitrary term,
36 more on this later.)
37
38     ['&', '!', <term1>, '|', <term2>, <term3>]
39
40 It is equivalent to this pseudo code using infix notation:
41
42     (not <term1>) and (<term2> or <term3>)
43
44 The second level of syntax deals with the term representation. A term is
45 a triple of the form (left, operator, right). That is, a term uses an infix
46 notation, and the available operators, and possible left and right operands
47 differ with those of the previous level. Here is a possible term:
48
49     ('company_id.name', '=', 'OpenERP')
50
51 The left and right operand don't have the same possible values. The left
52 operand is field name (related to the model for which the domain applies).
53 Actually, the field name can use the dot-notation to traverse relationships.
54 The right operand is a Python value whose type should match the used operator
55 and field type. In the above example, a string is used because the name field
56 of a company has type string, and because we use the '=' operator. When
57 appropriate, a 'in' operator can be used, and thus the right operand should be
58 a list.
59
60 Note: the non-uniform syntax could have been more uniform, but this would hide
61 an important limitation of the domain syntax. Say that the term representation
62 was ['=', 'company_id.name', 'OpenERP']. Used in a complete domain, this would
63 look like:
64
65   ['!', ['=', 'company_id.name', 'OpenERP']]
66
67 and you would be tempted to believe something like this would be possible:
68
69   ['!', ['=', 'company_id.name', ['&', ..., ...]]]
70
71 That is, a domain could be a valid operand. But this is not the case. A domain
72 is really limited to a two-level nature, and can not take a recursive form: a
73 domain is not a valid second-level operand.
74
75 Unaccent - Accent-insensitive search
76
77 OpenERP will use the SQL function 'unaccent' when available for the 'ilike' and
78 'not ilike' operators, and enabled in the configuration.
79 Normally the 'unaccent' function is obtained from the PostgreSQL 'unaccent'
80 contrib module[0].
81
82
83 ..todo: The following explanation should be moved in some external installation
84         guide
85
86 The steps to install the module might differ on specific PostgreSQL versions.
87 We give here some instruction for PostgreSQL 9.x on a Ubuntu system.
88
89 Ubuntu doesn't come yet with PostgreSQL 9.x, so an alternative package source
90 is used. We use Martin Pitt's PPA available at ppa:pitti/postgresql[1]. See
91 [2] for instructions. Basically:
92
93     > sudo add-apt-repository ppa:pitti/postgresql
94     > sudo apt-get update
95
96 Once the package list is up-to-date, you have to install PostgreSQL 9.0 and
97 its contrib modules.
98
99     > sudo apt-get install postgresql-9.0 postgresql-contrib-9.0
100
101 When you want to enable unaccent on some database:
102
103     > psql9 <database> -f /usr/share/postgresql/9.0/contrib/unaccent.sql
104
105 Here 'psql9' is an alias for the newly installed PostgreSQL 9.0 tool, together
106 with the correct port if necessary (for instance if PostgreSQL 8.4 is running
107 on 5432). (Other aliases can be used for createdb and dropdb.)
108
109     > alias psql9='/usr/lib/postgresql/9.0/bin/psql -p 5433'
110
111 You can check unaccent is working:
112
113     > psql9 <database> -c"select unaccent('hélène')"
114
115 Finally, to instruct OpenERP to really use the unaccent function, you have to
116 start the server specifying the --unaccent flag.
117
118 [0] http://developer.postgresql.org/pgdocs/postgres/unaccent.html
119 [1] https://launchpad.net/~pitti/+archive/postgresql
120 [2] https://launchpad.net/+help/soyuz/ppa-sources-list.html
121
122 """
123
124 import logging
125
126 from openerp.tools import flatten, reverse_enumerate
127 import fields
128 import openerp.modules
129 from openerp.osv.orm import MAGIC_COLUMNS
130
131 #.apidoc title: Domain Expressions
132
133 # Domain operators.
134 NOT_OPERATOR = '!'
135 OR_OPERATOR = '|'
136 AND_OPERATOR = '&'
137 DOMAIN_OPERATORS = (NOT_OPERATOR, OR_OPERATOR, AND_OPERATOR)
138
139 # List of available term operators. It is also possible to use the '<>'
140 # operator, which is strictly the same as '!='; the later should be prefered
141 # for consistency. This list doesn't contain '<>' as it is simpified to '!='
142 # by the normalize_operator() function (so later part of the code deals with
143 # only one representation).
144 # An internal (i.e. not available to the user) 'inselect' operator is also
145 # used. In this case its right operand has the form (subselect, params).
146 TERM_OPERATORS = ('=', '!=', '<=', '<', '>', '>=', '=?', '=like', '=ilike',
147                   'like', 'not like', 'ilike', 'not ilike', 'in', 'not in',
148                   'child_of')
149
150 # A subset of the above operators, with a 'negative' semantic. When the
151 # expressions 'in NEGATIVE_TERM_OPERATORS' or 'not in NEGATIVE_TERM_OPERATORS' are used in the code
152 # below, this doesn't necessarily mean that any of those NEGATIVE_TERM_OPERATORS is
153 # legal in the processed term.
154 NEGATIVE_TERM_OPERATORS = ('!=', 'not like', 'not ilike', 'not in')
155
156 TRUE_LEAF = (1, '=', 1)
157 FALSE_LEAF = (0, '=', 1)
158
159 TRUE_DOMAIN = [TRUE_LEAF]
160 FALSE_DOMAIN = [FALSE_LEAF]
161
162 _logger = logging.getLogger('expression')
163
164 def normalize(domain):
165     """Returns a normalized version of ``domain_expr``, where all implicit '&' operators
166        have been made explicit. One property of normalized domain expressions is that they
167        can be easily combined together as if they were single domain components.
168     """
169     assert isinstance(domain, (list, tuple)), "Domains to normalize must have a 'domain' form: a list or tuple of domain components"
170     if not domain:
171         return TRUE_DOMAIN
172     result = []
173     expected = 1                            # expected number of expressions
174     op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2}
175     for token in domain:
176         if expected == 0:                   # more than expected, like in [A, B]
177             result[0:0] = [AND_OPERATOR]             # put an extra '&' in front
178             expected = 1
179         result.append(token)
180         if isinstance(token, (list, tuple)): # domain term
181             expected -= 1
182         else:
183             expected += op_arity.get(token, 0) - 1
184     assert expected == 0
185     return result
186
187 def combine(operator, unit, zero, domains):
188     """Returns a new domain expression where all domain components from ``domains``
189        have been added together using the binary operator ``operator``. The given
190        domains must be normalized.
191
192        :param unit: the identity element of the domains "set" with regard to the operation
193                     performed by ``operator``, i.e the domain component ``i`` which, when
194                     combined with any domain ``x`` via ``operator``, yields ``x``.
195                     E.g. [(1,'=',1)] is the typical unit for AND_OPERATOR: adding it
196                     to any domain component gives the same domain.
197        :param zero: the absorbing element of the domains "set" with regard to the operation
198                     performed by ``operator``, i.e the domain component ``z`` which, when
199                     combined with any domain ``x`` via ``operator``, yields ``z``.
200                     E.g. [(1,'=',1)] is the typical zero for OR_OPERATOR: as soon as
201                     you see it in a domain component the resulting domain is the zero.
202        :param domains: a list of normalized domains.
203     """
204     result = []
205     count = 0
206     for domain in domains:
207         if domain == unit:
208             continue
209         if domain == zero:
210             return zero
211         if domain:
212             result += domain
213             count += 1
214     result = [operator] * (count - 1) + result
215     return result
216
217 def AND(domains):
218     """AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """
219     return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains)
220
221 def OR(domains):
222     """OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """
223     return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains)
224
225 def is_operator(element):
226     """Test whether an object is a valid domain operator. """
227     return isinstance(element, basestring) and element in DOMAIN_OPERATORS
228
229 # TODO change the share wizard to use this function.
230 def is_leaf(element, internal=False):
231     """ Test whether an object is a valid domain term.
232
233     :param internal: allow or not the 'inselect' internal operator in the term.
234     This normally should be always left to False.
235     """
236     INTERNAL_OPS = TERM_OPERATORS + ('inselect',)
237     return (isinstance(element, tuple) or isinstance(element, list)) \
238        and len(element) == 3 \
239        and (((not internal) and element[1] in TERM_OPERATORS + ('<>',)) \
240             or (internal and element[1] in INTERNAL_OPS + ('<>',)))
241
242 def normalize_leaf(left, operator, right):
243     """ Change a term's operator to some canonical form, simplifying later
244     processing.
245     """
246     original = operator
247     operator = operator.lower()
248     if operator == '<>':
249         operator = '!='
250     if isinstance(right, bool) and operator in ('in', 'not in'):
251         _logger.warning("The domain term '%s' should use the '=' or '!=' operator." % ((left, original, right),))
252         operator = '=' if operator == 'in' else '!='
253     if isinstance(right, (list, tuple)) and operator in ('=', '!='):
254         _logger.warning("The domain term '%s' should use the 'in' or 'not in' operator." % ((left, original, right),))
255         operator = 'in' if operator == '=' else 'not in'
256     return left, operator, right
257
258 def distribute_not(domain):
259     """ Distribute any '!' domain operators found inside a normalized domain.
260
261     Because we don't use SQL semantic for processing a 'left not in right'
262     query (i.e. our 'not in' is not simply translated to a SQL 'not in'),
263     it means that a '! left in right' can not be simply processed
264     by __leaf_to_sql by first emitting code for 'left in right' then wrapping
265     the result with 'not (...)', as it would result in a 'not in' at the SQL
266     level.
267
268     This function is thus responsible for pushing any '!' domain operators
269     inside the terms themselves. For example::
270
271          ['!','&',('user_id','=',4),('partner_id','in',[1,2])]
272             will be turned into:
273          ['|',('user_id','!=',4),('partner_id','not in',[1,2])]
274
275     """
276     def negate(leaf):
277         """Negates and returns a single domain leaf term,
278         using the opposite operator if possible"""
279         left, operator, right = leaf
280         mapping = {
281             '<': '>=',
282             '>': '<=',
283             '<=': '>',
284             '>=': '<',
285             '=': '!=',
286             '!=': '=',
287         }
288         if operator in ('in', 'like', 'ilike'):
289             operator = 'not ' + operator
290             return [(left, operator, right)]
291         if operator in ('not in', 'not like', 'not ilike'):
292             operator = operator[4:]
293             return [(left, operator, right)]
294         if operator in mapping:
295             operator = mapping[operator]
296             return [(left, operator, right)]
297         return [NOT_OPERATOR, (left, operator, right)]
298     def distribute_negate(domain):
299         """Negate the domain ``subtree`` rooted at domain[0],
300         leaving the rest of the domain intact, and return
301         (negated_subtree, untouched_domain_rest)
302         """
303         if is_leaf(domain[0]):
304             return negate(domain[0]), domain[1:]
305         if domain[0] == AND_OPERATOR:
306             done1, todo1 = distribute_negate(domain[1:])
307             done2, todo2 = distribute_negate(todo1)
308             return [OR_OPERATOR] + done1 + done2, todo2
309         if domain[0] == OR_OPERATOR:
310             done1, todo1 = distribute_negate(domain[1:])
311             done2, todo2 = distribute_negate(todo1)
312             return [AND_OPERATOR] + done1 + done2, todo2
313     if not domain:
314         return []
315     if domain[0] != NOT_OPERATOR:
316         return [domain[0]] + distribute_not(domain[1:])
317     if domain[0] == NOT_OPERATOR:
318         done, todo = distribute_negate(domain[1:])
319         return done + distribute_not(todo)
320
321 def select_from_where(cr, select_field, from_table, where_field, where_ids, where_operator):
322     # todo: merge into parent query as sub-query
323     res = []
324     if where_ids:
325         if where_operator in ['<','>','>=','<=']:
326             cr.execute('SELECT "%s" FROM "%s" WHERE "%s" %s %%s' % \
327                 (select_field, from_table, where_field, where_operator),
328                 (where_ids[0],)) # TODO shouldn't this be min/max(where_ids) ?
329             res = [r[0] for r in cr.fetchall()]
330         else: # TODO where_operator is supposed to be 'in'? It is called with child_of...
331             for i in range(0, len(where_ids), cr.IN_MAX):
332                 subids = where_ids[i:i+cr.IN_MAX]
333                 cr.execute('SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % \
334                     (select_field, from_table, where_field), (tuple(subids),))
335                 res.extend([r[0] for r in cr.fetchall()])
336     return res
337
338 def select_distinct_from_where_not_null(cr, select_field, from_table):
339     cr.execute('SELECT distinct("%s") FROM "%s" where "%s" is not null' % \
340                (select_field, from_table, select_field))
341     return [r[0] for r in cr.fetchall()]
342
343 class expression(object):
344     """
345     parse a domain expression
346     use a real polish notation
347     leafs are still in a ('foo', '=', 'bar') format
348     For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
349     """
350
351     def __init__(self, cr, uid, exp, table, context):
352         self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent
353         self.__field_tables = {}  # used to store the table to use for the sql generation. key = index of the leaf
354         self.__all_tables = set()
355         self.__joins = []
356         self.__main_table = None # 'root' table. set by parse()
357         # assign self.__exp with the normalized, parsed domain.
358         self.parse(cr, uid, distribute_not(normalize(exp)), table, context)
359
360     # TODO used only for osv_memory
361     @property
362     def exp(self):
363         return self.__exp[:]
364
365     def parse(self, cr, uid, exp, table, context):
366         """ transform the leaves of the expression """
367         self.__exp = exp
368         self.__main_table = table
369         self.__all_tables.add(table)
370
371         def child_of_domain(left, ids, left_model, parent=None, prefix=''):
372             """Returns a domain implementing the child_of operator for [(left,child_of,ids)],
373             either as a range using the parent_left/right tree lookup fields (when available),
374             or as an expanded [(left,in,child_ids)]"""
375             if left_model._parent_store and (not left_model.pool._init):
376                 # TODO: Improve where joins are implemented for many with '.', replace by:
377                 # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
378                 doms = []
379                 for o in left_model.browse(cr, uid, ids, context=context):
380                     if doms:
381                         doms.insert(0, OR_OPERATOR)
382                     doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
383                 if prefix:
384                     return [(left, 'in', left_model.search(cr, uid, doms, context=context))]
385                 return doms
386             else:
387                 def recursive_children(ids, model, parent_field):
388                     if not ids:
389                         return []
390                     ids2 = model.search(cr, uid, [(parent_field, 'in', ids)], context=context)
391                     return ids + recursive_children(ids2, model, parent_field)
392                 return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))]
393
394         def to_ids(value, field_obj):
395             """Normalize a single id or name, or a list of those, into a list of ids"""
396             names = []
397             if isinstance(value, basestring):
398                 names = [value]
399             if value and isinstance(value, (tuple, list)) and isinstance(value[0], basestring):
400                 names = value
401             if names:
402                 return flatten([[x[0] for x in field_obj.name_search(cr, uid, n, [], 'ilike', context=context, limit=None)] \
403                                     for n in names])
404             elif isinstance(value, (int, long)):
405                 return [value]
406             return list(value)
407
408         i = -1
409         while i + 1<len(self.__exp):
410             i += 1
411             e = self.__exp[i]
412             if is_operator(e) or e == TRUE_LEAF or e == FALSE_LEAF:
413                 continue
414
415             # check if the expression is valid
416             if not is_leaf(e):
417                 raise ValueError("Invalid term %r in domain expression %r" % (e, exp))
418
419             # normalize the leaf's operator
420             e = normalize_leaf(*e)
421             self.__exp[i] = e
422             left, operator, right = e
423
424             working_table = table # The table containing the field (the name provided in the left operand)
425             field_path = left.split('.', 1)
426
427             # If the field is _inherits'd, search for the working_table,
428             # and extract the field.
429             if field_path[0] in table._inherit_fields:
430                 while True:
431                     field = working_table._columns.get(field_path[0])
432                     if field:
433                         self.__field_tables[i] = working_table
434                         break
435                     next_table = working_table.pool.get(working_table._inherit_fields[field_path[0]][0])
436                     if next_table not in self.__all_tables:
437                         self.__joins.append('%s."%s"=%s."%s"' % (next_table._table, 'id', working_table._table, working_table._inherits[next_table._name]))
438                         self.__all_tables.add(next_table)
439                     working_table = next_table
440             # Or (try to) directly extract the field.
441             else:
442                 field = working_table._columns.get(field_path[0])
443
444             if not field:
445                 if left == 'id' and operator == 'child_of':
446                     ids2 = to_ids(right, table)
447                     dom = child_of_domain(left, ids2, working_table)
448                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
449                 else:
450                     # field could not be found in model columns, it's probably invalid, unless
451                     # it's one of the _log_access special fields
452                     # TODO: make these fields explicitly available in self.columns instead!
453                     if field_path[0] not in MAGIC_COLUMNS:
454                         raise ValueError("Invalid field %r in domain expression %r" % (left, exp))
455                 continue
456
457             field_obj = table.pool.get(field._obj)
458             if len(field_path) > 1:
459                 if field._type == 'many2one':
460                     right = field_obj.search(cr, uid, [(field_path[1], operator, right)], context=context)
461                     self.__exp[i] = (field_path[0], 'in', right)
462                 # Making search easier when there is a left operand as field.o2m or field.m2m
463                 if field._type in ['many2many', 'one2many']:
464                     right = field_obj.search(cr, uid, [(field_path[1], operator, right)], context=context)
465                     right1 = table.search(cr, uid, [(field_path[0],'in', right)], context=dict(context, active_test=False))
466                     self.__exp[i] = ('id', 'in', right1)
467
468                 if not isinstance(field, fields.property):
469                     continue
470
471             if field._properties and not field.store:
472                 # this is a function field that is not stored
473                 if not field._fnct_search:
474                     # the function field doesn't provide a search function and doesn't store
475                     # values in the database, so we must ignore it : we generate a dummy leaf
476                     self.__exp[i] = TRUE_LEAF
477                 else:
478                     subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context)
479                     if not subexp:
480                         self.__exp[i] = TRUE_LEAF
481                     else:
482                         # we assume that the expression is valid
483                         # we create a dummy leaf for forcing the parsing of the resulting expression
484                         self.__exp[i] = AND_OPERATOR
485                         self.__exp.insert(i + 1, TRUE_LEAF)
486                         for j, se in enumerate(subexp):
487                             self.__exp.insert(i + 2 + j, se)
488             # else, the value of the field is store in the database, so we search on it
489
490             elif field._type == 'one2many':
491                 # Applying recursivity on field(one2many)
492                 if operator == 'child_of':
493                     ids2 = to_ids(right, field_obj)
494                     if field._obj != working_table._name:
495                         dom = child_of_domain(left, ids2, field_obj, prefix=field._obj)
496                     else:
497                         dom = child_of_domain('id', ids2, working_table, parent=left)
498                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
499
500                 else:
501                     call_null = True
502
503                     if right is not False:
504                         if isinstance(right, basestring):
505                             ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, context=context, limit=None)]
506                             if ids2:
507                                 operator = 'in'
508                         else:
509                             if not isinstance(right, list):
510                                 ids2 = [right]
511                             else:
512                                 ids2 = right
513                         if not ids2:
514                             if operator in ['like','ilike','in','=']:
515                                 #no result found with given search criteria
516                                 call_null = False
517                                 self.__exp[i] = FALSE_LEAF
518                         else:
519                             ids2 = select_from_where(cr, field._fields_id, field_obj._table, 'id', ids2, operator)
520                             if ids2:
521                                 call_null = False
522                                 self.__exp[i] = ('id', 'in', ids2)
523
524                     if call_null:
525                         o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
526                         self.__exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, field_obj._table))
527
528             elif field._type == 'many2many':
529                 #FIXME
530                 if operator == 'child_of':
531                     def _rec_convert(ids):
532                         if field_obj == table:
533                             return ids
534                         return select_from_where(cr, field._id1, field._rel, field._id2, ids, operator)
535
536                     ids2 = to_ids(right, field_obj)
537                     dom = child_of_domain('id', ids2, field_obj)
538                     ids2 = field_obj.search(cr, uid, dom, context=context)
539                     self.__exp[i] = ('id', 'in', _rec_convert(ids2))
540                 else:
541                     call_null_m2m = True
542                     if right is not False:
543                         if isinstance(right, basestring):
544                             res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, context=context)]
545                             if res_ids:
546                                 operator = 'in'
547                         else:
548                             if not isinstance(right, list):
549                                 res_ids = [right]
550                             else:
551                                 res_ids = right
552                         if not res_ids:
553                             if operator in ['like','ilike','in','=']:
554                                 #no result found with given search criteria
555                                 call_null_m2m = False
556                                 self.__exp[i] = FALSE_LEAF
557                             else:
558                                 operator = 'in' # operator changed because ids are directly related to main object
559                         else:
560                             call_null_m2m = False
561                             m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
562                             self.__exp[i] = ('id', m2m_op, select_from_where(cr, field._id1, field._rel, field._id2, res_ids, operator) or [0])
563
564                     if call_null_m2m:
565                         m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
566                         self.__exp[i] = ('id', m2m_op, select_distinct_from_where_not_null(cr, field._id1, field._rel))
567
568             elif field._type == 'many2one':
569                 if operator == 'child_of':
570                     ids2 = to_ids(right, field_obj)
571                     if field._obj != working_table._name:
572                         dom = child_of_domain(left, ids2, field_obj, prefix=field._obj)
573                     else:
574                         dom = child_of_domain('id', ids2, working_table, parent=left)
575                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
576                 else:
577                     def _get_expression(field_obj, cr, uid, left, right, operator, context=None):
578                         if context is None:
579                             context = {}
580                         c = context.copy()
581                         c['active_test'] = False
582                         #Special treatment to ill-formed domains
583                         operator = ( operator in ['<','>','<=','>='] ) and 'in' or operator
584
585                         dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in'}
586                         if isinstance(right, tuple):
587                             right = list(right)
588                         if (not isinstance(right, list)) and operator in ['not in','in']:
589                             operator = dict_op[operator]
590                         elif isinstance(right, list) and operator in ['!=','=']: #for domain (FIELD,'=',['value1','value2'])
591                             operator = dict_op[operator]
592                         res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)]
593                         if operator in NEGATIVE_TERM_OPERATORS:
594                             res_ids.append(False) # TODO this should not be appended if False was in 'right'
595                         return (left, 'in', res_ids)
596                     # resolve string-based m2o criterion into IDs
597                     if isinstance(right, basestring) or \
598                             right and isinstance(right, (tuple,list)) and all(isinstance(item, basestring) for item in right):
599                         self.__exp[i] = _get_expression(field_obj, cr, uid, left, right, operator, context=context)
600                     else: 
601                         # right == [] or right == False and all other cases are handled by __leaf_to_sql()
602                         pass
603
604             else:
605                 # other field type
606                 # add the time part to datetime field when it's not there:
607                 if field._type == 'datetime' and self.__exp[i][2] and len(self.__exp[i][2]) == 10:
608
609                     self.__exp[i] = list(self.__exp[i])
610
611                     if operator in ('>', '>='):
612                         self.__exp[i][2] += ' 00:00:00'
613                     elif operator in ('<', '<='):
614                         self.__exp[i][2] += ' 23:59:59'
615
616                     self.__exp[i] = tuple(self.__exp[i])
617
618                 if field.translate:
619                     need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
620                     sql_operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
621                     if need_wildcard:
622                         right = '%%%s%%' % right
623
624                     subselect = '( SELECT res_id'          \
625                              '    FROM ir_translation'  \
626                              '   WHERE name = %s'       \
627                              '     AND lang = %s'       \
628                              '     AND type = %s'
629                     instr = ' %s'
630                     #Covering in,not in operators with operands (%s,%s) ,etc.
631                     if sql_operator in ['in','not in']:
632                         instr = ','.join(['%s'] * len(right))
633                         subselect += '     AND value ' + sql_operator +  ' ' +" (" + instr + ")"   \
634                              ') UNION ('                \
635                              '  SELECT id'              \
636                              '    FROM "' + working_table._table + '"'       \
637                              '   WHERE "' + left + '" ' + sql_operator + ' ' +" (" + instr + "))"
638                     else:
639                         subselect += '     AND value ' + sql_operator + instr +   \
640                              ') UNION ('                \
641                              '  SELECT id'              \
642                              '    FROM "' + working_table._table + '"'       \
643                              '   WHERE "' + left + '" ' + sql_operator + instr + ")"
644
645                     params = [working_table._name + ',' + left,
646                               context.get('lang', False) or 'en_US',
647                               'model',
648                               right,
649                               right,
650                              ]
651
652                     self.__exp[i] = ('id', 'inselect', (subselect, params))
653
654     def __leaf_to_sql(self, leaf, table):
655         left, operator, right = leaf
656
657         # final sanity checks - should never fail
658         assert operator in (TERM_OPERATORS + ('inselect',)), \
659             "Invalid operator %r in domain term %r" % (operator, leaf)
660         assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in table._all_columns \
661             or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf)
662
663         if leaf == TRUE_LEAF:
664             query = 'TRUE'
665             params = []
666
667         elif leaf == FALSE_LEAF:
668             query = 'FALSE'
669             params = []
670
671         elif operator == 'inselect':
672             query = '(%s."%s" in (%s))' % (table._table, left, right[0])
673             params = right[1]
674
675         elif operator in ['in', 'not in']:
676             # Two cases: right is a boolean or a list. The boolean case is an
677             # abuse and handled for backward compatibility.
678             if isinstance(right, bool):
679                 _logger.warning("The domain term '%s' should use the '=' or '!=' operator." % (leaf,))
680                 if operator == 'in':
681                     r = 'NOT NULL' if right else 'NULL'
682                 else:
683                     r = 'NULL' if right else 'NOT NULL'
684                 query = '(%s."%s" IS %s)' % (table._table, left, r)
685                 params = []
686             elif isinstance(right, (list, tuple)):
687                 params = right[:]
688                 check_nulls = False
689                 for i in range(len(params))[::-1]:
690                     if params[i] == False:
691                         check_nulls = True
692                         del params[i]
693
694                 if params:
695                     if left == 'id':
696                         instr = ','.join(['%s'] * len(params))
697                     else:
698                         instr = ','.join([table._columns[left]._symbol_set[0]] * len(params))
699                     query = '(%s."%s" %s (%s))' % (table._table, left, operator, instr)
700                 else:
701                     # The case for (left, 'in', []) or (left, 'not in', []).
702                     query = 'FALSE' if operator == 'in' else 'TRUE'
703
704                 if check_nulls and operator == 'in':
705                     query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
706                 elif not check_nulls and operator == 'not in':
707                     query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
708                 elif check_nulls and operator == 'not in':
709                     query = '(%s AND %s."%s" IS NOT NULL)' % (query, table._table, left) # needed only for TRUE.
710             else: # Must not happen
711                 raise ValueError("Invalid domain term %r" % (leaf,))
712
713         elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '='):
714             query = '(%s."%s" IS NULL or %s."%s" = false )' % (table._table, left, table._table, left)
715             params = []
716
717         elif (right is False or right is None) and (operator == '='):
718             query = '%s."%s" IS NULL ' % (table._table, left)
719             params = []
720
721         elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '!='):
722             query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table._table, left, table._table, left)
723             params = []
724
725         elif (right is False or right is None) and (operator == '!='):
726             query = '%s."%s" IS NOT NULL' % (table._table, left)
727             params = []
728
729         elif (operator == '=?'):
730             if (right is False or right is None):
731                 # '=?' is a short-circuit that makes the term TRUE if right is None or False
732                 query = 'TRUE'
733                 params = []
734             else:
735                 # '=?' behaves like '=' in other cases
736                 query, params = self.__leaf_to_sql((left, '=', right), table)
737
738         elif left == 'id':
739             query = '%s.id %s %%s' % (table._table, operator)
740             params = right
741
742         else:
743             need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
744             sql_operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
745
746             if left in table._columns:
747                 format = need_wildcard and '%s' or table._columns[left]._symbol_set[0]
748                 if self.has_unaccent and sql_operator in ('ilike', 'not ilike'):
749                     query = '(unaccent(%s."%s") %s unaccent(%s))' % (table._table, left, sql_operator, format)
750                 else:
751                     query = '(%s."%s" %s %s)' % (table._table, left, sql_operator, format)
752             elif left in MAGIC_COLUMNS:
753                     query = "(%s.\"%s\" %s %%s)" % (table._table, left, sql_operator)
754                     params = right
755             else: # Must not happen
756                 raise ValueError("Invalid field %r in domain term %r" % (left, leaf))
757
758             add_null = False
759             if need_wildcard:
760                 if isinstance(right, str):
761                     str_utf8 = right
762                 elif isinstance(right, unicode):
763                     str_utf8 = right.encode('utf-8')
764                 else:
765                     str_utf8 = str(right)
766                 params = '%%%s%%' % str_utf8
767                 add_null = not str_utf8
768             elif left in table._columns:
769                 params = table._columns[left]._symbol_set[1](right)
770
771             if add_null:
772                 query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
773
774         if isinstance(params, basestring):
775             params = [params]
776         return (query, params)
777
778
779     def to_sql(self):
780         stack = []
781         params = []
782         # Process the domain from right to left, using a stack, to generate a SQL expression.
783         for i, e in reverse_enumerate(self.__exp):
784             if is_leaf(e, internal=True):
785                 table = self.__field_tables.get(i, self.__main_table)
786                 q, p = self.__leaf_to_sql(e, table)
787                 params.insert(0, p)
788                 stack.append(q)
789             elif e == NOT_OPERATOR:
790                 stack.append('(NOT (%s))' % (stack.pop(),))
791             else:
792                 ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
793                 q1 = stack.pop()
794                 q2 = stack.pop()
795                 stack.append('(%s %s %s)' % (q1, ops[e], q2,))
796
797         assert len(stack) == 1
798         query = stack[0]
799         joins = ' AND '.join(self.__joins)
800         if joins:
801             query = '(%s) AND %s' % (joins, query)
802         return (query, flatten(params))
803
804     def get_tables(self):
805         return ['"%s"' % t._table for t in self.__all_tables]
806
807 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
808