[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.
This commit is contained in:
Raphael Collet
2020-05-20 07:56:44 +00:00
parent 0fa101a8bf
commit 84a595301e
6 changed files with 177 additions and 358 deletions
+5 -6
View File
@@ -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],
@@ -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],
+2 -1
View File
@@ -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 (
+18 -18
View File
@@ -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"
+2 -7
View File
@@ -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):
+145 -320
View File
@@ -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:
# 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