[REF] core: improve the API and implementation of Query

Introduce better methods for introducing tables, join clauses and where
clauses to the Query object, and other methods to generate a complete
SELECT query.  The Query object can also behave as a tuple of ids, where
the query is made on demand, and the result is memoized.
This commit is contained in:
Raphael Collet
2020-08-18 13:02:41 +00:00
parent 6f87bbbfc8
commit 0f7f163d2e
5 changed files with 280 additions and 281 deletions
+34 -2
View File
@@ -1014,6 +1014,37 @@ class TestQueries(TransactionCase):
''']):
Model.search(domain)
def test_order(self):
Model = self.env['res.partner']
Model.search([('name', 'like', 'foo')])
with self.assertQueries(['''
SELECT "res_partner".id
FROM "res_partner"
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name"::text LIKE %s))
ORDER BY "res_partner"."display_name"
''']):
Model.search([('name', 'like', 'foo')])
with self.assertQueries(['''
SELECT "res_partner".id
FROM "res_partner"
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name"::text LIKE %s))
ORDER BY "res_partner"."id"
''']):
Model.search([('name', 'like', 'foo')], order='id')
def test_count(self):
Model = self.env['res.partner']
Model.search([('name', 'like', 'foo')])
with self.assertQueries(['''
SELECT count(1)
FROM "res_partner"
WHERE (("res_partner"."active" = %s) AND ("res_partner"."name"::text LIKE %s))
''']):
Model.search_count([('name', 'like', 'foo')])
def test_translated_field(self):
self.env['res.lang']._activate_lang('fr_FR')
Model = self.env['res.partner.title'].with_context(lang='fr_FR')
@@ -1057,10 +1088,11 @@ class TestQueries(TransactionCase):
with self.assertQueries(['''
SELECT "res_users".id
FROM "res_users", "res_partner" AS "res_users__partner_id"
FROM "res_users"
LEFT JOIN "res_partner" AS "res_users__partner_id" ON
("res_users"."partner_id" = "res_users__partner_id"."id")
WHERE ("res_users"."active" = %s)
AND ("res_users"."id" = %s)
AND ("res_users"."partner_id" = "res_users__partner_id"."id")
AND ("res_users__partner_id"."id" = %s)
ORDER BY "res_users__partner_id"."name", "res_users"."login"
''']):
+63 -29
View File
@@ -8,41 +8,75 @@ from odoo.tests.common import BaseCase
class QueryTestCase(BaseCase):
def test_basic_query(self):
query = Query()
query.add_table('product_product')
query = Query(None, 'product_product')
query.add_table('product_template')
query.where_clause.append("product_product.template_id = product_template.id")
query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join
query.add_join(("product_product", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # outer join
self.assertEqual(query.get_sql()[0].strip(),
""""product_product" LEFT JOIN "res_user" as "product_product__user_id" ON ("product_product"."user_id" = "product_product__user_id"."id"),"product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") """.strip())
self.assertEqual(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
query.add_where("product_product.template_id = product_template.id")
# add inner join
alias = query.join("product_template", "categ_id", "product_category", "id", "categ_id")
self.assertEqual(alias, 'product_template__categ_id')
# add left join
alias = query.left_join("product_product", "user_id", "res_user", "id", "user_id")
self.assertEqual(alias, 'product_product__user_id')
from_clause, where_clause, where_params = query.get_sql()
self.assertEqual(from_clause,
'"product_product", "product_template" JOIN "product_category" AS "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" AS "product_product__user_id" ON ("product_product"."user_id" = "product_product__user_id"."id")')
self.assertEqual(where_clause, "product_product.template_id = product_template.id")
def test_query_chained_explicit_joins(self):
query = Query()
query.add_table('product_product')
query = Query(None, 'product_product')
query.add_table('product_template')
query.where_clause.append("product_product.template_id = product_template.id")
query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join
query.add_join(("product_template__categ_id", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # CHAINED outer join
self.assertEqual(query.get_sql()[0].strip(),
""""product_product","product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" as "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")""".strip())
self.assertEqual(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
query.add_where("product_product.template_id = product_template.id")
# add inner join
alias = query.join("product_template", "categ_id", "product_category", "id", "categ_id")
self.assertEqual(alias, 'product_template__categ_id')
# add CHAINED left join
alias = query.left_join("product_template__categ_id", "user_id", "res_user", "id", "user_id")
self.assertEqual(alias, 'product_template__categ_id__user_id')
from_clause, where_clause, where_params = query.get_sql()
self.assertEqual(from_clause,
'"product_product", "product_template" JOIN "product_category" AS "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" AS "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")')
self.assertEqual(where_clause, "product_product.template_id = product_template.id")
def test_mixed_query_chained_explicit_implicit_joins(self):
query = Query()
query.add_table('product_product')
query = Query(None, 'product_product')
query.add_table('product_template')
query.where_clause.append("product_product.template_id = product_template.id")
query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join
query.add_join(("product_template__categ_id", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # CHAINED outer join
query.add_table('account_account')
query.where_clause.append("product_category.expense_account_id = account_account.id") # additional implicit join
self.assertEqual(query.get_sql()[0].strip(),
""""product_product","product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" as "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id"),"account_account" """.strip())
self.assertEqual(query.get_sql()[1].strip(), """product_product.template_id = product_template.id AND product_category.expense_account_id = account_account.id""".strip())
query.add_where("product_product.template_id = product_template.id")
# add inner join
alias = query.join("product_template", "categ_id", "product_category", "id", "categ_id")
self.assertEqual(alias, 'product_template__categ_id')
# add CHAINED left join
alias = query.left_join("product_template__categ_id", "user_id", "res_user", "id", "user_id")
self.assertEqual(alias, 'product_template__categ_id__user_id')
# additional implicit join
query.add_table('account.account')
query.add_where("product_category.expense_account_id = account_account.id")
from_clause, where_clause, where_params = query.get_sql()
self.assertEqual(from_clause,
'"product_product", "product_template", "account.account" JOIN "product_category" AS "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" AS "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")')
self.assertEqual(where_clause, "product_product.template_id = product_template.id AND product_category.expense_account_id = account_account.id")
def test_raise_missing_lhs(self):
query = Query()
query.add_table('product_product')
self.assertRaises(AssertionError, query.add_join, ("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False)
query = Query(None, 'product_product')
with self.assertRaises(AssertionError):
query.join("product_template", "categ_id", "product_category", "id", "categ_id")
def test_long_aliases(self):
query = Query(None, 'product_product')
tmp = query.join('product_product', 'product_tmpl_id', 'product_template', 'id', 'product_tmpl_id')
self.assertEqual(tmp, 'product_product__product_tmpl_id')
# no hashing
tmp_cat = query.join(tmp, 'product_category_id', 'product_category', 'id', 'product_category_id')
self.assertEqual(tmp_cat, 'product_product__product_tmpl_id__product_category_id')
# hashing to limit identifier length
tmp_cat_cmp = query.join(tmp_cat, 'company_id', 'res_company', 'id', 'company_id')
self.assertEqual(tmp_cat_cmp, 'product_product__product_tmpl_id__product_category_id__9f0ddff7')
tmp_cat_stm = query.join(tmp_cat, 'salesteam_id', 'res_company', 'id', 'salesteam_id')
self.assertEqual(tmp_cat_stm, 'product_product__product_tmpl_id__product_category_id__953a466f')
# extend hashed identifiers
tmp_cat_cmp_par = query.join(tmp_cat_cmp, 'partner_id', 'res_partner', 'id', 'partner_id')
self.assertEqual(tmp_cat_cmp_par, 'product_product__product_tmpl_id__product_category_id__56d55687')
tmp_cat_stm_par = query.join(tmp_cat_stm, 'partner_id', 'res_partner', 'id', 'partner_id')
self.assertEqual(tmp_cat_stm_par, 'product_product__product_tmpl_id__product_category_id__9_363fdd')
+22 -34
View File
@@ -2362,11 +2362,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
"""
inherits_field = current_model._inherits[parent_model_name]
parent_model = self.env[parent_model_name]
parent_alias, parent_alias_statement = query.add_join((current_model._table, parent_model._table, inherits_field, 'id', inherits_field), implicit=True)
parent_alias = query.left_join(
current_model._table, inherits_field, parent_model._table, 'id', inherits_field,
)
return parent_alias
@api.model
def _inherits_join_calc(self, alias, fname, query, implicit=True, outer=False):
def _inherits_join_calc(self, alias, fname, query):
"""
Adds missing table select and join clause(s) to ``query`` for reaching
the field coming from an '_inherits' parent table (no duplicates).
@@ -2383,9 +2385,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
parent_model = self.env[field.related_field.model_name]
parent_fname = field.related[0]
# JOIN parent_model._table AS parent_alias ON alias.parent_fname = parent_alias.id
parent_alias, _ = query.add_join(
(alias, parent_model._table, parent_fname, 'id', parent_fname),
implicit=implicit, outer=outer,
parent_alias = query.left_join(
alias, parent_fname, parent_model._table, 'id', parent_fname,
)
model, alias, field = parent_model, parent_alias, field.related_field
# handle the case where the field is translated
@@ -3057,8 +3058,7 @@ Fields:
cr, user, context, su = env.args
# make a query object for selecting ids, and apply security rules to it
param_ids = object()
query = Query(['"%s"' % self._table], ['"%s".id IN %%s' % self._table], [param_ids])
query = Query(self.env.cr, self._table)
self._apply_ir_rules(query, 'read')
# the query may involve several tables: we need fully-qualified names
@@ -3073,17 +3073,13 @@ Fields:
# selected fields are: 'id' followed by fields_pre
qual_names = [qualify(name) for name in [self._fields['id']] + fields_pre]
# determine the actual query to execute
from_clause, where_clause, params = query.get_sql()
query_str = "SELECT %s FROM %s WHERE %s" % (",".join(qual_names), from_clause, where_clause)
# fetch one list of record values per field
param_pos = params.index(param_ids)
# determine the actual query to execute (last parameter is added below)
query.add_where('"%s".id IN %%s' % self._table)
query_str, params = query.select(*qual_names)
result = []
for sub_ids in cr.split_for_in_conditions(self.ids):
params[param_pos] = tuple(sub_ids)
cr.execute(query_str, params)
cr.execute(query_str, params + [sub_ids])
result += cr.fetchall()
else:
self.check_access_rule('read')
@@ -3325,21 +3321,18 @@ Fields:
if not self._ids:
return self
quoted_table = '"%s"' % self._table
query = Query([quoted_table])
query = Query(self._cr, self._table)
self._apply_ir_rules(query, operation)
if not query.where_clause:
return self
# detemine ids in database that satisfy ir.rules
valid_ids = set()
from_clause, where_clause, where_params = query.get_sql()
query_str = "SELECT {}.id FROM {} WHERE {} AND {}.id IN %s".format(
quoted_table, from_clause, where_clause, quoted_table,
)
query.add_where(f'"{self._table}".id IN %s')
query_str, params = query.select()
self._flush_search([])
for sub_ids in self._cr.split_for_in_conditions(self.ids):
self._cr.execute(query_str, where_params + [sub_ids])
self._cr.execute(query_str, params + [sub_ids])
valid_ids.update(row[0] for row in self._cr.fetchall())
# return new ids without origin and ids with origin in valid_ids
@@ -4160,7 +4153,7 @@ Fields:
if domain:
return expression.expression(domain, self).query
else:
return Query(['"%s"' % self._table])
return Query(self.env.cr, self._table)
def _check_qorder(self, word):
if not regex_order.match(word):
@@ -4203,10 +4196,8 @@ Fields:
:return: the qualified field name (or expression) to use for ``field``
"""
if self.env.lang:
alias, alias_statement = query.add_join(
(table_alias, 'ir_translation', 'id', 'res_id', field),
implicit=False,
outer=True,
alias = query.left_join(
table_alias, 'id', 'ir_translation', 'res_id', field,
extra='"{rhs}"."type" = \'model\' AND "{rhs}"."name" = %s AND "{rhs}"."lang" = %s AND "{rhs}"."value" != %s',
extra_params=["%s,%s" % (self._name, field), self.env.lang, ""],
)
@@ -4246,8 +4237,7 @@ Fields:
# Join the dest m2o table if it's not joined yet. We use [LEFT] OUTER join here
# as we don't want to exclude results that have NULL values for the m2o
join = (alias, dest_model._table, order_field, 'id', order_field)
dest_alias, _ = query.add_join(join, implicit=False, outer=True)
dest_alias = query.left_join(alias, order_field, dest_model._table, 'id', order_field)
return dest_model._generate_order_by_inner(dest_alias, m2o_order, query,
reverse_direction, seen)
@@ -4281,7 +4271,7 @@ Fields:
seen.add(key)
order_by_elements += self._generate_m2o_order_by(alias, order_field, query, do_reverse, seen)
elif field.store and field.column_type:
qualifield_name = self._inherits_join_calc(alias, order_field, query, implicit=False, outer=True)
qualifield_name = self._inherits_join_calc(alias, order_field, query)
if field.type == 'boolean':
qualifield_name = "COALESCE(%s, false)" % qualifield_name
order_by_elements.append("%s %s" % (qualifield_name, order_direction))
@@ -4402,10 +4392,8 @@ Fields:
if count:
# Ignore order, limit and offset when just counting, they don't make sense and could
# hurt performance
from_clause, where_clause, where_clause_params = query.get_sql()
where_str = where_clause and (" WHERE %s" % where_clause) or ''
query_str = 'SELECT count(1) FROM ' + from_clause + where_str
self._cr.execute(query_str, where_clause_params)
query_str, params = query.select("count(1)")
self._cr.execute(query_str, params)
res = self._cr.fetchone()
return res[0]
+7 -63
View File
@@ -119,7 +119,6 @@ import warnings
import logging
import traceback
from functools import partial
from zlib import crc32
from datetime import date, datetime, time
import odoo.modules
@@ -339,55 +338,6 @@ def _quote(to_quote):
return to_quote
def generate_table_alias(src_table_alias, joined_tables=[]):
""" Generate a standard table alias name. An alias is generated as following:
- the base is the source table name (that can already be an alias)
- then, each joined table is added in the alias using a 'link field name'
that is used to render unique aliases for a given path
- returns a tuple composed of the alias, and the full table alias to be
added in a from condition with quoting done
Examples:
- src_table_alias='res_users', join_tables=[]:
alias = ('res_users','"res_users"')
- src_model='res_users', join_tables=[(res.partner, 'parent_id')]
alias = ('res_users__parent_id', '"res_partner" as "res_users__parent_id"')
:param model src_table_alias: model source of the alias
:param list joined_tables: list of tuples
(dst_model, link_field)
:return tuple: (table_alias, alias statement for from clause with quotes added)
"""
alias = src_table_alias
if not joined_tables:
return '%s' % alias, '%s' % _quote(alias)
for link in joined_tables:
alias += '__' + link[1]
# Use an alternate alias scheme if length exceeds the PostgreSQL limit
# of 63 characters.
if len(alias) >= 64:
# We have to fit a crc32 hash and one underscore
# into a 63 character alias. The remaining space we can use to add
# a human readable prefix.
alias_hash = hex(crc32(alias.encode('utf-8')))[2:]
ALIAS_PREFIX_LENGTH = 63 - len(alias_hash) - 1
alias = "%s_%s" % (
alias[:ALIAS_PREFIX_LENGTH], alias_hash)
return '%s' % alias, '%s as %s' % (_quote(joined_tables[-1][0]), _quote(alias))
def get_alias_from_query(from_query):
""" :param string from_query: is something like :
- '"res_partner"' OR
- '"res_partner" as "res_users__partner_id"''
"""
from_splitted = from_query.split(' as ')
if len(from_splitted) > 1:
return from_splitted[0].replace('"', ''), from_splitted[1].replace('"', '')
else:
return from_splitted[0].replace('"', ''), from_splitted[0].replace('"', '')
def normalize_leaf(element):
""" Change a term's operator to some canonical form, simplifying later
processing. """
@@ -482,7 +432,7 @@ class expression(object):
self.expression = distribute_not(normalize_domain(domain))
# this object handles all the joins
self.query = Query(['"%s"' % model._table]) if query is None else query
self.query = Query(model.env.cr, model._table) if query is None else query
# parse the domain expression
self.parse()
@@ -692,9 +642,8 @@ class expression(object):
elif field.inherited:
parent_model = model.env[field.related_field.model_name]
parent_fname = model._inherits[parent_model._name]
parent_alias, _ = self.query.add_join(
(alias, parent_model._table, parent_fname, 'id', parent_fname),
implicit=False, outer=True,
parent_alias = self.query.left_join(
alias, parent_fname, parent_model._table, 'id', parent_fname,
)
push(leaf, parent_model, parent_alias)
@@ -719,9 +668,8 @@ 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
coalias, _ = self.query.add_join(
(alias, comodel._table, path[0], 'id', path[0]),
implicit=False, outer=True,
coalias = self.query.left_join(
alias, path[0], comodel._table, 'id', path[0],
)
push((path[1], operator, right), comodel, coalias)
@@ -729,10 +677,7 @@ class expression(object):
# use a subquery bypassing access rules and business logic
domain = [(path[1], operator, right)] + field.get_domain_list(model)
query = comodel.with_context(**field.context)._where_calc(domain)
subfrom, subwhere, subparams = query.get_sql()
subquery = 'SELECT "{}"."{}" FROM {} WHERE {}'.format(
comodel._table, field.inverse_name, subfrom, subwhere,
)
subquery, subparams = query.select('"%s"."%s"' % (comodel._table, field.inverse_name))
push(('id', 'inselect', (subquery, subparams)), model, alias, internal=True)
elif len(path) > 1 and field.store and field.auto_join:
@@ -968,8 +913,7 @@ class expression(object):
[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)
self.query.add_where(where_clause, where_params)
def __leaf_to_sql(self, leaf, model, alias):
left, operator, right = leaf
+154 -153
View File
@@ -2,6 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import warnings
from zlib import crc32
from odoo.tools import lazy_property
def _quote(to_quote):
@@ -10,177 +13,175 @@ def _quote(to_quote):
return to_quote
def _generate_table_alias(src_table_alias, link):
""" Generate a standard table alias name. An alias is generated as following:
- the base is the source table name (that can already be an alias)
- then, the joined table is added in the alias using a 'link field name'
that is used to render unique aliases for a given path
- the name is shortcut if it goes beyond PostgreSQL's identifier limits
Examples:
- src_table_alias='res_users', link='parent_id'
alias = 'res_users__parent_id'
:param str src_table_alias: alias of the source table
:param str link: field name
:return str: alias
"""
alias = "%s__%s" % (src_table_alias, link)
# Use an alternate alias scheme if length exceeds the PostgreSQL limit
# of 63 characters.
if len(alias) >= 64:
# We have to fit a crc32 hash and one underscore into a 63 character
# alias. The remaining space we can use to add a human readable prefix.
alias_hash = hex(crc32(alias.encode('utf-8')))[2:]
ALIAS_PREFIX_LENGTH = 63 - len(alias_hash) - 1
alias = "%s_%s" % (alias[:ALIAS_PREFIX_LENGTH], alias_hash)
return alias
class Query(object):
"""
Dumb implementation of a Query object, using 3 string lists so far
for backwards compatibility with the (table, where_clause, where_params) previously used.
TODO: To be improved after v6.0 to rewrite part of the ORM and add support for:
- auto-generated multiple table aliases
- multiple joins to the same table with different conditions
- dynamic right-hand-side values in domains (e.g. a.name = a.description)
- etc.
""" Simple implementation of a query object, managing tables with aliases,
join clauses (with aliases, condition and parameters), where clauses (with
parameters), order, limit and offset.
"""
def __init__(self, tables=None, where_clause=None, where_clause_params=None, joins=None, extras=None):
def __init__(self, cr, table, where_clause=None, where_params=()):
# database cursor
self._cr = cr
# holds the list of tables joined using default JOIN.
# the table names are stored double-quoted (backwards compatibility)
self._tables = tables or []
# tables {alias: table}
self._tables = {table: table}
# joins {alias: (kind, table, condition, condition_params)}
self._joins = {}
# holds the list of WHERE clause elements, to be joined with
# 'AND' when generating the final query
self.where_clause = where_clause or []
self._where_clauses = [where_clause] if where_clause else []
# holds the parameters for the formatting of `where_clause`, to be
# passed to psycopg's execute method.
self.where_clause_params = where_clause_params or []
self._where_params = list(where_params)
# holds table joins done explicitly, supporting outer joins. The JOIN
# condition should not be in `where_clause`. The dict is used as follows:
# self.joins = {
# 'table_a': [
# ('table_b', 'table_a_col1', 'table_b_col', 'LEFT JOIN'),
# ('table_c', 'table_a_col2', 'table_c_col', 'LEFT JOIN'),
# ('table_d', 'table_a_col3', 'table_d_col', 'JOIN'),
# ]
# }
# which should lead to the following SQL:
# SELECT ... FROM "table_a" LEFT JOIN "table_b" ON ("table_a"."table_a_col1" = "table_b"."table_b_col")
# LEFT JOIN "table_c" ON ("table_a"."table_a_col2" = "table_c"."table_c_col")
self.joins = joins or {}
# order, limit, offset
self.order = None
self.limit = None
self.offset = None
# holds extra conditions for table joins that should not be in the where
# clause but in the join condition itself. The dict is used as follows:
#
# self.extras = {
# ('table_a', ('table_b', 'table_a_col1', 'table_b_col', 'LEFT JOIN')):
# ('"table_b"."table_b_col3" = %s', [42])
# }
#
# which should lead to the following SQL:
#
# SELECT ... FROM "table_a"
# LEFT JOIN "table_b" ON ("table_a"."table_a_col1" = "table_b"."table_b_col" AND "table_b"."table_b_col3" = 42)
# ...
self.extras = extras or {}
def add_table(self, table, alias=None):
""" Add a table with a given alias to the from clause. """
alias = alias or table
assert alias not in self._tables and alias not in self._joins, "Alias %r already in %s" % (alias, str(self))
self._tables[alias] = table
def add_table(self, table):
table = _quote(table)
assert table not in self._tables, '%s already in %s' % (table, self)
self._tables.append(table)
def add_where(self, where_clause, where_params=()):
""" Add a condition to the where clause. """
self._where_clauses.append(where_clause)
self._where_params.extend(where_params)
def join(self, lhs_alias, lhs_column, rhs_table, rhs_column, link, extra=None, extra_params=()):
""" Add an INNER JOIN to the current table (if necessary), and return
the alias corresponding to ``rhs_table``.
"""
return self._join('JOIN', lhs_alias, lhs_column, rhs_table, rhs_column, link, extra, extra_params)
def left_join(self, lhs_alias, lhs_column, rhs_table, rhs_column, link, extra=None, extra_params=()):
""" Add a LEFT JOIN to the current table (if necessary), and return the
alias corresponding to ``rhs_table``.
"""
return self._join('LEFT JOIN', lhs_alias, lhs_column, rhs_table, rhs_column, link, extra, extra_params)
def _join(self, kind, lhs_alias, lhs_column, rhs_table, rhs_column, link, extra=None, extra_params=()):
assert lhs_alias in self._tables or lhs_alias in self._joins, "Alias %r not in %s" % (lhs_alias, str(self))
rhs_alias = _generate_table_alias(lhs_alias, link)
assert rhs_alias not in self._tables, "Alias %r already in %s" % (rhs_alias, str(self))
if rhs_alias not in self._joins:
condition = f'"{lhs_alias}"."{lhs_column}" = "{rhs_alias}"."{rhs_column}"'
condition_params = []
if extra:
condition = condition + " AND " + extra.format(lhs=lhs_alias, rhs=rhs_alias)
condition_params = list(extra_params)
if kind:
self._joins[rhs_alias] = (kind, rhs_table, condition, condition_params)
else:
self._tables[rhs_alias] = rhs_table
self.add_where(condition, condition_params)
return rhs_alias
def select(self, *args):
""" Return the SELECT query as a pair ``(query_string, query_params)``. """
from_clause, where_clause, params = self.get_sql()
query_str = 'SELECT {} FROM {} WHERE {}{}{}{}'.format(
", ".join(args or [_quote(next(iter(self._tables))) + ".id"]),
from_clause,
where_clause or "TRUE",
(" ORDER BY %s" % self.order) if self.order else "",
(" LIMIT %d" % self.limit) if self.limit else "",
(" OFFSET %d" % self.offset) if self.offset else "",
)
return query_str, params
def get_sql(self):
""" Returns (query_from, query_where, query_params). """
tables = []
joins = []
params = []
for alias, table in self._tables.items():
tables.append(f'"{table}"' if alias == table else f'"{table}" AS "{alias}"')
for alias, (kind, table, condition, condition_params) in self._joins.items():
joins.append(f'{kind} "{table}" AS "{alias}" ON ({condition})')
params.extend(condition_params)
from_clause = " ".join([", ".join(tables)] + joins)
where_clause = " AND ".join(self._where_clauses)
return from_clause, where_clause, params + self._where_params
@lazy_property
def _result(self):
query_str, params = self.select()
self._cr.execute(query_str, params)
return [row[0] for row in self._cr.fetchall()]
def __str__(self):
return '<osv.Query: %r with params: %r>' % self.select()
def __bool__(self):
return bool(self._result)
def __len__(self):
return len(self._result)
def __iter__(self):
return iter(self._result)
#
# deprecated attributes and methods
#
@property
def tables(self):
warnings.warn("deprecated Query.tables, use Query.get_sql() instead",
DeprecationWarning)
return self._tables
return tuple(
f'"{table}"' if alias == table else f'"{table}" AS "{alias}"'
for alias, table in self._tables
)
def _get_table_aliases(self):
from odoo.osv.expression import get_alias_from_query
return [get_alias_from_query(from_statement)[1] for from_statement in self._tables]
@property
def where_clause(self):
return tuple(self._where_clauses)
def _get_alias_mapping(self):
from odoo.osv.expression import get_alias_from_query
mapping = {}
for table in self._tables:
alias, statement = get_alias_from_query(table)
mapping[statement] = table
return mapping
@property
def where_clause_params(self):
return tuple(self._where_params)
def add_join(self, connection, implicit=True, outer=False, extra=None, extra_params=[]):
""" Join a destination table to the current table.
:param implicit: False if the join is an explicit join. This allows
to fall back on the previous implementation of ``join`` before
OpenERP 7.0. It therefore adds the JOIN specified in ``connection``
If True, the join is done implicitely, by adding the table alias
in the from clause and the join condition in the where clause
of the query. Implicit joins do not handle outer, extra, extra_params parameters.
:param connection: a tuple ``(lhs, table, lhs_col, col, link)``.
The join corresponds to the SQL equivalent of::
(lhs.lhs_col = table.col)
Note that all connection elements are strings. Please refer to expression.py for more details about joins.
:param outer: True if a LEFT OUTER JOIN should be used, if possible
(no promotion to OUTER JOIN is supported in case the JOIN
was already present in the query, as for the moment
implicit INNER JOINs are only connected from NON-NULL
columns so it would not be correct (e.g. for
``_inherits`` or when a domain criterion explicitly
adds filtering)
:param extra: A string with the extra join condition (SQL), or None.
This is used to provide an additional condition to the join
clause that cannot be added in the where clause (e.g., for LEFT
JOIN concerns). The condition string should refer to the table
aliases as "{lhs}" and "{rhs}".
:param extra_params: a list of parameters for the `extra` condition.
"""
from odoo.osv.expression import generate_table_alias
(lhs, table, lhs_col, col, link) = connection
alias, alias_statement = generate_table_alias(lhs, [(table, link)])
if implicit:
if alias_statement not in self._tables:
self._tables.append(alias_statement)
condition = '("%s"."%s" = "%s"."%s")' % (lhs, lhs_col, alias, col)
self.where_clause.append(condition)
else:
# already joined
pass
return alias, alias_statement
else:
aliases = self._get_table_aliases()
assert lhs in aliases, "Left-hand-side table %s must already be part of the query tables %s!" % (lhs, str(self._tables))
if alias_statement in self._tables:
# already joined, must ignore (promotion to outer and multiple joins not supported yet)
pass
else:
# add JOIN
self._tables.append(alias_statement)
join_tuple = (alias, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN')
self.joins.setdefault(lhs, []).append(join_tuple)
if extra or extra_params:
extra = (extra or '').format(lhs=lhs, rhs=alias)
self.extras[(lhs, join_tuple)] = (extra, extra_params)
return alias, alias_statement
def get_sql(self):
""" Returns (query_from, query_where, query_params). """
from odoo.osv.expression import get_alias_from_query
tables_to_process = list(self._tables)
alias_mapping = self._get_alias_mapping()
from_clause = []
from_params = []
def add_joins_for_table(lhs):
for (rhs, lhs_col, rhs_col, join) in self.joins.get(lhs, []):
tables_to_process.remove(alias_mapping[rhs])
from_clause.append(' %s %s ON ("%s"."%s" = "%s"."%s"' % \
(join, alias_mapping[rhs], lhs, lhs_col, rhs, rhs_col))
extra = self.extras.get((lhs, (rhs, lhs_col, rhs_col, join)))
if extra:
if extra[0]:
from_clause.append(' AND ')
from_clause.append(extra[0])
if extra[1]:
from_params.extend(extra[1])
from_clause.append(')')
add_joins_for_table(rhs)
for pos, table in enumerate(tables_to_process):
if pos > 0:
from_clause.append(',')
from_clause.append(table)
table_alias = get_alias_from_query(table)[1]
if table_alias in self.joins:
add_joins_for_table(table_alias)
return "".join(from_clause), " AND ".join(self.where_clause), from_params + self.where_clause_params
def __str__(self):
return '<osv.Query: "SELECT ... FROM %s WHERE %s" with params: %r>' % self.get_sql()
def add_join(self, connection, implicit=True, outer=False, extra=None, extra_params=()):
warnings.warn("deprecated Query.add_join, use Query.join() or Query.left_join() instead",
DeprecationWarning)
lhs_alias, rhs_table, lhs_column, rhs_column, link = connection
kind = '' if implicit else ('LEFT JOIN' if outer else 'JOIN')
rhs_alias = self._join(kind, lhs_alias, lhs_column, rhs_table, rhs_column, link, extra, extra_params)
return rhs_alias, f'"{rhs_table}" AS "{rhs_alias}"'