diff --git a/addons/purchase/report/purchase_report.py b/addons/purchase/report/purchase_report.py index e472a4ccd65..326c32fbb6d 100644 --- a/addons/purchase/report/purchase_report.py +++ b/addons/purchase/report/purchase_report.py @@ -184,15 +184,14 @@ class PurchaseReport(models.Model): SELECT extract(epoch from age(po.date_approve,po.create_date))/(24*60*60) AS po_days_to_purchase FROM purchase_order po WHERE po.id IN ( - SELECT order_id - FROM purchase_report - WHERE date_approve IS NOT NULL - AND %s ) + SELECT "purchase_report"."order_id" FROM %s WHERE %s) ) AS days_to_purchase """ - where, args = expression(domain + [('company_id', '=', self.env.company.id)], self).to_sql() - self.env.cr.execute(query % where, args) + subdomain = domain + [('company_id', '=', self.env.company.id), ('date_approve', '!=', False)] + subtables, subwhere, subparams = expression(subdomain, self).query.get_sql() + + self.env.cr.execute(query % (subtables, subwhere), subparams) res[0].update({ '__count': 1, avg_days_to_purchase.split(':')[0]: self.env.cr.fetchall()[0][0], diff --git a/addons/purchase_stock/report/purchase_report.py b/addons/purchase_stock/report/purchase_report.py index 8b632f60885..0215ec65353 100644 --- a/addons/purchase_stock/report/purchase_report.py +++ b/addons/purchase_stock/report/purchase_report.py @@ -54,15 +54,14 @@ class PurchaseReport(models.Model): SELECT extract(epoch from age(po.effective_date,COALESCE(po.date_planned, po.expected_date)))/(24*60*60) AS po_receipt_delay FROM purchase_order po WHERE po.id IN ( - SELECT order_id - FROM purchase_report - WHERE effective_date IS NOT NULL - AND %s ) + SELECT "purchase_report"."order_id" FROM %s WHERE %s) ) AS receipt_delay """ - where, args = expression(domain + [('company_id', '=', self.env.company.id)], self).to_sql() - self.env.cr.execute(query % where, args) + subdomain = domain + [('company_id', '=', self.env.company.id), ('effective_date', '!=', False)] + subtables, subwhere, subparams = expression(subdomain, self).query.get_sql() + + self.env.cr.execute(query % (subtables, subwhere), subparams) res[0].update({ '__count': 1, avg_receipt_delay.split(':')[0]: self.env.cr.fetchall()[0][0], diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index b4d0ea7f2c5..5767bf38e49 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -570,7 +570,8 @@ actual arch. # retrieve all the views transitively inheriting from view_id domain = self._get_inheriting_views_arch_domain(model) e = expression(domain, self.env['ir.ui.view']) - where_clause, where_params = e.to_sql() + from_clause, where_clause, where_params = e.query.get_sql() + assert from_clause == '"ir_ui_view"' self.flush(['active']) query = """ WITH RECURSIVE ir_ui_view_inherits AS ( diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py index 4e0e5fc0be9..d0e9811e2a1 100644 --- a/odoo/addons/base/tests/test_expression.py +++ b/odoo/addons/base/tests/test_expression.py @@ -1124,15 +1124,15 @@ class TestMany2one(TransactionCase): self.Partner.search([('company_id.partner_id.name', 'like', self.company.name)]) with self.assertQueries([''' - SELECT "res_company".id - FROM "res_company" - WHERE ("res_company"."name"::text LIKE %s) - ORDER BY "res_company"."sequence", "res_company"."name" - ''', ''' SELECT "res_country".id FROM "res_country" WHERE ("res_country"."code"::text LIKE %s) ORDER BY "res_country"."name" + ''', ''' + SELECT "res_company".id + FROM "res_company" + WHERE ("res_company"."name"::text LIKE %s) + ORDER BY "res_company"."sequence", "res_company"."name" ''', ''' SELECT "res_partner".id FROM "res_partner" @@ -1202,8 +1202,8 @@ class TestMany2one(TransactionCase): SELECT "res_partner".id FROM "res_partner", "res_company" AS "res_partner__company_id", "res_partner" AS "res_partner__company_id__partner_id" - WHERE ("res_partner"."company_id" = "res_partner__company_id"."id" - AND "res_partner__company_id"."partner_id" = "res_partner__company_id__partner_id"."id") + WHERE ("res_partner"."company_id" = "res_partner__company_id"."id") + AND ("res_partner__company_id"."partner_id" = "res_partner__company_id__partner_id"."id") AND ("res_partner__company_id__partner_id"."name"::text LIKE %s) ORDER BY "res_partner"."display_name" ''']): @@ -1221,10 +1221,10 @@ class TestMany2one(TransactionCase): # WRONG QUERY: it discards results where company_id or country_id are NULL with self.assertQueries([''' SELECT "res_partner".id - FROM "res_partner", "res_company" AS "res_partner__company_id", - "res_country" AS "res_partner__country_id" - WHERE ("res_partner"."company_id" = "res_partner__company_id"."id" - AND "res_partner"."country_id" = "res_partner__country_id"."id") + FROM "res_partner", "res_country" AS "res_partner__country_id", + "res_company" AS "res_partner__company_id" + WHERE ("res_partner"."country_id" = "res_partner__country_id"."id") + AND ("res_partner"."company_id" = "res_partner__company_id"."id") AND (("res_partner__company_id"."name"::text LIKE %s) OR ("res_partner__country_id"."code"::text LIKE %s)) ORDER BY "res_partner"."display_name" @@ -1372,8 +1372,8 @@ class TestOne2many(TransactionCase): SELECT "res_partner".id FROM "res_partner", "res_partner" AS "res_partner__child_ids", "res_partner_bank" AS "res_partner__child_ids__bank_ids" - WHERE ("res_partner"."id" = "res_partner__child_ids"."parent_id" - AND "res_partner__child_ids"."id" = "res_partner__child_ids__bank_ids"."partner_id") + WHERE ("res_partner"."id" = "res_partner__child_ids"."parent_id") + AND ("res_partner__child_ids"."id" = "res_partner__child_ids__bank_ids"."partner_id") AND (("res_partner__child_ids"."active" = %s) AND ("res_partner__child_ids__bank_ids"."sanitized_acc_number"::text LIKE %s)) ORDER BY "res_partner"."display_name" @@ -1390,8 +1390,8 @@ class TestOne2many(TransactionCase): SELECT "res_partner".id FROM "res_partner", "res_partner" AS "res_partner__child_ids", "res_partner_bank" AS "res_partner__child_ids__bank_ids" - WHERE ("res_partner"."id" = "res_partner__child_ids"."parent_id" - AND "res_partner__child_ids"."id" = "res_partner__child_ids__bank_ids"."partner_id") + WHERE ("res_partner"."id" = "res_partner__child_ids"."parent_id") + AND ("res_partner__child_ids"."id" = "res_partner__child_ids__bank_ids"."partner_id") AND ((NOT (("res_partner__child_ids"."name" = %s))) AND (("res_partner__child_ids__bank_ids"."sanitized_acc_number"::text LIKE %s) AND ("res_partner__child_ids__bank_ids"."id" in (%s,%s,%s)))) @@ -1410,9 +1410,9 @@ class TestOne2many(TransactionCase): FROM "res_partner", "res_partner" AS "res_partner__child_ids", "res_country_state" AS "res_partner__child_ids__state_id", "res_country" AS "res_partner__child_ids__state_id__country_id" - WHERE ("res_partner"."id" = "res_partner__child_ids"."parent_id" - AND "res_partner__child_ids"."state_id" = "res_partner__child_ids__state_id"."id" - AND "res_partner__child_ids__state_id"."country_id" = "res_partner__child_ids__state_id__country_id"."id") + WHERE ("res_partner"."id" = "res_partner__child_ids"."parent_id") + AND ("res_partner__child_ids"."state_id" = "res_partner__child_ids__state_id"."id") + AND ("res_partner__child_ids__state_id"."country_id" = "res_partner__child_ids__state_id__country_id"."id") AND (("res_partner__child_ids"."active" = %s) AND ("res_partner__child_ids__state_id__country_id"."code"::text LIKE %s)) ORDER BY "res_partner"."display_name" diff --git a/odoo/models.py b/odoo/models.py index bbdb91b0369..e9a5f9ff636 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -4112,14 +4112,9 @@ Record ids: %(records)s domain = [(self._active_name, '=', 1)] + domain if domain: - e = expression.expression(domain, self) - tables = e.get_tables() - where_clause, where_params = e.to_sql() - where_clause = [where_clause] if where_clause else [] + return expression.expression(domain, self).query else: - where_clause, where_params, tables = [], [], ['"%s"' % self._table] - - return Query(tables, where_clause, where_params) + return Query(['"%s"' % self._table]) def _check_qorder(self, word): if not regex_order.match(word): diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index d894de46b60..5bebe1859de 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -114,6 +114,7 @@ start the server specifying the ``--unaccent`` flag. """ import collections.abc +import warnings import logging import traceback @@ -122,6 +123,7 @@ from zlib import crc32 from datetime import date, datetime, time import odoo.modules +from odoo.osv.query import Query from odoo.tools import pycompat from odoo.tools.misc import get_lang from ..models import MAGIC_COLUMNS, BaseModel @@ -432,6 +434,15 @@ def is_leaf(element, internal=False): or tuple(element) in (TRUE_LEAF, FALSE_LEAF)) +def is_boolean(element): + return element == TRUE_LEAF or element == FALSE_LEAF + + +def check_leaf(element, internal=False): + if not is_operator(element) and not is_leaf(element, internal): + raise ValueError("Invalid leaf %s" % str(element)) + + # -------------------------------------------------- # SQL utils # -------------------------------------------------- @@ -459,185 +470,6 @@ def get_unaccent_wrapper(cr): return lambda x: "unaccent(%s)" % (x,) return lambda x: x -# -------------------------------------------------- -# ExtendedLeaf class for managing leafs and contexts -# ------------------------------------------------- - -class ExtendedLeaf(object): - """ Class wrapping a domain leaf, and giving some services and management - features on it. In particular it managed join contexts to be able to - construct queries through multiple models. - """ - - # -------------------------------------------------- - # Join / Context manipulation - # running examples: - # - res_users.name, like, foo: name is on res_partner, not on res_users - # - res_partner.bank_ids.name, like, foo: bank_ids is a one2many with _auto_join - # - res_partner.state_id.name, like, foo: state_id is a many2one with _auto_join - # A join: - # - link between src_table and dst_table, using src_field and dst_field - # i.e.: inherits: res_users.partner_id = res_partner.id - # i.e.: one2many: res_partner.id = res_partner_bank.partner_id - # i.e.: many2one: res_partner.state_id = res_country_state.id - # - done in the context of a field - # i.e.: inherits: 'partner_id' - # i.e.: one2many: 'bank_ids' - # i.e.: many2one: 'state_id' - # - table names use aliases: initial table followed by the context field - # names, joined using a '__' - # i.e.: inherits: res_partner as res_users__partner_id - # i.e.: one2many: res_partner_bank as res_partner__bank_ids - # i.e.: many2one: res_country_state as res_partner__state_id - # - join condition use aliases - # i.e.: inherits: res_users.partner_id = res_users__partner_id.id - # i.e.: one2many: res_partner.id = res_partner__bank_ids.parr_id - # i.e.: many2one: res_partner.state_id = res_partner__state_id.id - # Variables explanation: - # - src_table: working table before the join - # -> res_users, res_partner, res_partner - # - dst_table: working table after the join - # -> res_partner, res_partner_bank, res_country_state - # - src_table_link_name: field name used to link the src table, not - # necessarily a field (because 'id' is not a field instance) - # i.e.: inherits: 'partner_id', found in the inherits of the current table - # i.e.: one2many: 'id', not a field - # i.e.: many2one: 'state_id', the current field name - # - dst_table_link_name: field name used to link the dst table, not - # necessarily a field (because 'id' is not a field instance) - # i.e.: inherits: 'id', not a field - # i.e.: one2many: 'partner_id', _fields_id of the current field - # i.e.: many2one: 'id', not a field - # - context_field_name: field name used as a context to make the alias - # i.e.: inherits: 'partner_id': found in the inherits of the current table - # i.e.: one2many: 'bank_ids': current field name - # i.e.: many2one: 'state_id': current field name - # -------------------------------------------------- - - def __init__(self, leaf, model, join_context=None, internal=False): - """ Initialize the ExtendedLeaf - - :attr [string, tuple] leaf: operator or tuple-formatted domain - expression - :attr obj model: current working model - :attr list _models: list of chained models, updated when - adding joins - :attr list join_context: list of join contexts. This is a list of - tuples like ``(lhs, table, lhs_col, col, link)`` - - where - - lhs - source (left hand) model - model - destination (right hand) model - lhs_col - source model column for join condition - col - destination model column for join condition - link - link column between source and destination model - that is not necessarily (but generally) a real column used - in the condition (i.e. in many2one); this link is used to - compute aliases - """ - assert isinstance(model, BaseModel), 'Invalid leaf creation without table' - self.join_context = join_context or [] - self.leaf = leaf - # normalize the leaf's operator - self.normalize_leaf() - # set working variables; handle the context stack and previous tables - self.model = model - self._models = [] - for item in self.join_context: - self._models.append(item[0]) - self._models.append(model) - # check validity - self.check_leaf(internal) - - def __str__(self): - return '' % (str(self.leaf), self.model._table, ','.join(self._get_context_debug())) - - def generate_alias(self): - links = [(context[1]._table, context[4]) for context in self.join_context] - alias, alias_statement = generate_table_alias(self._models[0]._table, links) - return alias - - def add_join_context(self, model, lhs_col, table_col, link): - """ See above comments for more details. A join context is a tuple like: - ``(lhs, model, lhs_col, col, link)`` - - After adding the join, the model of the current leaf is updated. - """ - self.join_context.append((self.model, model, lhs_col, table_col, link)) - self._models.append(model) - self.model = model - - def get_join_conditions(self): - conditions = [] - alias = self._models[0]._table - for context in self.join_context: - previous_alias = alias - alias += '__' + context[4] - conditions.append('"%s"."%s"="%s"."%s"' % (previous_alias, context[2], alias, context[3])) - return conditions - - def get_tables(self): - tables = [] - links = [] - for context in self.join_context: - links.append((context[1]._table, context[4])) - alias, alias_statement = generate_table_alias(self._models[0]._table, links) - if alias_statement not in tables: - tables.append(alias_statement) - return tables - - def _get_context_debug(self): - names = ['"%s"."%s"="%s"."%s" (%s)' % (item[0]._table, item[2], item[1]._table, item[3], item[4]) for item in self.join_context] - return names - - # -------------------------------------------------- - # Leaf manipulation - # -------------------------------------------------- - - def check_leaf(self, internal=False): - """ Leaf validity rules: - - a valid leaf is an operator or a leaf - - a valid leaf has a field objects unless - - it is not a tuple - - it is an inherited field - - left is id, operator is 'child_of' - - left is in MAGIC_COLUMNS - """ - if not is_operator(self.leaf) and not is_leaf(self.leaf, internal): - raise ValueError("Invalid leaf %s" % str(self.leaf)) - - def is_operator(self): - return is_operator(self.leaf) - - def is_true_leaf(self): - return self.leaf == TRUE_LEAF - - def is_false_leaf(self): - return self.leaf == FALSE_LEAF - - def is_leaf(self, internal=False): - return is_leaf(self.leaf, internal=internal) - - def normalize_leaf(self): - self.leaf = normalize_leaf(self.leaf) - return True - -def create_substitution_leaf(leaf, new_elements, new_model=None, internal=False): - """ From a leaf, create a new leaf (based on the new_elements tuple - and new_model), that will have the same join context. Used to - insert equivalent leafs in the processing stack. """ - if new_model is None: - new_model = leaf.model - new_join_context = [tuple(context) for context in leaf.join_context] - new_leaf = ExtendedLeaf(new_elements, new_model, join_context=new_join_context, internal=internal) - return new_leaf - class expression(object): """ Parse a domain expression @@ -650,24 +482,23 @@ class expression(object): """ Initialize expression object and automatically parse the expression right after initialization. - :param domain: expression (using domain ('foo', '=', 'bar' format)) + :param domain: expression (using domain ('foo', '=', 'bar') format) :param model: root model - :attr list result: list that will hold the result of the parsing - as a list of ExtendedLeaf - :attr list joins: list of join conditions, such as - (res_country_state."id" = res_partner."state_id") :attr root_model: base model for the query - :attr list expression: the domain expression, that will be normalized - and prepared + :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.joins = [] self.root_model = model # 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]) + # parse the domain expression self.parse() @@ -676,13 +507,9 @@ class expression(object): # ---------------------------------------- def get_tables(self): - """ Returns the list of tables for SQL queries, like select from ... """ - tables = [_quote(self.root_model._table)] - for leaf in self.result: - for table in leaf.get_tables(): - if table not in tables: - tables.append(table) - return tables + warnings.warn("deprecated expression.get_tables(), use expression.query instead", + DeprecationWarning) + return self.query.tables # ---------------------------------------- # Parsing @@ -694,11 +521,26 @@ class expression(object): 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, ...). - Two things can happen as a processing result: + 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 added to the result + 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 @@ -791,49 +633,62 @@ class expression(object): def pop(): """ Pop a leaf to process. """ - return self.stack.pop() + return stack.pop() - def push(leaf): + def push(leaf, model, alias, internal=False): """ Push a leaf to be processed right after. """ - self.stack.append(leaf) + leaf = normalize_leaf(leaf) + check_leaf(leaf, internal) + stack.append((leaf, model, alias)) - def push_result(leaf): - """ Push a leaf to the results. This leaf has been fully processed - and validated. """ - self.result.append(leaf) + def pop_result(): + return result_stack.pop() - self.result = [] - self.stack = [ExtendedLeaf(leaf, self.root_model) for leaf in self.expression] - # process from right to left; expression is from left to right - self.stack.reverse() + def push_result(query, params): + result_stack.append((query, params)) - while self.stack: + # 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_model._table) + + # stack of SQL expressions in the form: (expr, params) + result_stack = [] + + while stack: # Get the next leaf to process - leaf = pop() - - # Get working variables - if leaf.is_operator(): - left, operator, right = leaf.leaf, None, None - elif leaf.is_true_leaf() or leaf.is_false_leaf(): - # because we consider left as a string - left, operator, right = ('%s' % leaf.leaf[0], leaf.leaf[1], leaf.leaf[2]) - else: - left, operator, right = leaf.leaf - path = left.split('.', 1) - - model = leaf.model - field = model._fields.get(path[0]) - comodel = model.env.get(getattr(field, 'comodel_name', None)) + leaf, model, alias = pop() # ---------------------------------------- # SIMPLE CASE # 1. leaf is an operator # 2. leaf is a true/false leaf - # -> add directly to result + # -> convert and add directly to result # ---------------------------------------- - if leaf.is_operator() or leaf.is_true_leaf() or leaf.is_false_leaf(): - push_result(leaf) + 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 @@ -846,24 +701,22 @@ class expression(object): # -> else: crash # ---------------------------------------- - elif not field: - raise ValueError("Invalid field %r in leaf %r" % (left, str(leaf))) + if not field: + raise ValueError("Invalid field %s.%s in leaf %s" % (model._name, path[0], str(leaf))) elif field.inherited: - # comments about inherits'd fields - # { 'field_name': ('parent_model', 'm2o_field_to_reach_parent', - # field_column_obj, origina_parent_model), ... } parent_model = model.env[field.related_field.model_name] parent_fname = model._inherits[parent_model._name] - leaf.add_join_context(parent_model, parent_fname, 'id', parent_fname) - push(leaf) + parent_alias, _ = self.query.add_join( + (alias, parent_model._table, parent_fname, 'id', parent_fname), + ) + push(leaf, parent_model, parent_alias) elif left == 'id' and operator in HIERARCHY_FUNCS: - ids2 = to_ids(right, model, leaf.leaf) + ids2 = to_ids(right, model, leaf) dom = HIERARCHY_FUNCS[operator](left, ids2, model) - for dom_leaf in reversed(dom): - new_leaf = create_substitution_leaf(leaf, dom_leaf, model) - push(new_leaf) + for dom_leaf in dom: + push(dom_leaf, model, alias) # ---------------------------------------- # PATH SPOTTED @@ -880,33 +733,34 @@ class expression(object): elif len(path) > 1 and field.store and field.type == 'many2one' and field.auto_join: # res_partner.state_id = res_partner__state_id.id - leaf.add_join_context(comodel, path[0], 'id', path[0]) - push(create_substitution_leaf(leaf, (path[1], operator, right), comodel)) + coalias, _ = self.query.add_join( + (alias, comodel._table, path[0], 'id', path[0]), + ) + push((path[1], operator, right), comodel, coalias) elif len(path) > 1 and field.store and field.type == 'one2many' and field.auto_join: # res_partner.id = res_partner__bank_ids.partner_id - leaf.add_join_context(comodel, 'id', field.inverse_name, path[0]) + coalias, _ = self.query.add_join( + (alias, comodel._table, 'id', field.inverse_name, path[0]), + ) domain = field.get_domain_list(model) - push(create_substitution_leaf(leaf, (path[1], operator, right), comodel)) if domain: - domain = normalize_domain(domain) - for elem in reversed(domain): - push(create_substitution_leaf(leaf, elem, comodel)) - push(create_substitution_leaf(leaf, AND_OPERATOR, comodel)) + push(AND_OPERATOR, comodel, coalias) + for elem in normalize_domain(domain): + push(elem, comodel, coalias) + push((path[1], operator, right), comodel, coalias) 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 - leaf.leaf = (path[0], 'in', right_ids) - push(leaf) + 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 - leaf.leaf = (path[0], 'in', right_ids) - push(leaf) + push((path[0], 'in', right_ids), model, alias) elif not field.store: # Non-stored field should provide an implementation of search. @@ -924,12 +778,8 @@ class expression(object): operator = 'in' domain = field.determine_domain(model, operator, right) - if not domain: - leaf.leaf = TRUE_LEAF - push(leaf) - else: - for elem in reversed(domain): - push(create_substitution_leaf(leaf, elem, model, internal=True)) + for elem in normalize_domain(domain): + push(elem, model, alias, internal=True) # ------------------------------------------------- # RELATIONAL FIELDS @@ -937,13 +787,13 @@ class expression(object): # Applying recursivity on field(one2many) elif field.type == 'one2many' and operator in HIERARCHY_FUNCS: - ids2 = to_ids(right, comodel, leaf.leaf) + 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 reversed(dom): - push(create_substitution_leaf(leaf, dom_leaf, model)) + for dom_leaf in dom: + push(dom_leaf, model, alias) elif field.type == 'one2many': domain = field.get_domain_list(model) @@ -974,14 +824,14 @@ class expression(object): # rewrite condition in terms of ids1 op1 = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - push(create_substitution_leaf(leaf, ('id', op1, ids1), model)) + 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(create_substitution_leaf(leaf, ('id', op1, (subquery, [])), internal=True)) + push(('id', op1, (subquery, [])), model, alias, internal=True) else: comodel_domain = [(field.inverse_name, '!=', False)] if inverse_is_int and domain: @@ -991,23 +841,23 @@ class expression(object): 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(create_substitution_leaf(leaf, ('id', op1, ids1), model)) + 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.leaf) + 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(create_substitution_leaf(leaf, ('id', 'in', ids2), model)) + push(('id', 'in', ids2), model, alias) else: subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2) - push(create_substitution_leaf(leaf, ('id', 'inselect', (subquery, [tuple(ids2) or (None,)])), internal=True)) + push(('id', 'inselect', (subquery, [tuple(ids2) or (None,)])), model, alias, internal=True) elif right is not False: # determine ids2 in comodel @@ -1025,23 +875,23 @@ class expression(object): 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(create_substitution_leaf(leaf, ('id', subop, (subquery, [ids2])), internal=True)) + 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(create_substitution_leaf(leaf, ('id', op1, (subquery, [])), internal=True)) + push(('id', op1, (subquery, [])), model, alias, internal=True) elif field.type == 'many2one': if operator in HIERARCHY_FUNCS: - ids2 = to_ids(right, comodel, leaf.leaf) + 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 reversed(dom): - push(create_substitution_leaf(leaf, dom_leaf, model)) + for dom_leaf in dom: + push(dom_leaf, model, alias) else: def _get_expression(comodel, left, right, operator): #Special treatment to ill-formed domains @@ -1061,10 +911,11 @@ class expression(object): # 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(create_substitution_leaf(leaf, _get_expression(comodel, left, right, operator), model)) + push(_get_expression(comodel, left, right, operator), model, alias) else: # right == [] or right == False and all other cases are handled by __leaf_to_sql() - push_result(leaf) + expr, params = self.__leaf_to_sql(leaf, model, alias) + push_result(expr, params) # ------------------------------------------------- # BINARY FIELDS STORED IN ATTACHMENT @@ -1076,12 +927,11 @@ class expression(object): 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(create_substitution_leaf(leaf, ('id', inselect_operator, (subselect, params)), model, internal=True)) + 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) - leaf.leaf = TRUE_LEAF - push(leaf) + push(TRUE_LEAF, model, alias) # ------------------------------------------------- # OTHER FIELDS @@ -1097,16 +947,16 @@ class expression(object): right += ' 23:59:59' else: right += ' 00:00:00' - push(create_substitution_leaf(leaf, (left, operator, right), model)) + 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(create_substitution_leaf(leaf, (left, operator, right), model)) + push((left, operator, right), model, alias) else: - push_result(leaf) - + 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') @@ -1146,25 +996,23 @@ class expression(object): 'model', right, ) - push(create_substitution_leaf(leaf, ('id', inselect_operator, (subselect, params)), model, internal=True)) + push(('id', inselect_operator, (subselect, params)), model, alias, internal=True) else: - push_result(leaf) + expr, params = self.__leaf_to_sql(leaf, model, alias) + push_result(expr, params) # ---------------------------------------- # END OF PARSING FULL DOMAIN - # -> generate joins + # -> put result in self.result and self.query # ---------------------------------------- - self.joins = list(tools.unique( - join - for leaf in self.result - for join in leaf.get_join_conditions() - )) + [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, eleaf): - model = eleaf.model - leaf = eleaf.leaf + def __leaf_to_sql(self, leaf, model, alias): left, operator, right = leaf # final sanity checks - should never fail @@ -1175,7 +1023,7 @@ class expression(object): assert not isinstance(right, BaseModel), \ "Invalid value %r in domain term %r" % (right, leaf) - table_alias = '"%s"' % (eleaf.generate_alias()) + table_alias = '"%s"' % alias if leaf == TRUE_LEAF: query = 'TRUE' @@ -1187,11 +1035,11 @@ class expression(object): elif operator == 'inselect': query = '(%s."%s" in (%s))' % (table_alias, left, right[0]) - params = right[1] + params = list(right[1]) elif operator == 'not inselect': query = '(%s."%s" not in (%s))' % (table_alias, left, right[0]) - params = right[1] + params = list(right[1]) elif operator in ['in', 'not in']: # Two cases: right is a boolean or a list. The boolean case is an @@ -1247,8 +1095,7 @@ class expression(object): params = [] else: # '=?' behaves like '=' in other cases - query, params = self.__leaf_to_sql( - create_substitution_leaf(eleaf, (left, '=', right), model)) + query, params = self.__leaf_to_sql((left, '=', right), model, alias) else: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') @@ -1274,28 +1121,6 @@ class expression(object): return query, params def to_sql(self): - stack = [] # stack of query strings - params = [] # query parameters, in reverse order - - # Process the domain from right to left, using a stack, to generate a SQL expression. - for leaf in reversed(self.result): - if leaf.is_leaf(internal=True): - q, ps = self.__leaf_to_sql(leaf) - stack.append(q) - params.extend(reversed(ps)) - elif leaf.leaf == NOT_OPERATOR: - stack.append('(NOT (%s))' % (stack.pop(),)) - else: - ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '} - q1 = stack.pop() - q2 = stack.pop() - stack.append('(%s %s %s)' % (q1, ops[leaf.leaf], q2,)) - - assert len(stack) == 1 - query = stack[0] - joins = ' AND '.join(self.joins) - if joins: - query = '(%s) AND %s' % (joins, query) - - params.reverse() - return query, params + warnings.warn("deprecated expression.to_sql(), use expression.query instead", + DeprecationWarning) + return self.result