Bugfix in Expression
[odoo/odoo.git] / bin / osv / expression.py
1 #!/usr/bin/env python
2 # -*- encoding: utf-8 -*-
3
4 from tools import flatten, reverse_enumerate
5
6
7 class expression(object):
8     """
9     parse a domain expression
10     use a real polish notation
11     leafs are still in a ('foo', '=', 'bar') format
12     For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html 
13     """
14
15     def _is_operator(self, element):
16         return isinstance(element, str) \
17            and element in ['&', '|', '!']
18
19     def _is_leaf(self, element, internal=False):
20         return (isinstance(element, tuple) or isinstance(element, list)) \
21            and len(element) == 3 \
22            and element[1] in ('=', '!=', '<>', '<=', '<', '>', '>=', '=like', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of') \
23            and ((not internal) or element[1] in ('inselect',))
24
25     def __execute_recursive_in(self, cr, s, f, w, ids):
26         res = []
27         for i in range(0, len(ids), cr.IN_MAX):
28             subids = ids[i:i+cr.IN_MAX]
29             cr.execute('SELECT "%s"'    \
30                        '  FROM "%s"'    \
31                        ' WHERE "%s" in (%s)' % (s, f, w, ','.join(['%d']*len(subids))),
32                        subids)
33             res.extend([r[0] for r in cr.fetchall()])
34         return res
35
36
37     def __init__(self, exp):
38         # check if the expression is valid
39         if not reduce(lambda acc, val: acc and (self._is_operator(val) or self._is_leaf(val)), exp, True):
40             raise ValueError('Bad domain expression: %r' % (exp,))
41         self.__exp = exp
42         self.__tables = {}  # used to store the table to use for the sql generation. key = index of the leaf
43         self.__joins = []
44         self.__main_table = None # 'root' table. set by parse()
45         self.__DUMMY_LEAF = (1, '=', 1) # a dummy leaf that must not be parsed or sql generated
46
47
48     def parse(self, cr, uid, table, context):
49         """ transform the leafs of the expression """
50         if not self.__exp:
51             return self
52
53         def _rec_get(ids, table, parent, left='id', prefix=''):
54             if table._parent_store and (not table.pool._init):
55 # TODO: Improve where joins are implemented for many with '.', replace by:
56 # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
57                 doms = []
58                 for o in table.browse(cr, uid, ids, context=context):
59                     if doms:
60                         doms.insert(0,'|')
61                     doms += ['&',('parent_left','<',o.parent_right),('parent_left','>=',o.parent_left)]
62                 if prefix:
63                     return [(left, 'in', table.search(cr, uid, doms, context=context))]
64                 return doms
65             else:
66                 if not ids:
67                     return []
68                 ids2 = table.search(cr, uid, [(parent, 'in', ids)], context=context)
69                 return [(prefix+left, 'in', ids2+ids)]
70
71         self.__main_table = table
72
73         i = -1
74         while i+1<len(self.__exp):
75             i+=1
76             e = self.__exp[i]
77             if self._is_operator(e) or e == self.__DUMMY_LEAF:
78                 continue
79             left, operator, right = e
80
81             working_table = table
82             if left in table._inherit_fields:
83                 working_table = table.pool.get(table._inherit_fields[left][0])
84                 if working_table not in self.__tables.values():
85                     self.__joins.append('%s.%s' % (table._table, table._inherits[working_table._name]))
86
87             self.__tables[i] = working_table
88
89             fargs = left.split('.', 1)
90             field = working_table._columns.get(fargs[0], False)
91             if not field:
92                 if left == 'id' and operator == 'child_of':
93                     dom = _rec_get(right, working_table, working_table._parent_name)
94                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
95                 continue
96
97             field_obj = table.pool.get(field._obj)
98             if len(fargs) > 1:
99                 if field._type == 'many2one':
100                     right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
101                     self.__exp[i] = (fargs[0], 'in', right)
102                 continue
103
104             if field._properties:
105                 # this is a function field
106                 if not field.store:
107                     if not field._fnct_search:
108                         # the function field doesn't provide a search function and doesn't store
109                         # values in the database, so we must ignore it : we generate a dummy leaf
110                         self.__exp[i] = self.__DUMMY_LEAF
111                     else:
112                         subexp = field.search(cr, uid, table, left, [self.__exp[i]])
113                         # we assume that the expression is valid 
114                         # we create a dummy leaf for forcing the parsing of the resulting expression
115                         self.__exp[i] = '&'
116                         self.__exp.insert(i + 1, self.__DUMMY_LEAF)
117                         for j, se in enumerate(subexp):
118                             self.__exp.insert(i + 2 + j, se)
119                 
120                 # else, the value of the field is store in the database, so we search on it
121
122
123             elif field._type == 'one2many':
124                 if isinstance(right, basestring):
125                     ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator)]
126                 else:
127                     ids2 = list(right)
128                 if not ids2:
129                     self.__exp[i] = ('id', '=', '0')
130                 else:
131                     self.__exp[i] = ('id', 'in', self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', ids2))
132
133             elif field._type == 'many2many':
134                 #FIXME
135                 if operator == 'child_of':
136                     if isinstance(right, basestring):
137                         ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], 'like')]
138                     else:
139                         ids2 = list(right)
140
141                     def _rec_convert(ids):
142                         if field_obj == table:
143                             return ids
144                         return self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, ids)
145
146                     self.__exp[i] = ('id', 'in', _rec_convert(ids2 + _rec_get(ids2, field_obj, working_table._parent_name)))
147                 else:
148                     if isinstance(right, basestring):
149                         res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator)]
150                     else:
151                         res_ids = list(right)
152                     self.__exp[i] = ('id', 'in', self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, res_ids) or [0])
153             elif field._type == 'many2one':
154                 if operator == 'child_of':
155                     if isinstance(right, basestring):
156                         ids2 = [x[0] for x in field_obj.search_name(cr, uid, right, [], 'like')]
157                     else:
158                         ids2 = list(right)
159
160                     self.__operator = 'in'
161                     if field._obj != working_table._name:
162                         dom = _rec_get(ids2, field_obj, working_table._parent_name, left=left, prefix=left+'.')
163                     else:
164                         dom = _rec_get(ids2, working_table, left)
165                     self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
166                 else:
167                     if isinstance(right, basestring):
168                         res_ids = field_obj.name_search(cr, uid, right, [], operator)
169                         right = map(lambda x: x[0], res_ids)
170                         self.__exp[i] = (left, 'in', right)
171             else:
172                 # other field type
173                 if field.translate:
174                     if operator in ('like', 'ilike', 'not like', 'not ilike'):
175                         right = '%%%s%%' % right
176
177                     query1 = '( SELECT res_id'          \
178                              '    FROM ir_translation'  \
179                              '   WHERE name = %s'       \
180                              '     AND lang = %s'       \
181                              '     AND type = %s'       \
182                              '     AND value ' + operator + ' %s'    \
183                              ') UNION ('                \
184                              '  SELECT id'              \
185                              '    FROM "' + working_table._table + '"'       \
186                              '   WHERE "' + left + '" ' + operator + ' %s' \
187                              ')'
188                     query2 = [working_table._name + ',' + left,
189                               context.get('lang', False) or 'en_US',
190                               'model',
191                               right,
192                               right,
193                              ]
194
195                     self.__exp[i] = ('id', 'inselect', (query1, query2))
196
197         return self
198
199     def __leaf_to_sql(self, leaf, table):
200         left, operator, right = leaf
201
202         if operator == 'inselect':
203             query = '(%s.%s in (%s))' % (table._table, left, right[0])
204             params = right[1]
205         elif operator in ['in', 'not in']:
206             params = right[:]
207             len_before = len(params)
208             for i in range(len_before)[::-1]:
209                 if params[i] == False:
210                     del params[i]
211
212             len_after = len(params)
213             check_nulls = len_after != len_before
214             query = '(1=0)'
215
216             if len_after:
217                 if left == 'id':
218                      instr = ','.join(['%d'] * len_after)
219                 else:
220                     instr = ','.join([table._columns[left]._symbol_set[0]] * len_after)
221                 query = '(%s.%s %s (%s))' % (table._table, left, operator, instr)
222
223             if check_nulls:
224                 query = '(%s OR %s IS NULL)' % (query, left)
225         else:
226             params = []
227             if right is False and operator == '=':
228                 query = '%s IS NULL' % left
229             elif right is False and operator in ['<>', '!=']:
230                 query = '%s IS NOT NULL' % left
231             else:
232                 if left == 'id':
233                     query = '%s.id %s %%s' % (table._table, operator)
234                     params = right
235                 else:
236                     like = operator in ('like', 'ilike', 'not like', 'not ilike')
237
238                     op = operator == '=like' and 'like' or operator
239                     if left in table._columns:
240                         format = like and '%s' or table._columns[left]._symbol_set[0]
241                         query = '(%s.%s %s %s)' % (table._table, left, op, format)
242                     else:
243                         query = "(%s.%s %s '%s')" % (table._table, left, op, right)
244
245                     add_null = False
246                     if like:
247                         if isinstance(right, str):
248                             str_utf8 = right
249                         elif isinstance(right, unicode):
250                             str_utf8 = right.encode('utf-8')
251                         else:
252                             str_utf8 = str(right)
253                         params = '%%%s%%' % str_utf8
254                         add_null = not str_utf8
255                     elif left in table._columns:
256                         params = table._columns[left]._symbol_set[1](right)
257
258                     if add_null:
259                         query = '(%s OR %s IS NULL)' % (query, left)
260
261         if isinstance(params, basestring):
262             params = [params]
263         return (query, params)
264
265
266     def to_sql(self):
267         stack = []
268         params = []
269         for i, e in reverse_enumerate(self.__exp):
270             if self._is_leaf(e, internal=True):
271                 table = self.__tables.has_key(i) and self.__tables[i] or self.__main_table
272                 q, p = self.__leaf_to_sql(e, table)
273                 params.insert(0, p)
274                 stack.append(q)
275             else:
276                 if e == '!':
277                     stack.append('(NOT (%s))' % (stack.pop(),))
278                 else:
279                     ops = {'&': ' AND ', '|': ' OR '}
280                     q1 = stack.pop()
281                     q2 = stack.pop()
282                     stack.append('(%s %s %s)' % (q1, ops[e], q2,))
283         
284         query = ' AND '.join(reversed(stack))
285         joins = ' AND '.join(map(lambda j: '%s.id = %s' % (self.__main_table._table, j), self.__joins))
286         if joins:
287             query = '(%s AND (%s))' % (joins, query)
288         return (query, flatten(params))
289
290     def get_tables(self):
291         return ['"%s"' % t._table for t in set(self.__tables.values())]
292
293 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
294