[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:
@@ -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"
|
||||
''']):
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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}"'
|
||||
|
||||
Reference in New Issue
Block a user