[IMP] core: add models defined by a query instead of a table/view

By adding a parameter `_table_query` on the Model, we can now have views
that depend on the context.  The query is used instead of the table name
in ORM operations.  This allows to pre-compute some values to improve
performance, instead of storing context values in the database.

Co-authored-by: william-andre <wan@odoo.com>
Co-authored-by: rco-odoo <rco@odoo.com>
This commit is contained in:
wan
2020-08-20 07:45:18 +00:00
committed by Raphael Collet
co-authored by rco-odoo
parent 9ad16bf84a
commit f2ceef0e2f
5 changed files with 57 additions and 31 deletions
+19
View File
@@ -80,3 +80,22 @@ class QueryTestCase(BaseCase):
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')
def test_table_expression(self):
query = Query(None, 'foo')
from_clause, where_clause, where_params = query.get_sql()
self.assertEqual(from_clause, '"foo"')
query = Query(None, 'bar', 'SELECT id FROM foo')
from_clause, where_clause, where_params = query.get_sql()
self.assertEqual(from_clause, '(SELECT id FROM foo) AS "bar"')
query = Query(None, 'foo')
query.add_table('bar', 'SELECT id FROM foo')
from_clause, where_clause, where_params = query.get_sql()
self.assertEqual(from_clause, '"foo", (SELECT id FROM foo) AS "bar"')
query = Query(None, 'foo')
query.join('foo', 'bar_id', 'SELECT id FROM foo', 'id', 'bar')
from_clause, where_clause, where_params = query.get_sql()
self.assertEqual(from_clause, '"foo" JOIN (SELECT id FROM foo) AS "foo__bar" ON ("foo"."bar_id" = "foo__bar"."id")')
+4 -3
View File
@@ -316,6 +316,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
correspond to the last one (in the inherits list order).
"""
_table = None #: SQL table name used by model if :attr:`_auto`
_table_query = None #: SQL expression of the table's content (optional)
_sequence = None #: SQL sequence to use for ID field
_sql_constraints = [] #: SQL constraints [(name, sql_def, message)]
@@ -3058,7 +3059,7 @@ Fields:
cr, user, context, su = env.args
# make a query object for selecting ids, and apply security rules to it
query = Query(self.env.cr, self._table)
query = Query(self.env.cr, self._table, self._table_query)
self._apply_ir_rules(query, 'read')
# the query may involve several tables: we need fully-qualified names
@@ -3321,7 +3322,7 @@ Fields:
if not self._ids:
return self
query = Query(self._cr, self._table)
query = Query(self.env.cr, self._table, self._table_query)
self._apply_ir_rules(query, operation)
if not query.where_clause:
return self
@@ -4153,7 +4154,7 @@ Fields:
if domain:
return expression.expression(domain, self).query
else:
return Query(self.env.cr, self._table)
return Query(self.env.cr, self._table, self._table_query)
def _check_qorder(self, word):
if not regex_order.match(word):
+5 -1
View File
@@ -491,7 +491,11 @@ class Registry(Mapping):
Verify that all tables are present and try to initialize those that are missing.
"""
env = odoo.api.Environment(cr, SUPERUSER_ID, {})
table2model = {model._table: name for name, model in env.items() if not model._abstract}
table2model = {
model._table: name
for name, model in env.items()
if not model._abstract and model.__class__._table_query is None
}
missing_tables = set(table2model).difference(existing_tables(cr, table2model))
if missing_tables:
+1 -1
View File
@@ -432,7 +432,7 @@ class expression(object):
self.expression = distribute_not(normalize_domain(domain))
# this object handles all the joins
self.query = Query(model.env.cr, model._table) if query is None else query
self.query = Query(model.env.cr, model._table, model._table_query) if query is None else query
# parse the domain expression
self.parse()
+28 -26
View File
@@ -1,16 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import warnings
from zlib import crc32
from odoo.tools import lazy_property
IDENT_RE = re.compile(r'^[a-z_][a-z0-9_$]*$', re.I)
def _quote(to_quote):
if '"' not in to_quote:
return '"%s"' % to_quote
return to_quote
def _from_table(table, alias):
""" Return a FROM clause element from ``table`` and ``alias``. """
if alias == table:
return f'"{alias}"'
elif IDENT_RE.match(table):
return f'"{table}" AS "{alias}"'
else:
return f'({table}) AS "{alias}"'
def _generate_table_alias(src_table_alias, link):
@@ -44,36 +51,36 @@ class Query(object):
""" 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.
:param cr: database cursor (for lazy evaluation)
:param alias: name or alias of the table
:param table: if given, a table expression (identifier or query)
"""
def __init__(self, cr, table, where_clause=None, where_params=()):
def __init__(self, cr, alias, table=None):
# database cursor
self._cr = cr
# tables {alias: table}
self._tables = {table: table}
self._tables = {alias: table or alias}
# 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_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_params = list(where_params)
# holds the list of WHERE clause elements (to be joined with 'AND'), and
# the list of parameters
self._where_clauses = []
self._where_params = []
# order, limit, offset
self.order = None
self.limit = None
self.offset = None
def add_table(self, table, alias=None):
def add_table(self, alias, table=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
self._tables[alias] = table or alias
def add_where(self, where_clause, where_params=()):
""" Add a condition to the where clause. """
@@ -116,7 +123,7 @@ class Query(object):
""" 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"]),
", ".join(args or [f'"{next(iter(self._tables))}".id']),
from_clause,
where_clause or "TRUE",
(" ORDER BY %s" % self.order) if self.order else "",
@@ -127,13 +134,11 @@ class Query(object):
def get_sql(self):
""" Returns (query_from, query_where, query_params). """
tables = []
tables = [_from_table(table, alias) for alias, table in self._tables.items()]
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})')
joins.append(f'{kind} {_from_table(table, alias)} ON ({condition})')
params.extend(condition_params)
from_clause = " ".join([", ".join(tables)] + joins)
@@ -165,10 +170,7 @@ class Query(object):
def tables(self):
warnings.warn("deprecated Query.tables, use Query.get_sql() instead",
DeprecationWarning)
return tuple(
f'"{table}"' if alias == table else f'"{table}" AS "{alias}"'
for alias, table in self._tables
)
return tuple(_from_table(table, alias) for alias, table in self._tables.items())
@property
def where_clause(self):
@@ -184,4 +186,4 @@ class Query(object):
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}"'
return rhs_alias, _from_table(rhs_table, rhs_alias)