[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:
@@ -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],
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user