d3c2a8adabb8e3ba2ce04b327e9b8d5df85c9beb
[odoo/odoo.git] / openerp / osv / expression.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 """ Domain expression processing
23
24 The main duty of this module is to compile a domain expression into a
25 SQL query. A lot of things should be documented here, but as a first
26 step in the right direction, some tests in test_osv_expression.yml
27 might give you some additional information.
28
29 For legacy reasons, a domain uses an inconsistent two-levels abstract
30 syntax (domains are regular Python data structures). At the first
31 level, a domain is an expression made of terms (sometimes called
32 leaves) and (domain) operators used in prefix notation. The available
33 operators at this level are '!', '&', and '|'. '!' is a unary 'not',
34 '&' is a binary 'and', and '|' is a binary 'or'.  For instance, here
35 is a possible domain. (<term> stands for an arbitrary term, more on
36 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
45 is a triple of the form (left, operator, right). That is, a term uses
46 an infix notation, and the available operators, and possible left and
47 right operands differ with those of the previous level. Here is a
48 possible term::
49
50     ('company_id.name', '=', 'OpenERP')
51
52 The left and right operand don't have the same possible values. The
53 left operand is field name (related to the model for which the domain
54 applies).  Actually, the field name can use the dot-notation to
55 traverse relationships.  The right operand is a Python value whose
56 type should match the used operator and field type. In the above
57 example, a string is used because the name field of a company has type
58 string, and because we use the '=' operator. When appropriate, a 'in'
59 operator can be used, and thus the right operand should be a list.
60
61 Note: the non-uniform syntax could have been more uniform, but this
62 would hide an important limitation of the domain syntax. Say that the
63 term representation was ['=', 'company_id.name', 'OpenERP']. Used in a
64 complete domain, this would look like::
65
66     ['!', ['=', 'company_id.name', 'OpenERP']]
67
68 and you would be tempted to believe something like this would be
69 possible::
70
71     ['!', ['=', 'company_id.name', ['&', ..., ...]]]
72
73 That is, a domain could be a valid operand. But this is not the
74 case. A domain is really limited to a two-level nature, and can not
75 take a recursive form: a domain is not a valid second-level operand.
76
77 Unaccent - Accent-insensitive search
78
79 OpenERP will use the SQL function 'unaccent' when available for the
80 'ilike' and 'not ilike' operators, and enabled in the configuration.
81 Normally the 'unaccent' function is obtained from `the PostgreSQL
82 'unaccent' contrib module
83 <http://developer.postgresql.org/pgdocs/postgres/unaccent.html>`_.
84
85 .. todo: The following explanation should be moved in some external
86          installation guide
87
88 The steps to install the module might differ on specific PostgreSQL
89 versions.  We give here some instruction for PostgreSQL 9.x on a
90 Ubuntu system.
91
92 Ubuntu doesn't come yet with PostgreSQL 9.x, so an alternative package
93 source is used. We use Martin Pitt's PPA available at
94 `ppa:pitti/postgresql
95 <https://launchpad.net/~pitti/+archive/postgresql>`_.
96
97 .. code-block:: sh
98
99     > sudo add-apt-repository ppa:pitti/postgresql
100     > sudo apt-get update
101
102 Once the package list is up-to-date, you have to install PostgreSQL
103 9.0 and its contrib modules.
104
105 .. code-block:: sh
106
107     > sudo apt-get install postgresql-9.0 postgresql-contrib-9.0
108
109 When you want to enable unaccent on some database:
110
111 .. code-block:: sh
112
113     > psql9 <database> -f /usr/share/postgresql/9.0/contrib/unaccent.sql
114
115 Here :program:`psql9` is an alias for the newly installed PostgreSQL
116 9.0 tool, together with the correct port if necessary (for instance if
117 PostgreSQL 8.4 is running on 5432). (Other aliases can be used for
118 createdb and dropdb.)
119
120 .. code-block:: sh
121
122     > alias psql9='/usr/lib/postgresql/9.0/bin/psql -p 5433'
123
124 You can check unaccent is working:
125
126 .. code-block:: sh
127
128     > psql9 <database> -c"select unaccent('hélène')"
129
130 Finally, to instruct OpenERP to really use the unaccent function, you have to
131 start the server specifying the ``--unaccent`` flag.
132
133 """
134
135 import logging
136 import traceback
137
138 import openerp.modules
139 from openerp.osv import fields
140 from openerp.osv.orm import MAGIC_COLUMNS
141 import openerp.tools as tools
142
143 #.apidoc title: Domain Expressions
144
145 # Domain operators.
146 NOT_OPERATOR = '!'
147 OR_OPERATOR = '|'
148 AND_OPERATOR = '&'
149 DOMAIN_OPERATORS = (NOT_OPERATOR, OR_OPERATOR, AND_OPERATOR)
150
151 # List of available term operators. It is also possible to use the '<>'
152 # operator, which is strictly the same as '!='; the later should be prefered
153 # for consistency. This list doesn't contain '<>' as it is simpified to '!='
154 # by the normalize_operator() function (so later part of the code deals with
155 # only one representation).
156 # Internals (i.e. not available to the user) 'inselect' and 'not inselect'
157 # operators are also used. In this case its right operand has the form (subselect, params).
158 TERM_OPERATORS = ('=', '!=', '<=', '<', '>', '>=', '=?', '=like', '=ilike',
159                   'like', 'not like', 'ilike', 'not ilike', 'in', 'not in',
160                   'child_of')
161
162 # A subset of the above operators, with a 'negative' semantic. When the
163 # expressions 'in NEGATIVE_TERM_OPERATORS' or 'not in NEGATIVE_TERM_OPERATORS' are used in the code
164 # below, this doesn't necessarily mean that any of those NEGATIVE_TERM_OPERATORS is
165 # legal in the processed term.
166 NEGATIVE_TERM_OPERATORS = ('!=', 'not like', 'not ilike', 'not in')
167
168 TRUE_LEAF = (1, '=', 1)
169 FALSE_LEAF = (0, '=', 1)
170
171 TRUE_DOMAIN = [TRUE_LEAF]
172 FALSE_DOMAIN = [FALSE_LEAF]
173
174 _logger = logging.getLogger(__name__)
175
176
177 # --------------------------------------------------
178 # Generic domain manipulation
179 # --------------------------------------------------
180
181 def normalize_domain(domain):
182     """Returns a normalized version of ``domain_expr``, where all implicit '&' operators
183        have been made explicit. One property of normalized domain expressions is that they
184        can be easily combined together as if they were single domain components.
185     """
186     assert isinstance(domain, (list, tuple)), "Domains to normalize must have a 'domain' form: a list or tuple of domain components"
187     if not domain:
188         return TRUE_DOMAIN
189     result = []
190     expected = 1                            # expected number of expressions
191     op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2}
192     for token in domain:
193         if expected == 0:                   # more than expected, like in [A, B]
194             result[0:0] = [AND_OPERATOR]             # put an extra '&' in front
195             expected = 1
196         result.append(token)
197         if isinstance(token, (list, tuple)):  # domain term
198             expected -= 1
199         else:
200             expected += op_arity.get(token, 0) - 1
201     assert expected == 0, 'This domain is syntactically not correct: %s' % (domain)
202     return result
203
204
205 def combine(operator, unit, zero, domains):
206     """Returns a new domain expression where all domain components from ``domains``
207        have been added together using the binary operator ``operator``. The given
208        domains must be normalized.
209
210        :param unit: the identity element of the domains "set" with regard to the operation
211                     performed by ``operator``, i.e the domain component ``i`` which, when
212                     combined with any domain ``x`` via ``operator``, yields ``x``.
213                     E.g. [(1,'=',1)] is the typical unit for AND_OPERATOR: adding it
214                     to any domain component gives the same domain.
215        :param zero: the absorbing element of the domains "set" with regard to the operation
216                     performed by ``operator``, i.e the domain component ``z`` which, when
217                     combined with any domain ``x`` via ``operator``, yields ``z``.
218                     E.g. [(1,'=',1)] is the typical zero for OR_OPERATOR: as soon as
219                     you see it in a domain component the resulting domain is the zero.
220        :param domains: a list of normalized domains.
221     """
222     result = []
223     count = 0
224     for domain in domains:
225         if domain == unit:
226             continue
227         if domain == zero:
228             return zero
229         if domain:
230             result += domain
231             count += 1
232     result = [operator] * (count - 1) + result
233     return result
234
235
236 def AND(domains):
237     """AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """
238     return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains)
239
240
241 def OR(domains):
242     """OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """
243     return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains)
244
245
246 def distribute_not(domain):
247     """ Distribute any '!' domain operators found inside a normalized domain.
248
249     Because we don't use SQL semantic for processing a 'left not in right'
250     query (i.e. our 'not in' is not simply translated to a SQL 'not in'),
251     it means that a '! left in right' can not be simply processed
252     by __leaf_to_sql by first emitting code for 'left in right' then wrapping
253     the result with 'not (...)', as it would result in a 'not in' at the SQL
254     level.
255
256     This function is thus responsible for pushing any '!' domain operators
257     inside the terms themselves. For example::
258
259          ['!','&',('user_id','=',4),('partner_id','in',[1,2])]
260             will be turned into:
261          ['|',('user_id','!=',4),('partner_id','not in',[1,2])]
262
263     """
264     def negate(leaf):
265         """Negates and returns a single domain leaf term,
266         using the opposite operator if possible"""
267         left, operator, right = leaf
268         mapping = {
269             '<': '>=',
270             '>': '<=',
271             '<=': '>',
272             '>=': '<',
273             '=': '!=',
274             '!=': '=',
275         }
276         if operator in ('in', 'like', 'ilike'):
277             operator = 'not ' + operator
278             return [(left, operator, right)]
279         if operator in ('not in', 'not like', 'not ilike'):
280             operator = operator[4:]
281             return [(left, operator, right)]
282         if operator in mapping:
283             operator = mapping[operator]
284             return [(left, operator, right)]
285         return [NOT_OPERATOR, (left, operator, right)]
286
287     def distribute_negate(domain):
288         """Negate the domain ``subtree`` rooted at domain[0],
289         leaving the rest of the domain intact, and return
290         (negated_subtree, untouched_domain_rest)
291         """
292         if is_leaf(domain[0]):
293             return negate(domain[0]), domain[1:]
294         if domain[0] == AND_OPERATOR:
295             done1, todo1 = distribute_negate(domain[1:])
296             done2, todo2 = distribute_negate(todo1)
297             return [OR_OPERATOR] + done1 + done2, todo2
298         if domain[0] == OR_OPERATOR:
299             done1, todo1 = distribute_negate(domain[1:])
300             done2, todo2 = distribute_negate(todo1)
301             return [AND_OPERATOR] + done1 + done2, todo2
302     if not domain:
303         return []
304     if domain[0] != NOT_OPERATOR:
305         return [domain[0]] + distribute_not(domain[1:])
306     if domain[0] == NOT_OPERATOR:
307         done, todo = distribute_negate(domain[1:])
308         return done + distribute_not(todo)
309
310
311 # --------------------------------------------------
312 # Generic leaf manipulation
313 # --------------------------------------------------
314
315 def _quote(to_quote):
316     if '"' not in to_quote:
317         return '"%s"' % to_quote
318     return to_quote
319
320
321 def generate_table_alias(src_table_alias, joined_tables=[]):
322     """ Generate a standard table alias name. An alias is generated as following:
323         - the base is the source table name (that can already be an alias)
324         - then, each joined table is added in the alias using a 'link field name'
325           that is used to render unique aliases for a given path
326         - returns a tuple composed of the alias, and the full table alias to be
327           added in a from condition with quoting done
328         Examples:
329         - src_table_alias='res_users', join_tables=[]:
330             alias = ('res_users','"res_users"')
331         - src_model='res_users', join_tables=[(res.partner, 'parent_id')]
332             alias = ('res_users__parent_id', '"res_partner" as "res_users__parent_id"')
333
334         :param model src_table_alias: model source of the alias
335         :param list joined_tables: list of tuples
336                                    (dst_model, link_field)
337
338         :return tuple: (table_alias, alias statement for from clause with quotes added)
339     """
340     alias = src_table_alias
341     if not joined_tables:
342         return '%s' % alias, '%s' % _quote(alias)
343     for link in joined_tables:
344         alias += '__' + link[1]
345     assert len(alias) < 64, 'Table alias name %s is longer than the 64 characters size accepted by default in postgresql.' % alias
346     return '%s' % alias, '%s as %s' % (_quote(joined_tables[-1][0]), _quote(alias))
347
348
349 def get_alias_from_query(from_query):
350     """ :param string from_query: is something like :
351         - '"res_partner"' OR
352         - '"res_partner" as "res_users__partner_id"''
353     """
354     from_splitted = from_query.split(' as ')
355     if len(from_splitted) > 1:
356         return from_splitted[0].replace('"', ''), from_splitted[1].replace('"', '')
357     else:
358         return from_splitted[0].replace('"', ''), from_splitted[0].replace('"', '')
359
360
361 def normalize_leaf(element):
362     """ Change a term's operator to some canonical form, simplifying later
363         processing. """
364     if not is_leaf(element):
365         return element
366     left, operator, right = element
367     original = operator
368     operator = operator.lower()
369     if operator == '<>':
370         operator = '!='
371     if isinstance(right, bool) and operator in ('in', 'not in'):
372         _logger.warning("The domain term '%s' should use the '=' or '!=' operator." % ((left, original, right),))
373         operator = '=' if operator == 'in' else '!='
374     if isinstance(right, (list, tuple)) and operator in ('=', '!='):
375         _logger.warning("The domain term '%s' should use the 'in' or 'not in' operator." % ((left, original, right),))
376         operator = 'in' if operator == '=' else 'not in'
377     return left, operator, right
378
379
380 def is_operator(element):
381     """ Test whether an object is a valid domain operator. """
382     return isinstance(element, basestring) and element in DOMAIN_OPERATORS
383
384
385 def is_leaf(element, internal=False):
386     """ Test whether an object is a valid domain term:
387         - is a list or tuple
388         - with 3 elements
389         - second element if a valid op
390
391         :param tuple element: a leaf in form (left, operator, right)
392         :param boolean internal: allow or not the 'inselect' internal operator
393             in the term. This should be always left to False.
394
395         Note: OLD TODO change the share wizard to use this function.
396     """
397     INTERNAL_OPS = TERM_OPERATORS + ('<>',)
398     if internal:
399         INTERNAL_OPS += ('inselect', 'not inselect')
400     return (isinstance(element, tuple) or isinstance(element, list)) \
401         and len(element) == 3 \
402         and element[1] in INTERNAL_OPS \
403         and ((isinstance(element[0], basestring) and element[0])
404              or element in (TRUE_LEAF, FALSE_LEAF))
405
406
407 # --------------------------------------------------
408 # SQL utils
409 # --------------------------------------------------
410
411 def select_from_where(cr, select_field, from_table, where_field, where_ids, where_operator):
412     # todo: merge into parent query as sub-query
413     res = []
414     if where_ids:
415         if where_operator in ['<', '>', '>=', '<=']:
416             cr.execute('SELECT "%s" FROM "%s" WHERE "%s" %s %%s' % \
417                 (select_field, from_table, where_field, where_operator),
418                 (where_ids[0],))  # TODO shouldn't this be min/max(where_ids) ?
419             res = [r[0] for r in cr.fetchall()]
420         else:  # TODO where_operator is supposed to be 'in'? It is called with child_of...
421             for i in range(0, len(where_ids), cr.IN_MAX):
422                 subids = where_ids[i:i + cr.IN_MAX]
423                 cr.execute('SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % \
424                     (select_field, from_table, where_field), (tuple(subids),))
425                 res.extend([r[0] for r in cr.fetchall()])
426     return res
427
428
429 def select_distinct_from_where_not_null(cr, select_field, from_table):
430     cr.execute('SELECT distinct("%s") FROM "%s" where "%s" is not null' % (select_field, from_table, select_field))
431     return [r[0] for r in cr.fetchall()]
432
433
434 # --------------------------------------------------
435 # ExtendedLeaf class for managing leafs and contexts
436 # -------------------------------------------------
437
438 class ExtendedLeaf(object):
439     """ Class wrapping a domain leaf, and giving some services and management
440         features on it. In particular it managed join contexts to be able to
441         construct queries through multiple models.
442     """
443
444     # --------------------------------------------------
445     # Join / Context manipulation
446     #   running examples:
447     #   - res_users.name, like, foo: name is on res_partner, not on res_users
448     #   - res_partner.bank_ids.name, like, foo: bank_ids is a one2many with _auto_join
449     #   - res_partner.state_id.name, like, foo: state_id is a many2one with _auto_join
450     # A join:
451     #   - link between src_table and dst_table, using src_field and dst_field
452     #       i.e.: inherits: res_users.partner_id = res_partner.id
453     #       i.e.: one2many: res_partner.id = res_partner_bank.partner_id
454     #       i.e.: many2one: res_partner.state_id = res_country_state.id
455     #   - done in the context of a field
456     #       i.e.: inherits: 'partner_id'
457     #       i.e.: one2many: 'bank_ids'
458     #       i.e.: many2one: 'state_id'
459     #   - table names use aliases: initial table followed by the context field
460     #     names, joined using a '__'
461     #       i.e.: inherits: res_partner as res_users__partner_id
462     #       i.e.: one2many: res_partner_bank as res_partner__bank_ids
463     #       i.e.: many2one: res_country_state as res_partner__state_id
464     #   - join condition use aliases
465     #       i.e.: inherits: res_users.partner_id = res_users__partner_id.id
466     #       i.e.: one2many: res_partner.id = res_partner__bank_ids.parr_id
467     #       i.e.: many2one: res_partner.state_id = res_partner__state_id.id
468     # Variables explanation:
469     #   - src_table: working table before the join
470     #       -> res_users, res_partner, res_partner
471     #   - dst_table: working table after the join
472     #       -> res_partner, res_partner_bank, res_country_state
473     #   - src_table_link_name: field name used to link the src table, not
474     #     necessarily a field (because 'id' is not a field instance)
475     #       i.e.: inherits: 'partner_id', found in the inherits of the current table
476     #       i.e.: one2many: 'id', not a field
477     #       i.e.: many2one: 'state_id', the current field name
478     #   - dst_table_link_name: field name used to link the dst table, not
479     #     necessarily a field (because 'id' is not a field instance)
480     #       i.e.: inherits: 'id', not a field
481     #       i.e.: one2many: 'partner_id', _fields_id of the current field
482     #       i.e.: many2one: 'id', not a field
483     #   - context_field_name: field name used as a context to make the alias
484     #       i.e.: inherits: 'partner_id': found in the inherits of the current table
485     #       i.e.: one2many: 'bank_ids': current field name
486     #       i.e.: many2one: 'state_id': current field name
487     # --------------------------------------------------
488
489     def __init__(self, leaf, model, join_context=None):
490         """ Initialize the ExtendedLeaf
491
492             :attr [string, tuple] leaf: operator or tuple-formatted domain
493                 expression
494             :attr obj model: current working model
495             :attr list _models: list of chained models, updated when
496                 adding joins
497             :attr list join_context: list of join contexts. This is a list of
498                 tuples like ``(lhs, table, lhs_col, col, link)``
499
500                 where
501
502                 lhs
503                     source (left hand) model
504                 model
505                     destination (right hand) model
506                 lhs_col
507                     source model column for join condition
508                 col
509                     destination model column for join condition
510                 link
511                     link column between source and destination model
512                     that is not necessarily (but generally) a real column used
513                     in the condition (i.e. in many2one); this link is used to
514                     compute aliases
515         """
516         assert model, 'Invalid leaf creation without table'
517         self.join_context = join_context or []
518         self.leaf = leaf
519         # normalize the leaf's operator
520         self.normalize_leaf()
521         # set working variables; handle the context stack and previous tables
522         self.model = model
523         self._models = []
524         for item in self.join_context:
525             self._models.append(item[0])
526         self._models.append(model)
527         # check validity
528         self.check_leaf()
529
530     def __str__(self):
531         return '<osv.ExtendedLeaf: %s on %s (ctx: %s)>' % (str(self.leaf), self.model._table, ','.join(self._get_context_debug()))
532
533     def generate_alias(self):
534         links = [(context[1]._table, context[4]) for context in self.join_context]
535         alias, alias_statement = generate_table_alias(self._models[0]._table, links)
536         return alias
537
538     def add_join_context(self, model, lhs_col, table_col, link):
539         """ See above comments for more details. A join context is a tuple like:
540                 ``(lhs, model, lhs_col, col, link)``
541
542             After adding the join, the model of the current leaf is updated.
543         """
544         self.join_context.append((self.model, model, lhs_col, table_col, link))
545         self._models.append(model)
546         self.model = model
547
548     def get_join_conditions(self):
549         conditions = []
550         alias = self._models[0]._table
551         for context in self.join_context:
552             previous_alias = alias
553             alias += '__' + context[4]
554             conditions.append('"%s"."%s"="%s"."%s"' % (previous_alias, context[2], alias, context[3]))
555         return conditions
556
557     def get_tables(self):
558         tables = set()
559         links = []
560         for context in self.join_context:
561             links.append((context[1]._table, context[4]))
562             alias, alias_statement = generate_table_alias(self._models[0]._table, links)
563             tables.add(alias_statement)
564         return tables
565
566     def _get_context_debug(self):
567         names = ['"%s"."%s"="%s"."%s" (%s)' % (item[0]._table, item[2], item[1]._table, item[3], item[4]) for item in self.join_context]
568         return names
569
570     # --------------------------------------------------
571     # Leaf manipulation
572     # --------------------------------------------------
573
574     def check_leaf(self):
575         """ Leaf validity rules:
576             - a valid leaf is an operator or a leaf
577             - a valid leaf has a field objects unless
578                 - it is not a tuple
579                 - it is an inherited field
580                 - left is id, operator is 'child_of'
581                 - left is in MAGIC_COLUMNS
582         """
583         if not is_operator(self.leaf) and not is_leaf(self.leaf, True):
584             raise ValueError("Invalid leaf %s" % str(self.leaf))
585
586     def is_operator(self):
587         return is_operator(self.leaf)
588
589     def is_true_leaf(self):
590         return self.leaf == TRUE_LEAF
591
592     def is_false_leaf(self):
593         return self.leaf == FALSE_LEAF
594
595     def is_leaf(self, internal=False):
596         return is_leaf(self.leaf, internal=internal)
597
598     def normalize_leaf(self):
599         self.leaf = normalize_leaf(self.leaf)
600         return True
601
602 def create_substitution_leaf(leaf, new_elements, new_model=None):
603     """ From a leaf, create a new leaf (based on the new_elements tuple
604         and new_model), that will have the same join context. Used to
605         insert equivalent leafs in the processing stack. """
606     if new_model is None:
607         new_model = leaf.model
608     new_join_context = [tuple(context) for context in leaf.join_context]
609     new_leaf = ExtendedLeaf(new_elements, new_model, join_context=new_join_context)
610     return new_leaf
611
612 class expression(object):
613     """ Parse a domain expression
614         Use a real polish notation
615         Leafs are still in a ('foo', '=', 'bar') format
616         For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
617     """
618
619     def __init__(self, cr, uid, exp, table, context):
620         """ Initialize expression object and automatically parse the expression
621             right after initialization.
622
623             :param exp: expression (using domain ('foo', '=', 'bar' format))
624             :param table: root model
625
626             :attr list result: list that will hold the result of the parsing
627                 as a list of ExtendedLeaf
628             :attr list joins: list of join conditions, such as
629                 (res_country_state."id" = res_partner."state_id")
630             :attr root_model: base model for the query
631             :attr list expression: the domain expression, that will be normalized
632                 and prepared
633         """
634         self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent
635         self.joins = []
636         self.root_model = table
637
638         # normalize and prepare the expression for parsing
639         self.expression = distribute_not(normalize_domain(exp))
640
641         # parse the domain expression
642         self.parse(cr, uid, context=context)
643
644     # ----------------------------------------
645     # Leafs management
646     # ----------------------------------------
647
648     def get_tables(self):
649         """ Returns the list of tables for SQL queries, like select from ... """
650         tables = []
651         for leaf in self.result:
652             for table in leaf.get_tables():
653                 if table not in tables:
654                     tables.append(table)
655         table_name = _quote(self.root_model._table)
656         if table_name not in tables:
657             tables.append(table_name)
658         return tables
659
660     # ----------------------------------------
661     # Parsing
662     # ----------------------------------------
663
664     def parse(self, cr, uid, context):
665         """ Transform the leaves of the expression
666
667             The principle is to pop elements from a leaf stack one at a time.
668             Each leaf is processed. The processing is a if/elif list of various
669             cases that appear in the leafs (many2one, function fields, ...).
670             Two things can happen as a processing result:
671             - the leaf has been modified and/or new leafs have to be introduced
672               in the expression; they are pushed into the leaf stack, to be
673               processed right after
674             - the leaf is added to the result
675
676             Some internal var explanation:
677                 :var obj working_model: model object, model containing the field
678                     (the name provided in the left operand)
679                 :var list field_path: left operand seen as a path (foo.bar -> [foo, bar])
680                 :var obj relational_model: relational model of a field (field._obj)
681                     ex: res_partner.bank_ids -> res.partner.bank
682         """
683
684         def to_ids(value, relational_model, context=None, limit=None):
685             """ Normalize a single id or name, or a list of those, into a list of ids
686                 :param {int,long,basestring,list,tuple} value:
687                     if int, long -> return [value]
688                     if basestring, convert it into a list of basestrings, then
689                     if list of basestring ->
690                         perform a name_search on relational_model for each name
691                         return the list of related ids
692             """
693             names = []
694             if isinstance(value, basestring):
695                 names = [value]
696             elif value and isinstance(value, (tuple, list)) and all(isinstance(item, basestring) for item in value):
697                 names = value
698             elif isinstance(value, (int, long)):
699                 return [value]
700             if names:
701                 name_get_list = [name_get[0] for name in names for name_get in relational_model.name_search(cr, uid, name, [], 'ilike', context=context, limit=limit)]
702                 return list(set(name_get_list))
703             return list(value)
704
705         def child_of_domain(left, ids, left_model, parent=None, prefix='', context=None):
706             """ Return a domain implementing the child_of operator for [(left,child_of,ids)],
707                 either as a range using the parent_left/right tree lookup fields
708                 (when available), or as an expanded [(left,in,child_ids)] """
709             if left_model._parent_store and (not left_model.pool._init):
710                 # TODO: Improve where joins are implemented for many with '.', replace by:
711                 # doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
712                 doms = []
713                 for o in left_model.browse(cr, uid, ids, context=context):
714                     if doms:
715                         doms.insert(0, OR_OPERATOR)
716                     doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
717                 if prefix:
718                     return [(left, 'in', left_model.search(cr, uid, doms, context=context))]
719                 return doms
720             else:
721                 def recursive_children(ids, model, parent_field):
722                     if not ids:
723                         return []
724                     ids2 = model.search(cr, uid, [(parent_field, 'in', ids)], context=context)
725                     return ids + recursive_children(ids2, model, parent_field)
726                 return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))]
727
728         def pop():
729             """ Pop a leaf to process. """
730             return self.stack.pop()
731
732         def push(leaf):
733             """ Push a leaf to be processed right after. """
734             self.stack.append(leaf)
735
736         def push_result(leaf):
737             """ Push a leaf to the results. This leaf has been fully processed
738                 and validated. """
739             self.result.append(leaf)
740
741         self.result = []
742         self.stack = [ExtendedLeaf(leaf, self.root_model) for leaf in self.expression]
743         # process from right to left; expression is from left to right
744         self.stack.reverse()
745
746         while self.stack:
747             # Get the next leaf to process
748             leaf = pop()
749
750             # Get working variables
751             working_model = leaf.model
752             if leaf.is_operator():
753                 left, operator, right = leaf.leaf, None, None
754             elif leaf.is_true_leaf() or leaf.is_false_leaf():
755                 # because we consider left as a string
756                 left, operator, right = ('%s' % leaf.leaf[0], leaf.leaf[1], leaf.leaf[2])
757             else:
758                 left, operator, right = leaf.leaf
759             field_path = left.split('.', 1)
760             field = working_model._columns.get(field_path[0])
761             if field and field._obj:
762                 relational_model = working_model.pool.get(field._obj)
763             else:
764                 relational_model = None
765
766             # ----------------------------------------
767             # SIMPLE CASE
768             # 1. leaf is an operator
769             # 2. leaf is a true/false leaf
770             # -> add directly to result
771             # ----------------------------------------
772
773             if leaf.is_operator() or leaf.is_true_leaf() or leaf.is_false_leaf():
774                 push_result(leaf)
775
776             # ----------------------------------------
777             # FIELD NOT FOUND
778             # -> from inherits'd fields -> work on the related model, and add
779             #    a join condition
780             # -> ('id', 'child_of', '..') -> use a 'to_ids'
781             # -> but is one on the _log_access special fields, add directly to
782             #    result
783             #    TODO: make these fields explicitly available in self.columns instead!
784             # -> else: crash
785             # ----------------------------------------
786
787             elif not field and field_path[0] in working_model._inherit_fields:
788                 # comments about inherits'd fields
789                 #  { 'field_name': ('parent_model', 'm2o_field_to_reach_parent',
790                 #                    field_column_obj, origina_parent_model), ... }
791                 next_model = working_model.pool.get(working_model._inherit_fields[field_path[0]][0])
792                 leaf.add_join_context(next_model, working_model._inherits[next_model._name], 'id', working_model._inherits[next_model._name])
793                 push(leaf)
794
795             elif left == 'id' and operator == 'child_of':
796                 ids2 = to_ids(right, working_model, context)
797                 dom = child_of_domain(left, ids2, working_model)
798                 for dom_leaf in reversed(dom):
799                     new_leaf = create_substitution_leaf(leaf, dom_leaf, working_model)
800                     push(new_leaf)
801
802             elif not field and field_path[0] in MAGIC_COLUMNS:
803                 push_result(leaf)
804
805             elif not field:
806                 raise ValueError("Invalid field %r in leaf %r" % (left, str(leaf)))
807
808             # ----------------------------------------
809             # PATH SPOTTED
810             # -> many2one or one2many with _auto_join:
811             #    - add a join, then jump into linked field: field.remaining on
812             #      src_table is replaced by remaining on dst_table, and set for re-evaluation
813             #    - if a domain is defined on the field, add it into evaluation
814             #      on the relational table
815             # -> many2one, many2many, one2many: replace by an equivalent computed
816             #    domain, given by recursively searching on the remaining of the path
817             # -> note: hack about fields.property should not be necessary anymore
818             #    as after transforming the field, it will go through this loop once again
819             # ----------------------------------------
820
821             elif len(field_path) > 1 and field._type == 'many2one' and field._auto_join:
822                 # res_partner.state_id = res_partner__state_id.id
823                 leaf.add_join_context(relational_model, field_path[0], 'id', field_path[0])
824                 push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_model))
825
826             elif len(field_path) > 1 and field._type == 'one2many' and field._auto_join:
827                 # res_partner.id = res_partner__bank_ids.partner_id
828                 leaf.add_join_context(relational_model, 'id', field._fields_id, field_path[0])
829                 domain = field._domain(working_model) if callable(field._domain) else field._domain
830                 push(create_substitution_leaf(leaf, (field_path[1], operator, right), relational_model))
831                 if domain:
832                     domain = normalize_domain(domain)
833                     for elem in reversed(domain):
834                         push(create_substitution_leaf(leaf, elem, relational_model))
835                     push(create_substitution_leaf(leaf, AND_OPERATOR, relational_model))
836
837             elif len(field_path) > 1 and field._auto_join:
838                 raise NotImplementedError('_auto_join attribute not supported on many2many field %s' % left)
839
840             elif len(field_path) > 1 and field._type == 'many2one':
841                 right_ids = relational_model.search(cr, uid, [(field_path[1], operator, right)], context=context)
842                 leaf.leaf = (field_path[0], 'in', right_ids)
843                 push(leaf)
844
845             # Making search easier when there is a left operand as field.o2m or field.m2m
846             elif len(field_path) > 1 and field._type in ['many2many', 'one2many']:
847                 right_ids = relational_model.search(cr, uid, [(field_path[1], operator, right)], context=context)
848                 table_ids = working_model.search(cr, uid, [(field_path[0], 'in', right_ids)], context=dict(context, active_test=False))
849                 leaf.leaf = ('id', 'in', table_ids)
850                 push(leaf)
851
852             # -------------------------------------------------
853             # FUNCTION FIELD
854             # -> not stored: error if no _fnct_search, otherwise handle the result domain
855             # -> stored: management done in the remaining of parsing
856             # -------------------------------------------------
857
858             elif isinstance(field, fields.function) and not field.store and not field._fnct_search:
859                 # this is a function field that is not stored
860                 # the function field doesn't provide a search function and doesn't store
861                 # values in the database, so we must ignore it : we generate a dummy leaf
862                 leaf.leaf = TRUE_LEAF
863                 _logger.error(
864                     "The field '%s' (%s) can not be searched: non-stored "
865                     "function field without fnct_search",
866                     field.string, left)
867                 # avoid compiling stack trace if not needed
868                 if _logger.isEnabledFor(logging.DEBUG):
869                     _logger.debug(''.join(traceback.format_stack()))
870                 push(leaf)
871
872             elif isinstance(field, fields.function) and not field.store:
873                 # this is a function field that is not stored
874                 fct_domain = field.search(cr, uid, working_model, left, [leaf.leaf], context=context)
875                 if not fct_domain:
876                     leaf.leaf = TRUE_LEAF
877                     push(leaf)
878                 else:
879                     # we assume that the expression is valid
880                     # we create a dummy leaf for forcing the parsing of the resulting expression
881                     for domain_element in reversed(fct_domain):
882                         push(create_substitution_leaf(leaf, domain_element, working_model))
883                     # self.push(create_substitution_leaf(leaf, TRUE_LEAF, working_model))
884                     # self.push(create_substitution_leaf(leaf, AND_OPERATOR, working_model))
885
886             # -------------------------------------------------
887             # RELATIONAL FIELDS
888             # -------------------------------------------------
889
890             # Applying recursivity on field(one2many)
891             elif field._type == 'one2many' and operator == 'child_of':
892                 ids2 = to_ids(right, relational_model, context)
893                 if field._obj != working_model._name:
894                     dom = child_of_domain(left, ids2, relational_model, prefix=field._obj)
895                 else:
896                     dom = child_of_domain('id', ids2, working_model, parent=left)
897                 for dom_leaf in reversed(dom):
898                     push(create_substitution_leaf(leaf, dom_leaf, working_model))
899
900             elif field._type == 'one2many':
901                 call_null = True
902
903                 if right is not False:
904                     if isinstance(right, basestring):
905                         ids2 = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, context=context, limit=None)]
906                         if ids2:
907                             operator = 'in'
908                     else:
909                         if not isinstance(right, list):
910                             ids2 = [right]
911                         else:
912                             ids2 = right
913                     if not ids2:
914                         if operator in ['like', 'ilike', 'in', '=']:
915                             #no result found with given search criteria
916                             call_null = False
917                             push(create_substitution_leaf(leaf, FALSE_LEAF, working_model))
918                     else:
919                         ids2 = select_from_where(cr, field._fields_id, relational_model._table, 'id', ids2, operator)
920                         if ids2:
921                             call_null = False
922                             o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
923                             push(create_substitution_leaf(leaf, ('id', o2m_op, ids2), working_model))
924
925                 if call_null:
926                     o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
927                     push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, relational_model._table)), working_model))
928
929             elif field._type == 'many2many':
930                 rel_table, rel_id1, rel_id2 = field._sql_names(working_model)
931                 #FIXME
932                 if operator == 'child_of':
933                     def _rec_convert(ids):
934                         if relational_model == working_model:
935                             return ids
936                         return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator)
937
938                     ids2 = to_ids(right, relational_model, context)
939                     dom = child_of_domain('id', ids2, relational_model)
940                     ids2 = relational_model.search(cr, uid, dom, context=context)
941                     push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), working_model))
942                 else:
943                     call_null_m2m = True
944                     if right is not False:
945                         if isinstance(right, basestring):
946                             res_ids = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, context=context)]
947                             if res_ids:
948                                 operator = 'in'
949                         else:
950                             if not isinstance(right, list):
951                                 res_ids = [right]
952                             else:
953                                 res_ids = right
954                         if not res_ids:
955                             if operator in ['like', 'ilike', 'in', '=']:
956                                 #no result found with given search criteria
957                                 call_null_m2m = False
958                                 push(create_substitution_leaf(leaf, FALSE_LEAF, working_model))
959                             else:
960                                 operator = 'in'  # operator changed because ids are directly related to main object
961                         else:
962                             call_null_m2m = False
963                             m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
964                             push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), working_model))
965
966                     if call_null_m2m:
967                         m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
968                         push(create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), working_model))
969
970             elif field._type == 'many2one':
971                 if operator == 'child_of':
972                     ids2 = to_ids(right, relational_model, context)
973                     if field._obj != working_model._name:
974                         dom = child_of_domain(left, ids2, relational_model, prefix=field._obj)
975                     else:
976                         dom = child_of_domain('id', ids2, working_model, parent=left)
977                     for dom_leaf in reversed(dom):
978                         push(create_substitution_leaf(leaf, dom_leaf, working_model))
979                 else:
980                     def _get_expression(relational_model, cr, uid, left, right, operator, context=None):
981                         if context is None:
982                             context = {}
983                         c = context.copy()
984                         c['active_test'] = False
985                         #Special treatment to ill-formed domains
986                         operator = (operator in ['<', '>', '<=', '>=']) and 'in' or operator
987
988                         dict_op = {'not in': '!=', 'in': '=', '=': 'in', '!=': 'not in'}
989                         if isinstance(right, tuple):
990                             right = list(right)
991                         if (not isinstance(right, list)) and operator in ['not in', 'in']:
992                             operator = dict_op[operator]
993                         elif isinstance(right, list) and operator in ['!=', '=']:  # for domain (FIELD,'=',['value1','value2'])
994                             operator = dict_op[operator]
995                         res_ids = [x[0] for x in relational_model.name_search(cr, uid, right, [], operator, limit=None, context=c)]
996                         if operator in NEGATIVE_TERM_OPERATORS:
997                             res_ids.append(False)  # TODO this should not be appended if False was in 'right'
998                         return left, 'in', res_ids
999                     # resolve string-based m2o criterion into IDs
1000                     if isinstance(right, basestring) or \
1001                             right and isinstance(right, (tuple, list)) and all(isinstance(item, basestring) for item in right):
1002                         push(create_substitution_leaf(leaf, _get_expression(relational_model, cr, uid, left, right, operator, context=context), working_model))
1003                     else:
1004                         # right == [] or right == False and all other cases are handled by __leaf_to_sql()
1005                         push_result(leaf)
1006
1007             # -------------------------------------------------
1008             # OTHER FIELDS
1009             # -> datetime fields: manage time part of the datetime
1010             #    field when it is not there
1011             # -> manage translatable fields
1012             # -------------------------------------------------
1013
1014             else:
1015                 if field._type == 'datetime' and right and len(right) == 10:
1016                     if operator in ('>', '>='):
1017                         right += ' 00:00:00'
1018                     elif operator in ('<', '<='):
1019                         right += ' 23:59:59'
1020                     push(create_substitution_leaf(leaf, (left, operator, right), working_model))
1021
1022                 elif field.translate and right:
1023                     need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
1024                     sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
1025                     if need_wildcard:
1026                         right = '%%%s%%' % right
1027
1028                     inselect_operator = 'inselect'
1029                     if sql_operator in NEGATIVE_TERM_OPERATORS:
1030                         # negate operator (fix lp:1071710)
1031                         sql_operator = sql_operator[4:] if sql_operator[:3] == 'not' else '='
1032                         inselect_operator = 'not inselect'
1033
1034                     subselect = '( SELECT res_id'          \
1035                              '    FROM ir_translation'  \
1036                              '   WHERE name = %s'       \
1037                              '     AND lang = %s'       \
1038                              '     AND type = %s'
1039                     instr = ' %s'
1040                     #Covering in,not in operators with operands (%s,%s) ,etc.
1041                     if sql_operator == 'in':
1042                         instr = ','.join(['%s'] * len(right))
1043                         subselect += '     AND value ' + sql_operator + ' ' + " (" + instr + ")"   \
1044                              ') UNION ('                \
1045                              '  SELECT id'              \
1046                              '    FROM "' + working_model._table + '"'       \
1047                              '   WHERE "' + left + '" ' + sql_operator + ' ' + " (" + instr + "))"
1048                     else:
1049                         subselect += '     AND value ' + sql_operator + instr +   \
1050                              ') UNION ('                \
1051                              '  SELECT id'              \
1052                              '    FROM "' + working_model._table + '"'       \
1053                              '   WHERE "' + left + '" ' + sql_operator + instr + ")"
1054
1055                     params = [working_model._name + ',' + left,
1056                               context.get('lang', False) or 'en_US',
1057                               'model',
1058                               right,
1059                               right,
1060                              ]
1061                     push(create_substitution_leaf(leaf, ('id', inselect_operator, (subselect, params)), working_model))
1062
1063                 else:
1064                     push_result(leaf)
1065
1066         # ----------------------------------------
1067         # END OF PARSING FULL DOMAIN
1068         # -> generate joins
1069         # ----------------------------------------
1070
1071         joins = set()
1072         for leaf in self.result:
1073             joins |= set(leaf.get_join_conditions())
1074         self.joins = list(joins)
1075
1076     def __leaf_to_sql(self, eleaf):
1077         model = eleaf.model
1078         leaf = eleaf.leaf
1079         left, operator, right = leaf
1080
1081         # final sanity checks - should never fail
1082         assert operator in (TERM_OPERATORS + ('inselect', 'not inselect')), \
1083             "Invalid operator %r in domain term %r" % (operator, leaf)
1084         assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in model._all_columns \
1085             or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf)
1086
1087         table_alias = '"%s"' % (eleaf.generate_alias())
1088
1089         if leaf == TRUE_LEAF:
1090             query = 'TRUE'
1091             params = []
1092
1093         elif leaf == FALSE_LEAF:
1094             query = 'FALSE'
1095             params = []
1096
1097         elif operator == 'inselect':
1098             query = '(%s."%s" in (%s))' % (table_alias, left, right[0])
1099             params = right[1]
1100
1101         elif operator == 'not inselect':
1102             query = '(%s."%s" not in (%s))' % (table_alias, left, right[0])
1103             params = right[1]
1104
1105         elif operator in ['in', 'not in']:
1106             # Two cases: right is a boolean or a list. The boolean case is an
1107             # abuse and handled for backward compatibility.
1108             if isinstance(right, bool):
1109                 _logger.warning("The domain term '%s' should use the '=' or '!=' operator." % (leaf,))
1110                 if operator == 'in':
1111                     r = 'NOT NULL' if right else 'NULL'
1112                 else:
1113                     r = 'NULL' if right else 'NOT NULL'
1114                 query = '(%s."%s" IS %s)' % (table_alias, left, r)
1115                 params = []
1116             elif isinstance(right, (list, tuple)):
1117                 params = list(right)
1118                 check_nulls = False
1119                 for i in range(len(params))[::-1]:
1120                     if params[i] == False:
1121                         check_nulls = True
1122                         del params[i]
1123
1124                 if params:
1125                     if left == 'id':
1126                         instr = ','.join(['%s'] * len(params))
1127                     else:
1128                         instr = ','.join([model._columns[left]._symbol_set[0]] * len(params))
1129                     query = '(%s."%s" %s (%s))' % (table_alias, left, operator, instr)
1130                 else:
1131                     # The case for (left, 'in', []) or (left, 'not in', []).
1132                     query = 'FALSE' if operator == 'in' else 'TRUE'
1133
1134                 if check_nulls and operator == 'in':
1135                     query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left)
1136                 elif not check_nulls and operator == 'not in':
1137                     query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left)
1138                 elif check_nulls and operator == 'not in':
1139                     query = '(%s AND %s."%s" IS NOT NULL)' % (query, table_alias, left)  # needed only for TRUE.
1140             else:  # Must not happen
1141                 raise ValueError("Invalid domain term %r" % (leaf,))
1142
1143         elif right == False and (left in model._columns) and model._columns[left]._type == "boolean" and (operator == '='):
1144             query = '(%s."%s" IS NULL or %s."%s" = false )' % (table_alias, left, table_alias, left)
1145             params = []
1146
1147         elif (right is False or right is None) and (operator == '='):
1148             query = '%s."%s" IS NULL ' % (table_alias, left)
1149             params = []
1150
1151         elif right == False and (left in model._columns) and model._columns[left]._type == "boolean" and (operator == '!='):
1152             query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table_alias, left, table_alias, left)
1153             params = []
1154
1155         elif (right is False or right is None) and (operator == '!='):
1156             query = '%s."%s" IS NOT NULL' % (table_alias, left)
1157             params = []
1158
1159         elif operator == '=?':
1160             if right is False or right is None:
1161                 # '=?' is a short-circuit that makes the term TRUE if right is None or False
1162                 query = 'TRUE'
1163                 params = []
1164             else:
1165                 # '=?' behaves like '=' in other cases
1166                 query, params = self.__leaf_to_sql(
1167                     create_substitution_leaf(eleaf, (left, '=', right), model))
1168
1169         elif left == 'id':
1170             query = '%s.id %s %%s' % (table_alias, operator)
1171             params = right
1172
1173         else:
1174             need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
1175             sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
1176
1177             if left in model._columns:
1178                 format = need_wildcard and '%s' or model._columns[left]._symbol_set[0]
1179                 if self.has_unaccent and sql_operator in ('ilike', 'not ilike'):
1180                     query = '(unaccent(%s."%s") %s unaccent(%s))' % (table_alias, left, sql_operator, format)
1181                 else:
1182                     query = '(%s."%s" %s %s)' % (table_alias, left, sql_operator, format)
1183             elif left in MAGIC_COLUMNS:
1184                     query = "(%s.\"%s\" %s %%s)" % (table_alias, left, sql_operator)
1185                     params = right
1186             else:  # Must not happen
1187                 raise ValueError("Invalid field %r in domain term %r" % (left, leaf))
1188
1189             add_null = False
1190             if need_wildcard:
1191                 if isinstance(right, str):
1192                     str_utf8 = right
1193                 elif isinstance(right, unicode):
1194                     str_utf8 = right.encode('utf-8')
1195                 else:
1196                     str_utf8 = str(right)
1197                 params = '%%%s%%' % str_utf8
1198                 add_null = not str_utf8
1199             elif left in model._columns:
1200                 params = model._columns[left]._symbol_set[1](right)
1201
1202             if add_null:
1203                 query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left)
1204
1205         if isinstance(params, basestring):
1206             params = [params]
1207         return query, params
1208
1209     def to_sql(self):
1210         stack = []
1211         params = []
1212         # Process the domain from right to left, using a stack, to generate a SQL expression.
1213         self.result.reverse()
1214         for leaf in self.result:
1215             if leaf.is_leaf(internal=True):
1216                 q, p = self.__leaf_to_sql(leaf)
1217                 params.insert(0, p)
1218                 stack.append(q)
1219             elif leaf.leaf == NOT_OPERATOR:
1220                 stack.append('(NOT (%s))' % (stack.pop(),))
1221             else:
1222                 ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
1223                 q1 = stack.pop()
1224                 q2 = stack.pop()
1225                 stack.append('(%s %s %s)' % (q1, ops[leaf.leaf], q2,))
1226
1227         assert len(stack) == 1
1228         query = stack[0]
1229         joins = ' AND '.join(self.joins)
1230         if joins:
1231             query = '(%s) AND %s' % (joins, query)
1232
1233         return query, tools.flatten(params)
1234
1235 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: