def test_mixed_query_chained_explicit_implicit_joins(self): query = Query() query.tables.extend(['"product_product"', '"product_template"']) query.where_clause.append( "product_product.template_id = product_template.id") query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join query.add_join(("product_template__categ_id", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # CHAINED outer join query.tables.append('"account.account"') query.where_clause.append( "product_category.expense_account_id = account_account.id" ) # additional implicit join self.assertEquals( query.get_sql()[0].strip(), """"product_product","product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" as "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id"),"account.account" """ .strip()) self.assertEquals( query.get_sql()[1].strip(), """product_product.template_id = product_template.id AND product_category.expense_account_id = account_account.id""" .strip())
def test_query_chained_explicit_joins(self): query = Query() query.tables.extend(['"product_product"', '"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join query.add_join(("product_template__categ_id", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # CHAINED outer join self.assertEquals(query.get_sql()[0].strip(), """"product_product","product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" as "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")""".strip()) self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
def test_basic_query(self): query = Query() query.tables.extend(['"product_product"', '"product_template"']) query.where_clause.append("product_product.template_id = product_template.id") query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join query.add_join(("product_product", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # outer join self.assertEquals(query.get_sql()[0].strip(), """"product_product" LEFT JOIN "res_user" as "product_product__user_id" ON ("product_product"."user_id" = "product_product__user_id"."id"),"product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") """.strip()) self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
def test_query_chained_explicit_joins(self): query = Query() query.add_table('product_product') query.add_table('product_template') query.where_clause.append( "product_product.template_id = product_template.id") query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join query.add_join(("product_template__categ_id", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # CHAINED outer join self.assertEqual( query.get_sql()[0].strip(), """"product_product","product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" as "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")""" .strip()) self.assertEqual( query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
class expression(object): """ Parse a domain expression Use a real polish notation Leafs are still in a ('foo', '=', 'bar') format For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html """ def __init__(self, domain, model, alias=None, query=None): """ Initialize expression object and automatically parse the expression right after initialization. :param domain: expression (using domain ('foo', '=', 'bar') format) :param model: root model :param alias: alias for the model table if query is provided :param query: optional query object holding the final result :attr root_model: base model for the query :attr expression: the domain to parse, normalized and prepared :attr result: the result of the parsing, as a pair (query, params) :attr query: Query object holding the final result """ self._unaccent = get_unaccent_wrapper(model._cr) self.root_model = model self.root_alias = alias or model._table # normalize and prepare the expression for parsing self.expression = distribute_not(normalize_domain(domain)) # this object handles all the joins self.query = Query(['"%s"' % model._table]) if query is None else query # parse the domain expression self.parse() # ---------------------------------------- # Leafs management # ---------------------------------------- def get_tables(self): warnings.warn("deprecated expression.get_tables(), use expression.query instead", DeprecationWarning) return self.query.tables # ---------------------------------------- # Parsing # ---------------------------------------- def parse(self): """ Transform the leaves of the expression The principle is to pop elements from a leaf stack one at a time. Each leaf is processed. The processing is a if/elif list of various cases that appear in the leafs (many2one, function fields, ...). Three things can happen as a processing result: - the leaf is a logic operator, and updates the result stack accordingly; - the leaf has been modified and/or new leafs have to be introduced in the expression; they are pushed into the leaf stack, to be processed right after; - the leaf is converted to SQL and added to the result stack Here is a suggested execution: step stack result_stack ['&', A, B] [] substitute B ['&', A, B1] [] convert B1 in SQL ['&', A] ["B1"] substitute A ['&', '|', A1, A2] ["B1"] convert A2 in SQL ['&', '|', A1] ["B1", "A2"] convert A1 in SQL ['&', '|'] ["B1", "A2", "A1"] apply operator OR ['&'] ["B1", "A1 or A2"] apply operator AND [] ["(A1 or A2) and B1"] Some internal var explanation: :var list path: left operand seen as a sequence of field names ("foo.bar" -> ["foo", "bar"]) :var obj model: model object, model containing the field (the name provided in the left operand) :var obj field: the field corresponding to `path[0]` :var obj column: the column corresponding to `path[0]` :var obj comodel: relational model of field (field.comodel) (res_partner.bank_ids -> res.partner.bank) """ cr, uid, context, su = self.root_model.env.args def to_ids(value, comodel, leaf): """ Normalize a single id or name, or a list of those, into a list of ids :param {int,long,basestring,list,tuple} value: if int, long -> return [value] if basestring, convert it into a list of basestrings, then if list of basestring -> perform a name_search on comodel for each name return the list of related ids """ names = [] if isinstance(value, str): names = [value] elif value and isinstance(value, (tuple, list)) and all(isinstance(item, str) for item in value): names = value elif isinstance(value, int): if not value: # given this nonsensical domain, it is generally cheaper to # interpret False as [], so that "X child_of False" will # match nothing _logger.warning("Unexpected domain [%s], interpreted as False", leaf) return [] return [value] if names: return list({ rid for name in names for rid, rname in comodel.name_search(name, [], 'ilike', limit=None) }) return list(value) def child_of_domain(left, ids, left_model, parent=None, prefix=''): """ Return a domain implementing the child_of operator for [(left,child_of,ids)], either as a range using the parent_path tree lookup field (when available), or as an expanded [(left,in,child_ids)] """ if not ids: return [FALSE_LEAF] if left_model._parent_store: doms = OR([ [('parent_path', '=like', rec.parent_path + '%')] for rec in left_model.browse(ids) ]) if prefix: return [(left, 'in', left_model.search(doms).ids)] return doms else: parent_name = parent or left_model._parent_name child_ids = set(ids) while ids: ids = left_model.search([(parent_name, 'in', ids)]).ids child_ids.update(ids) return [(left, 'in', list(child_ids))] def parent_of_domain(left, ids, left_model, parent=None, prefix=''): """ Return a domain implementing the parent_of operator for [(left,parent_of,ids)], either as a range using the parent_path tree lookup field (when available), or as an expanded [(left,in,parent_ids)] """ if left_model._parent_store: parent_ids = [ int(label) for rec in left_model.browse(ids) for label in rec.parent_path.split('/')[:-1] ] if prefix: return [(left, 'in', parent_ids)] return [('id', 'in', parent_ids)] else: parent_name = parent or left_model._parent_name parent_ids = set() for record in left_model.browse(ids): while record: parent_ids.add(record.id) record = record[parent_name] return [(left, 'in', list(parent_ids))] HIERARCHY_FUNCS = {'child_of': child_of_domain, 'parent_of': parent_of_domain} def pop(): """ Pop a leaf to process. """ return stack.pop() def push(leaf, model, alias, internal=False): """ Push a leaf to be processed right after. """ leaf = normalize_leaf(leaf) check_leaf(leaf, internal) stack.append((leaf, model, alias)) def pop_result(): return result_stack.pop() def push_result(query, params): result_stack.append((query, params)) # process domain from right to left; stack contains domain leaves, in # the form: (leaf, corresponding model, corresponding table alias) stack = [] for leaf in self.expression: push(leaf, self.root_model, self.root_alias) # stack of SQL expressions in the form: (expr, params) result_stack = [] while stack: # Get the next leaf to process leaf, model, alias = pop() # ---------------------------------------- # SIMPLE CASE # 1. leaf is an operator # 2. leaf is a true/false leaf # -> convert and add directly to result # ---------------------------------------- if is_operator(leaf): if leaf == NOT_OPERATOR: expr, params = pop_result() push_result('(NOT (%s))' % expr, params) else: ops = {AND_OPERATOR: '(%s AND %s)', OR_OPERATOR: '(%s OR %s)'} lhs, lhs_params = pop_result() rhs, rhs_params = pop_result() push_result(ops[leaf] % (lhs, rhs), lhs_params + rhs_params) continue if is_boolean(leaf): expr, params = self.__leaf_to_sql(leaf, model, alias) push_result(expr, params) continue # Get working variables left, operator, right = leaf path = left.split('.', 1) field = model._fields.get(path[0]) comodel = model.env.get(getattr(field, 'comodel_name', None)) # ---------------------------------------- # FIELD NOT FOUND # -> from inherits'd fields -> work on the related model, and add # a join condition # -> ('id', 'child_of', '..') -> use a 'to_ids' # -> but is one on the _log_access special fields, add directly to # result # TODO: make these fields explicitly available in self.columns instead! # -> else: crash # ---------------------------------------- if not field: raise ValueError("Invalid field %s.%s in leaf %s" % (model._name, path[0], str(leaf))) elif field.inherited: parent_model = model.env[field.related_field.model_name] parent_fname = model._inherits[parent_model._name] parent_alias, _ = self.query.add_join( (alias, parent_model._table, parent_fname, 'id', parent_fname), implicit=False, outer=True, ) push(leaf, parent_model, parent_alias) elif left == 'id' and operator in HIERARCHY_FUNCS: ids2 = to_ids(right, model, leaf) dom = HIERARCHY_FUNCS[operator](left, ids2, model) for dom_leaf in dom: push(dom_leaf, model, alias) # ---------------------------------------- # PATH SPOTTED # -> many2one or one2many with _auto_join: # - add a join, then jump into linked column: column.remaining on # src_table is replaced by remaining on dst_table, and set for re-evaluation # - if a domain is defined on the column, add it into evaluation # on the relational table # -> many2one, many2many, one2many: replace by an equivalent computed # domain, given by recursively searching on the remaining of the path # -> note: hack about columns.property should not be necessary anymore # as after transforming the column, it will go through this loop once again # ---------------------------------------- elif len(path) > 1 and field.store and field.type == 'many2one' and field.auto_join: # res_partner.state_id = res_partner__state_id.id coalias, _ = self.query.add_join( (alias, comodel._table, path[0], 'id', path[0]), implicit=False, outer=True, ) push((path[1], operator, right), comodel, coalias) elif len(path) > 1 and field.store and field.type == 'one2many' and field.auto_join: # use a subquery bypassing access rules and business logic domain = [(path[1], operator, right)] + field.get_domain_list(model) query = comodel.with_context(**field.context)._where_calc(domain) subfrom, subwhere, subparams = query.get_sql() subquery = 'SELECT "{}"."{}" FROM {} WHERE {}'.format( comodel._table, field.inverse_name, subfrom, subwhere, ) push(('id', 'inselect', (subquery, subparams)), model, alias, internal=True) elif len(path) > 1 and field.store and field.auto_join: raise NotImplementedError('auto_join attribute not supported on field %s' % field) elif len(path) > 1 and field.store and field.type == 'many2one': right_ids = comodel.with_context(active_test=False).search([('.'.join(path[1:]), operator, right)]).ids push((path[0], 'in', right_ids), model, alias) # Making search easier when there is a left operand as one2many or many2many elif len(path) > 1 and field.store and field.type in ('many2many', 'one2many'): right_ids = comodel.with_context(**field.context).search([('.'.join(path[1:]), operator, right)]).ids push((path[0], 'in', right_ids), model, alias) elif not field.store: # Non-stored field should provide an implementation of search. if not field.search: # field does not support search! _logger.error("Non-stored field %s cannot be searched.", field) if _logger.isEnabledFor(logging.DEBUG): _logger.debug(''.join(traceback.format_stack())) # Ignore it: generate a dummy leaf. domain = [] else: # Let the field generate a domain. if len(path) > 1: right = comodel.search([('.'.join(path[1:]), operator, right)]).ids operator = 'in' domain = field.determine_domain(model, operator, right) for elem in normalize_domain(domain): push(elem, model, alias, internal=True) # ------------------------------------------------- # RELATIONAL FIELDS # ------------------------------------------------- # Applying recursivity on field(one2many) elif field.type == 'one2many' and operator in HIERARCHY_FUNCS: ids2 = to_ids(right, comodel, leaf) if field.comodel_name != model._name: dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=field.comodel_name) else: dom = HIERARCHY_FUNCS[operator]('id', ids2, model, parent=left) for dom_leaf in dom: push(dom_leaf, model, alias) elif field.type == 'one2many': domain = field.get_domain_list(model) inverse_is_int = comodel._fields[field.inverse_name].type == 'integer' unwrap_inverse = (lambda ids: ids) if inverse_is_int else (lambda recs: recs.ids) if right is not False: # determine ids2 in comodel if isinstance(right, str): op2 = (TERM_OPERATORS_NEGATION[operator] if operator in NEGATIVE_TERM_OPERATORS else operator) ids2 = [x[0] for x in comodel.name_search(right, domain or [], op2, limit=None)] elif isinstance(right, collections.abc.Iterable): ids2 = right else: ids2 = [right] if ids2 and inverse_is_int and domain: ids2 = comodel.search([('id', 'in', ids2)] + domain).ids # determine ids1 in model related to ids2 if not ids2: ids1 = [] elif comodel._fields[field.inverse_name].store: ids1 = select_from_where(cr, field.inverse_name, comodel._table, 'id', ids2, operator) else: recs = comodel.browse(ids2).sudo().with_context(prefetch_fields=False) ids1 = unwrap_inverse(recs.mapped(field.inverse_name)) # rewrite condition in terms of ids1 op1 = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' push(('id', op1, ids1), model, alias) else: if comodel._fields[field.inverse_name].store and not (inverse_is_int and domain): # rewrite condition to match records with/without lines op1 = 'inselect' if operator in NEGATIVE_TERM_OPERATORS else 'not inselect' subquery = 'SELECT "%s" FROM "%s" where "%s" is not null' % (field.inverse_name, comodel._table, field.inverse_name) push(('id', op1, (subquery, [])), model, alias, internal=True) else: comodel_domain = [(field.inverse_name, '!=', False)] if inverse_is_int and domain: comodel_domain += domain recs = comodel.search(comodel_domain).sudo().with_context(prefetch_fields=False) # determine ids1 = records with lines ids1 = unwrap_inverse(recs.mapped(field.inverse_name)) # rewrite condition to match records with/without lines op1 = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' push(('id', op1, ids1), model, alias) elif field.type == 'many2many': rel_table, rel_id1, rel_id2 = field.relation, field.column1, field.column2 if operator in HIERARCHY_FUNCS: # determine ids2 in comodel ids2 = to_ids(right, comodel, leaf) domain = HIERARCHY_FUNCS[operator]('id', ids2, comodel) ids2 = comodel.search(domain).ids # rewrite condition in terms of ids2 if comodel == model: push(('id', 'in', ids2), model, alias) else: subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2) push(('id', 'inselect', (subquery, [tuple(ids2) or (None,)])), model, alias, internal=True) elif right is not False: # determine ids2 in comodel if isinstance(right, str): domain = field.get_domain_list(model) op2 = (TERM_OPERATORS_NEGATION[operator] if operator in NEGATIVE_TERM_OPERATORS else operator) ids2 = [x[0] for x in comodel.name_search(right, domain or [], op2, limit=None)] elif isinstance(right, collections.abc.Iterable): ids2 = right else: ids2 = [right] # rewrite condition in terms of ids2 subop = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect' subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2) ids2 = tuple(it for it in ids2 if it) or (None,) push(('id', subop, (subquery, [ids2])), model, alias, internal=True) else: # rewrite condition to match records with/without relations op1 = 'inselect' if operator in NEGATIVE_TERM_OPERATORS else 'not inselect' subquery = 'SELECT "%s" FROM "%s" where "%s" is not null' % (rel_id1, rel_table, rel_id1) push(('id', op1, (subquery, [])), model, alias, internal=True) elif field.type == 'many2one': if operator in HIERARCHY_FUNCS: ids2 = to_ids(right, comodel, leaf) if field.comodel_name != model._name: dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=field.comodel_name) else: dom = HIERARCHY_FUNCS[operator]('id', ids2, model, parent=left) for dom_leaf in dom: push(dom_leaf, model, alias) else: def _get_expression(comodel, left, right, operator): #Special treatment to ill-formed domains operator = (operator in ['<', '>', '<=', '>=']) and 'in' or operator dict_op = {'not in': '!=', 'in': '=', '=': 'in', '!=': 'not in'} if isinstance(right, tuple): right = list(right) if (not isinstance(right, list)) and operator in ['not in', 'in']: operator = dict_op[operator] elif isinstance(right, list) and operator in ['!=', '=']: # for domain (FIELD,'=',['value1','value2']) operator = dict_op[operator] res_ids = [x[0] for x in comodel.with_context(active_test=False).name_search(right, [], operator, limit=None)] if operator in NEGATIVE_TERM_OPERATORS: res_ids.append(False) # TODO this should not be appended if False was in 'right' return left, 'in', res_ids # resolve string-based m2o criterion into IDs if isinstance(right, str) or \ right and isinstance(right, (tuple, list)) and all(isinstance(item, str) for item in right): push(_get_expression(comodel, left, right, operator), model, alias) else: # right == [] or right == False and all other cases are handled by __leaf_to_sql() expr, params = self.__leaf_to_sql(leaf, model, alias) push_result(expr, params) # ------------------------------------------------- # BINARY FIELDS STORED IN ATTACHMENT # -> check for null only # ------------------------------------------------- elif field.type == 'binary' and field.attachment: if operator in ('=', '!=') and not right: inselect_operator = 'inselect' if operator in NEGATIVE_TERM_OPERATORS else 'not inselect' subselect = "SELECT res_id FROM ir_attachment WHERE res_model=%s AND res_field=%s" params = (model._name, left) push(('id', inselect_operator, (subselect, params)), model, alias, internal=True) else: _logger.error("Binary field '%s' stored in attachment: ignore %s %s %s", field.string, left, operator, right) push(TRUE_LEAF, model, alias) # ------------------------------------------------- # OTHER FIELDS # -> datetime fields: manage time part of the datetime # column when it is not there # -> manage translatable fields # ------------------------------------------------- else: if field.type == 'datetime' and right: if isinstance(right, str) and len(right) == 10: if operator in ('>', '<='): right += ' 23:59:59' else: right += ' 00:00:00' push((left, operator, right), model, alias) elif isinstance(right, date) and not isinstance(right, datetime): if operator in ('>', '<='): right = datetime.combine(right, time.max) else: right = datetime.combine(right, time.min) push((left, operator, right), model, alias) else: expr, params = self.__leaf_to_sql(leaf, model, alias) push_result(expr, params) elif field.translate is True and right: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator) if need_wildcard: right = '%%%s%%' % right if sql_operator == 'in': right = tuple(right) unaccent = self._unaccent if sql_operator.endswith('like') else lambda x: x left = unaccent(model._generate_translated_field(alias, left, self.query)) instr = unaccent('%s') push_result(f"{left} {sql_operator} {instr}", [right]) else: expr, params = self.__leaf_to_sql(leaf, model, alias) push_result(expr, params) # ---------------------------------------- # END OF PARSING FULL DOMAIN # -> put result in self.result and self.query # ---------------------------------------- [self.result] = result_stack where_clause, where_params = self.result self.query.where_clause.append(where_clause) self.query.where_clause_params.extend(where_params) def __leaf_to_sql(self, leaf, model, alias): left, operator, right = leaf # final sanity checks - should never fail assert operator in (TERM_OPERATORS + ('inselect', 'not inselect')), \ "Invalid operator %r in domain term %r" % (operator, leaf) assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in model._fields, \ "Invalid field %r in domain term %r" % (left, leaf) assert not isinstance(right, BaseModel), \ "Invalid value %r in domain term %r" % (right, leaf) table_alias = '"%s"' % alias if leaf == TRUE_LEAF: query = 'TRUE' params = [] elif leaf == FALSE_LEAF: query = 'FALSE' params = [] elif operator == 'inselect': query = '(%s."%s" in (%s))' % (table_alias, left, right[0]) params = list(right[1]) elif operator == 'not inselect': query = '(%s."%s" not in (%s))' % (table_alias, left, right[0]) params = list(right[1]) elif operator in ['in', 'not in']: # Two cases: right is a boolean or a list. The boolean case is an # abuse and handled for backward compatibility. if isinstance(right, bool): _logger.warning("The domain term '%s' should use the '=' or '!=' operator." % (leaf,)) if (operator == 'in' and right) or (operator == 'not in' and not right): query = '(%s."%s" IS NOT NULL)' % (table_alias, left) else: query = '(%s."%s" IS NULL)' % (table_alias, left) params = [] elif isinstance(right, (list, tuple)): params = [it for it in right if it != False] check_null = len(params) < len(right) if params: if left == 'id': instr = ','.join(['%s'] * len(params)) else: field = model._fields[left] instr = ','.join([field.column_format] * len(params)) params = [field.convert_to_column(p, model, validate=False) for p in params] query = '(%s."%s" %s (%s))' % (table_alias, left, operator, instr) else: # The case for (left, 'in', []) or (left, 'not in', []). query = 'FALSE' if operator == 'in' else 'TRUE' if (operator == 'in' and check_null) or (operator == 'not in' and not check_null): query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left) elif operator == 'not in' and check_null: query = '(%s AND %s."%s" IS NOT NULL)' % (query, table_alias, left) # needed only for TRUE. else: # Must not happen raise ValueError("Invalid domain term %r" % (leaf,)) elif left in model and model._fields[left].type == "boolean" and ((operator == '=' and right is False) or (operator == '!=' and right is True)): query = '(%s."%s" IS NULL or %s."%s" = false )' % (table_alias, left, table_alias, left) params = [] elif (right is False or right is None) and (operator == '='): query = '%s."%s" IS NULL ' % (table_alias, left) params = [] elif left in model and model._fields[left].type == "boolean" and ((operator == '!=' and right is False) or (operator == '==' and right is True)): query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table_alias, left, table_alias, left) params = [] elif (right is False or right is None) and (operator == '!='): query = '%s."%s" IS NOT NULL' % (table_alias, left) params = [] elif operator == '=?': if right is False or right is None: # '=?' is a short-circuit that makes the term TRUE if right is None or False query = 'TRUE' params = [] else: # '=?' behaves like '=' in other cases query, params = self.__leaf_to_sql((left, '=', right), model, alias) else: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator) cast = '::text' if sql_operator.endswith('like') else '' if left not in model: raise ValueError("Invalid field %r in domain term %r" % (left, leaf)) format = '%s' if need_wildcard else model._fields[left].column_format unaccent = self._unaccent if sql_operator.endswith('like') else lambda x: x column = '%s.%s' % (table_alias, _quote(left)) query = '(%s %s %s)' % (unaccent(column + cast), sql_operator, unaccent(format)) if (need_wildcard and not right) or (right and operator in NEGATIVE_TERM_OPERATORS): query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left) if need_wildcard: params = ['%%%s%%' % pycompat.to_text(right)] else: field = model._fields[left] params = [field.convert_to_column(right, model, validate=False)] return query, params def to_sql(self): warnings.warn("deprecated expression.to_sql(), use expression.query instead", DeprecationWarning) return self.result