2 # -*- coding: utf-8 -*-
3 ##############################################################################
5 # OpenERP, Open Source Management Solution
6 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
21 ##############################################################################
23 from openerp.tools import flatten, reverse_enumerate
26 #.apidoc title: Domain Expressions
32 TRUE_DOMAIN = [(1,'=',1)]
33 FALSE_DOMAIN = [(0,'=',1)]
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.
40 assert isinstance(domain, (list, tuple)), "Domains to normalize must have a 'domain' form: a list or tuple of domain components"
44 expected = 1 # expected number of expressions
45 op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2}
47 if expected == 0: # more than expected, like in [A, B]
48 result[0:0] = ['&'] # put an extra '&' in front
51 if isinstance(token, (list,tuple)): # domain term
54 expected += op_arity.get(token, 0) - 1
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``.
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.
75 for domain in domains:
83 result = [operator] * (count - 1) + result
87 """ AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """
88 return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains)
91 """ OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """
92 return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains)
95 class expression(object):
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
104 def _is_operator(cls, element):
105 return isinstance(element, (str, unicode)) and element in [AND_OPERATOR, OR_OPERATOR, NOT_OPERATOR]
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))
116 def __execute_recursive_in(self, cr, s, f, w, ids, op, type):
117 # todo: merge into parent query as sub-query
120 if op in ['<','>','>=','<=']:
121 cr.execute('SELECT "%s"' \
123 ' WHERE "%s" %s %%s' % (s, f, w, op), (ids[0],))
124 res.extend([r[0] for r in cr.fetchall()])
126 for i in range(0, len(ids), cr.IN_MAX):
127 subids = ids[i:i+cr.IN_MAX]
128 cr.execute('SELECT "%s"' \
130 ' WHERE "%s" IN %%s' % (s, f, w),(tuple(subids),))
131 res.extend([r[0] for r in cr.fetchall()])
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()])
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,))
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()
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
153 def parse(self, cr, uid, table, context):
154 """ transform the leafs of the expression """
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)]
163 for o in table.browse(cr, uid, ids, context=context):
165 doms.insert(0, OR_OPERATOR)
166 doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
168 return [(left, 'in', table.search(cr, uid, doms, context=context))]
171 def rg(ids, table, parent):
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))]
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.
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.
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)):
192 self.__main_table = table
193 self.__all_tables.add(table)
196 while i + 1<len(self.__exp):
199 if self._is_operator(e) or e == self.__DUMMY_LEAF:
201 left, operator, right = e
202 operator = operator.lower()
203 working_table = table
205 fargs = left.split('.', 1)
206 if fargs[0] in table._inherit_fields:
208 field = main_table._columns.get(fargs[0], False)
210 working_table = main_table
211 self.__field_tables[i] = working_table
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
219 field = working_table._columns.get(fargs[0], False)
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:]
227 field_obj = table.pool.get(field._obj)
229 if field._type == 'many2one':
230 right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
232 self.__exp[i] = ( 'id', '=', 0 )
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)
240 self.__exp[i] = ( 'id', '=', 0 )
242 self.__exp[i] = ('id', 'in', right1)
244 if not isinstance(field,fields.property):
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
254 subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context)
256 self.__exp[i] = self.__DUMMY_LEAF
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
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)
273 dom = _rec_get(ids2, working_table, parent=left)
274 self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
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)]
285 if not isinstance(right,list):
290 if operator in ['like','ilike','in','=']:
291 #no result found with given search criteria
293 self.__exp[i] = ('id','=',0)
296 operator = 'in' # operator changed because ids are directly related to main object
300 if operator in ['not like','not ilike','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))
306 if operator in ['not like','not ilike','not 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])
310 elif field._type == 'many2many':
312 if operator == 'child_of':
313 def _rec_convert(ids):
314 if field_obj == table:
316 return self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, ids, operator, field._type)
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))
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)]
330 if not isinstance(right, list):
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)
341 operator = 'in' # operator changed because ids are directly related to main object
343 call_null_m2m = False
345 if operator in ['not like','not ilike','not in','<>','!=']:
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])
351 if operator in ['not like','not ilike','not in','<>','!=']:
353 self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, [], operator, field._type) or [0])
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)
362 dom = _rec_get(ids2, working_table, parent=left)
363 self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
365 def _get_expression(field_obj,cr, uid, left, right, operator, context=None):
369 c['active_test'] = False
370 #Special treatment to ill-formed domains
371 operator = ( operator in ['<','>','<=','>='] ) and 'in' or operator
373 dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in','<>':'not in'}
374 if isinstance(right,tuple):
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)
384 right = map(lambda x: x[0], res_ids)
385 return (left, 'in', right)
389 if isinstance(right, basestring): # and not isinstance(field, fields.related):
391 elif isinstance(right,(list,tuple)):
394 if not isinstance(ele, basestring):
399 if operator in ('not in', '!=', '<>'):
400 # (many2one not in []) should return all records
401 self.__exp[i] = self.__DUMMY_LEAF
403 self.__exp[i] = ('id','=',0)
406 if operator in ['not like','not ilike','not in','<>','!=']:
408 #Is it ok to put 'left' and not 'id' ?
409 self.__exp[i] = (left,new_op,False)
412 self.__exp[i] = _get_expression(field_obj,cr, uid, left, right, operator, context=context)
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:
418 self.__exp[i] = list(self.__exp[i])
420 if operator in ('>', '>='):
421 self.__exp[i][2] += ' 00:00:00'
422 elif operator in ('<', '<='):
423 self.__exp[i][2] += ' 23:59:59'
425 self.__exp[i] = tuple(self.__exp[i])
428 if operator in ('like', 'ilike', 'not like', 'not ilike'):
429 right = '%%%s%%' % right
431 operator = operator == '=like' and 'like' or operator
433 query1 = '( SELECT res_id' \
434 ' FROM ir_translation' \
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 + ")" \
445 ' FROM "' + working_table._table + '"' \
446 ' WHERE "' + left + '" ' + operator + ' ' +" (" + instr + "))"
448 query1 += ' AND value ' + operator + instr + \
451 ' FROM "' + working_table._table + '"' \
452 ' WHERE "' + left + '" ' + operator + instr + ")"
454 query2 = [working_table._name + ',' + left,
455 context.get('lang', False) or 'en_US',
461 self.__exp[i] = ('id', 'inselect', (query1, query2))
464 def __leaf_to_sql(self, leaf, table):
465 if leaf == self.__DUMMY_LEAF:
467 left, operator, right = leaf
469 if operator == 'inselect':
470 query = '(%s.%s in (%s))' % (table._table, left, right[0])
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:
479 len_after = len(params)
480 check_nulls = len_after != len_before
485 instr = ','.join(['%s'] * len_after)
487 instr = ','.join([table._columns[left]._symbol_set[0]] * len_after)
488 query = '(%s.%s %s (%s))' % (table._table, left, operator, instr)
490 # the case for [field, 'in', []] or [left, 'not in', []]
492 query = '(%s.%s IS NULL)' % (table._table, left)
494 query = '(%s.%s IS NOT NULL)' % (table._table, left)
496 query = '(%s OR %s.%s IS NULL)' % (query, table._table, left)
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 == '=?'):
510 if (right is False or right is None):
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)
517 query = "(%s.%s %s '%%s')" % (table._table, left, op)
522 query = '%s.id %s %%s' % (table._table, operator)
525 like = operator in ('like', 'ilike', 'not like', 'not ilike')
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)
532 query = "(%s.%s %s '%s')" % (table._table, left, op, right)
536 if isinstance(right, str):
538 elif isinstance(right, unicode):
539 str_utf8 = right.encode('utf-8')
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)
548 query = '(%s OR %s IS NULL)' % (query, left)
550 if isinstance(params, basestring):
552 return (query, 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)
565 if e == NOT_OPERATOR:
566 stack.append('(NOT (%s))' % (stack.pop(),))
568 ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
571 stack.append('(%s %s %s)' % (q1, ops[e], q2,))
573 query = ' AND '.join(reversed(stack))
574 joins = ' AND '.join(self.__joins)
576 query = '(%s) AND (%s)' % (joins, query)
577 return (query, flatten(params))
579 def get_tables(self):
580 return ['"%s"' % t._table for t in self.__all_tables]
582 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: