summaryrefslogtreecommitdiff
path: root/odoo
diff options
context:
space:
mode:
authorRaphael Collet <rco@odoo.com>2020-05-05 12:56:07 +0000
committerRaphael Collet <rco@odoo.com>2020-05-20 07:56:44 +0000
commit84a595301edeb558f93ce5509efdcd8e30fd2a64 (patch)
tree92b20abade241617d6aa51737958c9489426fd84 /odoo
parent0fa101a8bf13671162bda91deb98bd0c7ec76f3f (diff)
[REF] expression: use and return a Query object
The management of the join contexts now relies on the `Query` object, that has all the necessary logic to do so. The refactoring also removes the class `ExtendedLeaf`, which is no longer necessary: the join contexts are all managed by a single `Query` object, and the parsing stack now only contains triples like (leaf, model, table_alias). Also inline directly the SQL translation made in `_to_sql`, which overall simplifies the management of SQL parameters in the domain compilation. The algorithm is illustrated in the code itself.
Diffstat (limited to 'odoo')
-rw-r--r--odoo/addons/base/models/ir_ui_view.py3
-rw-r--r--odoo/addons/base/tests/test_expression.py36
-rw-r--r--odoo/models.py9
-rw-r--r--odoo/osv/expression.py463
4 files changed, 166 insertions, 345 deletions
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,16 +1124,16 @@ 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"
WHERE (("res_partner"."company_id" IN (%s)) OR ("res_partner"."country_id" IN (%s)))
@@ -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 '<osv.ExtendedLeaf: %s on %s (ctx: %s)>' % (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:
- # Get the next leaf to process
- leaf = pop()
+ # 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)
- # 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)
+ # stack of SQL expressions in the form: (expr, params)
+ result_stack = []
- model = leaf.model
- field = model._fields.get(path[0])
- comodel = model.env.get(getattr(field, 'comodel_name', None))
+ 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
- # -> 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