Changed encoding to coding ref: PEP: 0263
[odoo/odoo.git] / bin / 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 tools import flatten, reverse_enumerate
24 import fields
25
26
27 class expression(object):
28     """
29     parse a domain expression
30     use a real polish notation
31     leafs are still in a ('foo', '=', 'bar') format
32     For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html 
33     """
34
35     def _is_operator(self, element):
36         return isinstance(element, (str, unicode)) and element in ['&', '|', '!']
37
38     def _is_leaf(self, element, internal=False):
39         OPS = ('=', '!=', '<>', '<=', '<', '>', '>=', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of')
40         INTERNAL_OPS = OPS + ('inselect',)
41         return (isinstance(element, tuple) or isinstance(element, list)) \
42            and len(element) == 3 \
43            and (((not internal) and element[1] in OPS) \
44                 or (internal and element[1] in INTERNAL_OPS))
45
46     def __execute_recursive_in(self, cr, s, f, w, ids, op, type):
47         res = []
48         if ids:
49             if op in ['<','>','>=','<=']:
50                 cr.execute('SELECT "%s"'    \
51                                '  FROM "%s"'    \
52                                ' WHERE "%s" %s %s' % (s, f, w, op, ids[0]))
53                 res.extend([r[0] for r in cr.fetchall()])
54             else:
55                 for i in range(0, len(ids), cr.IN_MAX):
56                     subids = ids[i:i+cr.IN_MAX]
57                     cr.execute('SELECT "%s"'    \
58                                '  FROM "%s"'    \
59                                ' WHERE "%s" in (%s)' % (s, f, w, ','.join(['%s']*len(subids))),
60                                subids)
61                     res.extend([r[0] for r in cr.fetchall()])
62         else:
63             cr.execute('SELECT distinct("%s")'    \
64                            '  FROM "%s" where "%s" is not null'  % (s, f, s)),
65             res.extend([r[0] for r in cr.fetchall()])
66         return res
67
68
69     def __init__(self, exp):
70         # check if the expression is valid
71         if not reduce(lambda acc, val: acc and (self._is_operator(val) or self._is_leaf(val)), exp, True):
72             raise ValueError('Bad domain expression: %r' % (exp,))
73         self.__exp = exp
74         self.__tables = {}  # used to store the table to use for the sql generation. key = index of the leaf
75         self.__joins = []
76         self.__main_table = None # 'root' table. set by parse()
77         self.__DUMMY_LEAF = (1, '=', 1) # a dummy leaf that must not be parsed or sql generated
78
79
80     def parse(self, cr, uid, table, context):
81         """ transform the leafs of the expression """
82         if not self.__exp:
83             return self
84
85         def _rec_get(ids, table, parent=None, left='id', prefix=''):
86             if table._parent_store and (not table.pool._init):
87 # TODO: Improve where joins are implemented for many with '.', replace by:
88 # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
89                 doms = []
90                 for o in table.browse(cr, uid, ids, context=context):
91                     if doms:
92                         doms.insert(0, '|')
93                     doms += ['&', ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
94                 if prefix:
95                     return [(left, 'in', table.search(cr, uid, doms, context=context))]
96                 return doms
97             else:
98                 def rg(ids, table, parent):
99                     if not ids:
100                         return []
101                     ids2 = table.search(cr, uid, [(parent, 'in', ids)], context=context)
102                     return ids + rg(ids2, table, parent)
103                 return [(left, 'in', rg(ids, table, parent or table._parent_name))]
104
105         self.__main_table = table
106
107         i = -1
108         while i + 1<len(self.__exp):
109             i += 1
110             e = self.__exp[i]
111             if self._is_operator(e) or e == self.__DUMMY_LEAF:
112                 continue
113             left, operator, right = e
114             working_table = table
115             main_table = table
116             fargs = left.split('.', 1)
117             index = i
118             if left in table._inherit_fields:
119                 while True:
120                     field = main_table._columns.get(fargs[0], False)
121                     if field:
122                         working_table = main_table
123                         self.__tables[i] = working_table
124                         break
125                     working_table = main_table.pool.get(main_table._inherit_fields[left][0])
126                     if working_table not in self.__tables.values():
127                         self.__joins.append(('%s.%s=%s.%s' % (working_table._table, 'id', main_table._table, main_table._inherits[working_table._name]), working_table._table))
128                         self.__tables[index] = working_table
129                         index += 1
130                     main_table = working_table
131             
132             field = working_table._columns.get(fargs[0], False)
133             if not field:
134                 if left == 'id' and operator == 'child_of':
135                     dom = _rec_get(right, working_table)
136                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
137                 continue
138
139             field_obj = table.pool.get(field._obj)
140             if len(fargs) > 1:
141                 if field._type == 'many2one':
142                     right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
143                     self.__exp[i] = (fargs[0], 'in', right)
144                 continue
145
146             if field._properties:
147                 # this is a function field
148                 if not field.store:
149                     if not field._fnct_search:
150                         # the function field doesn't provide a search function and doesn't store
151                         # values in the database, so we must ignore it : we generate a dummy leaf
152                         self.__exp[i] = self.__DUMMY_LEAF
153                     else:
154                         subexp = field.search(cr, uid, table, left, [self.__exp[i]])
155                         # we assume that the expression is valid
156                         # we create a dummy leaf for forcing the parsing of the resulting expression
157                         self.__exp[i] = '&'
158                         self.__exp.insert(i + 1, self.__DUMMY_LEAF)
159                         for j, se in enumerate(subexp):
160                             self.__exp.insert(i + 2 + j, se)
161
162                 # else, the value of the field is store in the database, so we search on it
163
164
165             elif field._type == 'one2many':
166                 call_null = True
167                 
168                 if right:
169                     if isinstance(right, basestring):
170                         ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, limit=None)]
171                         operator = 'in' 
172                     else:
173                         if not isinstance(right,list):
174                             ids2 = [right]
175                         else:
176                             ids2 = right    
177                     if not ids2:
178                         call_null = True
179                         operator = 'in' # operator changed because ids are directly related to main object
180                     else:
181                         call_null = False
182                         o2m_op = 'in'
183                         if operator in  ['not like','not ilike','not in','<>','!=']:
184                             o2m_op = 'not in'
185                         self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', ids2, operator, field._type))
186                 
187                 if call_null:
188                     o2m_op = 'not in'
189                     if operator in  ['not like','not ilike','not in','<>','!=']:
190                         o2m_op = 'in'                         
191                     self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', [], operator, field._type) or [0])      
192
193             elif field._type == 'many2many':
194                 #FIXME
195                 if operator == 'child_of':
196                     if isinstance(right, basestring):
197                         ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], 'like', limit=None)]
198                     else:
199                         ids2 = list(right)
200
201                     def _rec_convert(ids):
202                         if field_obj == table:
203                             return ids
204                         return self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, ids, operator, field._type)
205
206                     dom = _rec_get(ids2, field_obj)
207                     ids2 = field_obj.search(cr, uid, dom, context=context)
208                     self.__exp[i] = ('id', 'in', _rec_convert(ids2))
209                 else:
210                     call_null_m2m = True
211                     if right:
212                         if isinstance(right, basestring):
213                             res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator)]
214                             operator = 'in'
215                         else:
216                             if not isinstance(right, list):
217                                 res_ids = [right]
218                             else:
219                                 res_ids = right
220                         if not res_ids:
221                             call_null_m2m = True
222                             operator = 'in' # operator changed because ids are directly related to main object
223                         else:
224                             call_null_m2m = False
225                             m2m_op = 'in'        
226                             if operator in  ['not like','not ilike','not in','<>','!=']:
227                                 m2m_op = 'not in'
228                             
229                             self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, res_ids, operator, field._type) or [0])
230                     if call_null_m2m:
231                         m2m_op = 'not in'
232                         if operator in  ['not like','not ilike','not in','<>','!=']:
233                             m2m_op = 'in'                         
234                         self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, [], operator,  field._type) or [0])
235                         
236             elif field._type == 'many2one':
237                 if operator == 'child_of':
238                     if isinstance(right, basestring):
239                         ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], 'like', limit=None)]
240                     else:
241                         ids2 = list(right)
242
243                     self.__operator = 'in'
244                     if field._obj != working_table._name:
245                         dom = _rec_get(ids2, field_obj, left=left, prefix=field._obj)
246                     else:
247                         dom = _rec_get(ids2, working_table, parent=left)
248                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
249                 else:
250                     if isinstance(right, basestring): # and not isinstance(field, fields.related):
251                         c = context.copy()
252                         c['active_test'] = False
253                         res_ids = field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)
254                         right = map(lambda x: x[0], res_ids)
255                         self.__exp[i] = (left, 'in', right)
256             else:
257                 # other field type
258                 # add the time part to datetime field when it's not there:
259                 if field._type == 'datetime' and self.__exp[i][2] and len(self.__exp[i][2]) == 10:
260                     
261                     self.__exp[i] = list(self.__exp[i])
262                     
263                     if operator in ('>', '>='):
264                         self.__exp[i][2] += ' 00:00:00'
265                     elif operator in ('<', '<='):
266                         self.__exp[i][2] += ' 23:59:59'
267                     
268                     self.__exp[i] = tuple(self.__exp[i])
269                         
270                 if field.translate:
271                     if operator in ('like', 'ilike', 'not like', 'not ilike'):
272                         right = '%%%s%%' % right
273
274                     operator = operator == '=like' and 'like' or operator
275
276                     query1 = '( SELECT res_id'          \
277                              '    FROM ir_translation'  \
278                              '   WHERE name = %s'       \
279                              '     AND lang = %s'       \
280                              '     AND type = %s'
281                     instr = ' %s'
282                     #Covering in,not in operators with operands (%s,%s) ,etc.
283                     if operator in ['in','not in']:
284                         instr = ','.join(['%s'] * len(right))
285                         query1 += '     AND value ' + operator +  ' ' +" (" + instr + ")"   \
286                              ') UNION ('                \
287                              '  SELECT id'              \
288                              '    FROM "' + working_table._table + '"'       \
289                              '   WHERE "' + left + '" ' + operator + ' ' +" (" + instr + "))"
290                     else:
291                         query1 += '     AND value ' + operator + instr +   \
292                              ') UNION ('                \
293                              '  SELECT id'              \
294                              '    FROM "' + working_table._table + '"'       \
295                              '   WHERE "' + left + '" ' + operator + instr + ")"
296
297                     query2 = [working_table._name + ',' + left,
298                               context.get('lang', False) or 'en_US',
299                               'model',
300                               right,
301                               right,
302                              ]
303
304                     self.__exp[i] = ('id', 'inselect', (query1, query2))
305
306         return self
307
308     def __leaf_to_sql(self, leaf, table):
309         if leaf == self.__DUMMY_LEAF:
310             return ('(1=1)', [])
311         left, operator, right = leaf
312         
313         if operator == 'inselect':
314             query = '(%s.%s in (%s))' % (table._table, left, right[0])
315             params = right[1]
316         elif operator in ['in', 'not in']:
317             params = right[:]
318             len_before = len(params)
319             for i in range(len_before)[::-1]:
320                 if params[i] == False:
321                     del params[i]
322
323             len_after = len(params)
324             check_nulls = len_after != len_before
325             query = '(1=0)'
326
327             if len_after:
328                 if left == 'id':
329                     instr = ','.join(['%s'] * len_after)
330                 else:
331                     instr = ','.join([table._columns[left]._symbol_set[0]] * len_after)
332                 query = '(%s.%s %s (%s))' % (table._table, left, operator, instr)
333
334             if check_nulls:
335                 query = '(%s OR %s.%s IS NULL)' % (query, table._table, left)
336         else:
337             params = []
338             
339             if right == False and (leaf[0] in table._columns)  and table._columns[leaf[0]]._type=="boolean"  and (operator == '='):
340                 query = '(%s.%s IS NULL or %s.%s = false )' % (table._table, left,table._table, left)
341             elif (((right == False) and (type(right)==bool)) or (right is None)) and (operator == '='):
342                 query = '%s.%s IS NULL ' % (table._table, left)
343             elif right == False and (leaf[0] in table._columns)  and table._columns[leaf[0]]._type=="boolean"  and (operator in ['<>', '!=']):
344                 query = '(%s.%s IS NOT NULL and %s.%s != false)' % (table._table, left,table._table, left)
345             elif (((right == False) and (type(right)==bool)) or right is None) and (operator in ['<>', '!=']):
346                 query = '%s.%s IS NOT NULL' % (table._table, left)
347             else:
348                 if left == 'id':
349                     query = '%s.id %s %%s' % (table._table, operator)
350                     params = right
351                 else:
352                     like = operator in ('like', 'ilike', 'not like', 'not ilike')
353
354                     op = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
355                     if left in table._columns:
356                         format = like and '%s' or table._columns[left]._symbol_set[0]
357                         query = '(%s.%s %s %s)' % (table._table, left, op, format)
358                     else:
359                         query = "(%s.%s %s '%s')" % (table._table, left, op, right)
360
361                     add_null = False
362                     if like:
363                         if isinstance(right, str):
364                             str_utf8 = right
365                         elif isinstance(right, unicode):
366                             str_utf8 = right.encode('utf-8')
367                         else:
368                             str_utf8 = str(right)
369                         params = '%%%s%%' % str_utf8
370                         add_null = not str_utf8
371                     elif left in table._columns:
372                         params = table._columns[left]._symbol_set[1](right)
373
374                     if add_null:
375                         query = '(%s OR %s IS NULL)' % (query, left)
376
377         if isinstance(params, basestring):
378             params = [params]
379         return (query, params)
380
381
382     def to_sql(self):
383         stack = []
384         params = []
385         for i, e in reverse_enumerate(self.__exp):
386             if self._is_leaf(e, internal=True):
387                 table = self.__tables.get(i, self.__main_table)
388                 q, p = self.__leaf_to_sql(e, table)
389                 params.insert(0, p)
390                 stack.append(q)
391             else:
392                 if e == '!':
393                     stack.append('(NOT (%s))' % (stack.pop(),))
394                 else:
395                     ops = {'&': ' AND ', '|': ' OR '}
396                     q1 = stack.pop()
397                     q2 = stack.pop()
398                     stack.append('(%s %s %s)' % (q1, ops[e], q2,))
399
400         query = ' AND '.join(reversed(stack))
401         joins = ' AND '.join(map(lambda j: j[0], self.__joins))
402         if joins:
403             query = '(%s) AND (%s)' % (joins, query)
404         return (query, flatten(params))
405
406     def get_tables(self):
407         return ['"%s"' % t._table for t in set(self.__tables.values()+[self.__main_table])]
408
409 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
410