[MERGE] small code improvement
[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 from openerp.tools import flatten, reverse_enumerate
24 import fields
25
26 #.apidoc title: Domain Expressions
27
28 NOT_OPERATOR = '!'
29 OR_OPERATOR = '|'
30 AND_OPERATOR = '&'
31
32 TRUE_DOMAIN = [(1,'=',1)]
33 FALSE_DOMAIN = [(0,'=',1)]
34
35 def normalize(domain):
36     """Returns a normalized version of ``domain_expr``, where all implicit '&' operators
37        have been made explicit. One property of normalized domain expressions is that they
38        can be easily combined together as if they were single domain components.
39     """
40     assert isinstance(domain, (list, tuple)), "Domains to normalize must have a 'domain' form: a list or tuple of domain components"
41     if not domain:
42         return TRUE_DOMAIN
43     result = []
44     expected = 1                            # expected number of expressions
45     op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2}
46     for token in domain:
47         if expected == 0:                   # more than expected, like in [A, B]
48             result[0:0] = ['&']             # put an extra '&' in front
49             expected = 1
50         result.append(token)
51         if isinstance(token, (list,tuple)): # domain term
52             expected -= 1
53         else:
54             expected += op_arity.get(token, 0) - 1
55     assert expected == 0
56     return result
57
58 def combine(operator, unit, zero, domains):
59     """Returns a new domain expression where all domain components from ``domains``
60        have been added together using the binary operator ``operator``.
61
62        :param unit: the identity element of the domains "set" with regard to the operation
63                     performed by ``operator``, i.e the domain component ``i`` which, when
64                     combined with any domain ``x`` via ``operator``, yields ``x``. 
65                     E.g. [(1,'=',1)] is the typical unit for AND_OPERATOR: adding it
66                     to any domain component gives the same domain.
67        :param zero: the absorbing element of the domains "set" with regard to the operation
68                     performed by ``operator``, i.e the domain component ``z`` which, when
69                     combined with any domain ``x`` via ``operator``, yields ``z``. 
70                     E.g. [(1,'=',1)] is the typical zero for OR_OPERATOR: as soon as
71                     you see it in a domain component the resulting domain is the zero.
72     """
73     result = []
74     count = 0
75     for domain in domains:
76         if domain == unit:
77             continue
78         if domain == zero:
79             return zero
80         if domain:
81             result += domain
82             count += 1
83     result = [operator] * (count - 1) + result
84     return result
85
86 def AND(domains):
87     """ AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """
88     return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains)
89
90 def OR(domains):
91     """ OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """
92     return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains)
93
94
95 class expression(object):
96     """
97     parse a domain expression
98     use a real polish notation
99     leafs are still in a ('foo', '=', 'bar') format
100     For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
101     """
102
103     @classmethod
104     def _is_operator(cls, element):
105         return isinstance(element, (str, unicode)) and element in [AND_OPERATOR, OR_OPERATOR, NOT_OPERATOR]
106
107     @classmethod
108     def _is_leaf(cls, element, internal=False):
109         OPS = ('=', '!=', '<>', '<=', '<', '>', '>=', '=?', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of')
110         INTERNAL_OPS = OPS + ('inselect',)
111         return (isinstance(element, tuple) or isinstance(element, list)) \
112            and len(element) == 3 \
113            and (((not internal) and element[1] in OPS) \
114                 or (internal and element[1] in INTERNAL_OPS))
115
116     def __execute_recursive_in(self, cr, s, f, w, ids, op, type):
117         # todo: merge into parent query as sub-query
118         res = []
119         if ids:
120             if op in ['<','>','>=','<=']:
121                 cr.execute('SELECT "%s"'    \
122                                '  FROM "%s"'    \
123                                ' WHERE "%s" %s %%s' % (s, f, w, op), (ids[0],))
124                 res.extend([r[0] for r in cr.fetchall()])
125             else:
126                 for i in range(0, len(ids), cr.IN_MAX):
127                     subids = ids[i:i+cr.IN_MAX]
128                     cr.execute('SELECT "%s"'    \
129                                '  FROM "%s"'    \
130                                '  WHERE "%s" IN %%s' % (s, f, w),(tuple(subids),))
131                     res.extend([r[0] for r in cr.fetchall()])
132         else:
133             cr.execute('SELECT distinct("%s")'    \
134                            '  FROM "%s" where "%s" is not null'  % (s, f, s)),
135             res.extend([r[0] for r in cr.fetchall()])
136         return res
137
138     def __init__(self, exp):
139         # check if the expression is valid
140         if not reduce(lambda acc, val: acc and (self._is_operator(val) or self._is_leaf(val)), exp, True):
141             raise ValueError('Bad domain expression: %r' % (exp,))
142         self.__exp = exp
143         self.__field_tables = {}  # used to store the table to use for the sql generation. key = index of the leaf
144         self.__all_tables = set()
145         self.__joins = []
146         self.__main_table = None # 'root' table. set by parse()
147         self.__DUMMY_LEAF = (1, '=', 1) # a dummy leaf that must not be parsed or sql generated
148
149     @property
150     def exp(self):
151         return self.__exp[:]
152
153     def parse(self, cr, uid, table, context):
154         """ transform the leafs of the expression """
155         if not self.__exp:
156             return self
157
158         def _rec_get(ids, table, parent=None, left='id', prefix=''):
159             if table._parent_store and (not table.pool._init):
160 # TODO: Improve where joins are implemented for many with '.', replace by:
161 # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
162                 doms = []
163                 for o in table.browse(cr, uid, ids, context=context):
164                     if doms:
165                         doms.insert(0, OR_OPERATOR)
166                     doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
167                 if prefix:
168                     return [(left, 'in', table.search(cr, uid, doms, context=context))]
169                 return doms
170             else:
171                 def rg(ids, table, parent):
172                     if not ids:
173                         return []
174                     ids2 = table.search(cr, uid, [(parent, 'in', ids)], context=context)
175                     return ids + rg(ids2, table, parent)
176                 return [(left, 'in', rg(ids, table, parent or table._parent_name))]
177
178         def child_of_right_to_ids(value):
179             """ Normalize a single id, or a string, or a list of ids to a list of ids.
180
181             This function is always used with _rec_get() above, so it should be
182             called directly from _rec_get instead of repeatedly before _rec_get.
183
184             """
185             if isinstance(value, basestring):
186                 return [x[0] for x in field_obj.name_search(cr, uid, value, [], 'ilike', context=context, limit=None)]
187             elif isinstance(value, (int, long)):
188                 return [value]
189             else:
190                 return list(value)
191
192         self.__main_table = table
193         self.__all_tables.add(table)
194
195         i = -1
196         while i + 1<len(self.__exp):
197             i += 1
198             e = self.__exp[i]
199             if self._is_operator(e) or e == self.__DUMMY_LEAF:
200                 continue
201             left, operator, right = e
202             operator = operator.lower()
203             working_table = table
204             main_table = table
205             fargs = left.split('.', 1)
206             if fargs[0] in table._inherit_fields:
207                 while True:
208                     field = main_table._columns.get(fargs[0], False)
209                     if field:
210                         working_table = main_table
211                         self.__field_tables[i] = working_table
212                         break
213                     working_table = main_table.pool.get(main_table._inherit_fields[fargs[0]][0])
214                     if working_table not in self.__all_tables:
215                         self.__joins.append('%s.%s=%s.%s' % (working_table._table, 'id', main_table._table, main_table._inherits[working_table._name]))
216                         self.__all_tables.add(working_table)
217                     main_table = working_table
218
219             field = working_table._columns.get(fargs[0], False)
220             if not field:
221                 if left == 'id' and operator == 'child_of':
222                     ids2 = child_of_right_to_ids(right)
223                     dom = _rec_get(ids2, working_table)
224                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
225                 continue
226
227             field_obj = table.pool.get(field._obj)
228             if len(fargs) > 1:
229                 if field._type == 'many2one':
230                     right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
231                     if right == []:
232                         self.__exp[i] = ( 'id', '=', 0 )
233                     else:
234                         self.__exp[i] = (fargs[0], 'in', right)
235                 # Making search easier when there is a left operand as field.o2m or field.m2m
236                 if field._type in ['many2many','one2many']:
237                     right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
238                     right1 = table.search(cr, uid, [(fargs[0],'in', right)], context=context)
239                     if right1 == []:
240                         self.__exp[i] = ( 'id', '=', 0 )
241                     else:
242                         self.__exp[i] = ('id', 'in', right1)
243
244                 if not isinstance(field,fields.property):
245                     continue
246
247             if field._properties and not field.store:
248                 # this is a function field that is not stored
249                 if not field._fnct_search:
250                     # the function field doesn't provide a search function and doesn't store
251                     # values in the database, so we must ignore it : we generate a dummy leaf
252                     self.__exp[i] = self.__DUMMY_LEAF
253                 else:
254                     subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context)
255                     if not subexp:
256                         self.__exp[i] = self.__DUMMY_LEAF
257                     else:
258                         # we assume that the expression is valid
259                         # we create a dummy leaf for forcing the parsing of the resulting expression
260                         self.__exp[i] = AND_OPERATOR
261                         self.__exp.insert(i + 1, self.__DUMMY_LEAF)
262                         for j, se in enumerate(subexp):
263                             self.__exp.insert(i + 2 + j, se)
264             # else, the value of the field is store in the database, so we search on it
265
266             elif field._type == 'one2many':
267                 # Applying recursivity on field(one2many)
268                 if operator == 'child_of':
269                     ids2 = child_of_right_to_ids(right)
270                     if field._obj != working_table._name:
271                         dom = _rec_get(ids2, field_obj, left=left, prefix=field._obj)
272                     else:
273                         dom = _rec_get(ids2, working_table, parent=left)
274                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
275
276                 else:
277                     call_null = True
278
279                     if right is not False:
280                         if isinstance(right, basestring):
281                             ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, context=context, limit=None)]
282                             if ids2:
283                                 operator = 'in'
284                         else:
285                             if not isinstance(right,list):
286                                 ids2 = [right]
287                             else:
288                                 ids2 = right
289                         if not ids2:
290                             if operator in ['like','ilike','in','=']:
291                                 #no result found with given search criteria
292                                 call_null = False
293                                 self.__exp[i] = ('id','=',0)
294                             else:
295                                 call_null = True
296                                 operator = 'in' # operator changed because ids are directly related to main object
297                         else:
298                             call_null = False
299                             o2m_op = 'in'
300                             if operator in  ['not like','not ilike','not in','<>','!=']:
301                                 o2m_op = 'not in'
302                             self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', ids2, operator, field._type))
303
304                     if call_null:
305                         o2m_op = 'not in'
306                         if operator in  ['not like','not ilike','not in','<>','!=']:
307                             o2m_op = 'in'
308                         self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', [], operator, field._type) or [0])
309
310             elif field._type == 'many2many':
311                 #FIXME
312                 if operator == 'child_of':
313                     def _rec_convert(ids):
314                         if field_obj == table:
315                             return ids
316                         return self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, ids, operator, field._type)
317
318                     ids2 = child_of_right_to_ids(right)
319                     dom = _rec_get(ids2, field_obj)
320                     ids2 = field_obj.search(cr, uid, dom, context=context)
321                     self.__exp[i] = ('id', 'in', _rec_convert(ids2))
322                 else:
323                     call_null_m2m = True
324                     if right is not False:
325                         if isinstance(right, basestring):
326                             res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, context=context)]
327                             if res_ids:
328                                 operator = 'in'
329                         else:
330                             if not isinstance(right, list):
331                                 res_ids = [right]
332                             else:
333                                 res_ids = right
334                         if not res_ids:
335                             if operator in ['like','ilike','in','=']:
336                                 #no result found with given search criteria
337                                 call_null_m2m = False
338                                 self.__exp[i] = ('id','=',0)
339                             else:
340                                 call_null_m2m = True
341                                 operator = 'in' # operator changed because ids are directly related to main object
342                         else:
343                             call_null_m2m = False
344                             m2m_op = 'in'
345                             if operator in  ['not like','not ilike','not in','<>','!=']:
346                                 m2m_op = 'not in'
347
348                             self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, res_ids, operator, field._type) or [0])
349                     if call_null_m2m:
350                         m2m_op = 'not in'
351                         if operator in  ['not like','not ilike','not in','<>','!=']:
352                             m2m_op = 'in'
353                         self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, [], operator,  field._type) or [0])
354
355             elif field._type == 'many2one':
356                 if operator == 'child_of':
357                     ids2 = child_of_right_to_ids(right)
358                     self.__operator = 'in'
359                     if field._obj != working_table._name:
360                         dom = _rec_get(ids2, field_obj, left=left, prefix=field._obj)
361                     else:
362                         dom = _rec_get(ids2, working_table, parent=left)
363                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
364                 else:
365                     def _get_expression(field_obj,cr, uid, left, right, operator, context=None):
366                         if context is None:
367                             context = {}
368                         c = context.copy()
369                         c['active_test'] = False
370                         #Special treatment to ill-formed domains
371                         operator = ( operator in ['<','>','<=','>='] ) and 'in' or operator
372
373                         dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in','<>':'not in'}
374                         if isinstance(right,tuple):
375                             right = list(right)
376                         if (not isinstance(right,list)) and operator in ['not in','in']:
377                             operator = dict_op[operator]
378                         elif isinstance(right,list) and operator in ['<>','!=','=']: #for domain (FIELD,'=',['value1','value2'])
379                             operator = dict_op[operator]
380                         res_ids = field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)
381                         if not res_ids:
382                            return ('id','=',0)
383                         else:
384                             right = map(lambda x: x[0], res_ids)
385                             return (left, 'in', right)
386
387                     m2o_str = False
388                     if right:
389                         if isinstance(right, basestring): # and not isinstance(field, fields.related):
390                             m2o_str = True
391                         elif isinstance(right,(list,tuple)):
392                             m2o_str = True
393                             for ele in right:
394                                 if not isinstance(ele, basestring):
395                                     m2o_str = False
396                                     break
397                     elif right == []:
398                         m2o_str = False
399                         if operator in ('not in', '!=', '<>'):
400                             # (many2one not in []) should return all records
401                             self.__exp[i] = self.__DUMMY_LEAF
402                         else:
403                             self.__exp[i] = ('id','=',0)
404                     else:
405                         new_op = '='
406                         if operator in  ['not like','not ilike','not in','<>','!=']:
407                             new_op = '!='
408                         #Is it ok to put 'left' and not 'id' ?
409                         self.__exp[i] = (left,new_op,False)
410
411                     if m2o_str:
412                         self.__exp[i] = _get_expression(field_obj,cr, uid, left, right, operator, context=context)
413             else:
414                 # other field type
415                 # add the time part to datetime field when it's not there:
416                 if field._type == 'datetime' and self.__exp[i][2] and len(self.__exp[i][2]) == 10:
417
418                     self.__exp[i] = list(self.__exp[i])
419
420                     if operator in ('>', '>='):
421                         self.__exp[i][2] += ' 00:00:00'
422                     elif operator in ('<', '<='):
423                         self.__exp[i][2] += ' 23:59:59'
424
425                     self.__exp[i] = tuple(self.__exp[i])
426
427                 if field.translate:
428                     if operator in ('like', 'ilike', 'not like', 'not ilike'):
429                         right = '%%%s%%' % right
430
431                     operator = operator == '=like' and 'like' or operator
432
433                     query1 = '( SELECT res_id'          \
434                              '    FROM ir_translation'  \
435                              '   WHERE name = %s'       \
436                              '     AND lang = %s'       \
437                              '     AND type = %s'
438                     instr = ' %s'
439                     #Covering in,not in operators with operands (%s,%s) ,etc.
440                     if operator in ['in','not in']:
441                         instr = ','.join(['%s'] * len(right))
442                         query1 += '     AND value ' + operator +  ' ' +" (" + instr + ")"   \
443                              ') UNION ('                \
444                              '  SELECT id'              \
445                              '    FROM "' + working_table._table + '"'       \
446                              '   WHERE "' + left + '" ' + operator + ' ' +" (" + instr + "))"
447                     else:
448                         query1 += '     AND value ' + operator + instr +   \
449                              ') UNION ('                \
450                              '  SELECT id'              \
451                              '    FROM "' + working_table._table + '"'       \
452                              '   WHERE "' + left + '" ' + operator + instr + ")"
453
454                     query2 = [working_table._name + ',' + left,
455                               context.get('lang', False) or 'en_US',
456                               'model',
457                               right,
458                               right,
459                              ]
460
461                     self.__exp[i] = ('id', 'inselect', (query1, query2))
462         return self
463
464     def __leaf_to_sql(self, leaf, table):
465         if leaf == self.__DUMMY_LEAF:
466             return ('(1=1)', [])
467         left, operator, right = leaf
468
469         if operator == 'inselect':
470             query = '(%s.%s in (%s))' % (table._table, left, right[0])
471             params = right[1]
472         elif operator in ['in', 'not in']:
473             params = right and right[:] or []
474             len_before = len(params)
475             for i in range(len_before)[::-1]:
476                 if params[i] == False:
477                     del params[i]
478
479             len_after = len(params)
480             check_nulls = len_after != len_before
481             query = '(1=0)'
482
483             if len_after:
484                 if left == 'id':
485                     instr = ','.join(['%s'] * len_after)
486                 else:
487                     instr = ','.join([table._columns[left]._symbol_set[0]] * len_after)
488                 query = '(%s.%s %s (%s))' % (table._table, left, operator, instr)
489             else:
490                 # the case for [field, 'in', []] or [left, 'not in', []]
491                 if operator == 'in':
492                     query = '(%s.%s IS NULL)' % (table._table, left)
493                 else:
494                     query = '(%s.%s IS NOT NULL)' % (table._table, left)
495             if check_nulls:
496                 query = '(%s OR %s.%s IS NULL)' % (query, table._table, left)
497         else:
498             params = []
499
500             if right == False and (leaf[0] in table._columns)  and table._columns[leaf[0]]._type=="boolean"  and (operator == '='):
501                 query = '(%s.%s IS NULL or %s.%s = false )' % (table._table, left,table._table, left)
502             elif (((right == False) and (type(right)==bool)) or (right is None)) and (operator == '='):
503                 query = '%s.%s IS NULL ' % (table._table, left)
504             elif right == False and (leaf[0] in table._columns)  and table._columns[leaf[0]]._type=="boolean"  and (operator in ['<>', '!=']):
505                 query = '(%s.%s IS NOT NULL and %s.%s != false)' % (table._table, left,table._table, left)
506             elif (((right == False) and (type(right)==bool)) or right is None) and (operator in ['<>', '!=']):
507                 query = '%s.%s IS NOT NULL' % (table._table, left)
508             elif (operator == '=?'):
509                 op = '='
510                 if (right is False or right is None):
511                     return ( 'TRUE',[])
512                 if left in table._columns:
513                         format = table._columns[left]._symbol_set[0]
514                         query = '(%s.%s %s %s)' % (table._table, left, op, format)
515                         params = table._columns[left]._symbol_set[1](right)
516                 else:
517                         query = "(%s.%s %s '%%s')" % (table._table, left, op)
518                         params = right
519
520             else:
521                 if left == 'id':
522                     query = '%s.id %s %%s' % (table._table, operator)
523                     params = right
524                 else:
525                     like = operator in ('like', 'ilike', 'not like', 'not ilike')
526
527                     op = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
528                     if left in table._columns:
529                         format = like and '%s' or table._columns[left]._symbol_set[0]
530                         query = '(%s.%s %s %s)' % (table._table, left, op, format)
531                     else:
532                         query = "(%s.%s %s '%s')" % (table._table, left, op, right)
533
534                     add_null = False
535                     if like:
536                         if isinstance(right, str):
537                             str_utf8 = right
538                         elif isinstance(right, unicode):
539                             str_utf8 = right.encode('utf-8')
540                         else:
541                             str_utf8 = str(right)
542                         params = '%%%s%%' % str_utf8
543                         add_null = not str_utf8
544                     elif left in table._columns:
545                         params = table._columns[left]._symbol_set[1](right)
546
547                     if add_null:
548                         query = '(%s OR %s IS NULL)' % (query, left)
549
550         if isinstance(params, basestring):
551             params = [params]
552         return (query, params)
553
554
555     def to_sql(self):
556         stack = []
557         params = []
558         for i, e in reverse_enumerate(self.__exp):
559             if self._is_leaf(e, internal=True):
560                 table = self.__field_tables.get(i, self.__main_table)
561                 q, p = self.__leaf_to_sql(e, table)
562                 params.insert(0, p)
563                 stack.append(q)
564             else:
565                 if e == NOT_OPERATOR:
566                     stack.append('(NOT (%s))' % (stack.pop(),))
567                 else:
568                     ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
569                     q1 = stack.pop()
570                     q2 = stack.pop()
571                     stack.append('(%s %s %s)' % (q1, ops[e], q2,))
572
573         query = ' AND '.join(reversed(stack))
574         joins = ' AND '.join(self.__joins)
575         if joins:
576             query = '(%s) AND (%s)' % (joins, query)
577         return (query, flatten(params))
578
579     def get_tables(self):
580         return ['"%s"' % t._table for t in self.__all_tables]
581
582 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
583